This commit is contained in:
2026-08-26 13:58:39 +08:00
parent a3d04eaad8
commit 5daaf1a5fa
6 changed files with 103 additions and 59 deletions
@@ -36,7 +36,7 @@ describe('ContainerOverviewPanel', () => {
expect(wrapper.text()).not.toContain('PHOTO_STEP_EMPTY_CONTAINER')
expect(wrapper.text()).toContain('一项待复核')
expect(wrapper.text()).toContain('码头查验')
expect(wrapper.text()).toContain('更新预计到港时间')
expect(wrapper.text()).not.toContain('更新预计到港时间')
})
it('emits stable document types and does not expose file ids in visible text', async () => {
@@ -89,7 +89,7 @@ describe('ContainerOverviewPanel', () => {
expect(section.text()).toContain('复核员')
expect(section.text()).toContain('查验解除')
expect(section.text()).not.toContain('更新预计到港时间')
expect(wrapper.find('[aria-label="运输轨迹和操作日志"]').text()).toContain('更新预计到港时间')
expect(wrapper.find('[aria-label="运输轨迹和操作日志"]').exists()).toBe(false)
})
it('emits the unchanged manage action for mark and cancel buttons', async () => {
@@ -137,7 +137,7 @@ describe('ContainerOverviewPanel', () => {
wrapper.unmount()
})
it('maps PDA events to Chinese and hides long technical keys from the main timeline', () => {
it('delegates warehouse events to the unified fulfillment timeline without exposing technical keys here', () => {
const longKey = 'pda-submit-1000001-2092438843630391297-2092437496092790785--0c642e80891d76a4-m9428-76'
const timeline = [
{ eventType: 'PDA_CLAIM', detail: `claim-${longKey}`, operatorId: '1', operatorName: 'platform_admin', occurredAt: '2026-08-26T10:28:00' },
@@ -145,9 +145,9 @@ describe('ContainerOverviewPanel', () => {
{ eventType: 'PDA_COMPLETE_LOADING', detail: `complete-${longKey}`, operatorId: '1', operatorName: 'platform_admin', occurredAt: '2026-08-26T10:29:00' },
]
const wrapper = mount(ContainerOverviewPanel, { props: { overview: { ...overview, pda: null, timeline }, canManageDocuments: true, canManageCustoms: true, canManageException: true }, global: { plugins: [ElementPlus] } })
expect(wrapper.text()).toContain('领取 PDA 任务')
expect(wrapper.text()).toContain('提交拣货结果')
expect(wrapper.text()).toContain('完成装柜')
expect(wrapper.text()).not.toContain('领取 PDA 任务')
expect(wrapper.text()).not.toContain('提交拣货结果')
expect(wrapper.text()).not.toContain('完成装柜')
expect(wrapper.text()).not.toContain(longKey)
})
@@ -89,13 +89,6 @@ function eventLabel(value: string) {
PDA_CLAIM: '领取 PDA 任务', PDA_BATCH: '提交拣货结果', PDA_COMPLETE_LOADING: '完成装柜',
} as Record<string, string>)[value] || value || '状态更新'
}
function eventSummary(entry: ContainerOverview['timeline'][number]) {
if (entry.eventType === 'PDA_CLAIM') return 'PDA 作业任务已领取'
if (entry.eventType === 'PDA_BATCH') return '拣货结果已提交'
if (entry.eventType === 'PDA_COMPLETE_LOADING') return '装柜作业已完成'
const detail = String(entry.detail || '').trim()
return !detail || detail.length > 120 || /(?:operation|idempot|claim-|pda-submit|complete-)/i.test(detail) ? '操作已记录' : detail
}
function revokePhotoUrls() {
Object.values(photoStates).forEach((state) => { if (state.url) URL.revokeObjectURL(state.url) })
Object.keys(photoStates).forEach((key) => delete photoStates[key])
@@ -153,17 +146,12 @@ onBeforeUnmount(() => { photoGeneration += 1; revokePhotoUrls() })
<template v-else><div class="pda-summary"><el-progress :percentage="pdaProgress" :status="overview.pda.unresolvedItemCount ? 'exception' : undefined" /><div class="metric-grid"><div><span>任务明细</span><strong>{{ overview.pda.itemCount }}</strong></div><div><span>已处理</span><strong>{{ overview.pda.completedItemCount }}</strong></div><div><span>未解决</span><strong>{{ overview.pda.unresolvedItemCount }}</strong></div><div><span>有效照片</span><strong>{{ overview.pda.photoCounts.reduce((sum, item) => sum + item.count, 0) }}</strong></div></div></div><el-alert v-if="workflowStatusLabel(overview.pda.workflowStatus, overview.pda.taskStatus) === '待人工复核'" type="warning" :closable="false" title="PDA 作业已完成,等待人工复核后才能完成装柜。" /><el-alert v-else-if="workflowStatusLabel(overview.pda.workflowStatus, overview.pda.taskStatus) === '作业中'" type="info" :closable="false" title="请继续在 PDA 完成拣货、拍照和封条登记。" /><div v-if="activePhotos.length" class="photo-gallery"><button v-for="photo in activePhotos" :key="photo.evidenceId" type="button" class="photo-thumb" :aria-label="`预览装柜照片 ${photo.fileName}`" :disabled="!photoStates[photo.evidenceId]?.url" @click="openPhoto(photo)"><span v-if="photoStates[photo.evidenceId]?.loading" class="photo-placeholder">加载中</span><span v-else-if="photoStates[photo.evidenceId]?.error" class="photo-placeholder photo-placeholder--error">{{ photoStates[photo.evidenceId]?.error }}</span><img v-else-if="photoStates[photo.evidenceId]?.url" :src="photoStates[photo.evidenceId]?.url || ''" :alt="`${evidenceTypeLabel(photo.evidenceType)} ${photo.fileName}`" /><span class="photo-caption"><strong>{{ evidenceTypeLabel(photo.evidenceType) }}</strong><small>{{ formatTime(photo.uploadedAt) }}</small></span></button></div><p v-else class="compact-empty">暂无可查看的有效照片</p><el-alert v-if="overview.pda.exceptionReasons.length" type="warning" :closable="false" :title="overview.pda.exceptionReasons.join(';')" /></template>
</section>
<section class="overview-section" aria-label="运输轨迹和操作日志">
<header><h3>运输轨迹与操作日志</h3><span>{{ overview.timeline.length }} 条</span></header>
<el-empty v-if="overview.timeline.length === 0" description="暂无轨迹或操作记录" :image-size="64" />
<ol v-else class="compact-timeline"><li v-for="(entry, index) in overview.timeline" :key="`${entry.eventType}-${entry.occurredAt}-${index}`"><time>{{ formatTime(entry.occurredAt) }}</time><strong>{{ eventLabel(entry.eventType) }}</strong><span>{{ entry.operatorName || '系统' }}</span><small>{{ eventSummary(entry) }}</small></li></ol>
</section>
</div>
</template>
<style scoped>
.overview-panel { display: grid; gap: 12px; }.overview-section:nth-child(3){order:1}.overview-section:nth-child(2){order:2}.overview-section:nth-child(1){order:3}.overview-section:nth-child(4){order:4}
.overview-panel { display: grid; gap: 12px; }.overview-section:nth-child(3){order:1}.overview-section:nth-child(2){order:2}.overview-section:nth-child(1){order:3}
.overview-section { min-width: 0; padding: 12px 16px; border: 1px solid var(--cc-border); background: #fff; }
.overview-section > header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; }
.overview-section h3 { margin: 0; font-size: 15px; }
@@ -372,7 +372,7 @@ onMounted(async () => { await load(); if (route.query.action === 'documents') op
</div>
</template>
</section>
<div class="surface-panel carrier-panel"><CarrierScheduleQueryPanel :key="carrierRefreshKey" :ocean-booking-id="detail.oceanBookingId" :container-booking-id="detail.id" /></div>
<div class="surface-panel carrier-panel"><CarrierScheduleQueryPanel :key="carrierRefreshKey" :ocean-booking-id="detail.oceanBookingId" :container-booking-id="detail.id" :warehouse-timeline="overview?.timeline || []" /></div>
</template>
<AppDialog v-model="documentOpen" :title="documentTitle" width="min(520px, calc(100vw - 28px))" confirm-text="上传资料" :confirm-loading="documentSaving" :confirm-disabled="!documentFile" @confirm="saveDocument"><el-form label-position="top"><el-form-item :label="documentTitle"><el-upload :auto-upload="false" :limit="1" :on-change="selectDocumentFile" :on-remove="removeDocumentFile"><el-button><FileUp :size="16" />选择文件</el-button><template #tip><div class="el-upload__tip">{{ selectedDocument ? '上传新文件将替换当前资料并保留历史。' : `请选择需要提交的${documentTitle}。` }}</div></template></el-upload></el-form-item></el-form><LoadErrorState v-if="documentError" compact title="资料上传失败" :message="documentError.message" :show-retry="false"><template #details><ApiErrorMeta :details="documentError" /></template></LoadErrorState></AppDialog>