优化
This commit is contained in:
@@ -21,6 +21,14 @@ import { useWorkSyncRead } from './useWorkSyncRead'
|
||||
const props = defineProps<{ itemKey: string; initialAction?: WorkAction | null; canLeave: LeaveGuard }>()
|
||||
const emit = defineEmits<{ open: [key: string, action?: WorkAction]; external: [path: string]; saved: [result?: WorkCommandResult, command?: WorkCommand['command']]; updated: []; container: [id: string, label: string] }>()
|
||||
const detail = ref<WorkDetail | null>(null), loading = ref(false), error = ref<ApiErrorDetails | null>(null), action = ref<WorkAction | null>(null)
|
||||
const attentionReasons = computed(() => [...new Set([
|
||||
...(detail.value?.item.blockReasons || []),
|
||||
...(detail.value?.item.missingFields || []),
|
||||
...(detail.value?.item.overdueReasons || []),
|
||||
].map(workReason))])
|
||||
const needsFollowup = computed(() => !!detail.value && detail.value.item.executionStatus !== 'COMPLETED' && (!detail.value.item.owner || !detail.value.item.followupDate))
|
||||
const historyLabels: Record<string, string> = { BASELINE_OBSERVED: '同步原业务状态', GENERATED: '创建工作事项', CONFIRM_ROUTE: '确认路线', SAVE_FOLLOWUP: '记录跟进', RESOLVE_REVIEW: '处理复核', SAVE_REQUIREMENT: '保存预约要求', HANDOFF_RETURN: '交接还空责任', ACCEPT_RETURN: '确认接手还空' }
|
||||
function historyTime(value: string) { return value.replace('T', ' ').replace(/\.\d+(?=Z|[+-]\d{2}:\d{2}$)/, '').replace(/Z$/, ' UTC') }
|
||||
const receivingOwners = computed(() => [detail.value?.item.owner, ...(detail.value?.relatedItems.map(item => item.owner) || [])].filter((person): person is { id: string; name: string } => !!person?.id))
|
||||
const actionNotice = ref('')
|
||||
const saveCompleted = ref(false)
|
||||
@@ -105,17 +113,33 @@ onMounted(async () => {
|
||||
onBeforeUnmount(() => { ++request; controller?.abort() })
|
||||
</script>
|
||||
<template>
|
||||
<div :aria-busy="loading">
|
||||
<div class="work-detail" :aria-busy="loading">
|
||||
<LoadErrorState v-if="error" title="工作事项读取失败" :message="error.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="error" /></template></LoadErrorState>
|
||||
<p v-if="loading" role="status">{{ syncing ? '预约工作清单同步中,正在自动读取最新事项;已保存内容无需重复提交。' : '正在读取当前事项与操作资格…' }}</p>
|
||||
<template v-if="detail">
|
||||
<h3>{{ detail.item.title }}</h3><p>{{ typeLabels[detail.item.type] }} · {{ flowLabels[detail.item.flow] }} · {{ workStatus(detail.item) }}</p>
|
||||
<p>{{ workDate(detail.item) }} · {{ detail.item.timezone || '地点时区待核实' }}</p><p>{{ detail.item.destinationName || detail.item.originName || '地点待核实' }} · {{ detail.item.owner?.name || '待分派' }}</p>
|
||||
<p class="followup-summary">下次跟进:{{ detail.item.followupDate || '尚未安排' }}<span v-if="detail.item.dateBasis === 'NONE'"> · 作业日期未定,跟进日不作为作业日期</span></p>
|
||||
<p v-if="detail.item.executionStatus !== 'COMPLETED' && (!detail.item.owner || !detail.item.followupDate)" class="work-warning">{{ !detail.item.owner ? '负责人待分派。' : '' }}{{ !detail.item.followupDate ? '请补充下一次跟进安排。' : '' }}请记录跟进;若无操作权限,请联系负责人。</p>
|
||||
<div class="container-links"><button v-for="container in detail.item.containers" :key="container.containerBookingId" type="button" @click="emit('container', container.containerBookingId, container.containerNo || container.bookingNo || '本柜')">{{ container.containerNo || container.bookingNo || '—' }} · 本柜待办</button></div>
|
||||
<p v-for="reason in [...detail.item.blockReasons, ...detail.item.missingFields, ...detail.item.overdueReasons]" :key="reason" class="work-warning">{{ workReason(reason) }}</p>
|
||||
<header class="detail-overview">
|
||||
<div class="detail-title"><h3>{{ detail.item.title }}</h3><el-tag :type="detail.item.executionStatus === 'COMPLETED' ? 'success' : detail.item.reviewRequired ? 'warning' : 'info'" size="small">{{ workStatus(detail.item) }}</el-tag></div>
|
||||
<p class="detail-category">{{ typeLabels[detail.item.type] }} · {{ flowLabels[detail.item.flow] }}</p>
|
||||
</header>
|
||||
<dl class="density-fields detail-facts" aria-label="事项关键信息">
|
||||
<div><dt>作业时间</dt><dd>{{ workDate(detail.item) }}</dd></div>
|
||||
<div><dt>作业时区</dt><dd>{{ detail.item.timezone || '地点时区待核实' }}</dd></div>
|
||||
<div><dt>作业地点</dt><dd>{{ detail.item.destinationName || detail.item.originName || '地点待核实' }}</dd></div>
|
||||
<div><dt>负责人</dt><dd>{{ detail.item.owner?.name || '待分派' }}</dd></div>
|
||||
<div class="followup-summary"><dt>下次跟进</dt><dd>{{ detail.item.followupDate || '尚未安排' }}<span v-if="detail.item.dateBasis === 'NONE'" class="field-note">作业日期未定,跟进日不作为作业日期</span></dd></div>
|
||||
</dl>
|
||||
<div v-if="detail.item.containers.length" class="container-links"><span>关联货柜</span><el-button v-for="container in detail.item.containers" :key="container.containerBookingId" class="density-action" data-tone="primary" @click="emit('container', container.containerBookingId, container.containerNo || container.bookingNo || '本柜')">{{ container.containerNo || container.bookingNo || '—' }} · 本柜待办</el-button></div>
|
||||
<aside v-if="needsFollowup || attentionReasons.length" class="attention-panel" aria-label="事项提醒">
|
||||
<h4>事项提醒</h4>
|
||||
<ul>
|
||||
<li v-if="needsFollowup">{{ !detail.item.owner ? '负责人待分派。' : '' }}{{ !detail.item.followupDate ? '请补充下一次跟进安排。' : '' }}请记录跟进;若无操作权限,请联系负责人。</li>
|
||||
<li v-for="reason in attentionReasons" :key="reason">{{ reason }}</li>
|
||||
</ul>
|
||||
</aside>
|
||||
<p v-if="detail.returnHandoff">还空责任:{{ detail.returnHandoff.from.name }} → {{ detail.returnHandoff.to.name }} · {{ detail.returnHandoff.status === 'PENDING_ACCEPTANCE' ? '接手方待确认,持续跟进' : '已确认接手' }}</p>
|
||||
<section class="current-work" aria-label="当前处理">
|
||||
<h4>{{ detail.item.executionStatus === 'COMPLETED' ? '事项结果' : '当前处理' }}</h4>
|
||||
<p v-if="detail.item.executionStatus === 'COMPLETED'" class="completion-summary">当前事项已完成,可查看关联工作和历史记录。</p>
|
||||
<template v-if="!action && !continuation">
|
||||
<p v-if="saveCompleted" role="status" class="save-result">本次保存已完成。{{ loading ? '正在读取当前下一步…' : error ? '下一步读取失败,请重试读取事项,无需重复提交已保存内容。' : detail.item.executionStatus === 'COMPLETED' ? '当前事项已完成,可返回查看其他待办。' : '请按当前下一步继续处理。' }}</p>
|
||||
<el-button v-if="detail.item.nextAction" class="primary-next-action" type="primary" :disabled="loading || !!error" @click="choose(detail.item.nextAction)">{{ detail.item.nextAction.label }}</el-button>
|
||||
@@ -133,15 +157,43 @@ onBeforeUnmount(() => { ++request; controller?.abort() })
|
||||
<WorkDispatchForm v-else-if="dispatchBatchKeys?.length" :key="JSON.stringify(dispatchBatchKeys)" :batch-keys="dispatchBatchKeys" :can-write="canAct" @updated="emit('updated')" @external="path => emit('external', path)" @batch="openBatch" />
|
||||
<WorkExistingDispatchForm v-else-if="existingDispatches.length" :key="JSON.stringify(existingDispatches)" :dispatches="existingDispatches" :can-write="canAct" @updated="emit('updated')" @external="path => emit('external', path)" />
|
||||
<WorkReceiptPlans v-else-if="action?.target.kind === 'WAREHOUSE_RECEIPT'" :key="action.target.containerBookingId" :container-booking-id="action.target.containerBookingId" :can-write="canAct" @updated="emit('updated')" @external="path => emit('external', path)" />
|
||||
<section v-else-if="action?.target.kind === 'READ_ONLY'" aria-label="原业务进度"><h4>原业务进度</h4><p v-for="reference in action.target.references" :key="`${reference.kind}:${reference.key}`"><el-button v-if="referenceRoute(reference)" :disabled="!canAct" @click="emit('external', referenceRoute(reference)!)">{{ reference.label }} · {{ reference.businessNo }}</el-button><span v-else>{{ reference.label }} · {{ reference.businessNo || '—' }}</span></p></section>
|
||||
<section v-else-if="action?.target.kind === 'READ_ONLY' && action.target.references.length" aria-label="原业务进度"><h4>原业务进度</h4><p v-for="reference in action.target.references" :key="`${reference.kind}:${reference.key}`"><el-button v-if="referenceRoute(reference)" :disabled="!canAct" @click="emit('external', referenceRoute(reference)!)">{{ reference.label }} · {{ reference.businessNo }}</el-button><span v-else>{{ reference.label }} · {{ reference.businessNo || '—' }}</span></p></section>
|
||||
<details v-if="secondaryActions.length"><summary>更多操作</summary><div class="more-actions"><el-button v-for="candidate in secondaryActions" :key="`${candidate.code}:${candidate.targetWorkItemKey}`" :disabled="loading || !!error" @click="choose(candidate)">{{ candidate.label }}</el-button></div></details>
|
||||
<details><summary>关联工作 · {{ detail.relatedItems.length }} 项</summary><div v-for="related in detail.relatedItems" :key="related.workItemKey" class="related-row"><div>{{ related.title }}<p>{{ workStatus(related) }} · {{ workDate(related) }}</p></div><el-button @click="emit('open', related.workItemKey)">查看</el-button></div></details>
|
||||
<details><summary>原业务记录 · {{ detail.references.length }} 项</summary><p v-for="reference in detail.references" :key="`${reference.kind}:${reference.key}`">{{ reference.label }} · {{ reference.businessNo || '—' }}</p></details>
|
||||
<details :open="action?.code === 'VIEW_HISTORY'"><summary>操作记录</summary><div v-for="(entry, index) in detail.history" :key="index" class="history-entry"><b>{{ entry.action }}</b><p>{{ entry.summary }}</p><small>{{ entry.operatorName || '—' }} · {{ entry.occurredAt }}</small></div><p v-if="!detail.history.length">暂无记录</p></details>
|
||||
</section>
|
||||
<section class="detail-records" aria-label="关联资料与历史">
|
||||
<details><summary>关联工作 · {{ detail.relatedItems.length }} 项</summary><div v-for="related in detail.relatedItems" :key="related.workItemKey" class="related-row"><div>{{ related.title }}<p>{{ workStatus(related) }} · {{ workDate(related) }}</p></div><el-button class="density-action" @click="emit('open', related.workItemKey)">查看</el-button></div><p v-if="!detail.relatedItems.length">暂无关联工作</p></details>
|
||||
<details><summary>原业务记录 · {{ detail.references.length }} 项</summary><p v-for="reference in detail.references" :key="`${reference.kind}:${reference.key}`">{{ reference.label }} · {{ reference.businessNo || '—' }}</p><p v-if="!detail.references.length">暂无原业务记录</p></details>
|
||||
<details :open="action?.code === 'VIEW_HISTORY'"><summary>操作记录 · {{ detail.history.length }} 条</summary><div v-for="(entry, index) in detail.history" :key="index" class="history-entry"><div class="history-heading"><b>{{ historyLabels[entry.action] || '事项记录' }}</b><time :datetime="entry.occurredAt">{{ historyTime(entry.occurredAt) }}</time></div><p>{{ entry.summary }}</p><small>{{ entry.operatorName || '—' }}<span v-if="!historyLabels[entry.action]"> · {{ entry.action }}</span></small></div><p v-if="!detail.history.length">暂无记录</p></details>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.container-links, .more-actions { display: flex; flex-wrap: wrap; gap: var(--admin-space-3); }.container-links button { background: transparent; border: 0; color: var(--el-color-primary); cursor: pointer; font: inherit; }
|
||||
.related-row { display: flex; align-items: center; justify-content: space-between; gap: var(--admin-space-3); }.related-row, .history-entry { border-bottom: 1px solid var(--cc-border); padding-block: var(--admin-space-3); }details { margin-top: var(--admin-space-5); }summary { cursor: pointer; padding-block: var(--admin-space-3); }.work-warning { color: var(--el-color-warning-dark-2); }
|
||||
.work-detail { font-size: var(--admin-font-body); line-height: 1.6; color: var(--el-text-color-primary); }
|
||||
.detail-overview { margin-bottom: var(--admin-space-5); }
|
||||
.detail-title { display: flex; align-items: center; flex-wrap: wrap; gap: var(--admin-space-3); }
|
||||
.detail-title h3 { margin: 0; font-size: var(--admin-font-title); }
|
||||
.detail-category { margin: var(--admin-space-1) 0 0; color: var(--el-text-color-secondary); font-size: var(--admin-font-caption); }
|
||||
.work-detail .detail-facts { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.followup-summary { grid-column: 1 / -1; }
|
||||
.field-note { margin-left: var(--admin-space-5); color: var(--el-text-color-secondary); font-size: var(--admin-font-caption); }
|
||||
.container-links, .more-actions { display: flex; align-items: center; flex-wrap: wrap; gap: var(--admin-space-3); }
|
||||
.container-links { margin-top: var(--admin-space-3); }
|
||||
.container-links > span { color: var(--el-text-color-secondary); font-size: var(--admin-font-caption); }
|
||||
.attention-panel { margin-top: var(--admin-space-5); padding: var(--admin-space-3) var(--admin-space-5); border-left: 3px solid var(--el-color-warning); background: var(--el-color-warning-light-9); color: var(--el-color-warning-dark-2); }
|
||||
.attention-panel h4, .current-work > h4 { margin: 0 0 var(--admin-space-1); font-size: var(--admin-font-section); }
|
||||
.attention-panel ul { margin: 0; padding-left: var(--admin-space-6); font-size: var(--admin-font-caption); }
|
||||
.current-work { margin-top: var(--admin-space-5); padding-block: var(--admin-space-5); border-top: 1px solid var(--cc-border); }
|
||||
.current-work > p { margin: var(--admin-space-2) 0; }
|
||||
.completion-summary { color: var(--el-color-success-dark-2); }
|
||||
.detail-records { border-top: 1px solid var(--cc-border); }
|
||||
.detail-records > details { border-bottom: 1px solid var(--cc-border); }
|
||||
summary { cursor: pointer; padding-block: var(--admin-space-3); font-weight: 600; }
|
||||
summary:focus-visible { outline: 2px solid var(--el-color-primary); outline-offset: -2px; }
|
||||
.detail-records p { margin: var(--admin-space-1) 0; }
|
||||
.related-row, .history-heading { display: flex; align-items: baseline; justify-content: space-between; flex-wrap: wrap; gap: var(--admin-space-3); }
|
||||
.related-row, .history-entry { border-bottom: 1px solid var(--cc-border); padding-block: var(--admin-space-3); }
|
||||
.related-row:last-child, .history-entry:last-child { border-bottom: 0; }
|
||||
.related-row p, .history-entry small, .history-heading time { font-size: var(--admin-font-caption); color: var(--el-text-color-secondary); }
|
||||
.work-warning { color: var(--el-color-warning-dark-2); }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user