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 96d2692a..a4716a65 100644 --- a/src/modules/container-loading/pages/ContainerDetailView.emptyReturn.behavior.test.ts +++ b/src/modules/container-loading/pages/ContainerDetailView.emptyReturn.behavior.test.ts @@ -42,6 +42,7 @@ describe('ContainerDetailView empty return', () => { vi.mocked(getContainerOutboundDispatch).mockReset().mockResolvedValue({ aggregate: { dispatchStatus: 'WAITING_FOR_LOADING', gate: { inbounds: [] } }, timeline: [] } as never) vi.mocked(getContainer).mockReset().mockResolvedValue(base as never) vi.mocked(getContainerOverview).mockReset().mockResolvedValue(overview as never) + vi.mocked(getConfirmedLoadingList).mockReset().mockResolvedValue({ planNo: 'LP-1', items: [] } as never) vi.mocked(recordTerminalDrayageEmptyReturn).mockReset().mockResolvedValue({ ...base.terminalDrayage, status: 'EMPTY_RETURNED', version: 4, actualEmptyReturnAt: '2026-09-09T12:00:00', allowedActions: [] } as never) }) @@ -100,6 +101,29 @@ describe('ContainerDetailView empty return', () => { expect(wrapper.find('.detail-section__heading').findAll('button').some((item) => item.text() === '确认还柜')).toBe(false) }) + it('falls back to confirmed loading facts for destination and terminal drayage for unloading time', async () => { + vi.mocked(getContainer).mockResolvedValue({ + ...base, + destinationWarehouseCode: null, + destinationWarehouseName: null, + warehouseId: '8', + terminalTruckingCompany: null, + actualFacts: {}, + } as never) + vi.mocked(getContainerOverview).mockResolvedValue({ ...overview, loadingPlanId: '7001' } as never) + vi.mocked(getConfirmedLoadingList).mockResolvedValue({ + planNo: 'LP-1', + items: [{ destinationWarehouse: 'ABE4' }, { destinationWarehouse: 'ABE4' }], + } as never) + + const wrapper = await mountDetail() + + expect(getConfirmedLoadingList).toHaveBeenCalledExactlyOnceWith('7001', '8') + expect(wrapper.text()).toContain('ABE4') + expect(wrapper.text()).toContain('港运车队') + expect(wrapper.text()).toContain('2026-09-07 10:00') + }) + it.each([ ['缺权限', false, ['RECORD_EMPTY_RETURN'], ['RECORD_EMPTY_RETURN'], 'UNLOADED'], ['缺服务端动作', true, [], [], 'UNLOADED'], diff --git a/src/modules/container-loading/pages/ContainerDetailView.vue b/src/modules/container-loading/pages/ContainerDetailView.vue index c4fe9dfb..d2593373 100644 --- a/src/modules/container-loading/pages/ContainerDetailView.vue +++ b/src/modules/container-loading/pages/ContainerDetailView.vue @@ -164,7 +164,14 @@ const loadingListHeading = computed(() => loadingListMode.value === 'confirmed' const loadingListDescription = computed(() => loadingListMode.value === 'confirmed' ? '装柜明细、PDA 证据与报关关联均以当前柜次为边界。' : '核对预装明细与 PDA 作业准备情况,装柜完成后再处理整柜报关。') -const overseasDestinationText = computed(() => [detail.value?.destinationWarehouseCode, detail.value?.destinationWarehouseName].filter(Boolean).join(' / ') || '—') +const overseasDestinationText = computed(() => { + const bookingDestination = [detail.value?.destinationWarehouseCode, detail.value?.destinationWarehouseName] + .filter(Boolean).join(' / ') + if (bookingDestination) return bookingDestination + const destinations = new Set((loadingDetail.value?.items || []) + .map((item) => String(item.destinationWarehouse || '').trim()).filter(Boolean)) + return [...destinations].join('、') || '—' +}) const plannedChannelName = computed(() => getSmartLoadingChannelGroup(detail.value?.channelGroupCode)?.name || '—') const emptyReturnWarningText = computed(() => { const warning = workbenchDetail.value?.terminalDrayage?.emptyReturnWarning @@ -284,6 +291,9 @@ async function load() { workbenchDetail.value = resource; detail.value = resource; overview.value = aggregate if (activeTab.value === 'loading-list' && !canViewCurrentLoadingList.value) activeTab.value = 'overview' if (isDomestic.value && activeTab.value === 'loading-list') await loadLoadingDetail(resource, aggregate) + if (isOverseas.value && resource.planStatus === 'COMPLETED' && canViewLoadingList.value) { + await loadLoadingDetail(resource, aggregate) + } } catch (cause) { error.value = getApiErrorDetails(cause, '货柜详情加载失败') } finally { loading.value = false } } @@ -515,7 +525,7 @@ onMounted(async () => { await load(); if (isOverseas.value && route.query.action

货柜信息

-
真实订舱
{{ detail.oceanBookingId ? (detail.billOfLadingNo || '已关联') : '未关联' }}关联已放舱订舱
计划渠道
{{ plannedChannelName }}
实际渠道
{{ detail.channelName || '待关联订舱' }}
异常状态
{{ detail.exceptionActive ? '异常' : '正常' }}
目的仓
{{ detail.destinationWarehouseName || detail.destinationWarehouseCode || '—' }}
拖车公司
{{ detail.terminalTruckingCompany || '—' }}
+
真实订舱
{{ detail.oceanBookingId ? (detail.billOfLadingNo || '已关联') : '未关联' }}关联已放舱订舱
计划渠道
{{ plannedChannelName }}
实际渠道
{{ detail.channelName || '待关联订舱' }}
异常状态
{{ detail.exceptionActive ? '异常' : '正常' }}
目的仓
{{ overseasDestinationText }}
拖车公司
{{ detail.terminalTruckingCompany || workbenchDetail?.terminalDrayage?.fleetSupplierName || '—' }}
@@ -555,7 +565,7 @@ onMounted(async () => { await load(); if (isOverseas.value && route.query.action

提柜、卸柜与还空

确认还柜
-
码头拖车状态
{{ workbenchDetail?.statusSummary?.terminalDrayageStatus || '—' }}
码头车队
{{ workbenchDetail?.terminalDrayage?.fleetSupplierName || '—' }}
预约提柜
{{ formatTime(workbenchDetail?.terminalDrayage?.pickupAppointmentAt) }}
实际提柜
{{ formatTime(workbenchDetail?.terminalDrayage?.actualPickupAt) }}
卸柜完成
{{ formatTime(workbenchDetail?.actualFacts?.unloadingCompletedAt as string | null) }}
实际还柜
{{ formatTime(workbenchDetail?.terminalDrayage?.actualEmptyReturnAt) }}
+
码头拖车状态
{{ workbenchDetail?.statusSummary?.terminalDrayageStatus || '—' }}
码头车队
{{ workbenchDetail?.terminalDrayage?.fleetSupplierName || '—' }}
预约提柜
{{ formatTime(workbenchDetail?.terminalDrayage?.pickupAppointmentAt) }}
实际提柜
{{ formatTime(workbenchDetail?.terminalDrayage?.actualPickupAt) }}
卸柜完成
{{ formatTime((workbenchDetail?.actualFacts?.unloadingCompletedAt || workbenchDetail?.terminalDrayage?.unloadingCompletedAt) as string | null) }}
实际还柜
{{ formatTime(workbenchDetail?.terminalDrayage?.actualEmptyReturnAt) }}
diff --git a/src/modules/overseas-warehouse/api/inboundPlans.ts b/src/modules/overseas-warehouse/api/inboundPlans.ts index 375ce2ef..e68fc824 100644 --- a/src/modules/overseas-warehouse/api/inboundPlans.ts +++ b/src/modules/overseas-warehouse/api/inboundPlans.ts @@ -47,6 +47,9 @@ export interface InboundPlanDetailDto { planNo: string customerId: string sourceOrderId: string + sourceOrderNo?: string | null + customerName?: string | null + orderType?: string | null containerNo?: string | null overseasWarehouseCode?: string | null overseasWarehouseName?: string | null @@ -56,6 +59,9 @@ export interface InboundPlanDetailDto { totalVolumeCbm: number expectedPackageCount?: number pickedPackageCount?: number + createdAt?: string | null + updatedAt?: string | null + remark?: string | null lines: Array<{ id: string; orderId: string; inboundCode?: string | null; fbaWarehouseCode?: string | null; destinationWarehouseId?: string | null; destinationWarehouseCode?: string | null; destinationWarehouseName?: string | null; volumeCbm?: number | null; requiredPackageCount: number; plannedPalletCount: number }> pallets: Array<{ id: string; palletNo?: string | null; destinationWarehouseId?: string | null; destinationWarehouseCode?: string | null; destinationWarehouseName?: string | null; capacityCbm?: number | null; allocatedVolumeCbm?: number | null; sequenceNo: number; warehousePalletId?: string | null; warehouseLocationId?: string | null; warehouseResourceLockId?: string | null; assignmentStatus?: string | null; lockedAt?: string | null; actualVolumeCbm?: number | null; actualPackageCount: number }> allocations: Array<{ lineId: string; palletId: string; allocatedVolumeCbm?: number | null; plannedPackageCount: number; countedPackageCount: number; pickedPackageCount: number; actualVolumeCbm?: number | null; status?: string | null; version: number }> diff --git a/src/modules/overseas-warehouse/pages/InboundOrderDetailView.vue b/src/modules/overseas-warehouse/pages/InboundOrderDetailView.vue index 5867e666..16e87390 100644 --- a/src/modules/overseas-warehouse/pages/InboundOrderDetailView.vue +++ b/src/modules/overseas-warehouse/pages/InboundOrderDetailView.vue @@ -7,11 +7,13 @@ import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.v import LoadErrorState from '@/shared/components/LoadErrorState.vue' import { getInboundPlan, type InboundPlanDetailDto } from '../api/inboundPlans' import OverseasDetailShell from '../components/OverseasDetailShell.vue' +import { formatOverseasDateTime, lastMileOrderTypeLabel } from '../lastMilePresentation' const route = useRoute(); const router = useRouter() const planNo = computed(() => String(route.params.orderId || '')) const customerId = computed(() => String(route.query.customerId || '')) const detail = ref(null); const loading = ref(false); const loadError = ref(null) +const referenceValue = computed(() => detail.value?.planNo || (/^\d+$/.test(planNo.value) ? '—' : planNo.value)) function text(value: unknown) { return String(value ?? '').trim() || '—' } function receivingWarehouse(code: unknown, name: unknown) { const normalizedCode = String(code ?? '').trim() @@ -21,11 +23,11 @@ function receivingWarehouse(code: unknown, name: unknown) { function status(value: unknown) { const labels: Record = { PENDING_ASSIGNMENT: '待入库', PDA_PENDING: '待入库', ASSIGNED: '待入库', PICKING: '拣货中', PARTIAL_INBOUND: '部分入库', COMPLETED: '全部入库' }; return labels[String(value || '')] || text(value) } const firstLine = computed(() => detail.value?.lines[0]) const fieldValues = computed>(() => ({ - 入库单号: text(detail.value?.planNo), 入仓码: text(firstLine.value?.inboundCode), 类型: '—', 货主: '—', 关联订单: '—', + 入库单号: text(detail.value?.planNo), 入仓码: text(firstLine.value?.inboundCode), 类型: lastMileOrderTypeLabel(detail.value?.orderType), 货主: text(detail.value?.customerName), 关联订单: text(detail.value?.sourceOrderNo), 海外仓: receivingWarehouse(detail.value?.overseasWarehouseCode, detail.value?.overseasWarehouseName), FBA仓库: text(firstLine.value?.fbaWarehouseCode), 总箱数: detail.value?.expectedPackageCount ?? detail.value?.lines.reduce((sum, line) => sum + line.requiredPackageCount, 0) ?? 0, 应拣货数: detail.value?.expectedPackageCount ?? detail.value?.lines.reduce((sum, line) => sum + line.requiredPackageCount, 0) ?? 0, 实际拣货数: detail.value?.pickedPackageCount ?? detail.value?.allocations.reduce((sum, item) => sum + item.pickedPackageCount, 0) ?? 0, '计费体积(m³)': Number(detail.value?.totalVolumeCbm || 0), 总托盘数: Number(detail.value?.palletCount || 0), 状态: status(detail.value?.status), - 预约状态: '—', 预约送仓时间: '—', 预计装车时间: '—', 排车状态: '—', 创建时间: '—', 更新时间: '—', 备注: '—', + 预约状态: '—', 预约送仓时间: '—', 预计装车时间: '—', 排车状态: '—', 创建时间: formatOverseasDateTime(detail.value?.createdAt), 更新时间: formatOverseasDateTime(detail.value?.updatedAt), 备注: text(detail.value?.remark), })) const groups = [ { title: '单据信息', fields: ['入库单号', '入仓码', '类型', '货主', '关联订单'] }, @@ -53,7 +55,7 @@ function back() { void router.push({ name: 'overseas-inbound-orders' }) } onMounted(load) -