diff --git a/src/modules/overseas-master-data/components/LooseboardCargoDialog.test.ts b/src/modules/overseas-master-data/components/LooseboardCargoDialog.test.ts new file mode 100644 index 00000000..29ff9283 --- /dev/null +++ b/src/modules/overseas-master-data/components/LooseboardCargoDialog.test.ts @@ -0,0 +1,32 @@ +import { shallowMount, flushPromises } from '@vue/test-utils' +import { beforeEach, expect, it, vi } from 'vitest' +import Dialog from './LooseboardCargoDialog.vue' +import { candidate, detail } from '../../overseas-warehouse/dispatchTestFixtures' +import * as api from '@/modules/overseas-warehouse' + +vi.mock('@/modules/overseas-warehouse', async () => ({ + getDispatch: vi.fn(), listLooseboardCargo: vi.fn(), addLooseboardCargo: vi.fn(), + expandSelectionGroups: (await import('../../overseas-warehouse/dispatchContracts')).expandSelectionGroups, +})) +vi.mock('element-plus', () => ({ ElMessage: { success: vi.fn() } })) +beforeEach(() => { vi.clearAllMocks() }) + +it('selects one looseboard order and submits all original pallet selections together', async () => { + const first = candidate({ orderNo: 'LB1', availablePackages: 9 }) + const second = candidate({ sourceKey: 'source-2', sourceVersion: 'version-2', selection: { ...first.selection, planPalletId: '2', sourceVersion: 'version-2' } }) + first.selectionGroup = { ...first.selectionGroup, sources: [...first.selectionGroup.sources, ...second.selectionGroup.sources], pallets: [...first.selectionGroup.pallets, { warehousePalletId: null, palletNo: 'P2', sourceKeys: [second.sourceKey], packageCount: 2 }] } + const dispatch = detail(); dispatch.actions.ADD_CARGO = { allowed: true, blockCode: null, blockReason: null } + vi.mocked(api.getDispatch).mockResolvedValue(dispatch) + vi.mocked(api.listLooseboardCargo).mockResolvedValue({ candidates: [first], total: 1, page: 1, size: 20, pages: 1, complete: true, snapshotAt: '2026-11-05T00:00:00Z' }) + const wrapper = shallowMount(Dialog, { props: { modelValue: false, dispatchNo: 'PC/1' }, global: { renderStubDefaultSlot: true, stubs: { ElAlert: true } } }) + await wrapper.setProps({ modelValue: true }); await flushPromises() + const table = wrapper.getComponent({ name: 'DataTable' }) + expect(table.props('data')).toHaveLength(1) + expect(table.props('columns').map((column: { label?: string }) => column.label)).toContain('托数') + expect(table.props('columns').map((column: { label?: string }) => column.label)).not.toContain('计划 / 实物托盘') + table.vm.$emit('selection-change', [first]); await flushPromises() + wrapper.getComponent({ name: 'AppDialog' }).vm.$emit('confirm'); await flushPromises() + expect(api.addLooseboardCargo).toHaveBeenCalledWith('PC/1', expect.objectContaining({ cargo: [first.selection, second.selection] })) + expect(wrapper.emitted('update:modelValue')).toEqual([[false]]) + wrapper.unmount() +}) diff --git a/src/modules/overseas-master-data/components/LooseboardCargoDialog.vue b/src/modules/overseas-master-data/components/LooseboardCargoDialog.vue index 7d8dfca9..29fa23a4 100644 --- a/src/modules/overseas-master-data/components/LooseboardCargoDialog.vue +++ b/src/modules/overseas-master-data/components/LooseboardCargoDialog.vue @@ -15,7 +15,7 @@ const page=ref(1), size=ref(20), total=ref(0), loading=ref(false), saving=ref(fa const columns:DataTableColumn[]=[ {key:'selection',type:'selection',width:45}, {key:'orderNo',label:'订单号',minWidth:180}, {key:'customerName',label:'客户',minWidth:140}, {key:'inboundCode',label:'入仓号',minWidth:150}, - {key:'sourceStage',label:'货物阶段',width:110,formatter:(row:Candidate)=>!row.selection.warehousePalletId ? '申报待到仓' : '已实收'}, {key:'palletNo',label:'计划 / 实物托盘',minWidth:150}, {key:'destination',label:'目的地',minWidth:180,formatter:(row:Candidate)=>row.destination.warehouseCode || row.destination.address || '—'}, + {key:'sourceStage',label:'货物阶段',width:110,formatter:(row:Candidate)=>row.selectionGroup?.sources.every(source=>source.selection.warehousePalletId) ? '已实收' : '申报待到仓'}, {key:'palletCount',label:'托数',width:80,formatter:(row:Candidate)=>row.selectionGroup?.pallets.length ?? '—'}, {key:'destination',label:'目的地',minWidth:180,formatter:(row:Candidate)=>row.destination.warehouseCode || row.destination.address || '—'}, {key:'availablePackages',label:'计划 / 可用箱数',width:100}, {key:'blockReasons',label:'资格',minWidth:200,formatter:(row:Candidate)=>row.blockReasons.length?row.blockReasons.join(';'):'可添加'}, ] diff --git a/src/modules/overseas-warehouse/components/DispatchDocumentsDialog.test.ts b/src/modules/overseas-warehouse/components/DispatchDocumentsDialog.test.ts index 21c79eca..5ce189ee 100644 --- a/src/modules/overseas-warehouse/components/DispatchDocumentsDialog.test.ts +++ b/src/modules/overseas-warehouse/components/DispatchDocumentsDialog.test.ts @@ -54,7 +54,7 @@ it('hides the single delivery selector and saves receipt files to that delivery await wrapper.get('.save').trigger('click'); await flushPromises() expect(saveDeliveryDocuments).toHaveBeenCalledWith('DL-1', '101', expect.objectContaining({ bolNo: 'BOL-ORIGINAL', documents: [{ category: 'LOADING_PHOTO', fileIds: [] }, { category: 'BOL', fileIds: [] }, { category: 'RECEIPT', fileIds: ['99'] }] })) expect(wrapper.emitted('saved')).toHaveLength(1) - expect(wrapper.getComponent(DeliveryDocumentsDialog).props('modelValue')).toBe(true) + expect(wrapper.emitted('update:modelValue')).toEqual([[false]]) wrapper.unmount() }) it('switches customer scope exactly and keeps unsaved delivery edits when switching tabs', async () => { @@ -91,6 +91,18 @@ it('switches customer scope exactly and keeps unsaved delivery edits when switch expect(saveDeliveryDocuments).not.toHaveBeenCalled() confirmation.mockRestore(); wrapper.unmount() }) +it('keeps the management dialog and draft open when saving fails', async () => { + vi.mocked(saveDeliveryDocuments).mockRejectedValue(new Error('保存失败')) + const wrapper = setup(); await flushPromises() + const receipt = wrapper.getComponent(DeliveryDocumentsDialog) + await receipt.findAll('button').find(button => button.text() === '删除')!.trigger('click') + await wrapper.get('.save').trigger('click'); await flushPromises() + expect(wrapper.emitted('update:modelValue')).toBeUndefined() + expect(wrapper.emitted('saved')).toBeUndefined() + expect(receipt.text()).not.toContain('existing-pod.pdf') + expect(wrapper.text()).toContain('保存失败') + wrapper.unmount() +}) it('does not request delivery documents without delivery list permission', async () => { can.mockImplementation((value?: string) => value !== 'platform:overseas-warehouse:delivery:list') const wrapper = setup(); await flushPromises() diff --git a/src/modules/overseas-warehouse/components/DispatchDocumentsDialog.vue b/src/modules/overseas-warehouse/components/DispatchDocumentsDialog.vue index 5ffaf805..061250ff 100644 --- a/src/modules/overseas-warehouse/components/DispatchDocumentsDialog.vue +++ b/src/modules/overseas-warehouse/components/DispatchDocumentsDialog.vue @@ -55,7 +55,7 @@ async function canLeave() { async function selectDelivery(value: string) { if (value !== selected.value && await canLeave()) selected.value = value } async function beforeClose(done: () => void) { if (await canLeave()) done() } function selectTab(value: string) { if (!busy.value) { tab.value = value } } -function saved() { emit('saved') } +function saved() { open.value = false; emit('saved') } function pickupCompleted() { open.value = false; emit('saved') } async function loadingSaved() { emit('saved'); await load() } watch(() => [open.value, props.dispatchNo] as const, ([visible]) => { diff --git a/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.test.ts b/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.test.ts index fddf4b26..004395b2 100644 --- a/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.test.ts +++ b/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.test.ts @@ -58,6 +58,19 @@ describe('DeliveryOrderDetailView', () => { expect(wrapper.findComponent({ name: 'BusinessDocumentFees' }).exists()).toBe(false) wrapper.unmount() }) + it('renders document and receipt operation nodes in Chinese', async () => { + const value = await vi.mocked(getDeliveryOrder)('DEL/1', '100') + value.operations = ['FIRSTLEG_COMPLETION_PENDING', 'SAVE_DOCUMENTS', 'RECEIPT_AUTO_COMPLETE'].map((operationNode, index) => ({ id: String(index), operationNode, operatedAt: '2026-11-05T16:59:55', result: '已保存' })) + vi.mocked(getDeliveryOrder).mockResolvedValue(value) + const wrapper = mount(DeliveryOrderDetailView, { global: { plugins: [ElementPlus] } }) + await flushPromises() + await wrapper.find('.el-tabs__item:nth-child(4)').trigger('click') + expect(wrapper.text()).toContain('来源货物签收待齐') + expect(wrapper.text()).toContain('保存派送资料') + expect(wrapper.text()).toContain('签收文件自动完成派送') + for (const operation of value.operations) expect(wrapper.text()).not.toContain(operation.operationNode) + wrapper.unmount() + }) it('shows frozen original coverage separately from new fees', async () => { const value = await vi.mocked(getDeliveryOrder)('DEL/1', '100') value.feeCoverage = { cargoHash: 'hash', cargo: [], confirmedAt: '2026-10-03T00:00:00Z', references: [{ diff --git a/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.vue b/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.vue index fed130c1..be21febc 100644 --- a/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.vue +++ b/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.vue @@ -29,7 +29,7 @@ const methodLabels: Record = { PICKUP: '普通自提', AMAZON_PI const pickupLabels: Record = { ONE_PICKUP_ONE_DROP: '一提一卸', ONE_PICKUP_MULTI_DROP: '一提多卸', MULTI_PICKUP_ONE_DROP: '多提一卸', MULTI_PICKUP_MULTI_DROP: '多提多卸', WAREHOUSE_PICKUP: '仓库自提', DOOR_PICKUP: '上门提货', SELF_PICKUP: '自提' } const loadingLabels: Record = { PALLET: '托盘', FLOOR: '地板', MIXED: '混装', CARTON: '散箱' } const feeLabels: Record = { DELIVERY_FEE: '派送费用', PRIVATE_LAST_MILE_DELIVERY: '私人仓尾程派送费', EXPRESS_FEE: '快递费用', LOADING_FEE: '装车费', APPOINTMENT_FEE: '买约费用', OTHER: '其他费用' } -const operationLabels: Record = { CREATE_DELIVERY: '创建派送单', ASSIGN_VEHICLE: '车队分配', DISPATCH: '开始派送', SIGN: '签收', COMPLETE_DELIVERY: '完成派送', UPDATE_DELIVERY_STATUS: '更新派送状态', CUSTOMER_VISIT: '客户回访', MARK_RETURN: '标记退货', CANCEL_OUTBOUND: '取消出库', CANCEL_OUTBOUND_REASON: '取消原因', CONFIRM_EXTERNAL_FREIGHT: '确认外接运费' } +const operationLabels: Record = { CREATE_DELIVERY: '创建派送单', ASSIGN_VEHICLE: '车队分配', DISPATCH: '开始派送', SIGN: '签收', COMPLETE_DELIVERY: '完成派送', UPDATE_DELIVERY_STATUS: '更新派送状态', CUSTOMER_VISIT: '客户回访', MARK_RETURN: '标记退货', CANCEL_OUTBOUND: '取消出库', CANCEL_OUTBOUND_REASON: '取消原因', CONFIRM_EXTERNAL_FREIGHT: '确认外接运费', SAVE_DOCUMENTS: '保存派送资料', RECEIPT_AUTO_COMPLETE: '签收文件自动完成派送', FIRSTLEG_COMPLETION_PENDING: '来源货物签收待齐' } const resultLabels: Record = { CREATED: '已创建' } function show(value: unknown) { const text = String(value ?? '').trim(); return text || '—' }