diff --git a/src/modules/appointment-calendar/AppointmentCreatePanel.vue b/src/modules/appointment-calendar/AppointmentCreatePanel.vue index 607e8786..5b0667e0 100644 --- a/src/modules/appointment-calendar/AppointmentCreatePanel.vue +++ b/src/modules/appointment-calendar/AppointmentCreatePanel.vue @@ -135,7 +135,6 @@ onBeforeUnmount(() => { ++generation; ++selectionGeneration; ++destinationsReque 确认开始时差{{ createdForm.startOffset || '—' }}确认结束时差{{ createdForm.endOffset || '—' }} {{ createdForm.appointmentStatus === 'FAILED' ? '原因' : '备注' }}{{ createdForm.appointmentRemark || '—' }} -

预约已保存,请继续在原业务安排车辆。

diff --git a/src/modules/container-loading/components/ContainerOverviewPanel.test.ts b/src/modules/container-loading/components/ContainerOverviewPanel.test.ts index 282fff59..4238218d 100644 --- a/src/modules/container-loading/components/ContainerOverviewPanel.test.ts +++ b/src/modules/container-loading/components/ContainerOverviewPanel.test.ts @@ -10,7 +10,7 @@ vi.mock('../api/platformContainerLoading', () => ({ downloadContainerPdaPhoto: v const overview: ContainerOverview = { container: { id: '7', cabinetNo: 'CB-1', containerNo: 'MSCU-1', status: 'LOADING', warehouseId: '8', warehouseName: '义乌仓', voyageNo: 'V1', vesselName: '船一', pickupCode: '00123456', pickupCodeUpdatedBy: '13', pickupCodeUpdatedByName: '操作员', pickupCodeUpdatedAt: '2026-08-13T10:30:00', version: 2 }, loadingPlanId: '9', - pda: { taskId: '10', loadingPlanId: '9', containerBookingId: '7', taskStatus: 'IN_PROGRESS', workflowStatus: 'AWAITING_REVIEW', taskVersion: 2, itemCount: 4, completedItemCount: 4, unresolvedItemCount: 0, photoCounts: [{ evidenceType: 'PHOTO_STEP_EMPTY_CONTAINER', count: 2 }], photos: [{ evidenceId: '20', tenantId: '21', inboundId: '22', evidenceType: 'PHOTO_STEP_EMPTY_CONTAINER', fileId: '23', fileName: 'empty.jpg', status: 'ACTIVE', downloadPath: '/api/platform/v1/containers/7/pda/photos/23/download', uploadedAt: '2026-08-13T12:30:00' }], exceptionReasons: ['一项待复核'] }, + pda: { taskId: '10', loadingPlanId: '9', containerBookingId: '7', taskStatus: 'IN_PROGRESS', workflowStatus: 'AWAITING_REVIEW', taskVersion: 2, itemCount: 4, completedItemCount: 4, unresolvedItemCount: 0, photoCounts: [{ evidenceType: 'PHOTO_STEP_1', count: 2 }], photos: [{ evidenceId: '20', tenantId: '21', inboundId: '22', evidenceType: 'PHOTO_STEP_1', fileId: '23', fileName: 'empty.jpg', status: 'ACTIVE', downloadPath: '/api/platform/v1/containers/7/pda/photos/23/download', uploadedAt: '2026-08-13T12:30:00' }], exceptionReasons: ['一项待复核'] }, documents: [{ id: '11', documentType: 'DELIVERY_ORDER', fileId: '9007199254740999', fileName: 'do.pdf', fileHash: null, uploadedBy: '13', uploadedByName: '操作员', uploadedAt: '2026-08-13T10:00:00', version: 1, completed: false }], exception: { active: true, reason: '码头查验', markedBy: '13', markedByName: '操作员', markedAt: '2026-08-13T11:00:00' }, exceptionHistory: [{ id: '14', action: 'MARK', reason: '码头查验', operatorId: '13', operatorName: '操作员', occurredAt: '2026-08-13T11:00:00' }], @@ -26,14 +26,19 @@ describe('ContainerOverviewPanel', () => { }) it('renders documents, PDA progress, exception history and timeline from the aggregate DTO', async () => { - const wrapper = mount(ContainerOverviewPanel, { props: { overview, canManageDocuments: true, canManageCustoms: true, canManageException: true }, global: { plugins: [ElementPlus] } }) + const types = ['PHOTO_STEP_1', 'PHOTO_STEP_2', 'PHOTO_STEP_3', 'PHOTO_STEP_4', 'SEAL'] + const pda = { ...overview.pda!, photoCounts: types.map(evidenceType => ({ evidenceType, count: 1 })), photos: types.map((evidenceType, index) => ({ + ...overview.pda!.photos[0]!, evidenceId: String(20 + index), fileId: String(23 + index), evidenceType, + downloadPath: `/api/platform/v1/containers/7/pda/photos/${23 + index}/download`, + })) } + const wrapper = mount(ContainerOverviewPanel, { props: { overview: { ...overview, pda }, canManageDocuments: true, canManageCustoms: true, canManageException: true }, global: { plugins: [ElementPlus] } }) await flushPromises() expect(wrapper.text()).toContain('DO 资料') expect(wrapper.text()).toContain('do.pdf') expect(wrapper.text()).toContain('PDA 作业进度') expect(wrapper.text()).toContain('待人工复核') - expect(wrapper.text()).toContain('空柜照片') - expect(wrapper.text()).not.toContain('PHOTO_STEP_EMPTY_CONTAINER') + expect(wrapper.findAll('.photo-caption strong').map(caption => caption.text())).toEqual(['空仓照片', '半仓照片', '满仓照片', '锁仓照片', '带锁码照']) + expect(wrapper.text()).not.toContain('PHOTO_STEP_') expect(wrapper.text()).toContain('一项待复核') expect(wrapper.text()).toContain('码头查验') expect(wrapper.text()).not.toContain('更新预计到港时间') diff --git a/src/modules/container-loading/components/ContainerOverviewPanel.vue b/src/modules/container-loading/components/ContainerOverviewPanel.vue index 14339177..c34e0d27 100644 --- a/src/modules/container-loading/components/ContainerOverviewPanel.vue +++ b/src/modules/container-loading/components/ContainerOverviewPanel.vue @@ -77,8 +77,8 @@ const workflowStatusLabels: Record = { LOADING: '作业中', AWAITING_REVIEW: '待人工复核', COMPLETED: '已完成', } const evidenceTypeLabels: Record = { - PHOTO_STEP_EMPTY_CONTAINER: '空柜照片', PHOTO_STEP_HALF_LOADED: '半柜照片', PHOTO_STEP_FULL_CONTAINER: '满柜照片', - EMPTY_CONTAINER: '空柜照片', HALF_LOADED: '半柜照片', FULL_CONTAINER: '满柜照片', SEAL: '封条照片', + PHOTO_STEP_1: '空仓照片', PHOTO_STEP_2: '半仓照片', PHOTO_STEP_3: '满仓照片', + PHOTO_STEP_4: '锁仓照片', SEAL: '带锁码照', } function workflowStatusLabel(workflowStatus: string | null | undefined, taskStatus: string) { return workflowStatusLabels[workflowStatus || taskStatus] || '作业状态待同步' } function evidenceTypeLabel(value: string) { return evidenceTypeLabels[value] || '装柜照片' } diff --git a/src/modules/container-loading/pages/ContainerDetailView.emptyReturn.behavior.test.ts b/src/modules/container-loading/pages/ContainerDetailView.emptyReturn.behavior.test.ts index a7653528..5dde558d 100644 --- a/src/modules/container-loading/pages/ContainerDetailView.emptyReturn.behavior.test.ts +++ b/src/modules/container-loading/pages/ContainerDetailView.emptyReturn.behavior.test.ts @@ -245,6 +245,7 @@ describe('ContainerDetailView empty return', () => { vi.mocked(getContainer).mockResolvedValueOnce(base as never).mockResolvedValue({ ...base, workbenchStage: 'EMPTY_RETURNED', statusSummary: { terminalDrayageStatus: 'EMPTY_RETURNED' }, allowedActions: [], terminalDrayage: { ...base.terminalDrayage, status: 'EMPTY_RETURNED', version: 4, actualEmptyReturnAt: '2026-09-09T12:00:00', allowedActions: [] } } as never) const wrapper = await mountDetail() expect(wrapper.text()).toContain('待还柜') + expect(wrapper.findAll('dt').find(item => item.text() === '码头拖车状态')!.element.nextElementSibling!.textContent).toBe('待还柜') expect(wrapper.text()).not.toContain('装柜完成') await wrapper.findAll('button').find((item) => item.text() === '确认还柜')!.trigger('click') await flushPromises() @@ -257,6 +258,7 @@ describe('ContainerDetailView empty return', () => { expect(recordTerminalDrayageEmptyReturn).toHaveBeenCalledWith('9001', expect.objectContaining({ expectedVersion: 3, actualAt: '2026-09-09T12:00:00', remark: undefined }), expect.any(String)) expect(getContainer).toHaveBeenCalledTimes(2) expect(wrapper.text()).toContain('已还柜') + expect(wrapper.findAll('dt').find(item => item.text() === '码头拖车状态')!.element.nextElementSibling!.textContent).toBe('已还柜') expect(wrapper.find('.detail-section__heading').findAll('button').some((item) => item.text() === '确认还柜')).toBe(false) }) @@ -319,6 +321,27 @@ describe('domestic container loading list and booking link', () => { return wrapper } + it('keeps customs and PDA information only in the overview when switching to the loading list and back', async () => { + canAccess.mockImplementation((code) => ['platform:firstleg:container:list', 'platform:firstleg:loading-list:list', 'platform:firstleg:customs:list'].includes(code)) + vi.mocked(getContainer).mockResolvedValue({ ...domestic, planStatus: 'COMPLETED' } as never) + const wrapper = await mountDomestic() + const pdaPanels = () => wrapper.findAllComponents({ name: 'ContainerOverviewPanel' }).filter(panel => panel.props('sections')?.includes('pda')) + expect(pdaPanels()).toHaveLength(1) + expect(wrapper.findAllComponents({ name: 'ContainerCustomsPanel' })).toHaveLength(1) + await wrapper.findAll('[role="tab"]').find(tab => accessibleText(tab) === '装柜清单')!.trigger('click') + await flushPromises() + const list = wrapper.get('[aria-labelledby="container-loading-list-heading"]') + expect(list.findComponent({ name: 'LoadingListDetailDrawer' }).exists()).toBe(true) + expect(list.findComponent({ name: 'ContainerOverviewPanel' }).exists()).toBe(false) + expect(list.findComponent({ name: 'ContainerCustomsPanel' }).exists()).toBe(false) + expect(pdaPanels()).toHaveLength(1) + expect(wrapper.findAllComponents({ name: 'ContainerCustomsPanel' })).toHaveLength(1) + await wrapper.findAll('[role="tab"]').find(tab => accessibleText(tab) === '送柜报关')!.trigger('click') + await flushPromises() + expect(pdaPanels()[0]!.isVisible()).toBe(true) + expect(wrapper.findComponent({ name: 'ContainerCustomsPanel' }).isVisible()).toBe(true) + }) + it.each(['WAIT_LOADING', 'EDITING', 'LOADING', 'COMPLETED'])('opens the loading-list tab using the correct API for %s', async (planStatus) => { vi.mocked(getContainer).mockResolvedValue({ ...domestic, planStatus } as never) const wrapper = await mountDomestic() diff --git a/src/modules/container-loading/pages/ContainerDetailView.vue b/src/modules/container-loading/pages/ContainerDetailView.vue index a375411f..c7c67bae 100644 --- a/src/modules/container-loading/pages/ContainerDetailView.vue +++ b/src/modules/container-loading/pages/ContainerDetailView.vue @@ -302,16 +302,17 @@ const oceanBookingLinkOpen = ref(false) const selectedDocument = computed(() => overview.value?.documents.find((item) => item.documentType === documentType.value) || null) const documentTitle = computed(() => 'DO 资料') const title = computed(() => detail.value?.containerNo || detail.value?.cabinetNo || '货柜详情') +const overseasStageLabels: Record = { + WAIT_LOADING: '待装柜', LOADING: '装柜中', WAIT_CUSTOMS: '待出口报关', WAIT_OUTBOUND: '待国内出仓', + DEPARTED_WAREHOUSE: '已从国内仓出仓', IN_TRANSIT: '海运在途', ARRIVED_DESTINATION_PORT: '已到目的港', + WAITING_FOR_ARRIVAL: '待到港', ARRIVED: '已到港', PICKUP_READY: '可安排提柜', PICKUP_SCHEDULED: '已预约提柜', + PICKED_UP: '已提柜', UNLOADED: '待还柜', EMPTY_RETURNED: '已还柜', +} const overseasStageText = computed(() => { const status = workbenchDetail.value?.statusSummary?.terminalDrayageStatus || workbenchDetail.value?.terminalDrayage?.status || workbenchDetail.value?.workbenchStage - return ({ - WAIT_LOADING: '待装柜', LOADING: '装柜中', WAIT_CUSTOMS: '待出口报关', WAIT_OUTBOUND: '待国内出仓', - DEPARTED_WAREHOUSE: '已从国内仓出仓', IN_TRANSIT: '海运在途', ARRIVED_DESTINATION_PORT: '已到目的港', - WAITING_FOR_ARRIVAL: '待到港', ARRIVED: '已到港', PICKUP_READY: '可安排提柜', PICKUP_SCHEDULED: '已预约提柜', - PICKED_UP: '已提柜', UNLOADED: '待还柜', EMPTY_RETURNED: '已还柜', - } as Record)[status || ''] || '海外履约中' + return overseasStageLabels[status || ''] || '海外履约中' }) const primaryStatusText = computed(() => isDomestic.value ? statusText(detail.value?.planStatus) : overseasStageText.value) const loadingListMode = computed(() => detail.value?.planStatus === 'COMPLETED' ? 'confirmed' : 'preload') @@ -664,13 +665,11 @@ onMounted(async () => {

提柜、卸柜与还空

提柜安排与执行确认还柜
-
码头拖车状态
{{ workbenchDetail?.statusSummary?.terminalDrayageStatus || '—' }}
码头车队
{{ workbenchDetail?.terminalDrayage?.fleetSupplierName || '—' }}
预计到仓
{{ expectedArrivalText }}
实际到仓
{{ formatTime(arrivalTime?.actualWarehouseArrivalAt) }}
卸柜完成
{{ formatTime((workbenchDetail?.actualFacts?.unloadingCompletedAt || workbenchDetail?.terminalDrayage?.unloadingCompletedAt) as string | null) }}
实际还柜
{{ formatTime(workbenchDetail?.terminalDrayage?.actualEmptyReturnAt) }}
+
码头拖车状态
{{ overseasStageLabels[workbenchDetail?.statusSummary?.terminalDrayageStatus || ''] || '—' }}
码头车队
{{ workbenchDetail?.terminalDrayage?.fleetSupplierName || '—' }}
预计到仓
{{ expectedArrivalText }}
实际到仓
{{ formatTime(arrivalTime?.actualWarehouseArrivalAt) }}
卸柜完成
{{ formatTime((workbenchDetail?.actualFacts?.unloadingCompletedAt || workbenchDetail?.terminalDrayage?.unloadingCompletedAt) as string | null) }}
实际还柜
{{ formatTime(workbenchDetail?.terminalDrayage?.actualEmptyReturnAt) }}

{{ loadingListHeading }}

- -
@@ -702,7 +701,7 @@ onMounted(async () => {

正在核对码头拖车当前状态…

-