From d990637b387291a6b692fcb1514355f5aa10cfe6 Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Wed, 28 Oct 2026 16:56:32 +0800
Subject: [PATCH] =?UTF-8?q?=E7=BB=9F=E4=B8=80=E5=B1=A5=E7=BA=A6=E7=8A=B6?=
=?UTF-8?q?=E6=80=81=E5=92=8C=E8=8A=82=E7=82=B9=E6=97=A5=E5=BF=97=E5=B1=95?=
=?UTF-8?q?=E7=A4=BA=E5=B9=B6=E6=95=B4=E7=90=86=E8=A3=85=E6=9F=9C=E4=B8=8E?=
=?UTF-8?q?=E5=B0=BE=E7=A8=8B=E7=95=8C=E9=9D=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../AppointmentCreatePanel.vue | 1 -
.../components/ContainerOverviewPanel.test.ts | 13 ++++--
.../components/ContainerOverviewPanel.vue | 4 +-
...nerDetailView.emptyReturn.behavior.test.ts | 23 ++++++++++
.../pages/ContainerDetailView.vue | 19 ++++----
.../components/PalletLabelPrintDialog.test.ts | 8 ++--
.../components/PalletLabelPrintDialog.vue | 5 +--
.../overseas-warehouse/PROTOTYPE_MATRIX.md | 2 +-
.../pages/DispatchesView.test.ts | 16 +++++++
.../pages/DispatchesView.vue | 2 +-
.../pages/LastMileOrdersView.behavior.test.ts | 40 ++++++++++++++++-
.../pages/LastMileOrdersView.vue | 12 +++--
...CarrierScheduleQueryPanel.behavior.test.ts | 34 ++++++++++++++
.../components/CarrierScheduleQueryPanel.vue | 9 +++-
src/modules/port-schedules/types.ts | 1 +
.../UnifiedOrdersView.fulfillment.test.ts | 40 +++++++++++++++--
.../pages/UnifiedOrdersView.vue | 8 ++--
src/modules/unified-orders/status.test.ts | 28 ++++++++++++
src/modules/unified-orders/status.ts | 44 +++++++++++++++++++
src/styles/adminDensity.test.ts | 4 +-
20 files changed, 270 insertions(+), 43 deletions(-)
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) }}
@@ -702,7 +701,7 @@ onMounted(async () => {
正在核对码头拖车当前状态…
- 时间沿用原业务当地时间。
+
| {{ terminal?.containerNo || detail?.containerNo || '—' }} |
| {{ terminal?.fleetSupplierName || (terminal?.fleetSupplierId ? '—' : '尚未安排') }} | {{ terminal?.actualPickupAt || '尚未登记' }} |
| visible && searchTerminalSuppliers()"> | |
| |
|
|
diff --git a/src/modules/loading-lists/components/PalletLabelPrintDialog.test.ts b/src/modules/loading-lists/components/PalletLabelPrintDialog.test.ts
index b525cdcc..2ef006e8 100644
--- a/src/modules/loading-lists/components/PalletLabelPrintDialog.test.ts
+++ b/src/modules/loading-lists/components/PalletLabelPrintDialog.test.ts
@@ -40,6 +40,9 @@ describe('PalletLabelPrintDialog', () => {
await flushPromises()
const dialog = wrapper.getComponent(AppDialog)
expect(dialog.props('title')).toBe(dense ? '散货托盘码 · HLCU1723867' : '散货托盘码')
+ expect(dialog.text()).not.toContain('编号规则')
+ expect(dialog.text()).not.toContain('一件代发托盘不在此清单')
+ expect(dialog.find('section[data-tone]').exists()).toBe(false)
wrapper.getComponent({ name: 'ElRadioGroup' }).vm.$emit('update:modelValue', 'WAREHOUSE')
await flushPromises()
wrapper.getComponent({ name: 'ElInputNumber' }).vm.$emit('update:modelValue', 1)
@@ -58,7 +61,7 @@ describe('PalletLabelPrintDialog', () => {
click.mockRestore()
})
- it('shows the server-generated internal code rule and both warehouse codes', async () => {
+ it('keeps server-generated label numbers and warehouse codes without the extra numbering explanation', async () => {
const wrapper = mount(PalletLabelPrintDialog, {
props: { planId: '10', warehouseId: '20' },
global: {
@@ -67,7 +70,6 @@ describe('PalletLabelPrintDialog', () => {
LoadErrorState: true,
Printer: true,
'el-button': { template: '' },
- 'el-alert': { props: ['title'], template: '{{ title }}
' },
'el-radio-group': { template: '
' },
'el-radio': { template: '' },
'el-select': { template: '
' },
@@ -83,7 +85,7 @@ describe('PalletLabelPrintDialog', () => {
await nextTick()
expect(preparePalletLabels).toHaveBeenCalledWith('10', '20')
- expect(wrapper.text()).toContain('HLCU1723867-FBA地址内部编号-三位序号')
+ expect(wrapper.text()).not.toContain('编号规则')
expect(wrapper.text()).toContain('ACY2')
expect(wrapper.text()).toContain('101')
expect(wrapper.text()).toContain('HLCU1723867-101-001')
diff --git a/src/modules/loading-lists/components/PalletLabelPrintDialog.vue b/src/modules/loading-lists/components/PalletLabelPrintDialog.vue
index a4d9bfc1..a1f02e95 100644
--- a/src/modules/loading-lists/components/PalletLabelPrintDialog.vue
+++ b/src/modules/loading-lists/components/PalletLabelPrintDialog.vue
@@ -56,12 +56,11 @@ async function print() {
-
-
+
全部打印({{ manifest.totalCount }} 张)按 FBA 仓补打
张(从该仓第 1 张起)
-
+
{{ row.destinationWarehouseName || '—' }}{{ row.labels[0]?.palletCode || '—' }} ~ {{ row.labels[row.labels.length - 1]?.palletCode }}
diff --git a/src/modules/overseas-warehouse/PROTOTYPE_MATRIX.md b/src/modules/overseas-warehouse/PROTOTYPE_MATRIX.md
index 224dc85c..ca3c8fb8 100644
--- a/src/modules/overseas-warehouse/PROTOTYPE_MATRIX.md
+++ b/src/modules/overseas-warehouse/PROTOTYPE_MATRIX.md
@@ -446,7 +446,7 @@
| 页面 | 操作 | 原型/确认依据 | 承载与实现 | 验收 |
|---|---|---|---|---|
-| 排车列表:装车中、装车后、派送中、配送完成 | 管理资料 | 用户管理资料截图及同车/派送单资料共用确认;2026-10-22 用户截图确认移除资料核对、油耗和装车照片改表格;签收完成即可,其他可补 | 1100px居中可全屏弹框,签收文件/油耗图片/装车资料三个页签;嵌入编辑器隐藏资料核对;三个页签仅显示各自资料,装车资料只保留整车装车照片,不再附带BOL及车辆/司机附件,保存资料按钮仅在签收页签显示,切换保留签收草稿;油耗两行、装车照片五行表格,操作列在首列,保留预览、上传/替换和原保存行为;装车中及装车后默认打开装车资料,可同步上传,确认装车且资料齐全后自动派送;按关联派送单切换,共用原派送资料编辑器;油耗沿本车附加照片保存 | 4文件27项定向测试及受影响组件与测试源码类型检查通过;页签隔离追加修改后5项定向测试及组件/测试类型检查通过;1920×1080、100%待人工截图确认 |
+| 排车列表:装车中、装车后、派送中 | 管理资料 | 用户管理资料截图及同车/派送单资料共用确认;2026-10-22 用户截图确认移除资料核对、油耗和装车照片改表格;签收完成即可,其他可补 | 1100px居中可全屏弹框,签收文件/油耗图片/装车资料三个页签;嵌入编辑器隐藏资料核对;三个页签仅显示各自资料,装车资料只保留整车装车照片,不再附带BOL及车辆/司机附件,保存资料按钮仅在签收页签显示,切换保留签收草稿;油耗两行、装车照片五行表格,操作列在首列,保留预览、上传/替换和原保存行为;装车中及装车后默认打开装车资料,可同步上传,确认装车且资料齐全后自动派送;按关联派送单切换,共用原派送资料编辑器;油耗沿本车附加照片保存 | 4文件27项定向测试及受影响组件与测试源码类型检查通过;页签隔离追加修改后5项定向测试及组件/测试类型检查通过;1920×1080、100%待人工截图确认 |
| 仓库派送排车列表 → 维护车辆 | 提交审核入口合并 | 2026-10-21 用户确认:隐藏调整卸货顺序,提交审核移入车辆维护弹框,不需要备注 | 沿用原居中弹框;底部保存/提交审核,提交先保存本次资料及三类照片再按新版本送审;去掉维护和提交备注输入,保留审核驳回原因;详情保持只读 | 定向自动检查单独报告;1920×1080、100%待人工截图确认 |
diff --git a/src/modules/overseas-warehouse/pages/DispatchesView.test.ts b/src/modules/overseas-warehouse/pages/DispatchesView.test.ts
index 1c8f79d0..71ecf4a1 100644
--- a/src/modules/overseas-warehouse/pages/DispatchesView.test.ts
+++ b/src/modules/overseas-warehouse/pages/DispatchesView.test.ts
@@ -448,6 +448,22 @@ it('clears an earlier input rejection after the corrected workflow succeeds', as
wrapper.unmount()
})
+it.each([
+ ['DELIVERING', 3, true],
+ ['DELIVERED', 4, false],
+] as const)('only opens list document management before delivery completes in %s', async (stage, tabIndex, available) => {
+ vi.mocked(listDispatches).mockResolvedValue(page([listRow({ taskType: 'DELIVERY', stage, actions: {} })]))
+ const wrapper = mount(DispatchesView, { global: { ...options.global, stubs: { ...options.global.stubs, DispatchDocumentsDialog: true } } }); await flushPromises()
+ await wrapper.get('[aria-label="排车执行阶段"]').findAll('button')[tabIndex]!.trigger('click'); await flushPromises()
+ expect(listDispatches).toHaveBeenLastCalledWith(expect.objectContaining({ stage }))
+ const row = wrapper.findAllComponents(RowActions).find(entry => entry.props('moreLabel') === '更多操作 · PC/1')!
+ expect((row.props('actions') as Array<{ key: string }>).some(item => item.key === 'documents')).toBe(available)
+ expect(row.findAll('button').some(button => button.text() === '管理资料')).toBe(available)
+ row.vm.$emit('action', 'documents'); await flushPromises()
+ expect(wrapper.getComponent({ name: 'DispatchDocumentsDialog' }).props()).toMatchObject({ modelValue: available, dispatchNo: available ? 'PC/1' : '' })
+ wrapper.unmount()
+})
+
it('opens the exception form from the dispatching row and hides it without permission', async () => {
const gate = { allowed: true, blockCode: null, blockReason: null }
const current = reviews()
diff --git a/src/modules/overseas-warehouse/pages/DispatchesView.vue b/src/modules/overseas-warehouse/pages/DispatchesView.vue
index 99cc1993..60b3a5f4 100644
--- a/src/modules/overseas-warehouse/pages/DispatchesView.vue
+++ b/src/modules/overseas-warehouse/pages/DispatchesView.vue
@@ -122,7 +122,7 @@ function switchStage(value: DispatchStage) {
function switchSection(value: BeforeLoadingSection) { section.value = value; page.value = 1; void load() }
function openDetail(row: DispatchListRow) { void router.push({ name: 'overseas-dispatch-detail', params: { dispatchNo: row.dispatchNo } }) }
const rowActionOrder: DispatchAction[] = ['EDIT', 'SUBMIT_REVIEW', 'DISPATCH_REVIEW', 'LOADING_REVIEW', 'RETURN_TO_REVIEW', 'LOADING_COMPLETE', 'MARK_EXCEPTION', 'CANCEL']
-function canManageDocuments(row: DispatchListRow) { return (row.taskType || 'DELIVERY') === 'DELIVERY' && row.status === 'ACTIVE' && ['LOADING', 'AFTER_LOADING', 'DELIVERING', 'DELIVERED'].includes(row.stage) }
+function canManageDocuments(row: DispatchListRow) { return (row.taskType || 'DELIVERY') === 'DELIVERY' && row.status === 'ACTIVE' && ['LOADING', 'AFTER_LOADING', 'DELIVERING'].includes(row.stage) }
// 仅当前调度审核通过的仓库派送排车可下载,退回或审核失效后重新收起入口。
function canDownloadBol(row: DispatchListRow) { return (row.taskType || 'DELIVERY') === 'DELIVERY' && row.reviews?.dispatchReview?.status === 'APPROVED' }
function rowActions(row: DispatchListRow): RowActionItem[] {
diff --git a/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts b/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts
index d184781e..be4a1b51 100644
--- a/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts
+++ b/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts
@@ -10,6 +10,8 @@ import { ApiResponseError } from '@/shared/api/http'
import DataTable from '@/shared/components/DataTable.vue'
import AppDialog from '@/shared/components/AppDialog.vue'
import QueryToolbar from '@/shared/components/QueryToolbar.vue'
+import LayoutPreferenceList from '@/shared/components/LayoutPreferenceList.vue'
+import { preferenceStorageKey } from '@/shared/utils/uiLayoutPreference'
import { listAllPlatformWarehouses } from '@/modules/warehouses'
import { listContainerArrivalTimes } from '@/modules/container-loading'
import {
@@ -322,10 +324,46 @@ describe('LastMileOrdersView pending order and container conversion', () => {
expect(toolbar.props('availableFields')?.some((field) => field.key === 'orderType')).toBe(true)
})
+ it('uses the saved first three filters when collapsed and keeps reordered fields after remounting', async () => {
+ localStorage.setItem(preferenceStorageKey('query', 'anonymous', 'overseas-last-mile-orders-query'), JSON.stringify({
+ version: 1, order: ['orderNo', 'containerNo', 'inboundCode'], hidden: [],
+ }))
+ const wrapper = await mountView()
+ const toolbar = wrapper.findComponent(QueryToolbar)
+ const placeholders = () => toolbar.findAll('.query-toolbar__field').map(field => field.get('input').attributes('placeholder'))
+ expect(placeholders()).toEqual(['请输入尾程单号', '请输入集装箱号', '请输入入仓码'])
+ const initialCalls = vi.mocked(listLastMileOrders).mock.calls.length
+ toolbar.vm.$emit('update:modelValue', { ...toolbar.props('modelValue'), containerNo: 'CONT-15' })
+ await wrapper.vm.$nextTick()
+ await button(wrapper, '高级筛选')!.trigger('click')
+ expect(placeholders()).toHaveLength(16)
+ expect(placeholders().slice(0, 3)).toEqual(['请输入尾程单号', '请输入集装箱号', '请输入入仓码'])
+ await toolbar.get('button[aria-label="调整筛选布局"]').trigger('click')
+ await flushPromises()
+ toolbar.findComponent(LayoutPreferenceList).vm.$emit('move', 'externalBookingNo', 0)
+ await wrapper.vm.$nextTick()
+ await button(wrapper, '收起筛选')!.trigger('click')
+ expect(placeholders()).toEqual(['请输入外接预订号', '请输入尾程单号', '请输入集装箱号'])
+ expect(toolbar.props('modelValue').containerNo).toBe('CONT-15')
+ expect(vi.mocked(listLastMileOrders)).toHaveBeenCalledTimes(initialCalls)
+ wrapper.unmount()
+
+ const restored = await mountView()
+ const restoredToolbar = restored.findComponent(QueryToolbar)
+ const restoredPlaceholders = () => restoredToolbar.findAll('.query-toolbar__field').map(field => field.get('input').attributes('placeholder'))
+ expect(restoredPlaceholders()).toEqual(['请输入外接预订号', '请输入尾程单号', '请输入集装箱号'])
+ await restoredToolbar.get('button[aria-label="调整筛选布局"]').trigger('click')
+ await flushPromises()
+ restoredToolbar.findComponent(LayoutPreferenceList).vm.$emit('reset')
+ await restored.vm.$nextTick()
+ expect(restoredPlaceholders()).toEqual(['请输入尾程单号', '请输入入仓码', '请输入仓库名称或编码'])
+ })
+
it('preserves advanced conditions when collapsed and clears them on reset', async () => {
const wrapper = await mountView()
const toolbar = wrapper.findComponent(QueryToolbar)
- expect(toolbar.props('fields').map((field) => field.key)).toEqual(['orderNo', 'inboundCode', 'warehouse'])
+ expect(toolbar.findAll('.query-toolbar__field input').map(input => input.attributes('placeholder')))
+ .toEqual(['请输入尾程单号', '请输入入仓码', '请输入仓库名称或编码'])
const initialCalls = vi.mocked(listLastMileOrders).mock.calls.length
await button(wrapper, '高级筛选')!.trigger('click')
expect(toolbar.props('fields')).toHaveLength(16)
diff --git a/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue b/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue
index 180f2442..ae8bce91 100644
--- a/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue
+++ b/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue
@@ -195,6 +195,9 @@ const statusTabs = ['全部', '开船前', '开船后'] as const
const showAdvancedFilters = ref(false)
const filters = ref({ businessType: '', businessOrderNo: '', externalBookingNo: '', orderNo: '', relabelNo: '', orderType: '', warehouse: '', inboundCode: '', lxCode: '', sourceOrderNo: '', containerNo: '', status: '', transferWarehouseCode: '', abnormal: '', detained: '', createdAt: [] })
const allFields: QueryField[] = [
+ { key: 'orderNo', type: 'input', label: '尾程单号', width: 190, placeholder: '请输入尾程单号' },
+ { key: 'inboundCode', type: 'input', label: '入仓码', width: 180, placeholder: '请输入入仓码' },
+ { key: 'warehouse', type: 'input', label: '海外仓', width: 210, placeholder: '请输入仓库名称或编码' },
{ key: 'businessType', type: 'select', label: '业务来源', width: 180, placeholder: '请选择业务来源', options: [
{ label: '本土散板(全部来源)', value: 'LOCAL_LOOSEBOARD' }, { label: '本土卡车', value: 'US_TRUCK' },
{ label: '仓储转运', value: 'WAREHOUSE_TRANSFER' }, { label: '头程订单', value: 'FIRST_LEG' },
@@ -202,9 +205,7 @@ const allFields: QueryField[] = [
] },
{ key: 'businessOrderNo', type: 'input', label: '关联商业单号', width: 210, placeholder: '请输入商业单号' },
{ key: 'externalBookingNo', type: 'input', label: '外接预订号', width: 210, placeholder: '请输入外接预订号' },
- { key: 'orderNo', type: 'input', label: '尾程单号', width: 190, placeholder: '请输入尾程单号' },
{ key: 'relabelNo', type: 'input', label: '换标单号', width: 190, placeholder: '请输入换标单号' },
- { key: 'inboundCode', type: 'input', label: '入仓码', width: 180, placeholder: '请输入入仓码' },
{ key: 'lxCode', type: 'input', label: 'LX代码', width: 180, placeholder: '请输入LX代码' },
{ key: 'orderType', type: 'select', label: '履约类型', width: 170, placeholder: '请选择履约类型', options: [
{ label: '头程订单', value: 'FIRST_LEG' }, { label: '换标下单', value: 'RELABEL_APPLICATION' },
@@ -212,7 +213,6 @@ const allFields: QueryField[] = [
{ label: '退货换标', value: 'RETURN_RELABEL' }, { label: '一件代发', value: 'ONE_CLICK_DROPSHIP' },
{ label: '货物废弃订单', value: 'CARGO_DISCARD' },
] },
- { key: 'warehouse', type: 'input', label: '海外仓', width: 210, placeholder: '请输入仓库名称或编码' },
{ key: 'sourceOrderNo', type: 'input', label: '来源订单号', width: 190, placeholder: '请输入来源订单号' },
{ key: 'containerNo', type: 'input', label: '集装箱号', width: 180, placeholder: '请输入集装箱号' },
{ key: 'transferWarehouseCode', type: 'input', label: '转仓代码', width: 170, placeholder: '请输入转仓代码' },
@@ -234,9 +234,7 @@ const allFields: QueryField[] = [
const availableFields = computed(() => allFields.filter((field) => statusTab.value === '全部' || field.key !== 'orderType'))
const fields = computed(() => statusTab.value === '柜子'
? allFields.filter((field) => ['containerNo', 'createdAt'].includes(field.key))
- : showAdvancedFilters.value
- ? availableFields.value
- : allFields.filter((field) => ['orderNo', 'inboundCode', 'warehouse'].includes(field.key)))
+ : availableFields.value)
const selectedContainer = computed(() => containerOptions.value.find((row) => row.sourceContainerBookingId === selectedContainerBookingId.value))
const selectedReceivingWarehouse = computed(() => receivingWarehouseOptions.value.find((row) => (
row.warehouseId === receivingWarehouseForm.warehouseId
@@ -1164,7 +1162,7 @@ onMounted(() => { void load() })