From 9eec68f7c5f7ea9de889758d62ae115327a2840f Mon Sep 17 00:00:00 2001 From: wujinliang <2318309077@qq.com> Date: Wed, 26 Aug 2026 18:13:12 +0800 Subject: [PATCH 01/57] 1 --- .../pages/FinanceCustomerAccountsView.test.ts | 1 + .../pages/FinanceCustomerAccountsView.vue | 2 +- .../pages/FinanceCustomerArapView.test.ts | 14 +++++++++++ .../finance/pages/FinanceCustomerArapView.vue | 5 ++-- .../components/InboundTaskList.test.ts | 9 +++++++ .../inbounds/components/InboundTaskList.vue | 9 ++++--- .../warehouses/pages/WarehousesView.vue | 24 ++++++++++++------- 7 files changed, 49 insertions(+), 15 deletions(-) diff --git a/src/modules/finance/pages/FinanceCustomerAccountsView.test.ts b/src/modules/finance/pages/FinanceCustomerAccountsView.test.ts index 81c4b6fd..a0350dd8 100644 --- a/src/modules/finance/pages/FinanceCustomerAccountsView.test.ts +++ b/src/modules/finance/pages/FinanceCustomerAccountsView.test.ts @@ -57,6 +57,7 @@ describe('FinanceCustomerAccountsView delivery', () => { expect(page).toContain('待收金额') expect(page).toContain('待开账金额') expect(page).toContain('class="ledger-query"') + expect(page).toContain('@search="ledgerPage = 1; void loadLedger($event)"') expect(page).toContain('class="ledger-table"') expect(page).toContain(' diff --git a/src/modules/overseas-warehouse/pages/PalletsView.test.ts b/src/modules/overseas-warehouse/pages/PalletsView.test.ts index 1968a7ca..ea0a8631 100644 --- a/src/modules/overseas-warehouse/pages/PalletsView.test.ts +++ b/src/modules/overseas-warehouse/pages/PalletsView.test.ts @@ -57,12 +57,8 @@ describe('PalletsView', () => { await wrapper.findAll('button').find((button) => button.text().includes('新增托盘'))!.trigger('click') await flushPromises() - const warehouse = wrapper.findAllComponents({ name: 'ElSelect' }).find((item) => item.props('placeholder') === '请选择海外仓')! - const fbaWarehouse = wrapper.findAllComponents({ name: 'ElSelect' }).find((item) => item.props('placeholder') === '请选择所属 FBA 仓')! - const palletCode = wrapper.findAllComponents({ name: 'ElInput' }).find((item) => item.props('placeholder') === '请输入托盘编码')! - warehouse.vm.$emit('update:modelValue', '200') - fbaWarehouse.vm.$emit('update:modelValue', 'ABE3') - palletCode.vm.$emit('update:modelValue', 'PLT-LAX-ABE3') + const setupState = wrapper.vm.$.setupState as { form: { warehouseId: string; fbaWarehouseCode: string; code: string } } + Object.assign(setupState.form, { warehouseId: '200', fbaWarehouseCode: 'ABE3', code: 'PLT-LAX-ABE3' }) await flushPromises() wrapper.findComponent(AppDialog).vm.$emit('confirm') await flushPromises() diff --git a/src/modules/overseas-warehouse/pages/RelabelOrderDetailView.vue b/src/modules/overseas-warehouse/pages/RelabelOrderDetailView.vue index b9e05f93..da38e0f1 100644 --- a/src/modules/overseas-warehouse/pages/RelabelOrderDetailView.vue +++ b/src/modules/overseas-warehouse/pages/RelabelOrderDetailView.vue @@ -25,7 +25,7 @@ function status(value: unknown) { const labels: Record = { PENDING_PROCESSING: '待处理', PROCESSING: '处理中', COMPLETED: '已完成', REJECTED: '已驳回', CANCELLED: '已取消' } return labels[String(value || '')] || text(value) } -const referenceValue = computed(() => text(detail.value?.application.relabelNo || orderId.value)) +const referenceValue = computed(() => text(detail.value?.application.relabelNo || '换标单号待补充')) const fieldValues = computed>(() => { const value = detail.value const items = value?.items || [] diff --git a/src/modules/overseas-warehouse/pages/prototypeContract.test.ts b/src/modules/overseas-warehouse/pages/prototypeContract.test.ts index 684e896c..904b6d87 100644 --- a/src/modules/overseas-warehouse/pages/prototypeContract.test.ts +++ b/src/modules/overseas-warehouse/pages/prototypeContract.test.ts @@ -354,8 +354,8 @@ describe('overseas warehouse first-batch prototype contract', () => { 'actions', 'sku', 'productName', 'englishName', 'category', 'length', 'width', 'height', 'weight', ]) expect(columnKeys('InventoryTransactionsView.vue')).toEqual([ - 'occurredAt', 'owner', 'warehouse', 'sku', 'productName', 'businessType', 'businessOrderNo', - 'inboundOrderNo', 'outboundOrderNo', 'inboundQuantity', 'outboundQuantity', 'balance', 'operator', 'remark', + 'actions', 'occurredAt', 'owner', 'warehouse', 'sku', 'productName', 'businessType', 'businessOrderNo', + 'inboundCode', 'inboundOrderNo', 'outboundOrderNo', 'inboundQuantity', 'outboundQuantity', 'balance', 'operator', 'remark', ]) }) }) diff --git a/src/modules/overseas-warehouse/pages/realApiMount.test.ts b/src/modules/overseas-warehouse/pages/realApiMount.test.ts index e194e034..f1cb0aa4 100644 --- a/src/modules/overseas-warehouse/pages/realApiMount.test.ts +++ b/src/modules/overseas-warehouse/pages/realApiMount.test.ts @@ -25,10 +25,10 @@ describe('overseas warehouse mounted real API pages', () => { vi.stubGlobal('ResizeObserver', ResizeObserverStub); vi.stubGlobal('localStorage', memoryStorage()); routing.push.mockReset(); routing.route.params = {}; routing.route.query = {} vi.mocked(listPlatformTenants).mockReset().mockResolvedValue({ records: [{ id: '10001', tenantName: '客户甲', status: 'ACTIVE' } as never], page: 1, size: 200, total: 1, pages: 1 }) vi.mocked(getInboundPlan).mockReset().mockResolvedValue({ id: '9007199254740993', planNo: 'IP-1', customerId: '10001', sourceOrderId: '9007199254740995', status: 'PENDING_ASSIGNMENT', lineCount: 1, palletCount: 1, totalVolumeCbm: 2, lines: [{ id: '1', orderId: '9007199254740995', inboundCode: 'IN-1', fbaWarehouseCode: 'ONT8', destinationWarehouseName: '洛杉矶仓', volumeCbm: 2, requiredPackageCount: 10, plannedPalletCount: 1 }], pallets: [], allocations: [] }) - vi.mocked(listInventoryLedger).mockReset().mockResolvedValue({ records: [{ id: '9007199254740997', customerId: '10001', destinationWarehouseCode: 'LAX', inboundOrderNo: 'IP-1', businessType: 'STOCK_IN', beforePackages: 0, deltaPackages: 10, afterPackages: 10, occurredAt: '2026-08-20T10:00:00' }], page: 1, size: 20, total: 1, pages: 1 }) + vi.mocked(listInventoryLedger).mockReset().mockResolvedValue({ records: [{ id: '9007199254740997', customerId: '10001', customerName: '客户甲', destinationWarehouseCode: 'LAX', inboundCode: 'NYEXX2608300001', inboundOrderNo: 'IP-1', businessType: 'STOCK_IN', beforePackages: 0, deltaPackages: 10, afterPackages: 10, occurredAt: '2026-08-20T10:00:00' }], page: 1, size: 20, total: 1, pages: 1 }) vi.mocked(getLastMileOrder).mockReset().mockResolvedValue({ order: { id: '9007199254740999', customerId: '10001', customerName: '华远贸易', orderNo: 'LMO-260827-0001', currentStatus: 'PENDING_ASSIGNMENT' }, cargo: [], statusHistory: [], fees: [], operationHistory: [] }) }) it('mounts inbound detail with a string-safe source order id', async () => { routing.route.params = { orderId: 'IP-1' }; routing.route.query = { customerId: '10001' }; const wrapper = mount(InboundOrderDetailView, { global: { plugins: [ElementPlus] } }); await flushPromises(); expect(getInboundPlan).toHaveBeenCalledWith('IP-1', '10001'); expect(wrapper.text()).toContain('9007199254740995'); expect(wrapper.text()).toContain('洛杉矶仓') }) - it('mounts inventory ledger as read-only facts', async () => { const wrapper = mount(InventoryTransactionsView, { global: { plugins: [ElementPlus] } }); await flushPromises(); expect(listInventoryLedger).toHaveBeenCalledWith(expect.objectContaining({ page: 1, size: 10 })); expect(listInventoryLedger).not.toHaveBeenCalledWith(expect.objectContaining({ customerId: expect.anything() })); expect(wrapper.text()).toContain('IP-1'); expect(wrapper.text()).toContain('10'); expect(wrapper.text()).toContain('不提供出库单跳转') }) + it('mounts inventory ledger as read-only business facts without exposing customer ids', async () => { const wrapper = mount(InventoryTransactionsView, { global: { plugins: [createPinia(), ElementPlus] } }); await flushPromises(); expect(listInventoryLedger).toHaveBeenCalledWith(expect.objectContaining({ page: 1, size: 10 })); expect(listInventoryLedger).not.toHaveBeenCalledWith(expect.objectContaining({ customerId: expect.anything() })); expect(wrapper.text()).toContain('客户甲'); expect(wrapper.text()).toContain('NYEXX2608300001'); expect(wrapper.text()).not.toContain('10001'); expect(wrapper.text()).toContain('IP-1'); expect(wrapper.text()).toContain('10'); expect(wrapper.text()).toContain('不提供出库单跳转') }) it('renders the backend owner and complete short order number in last-mile detail', async () => { routing.route.params = { orderId: '9007199254740999' }; const wrapper = mount(LastMileOrderDetailView, { global: { plugins: [createPinia(), ElementPlus] } }); await flushPromises(); expect(getLastMileOrder).toHaveBeenCalledWith('9007199254740999'); expect(wrapper.text()).toContain('华远贸易'); expect(wrapper.text()).toContain('LMO-260827-0001'); expect(wrapper.get('.order-reference-text').attributes('title')).toBe('LMO-260827-0001') }) }) diff --git a/src/modules/overseas-warehouse/types.ts b/src/modules/overseas-warehouse/types.ts index 3f69f128..919b2935 100644 --- a/src/modules/overseas-warehouse/types.ts +++ b/src/modules/overseas-warehouse/types.ts @@ -176,6 +176,7 @@ export interface InventoryTransactionListItem { productName: string businessType: string businessOrderNo: string + inboundCode: string inboundOrderNo: string /** Read-only inventory fact; it is not an outbound-workflow link. */ outboundOrderNo: string diff --git a/src/modules/suppliers/api/platformSuppliers.ts b/src/modules/suppliers/api/platformSuppliers.ts index 53190823..dedb058d 100644 --- a/src/modules/suppliers/api/platformSuppliers.ts +++ b/src/modules/suppliers/api/platformSuppliers.ts @@ -9,7 +9,7 @@ function normalizeSupplier(raw: Supplier): Supplier { return { ...raw, id: String(raw.id), tenantId: String(raw.tenantId), - carrierCodes: [...raw.carrierCodes], + carrierCodes: [...(raw.carrierCodes || [])], licenseFileId: id(raw.licenseFileId), createdBy: id(raw.createdBy), updatedBy: id(raw.updatedBy), version: Number(raw.version), deleted: Boolean(raw.deleted), } diff --git a/src/modules/unified-orders/components/OrderFulfillmentTreePanel.vue b/src/modules/unified-orders/components/OrderFulfillmentTreePanel.vue index d49f8360..e66c1cbe 100644 --- a/src/modules/unified-orders/components/OrderFulfillmentTreePanel.vue +++ b/src/modules/unified-orders/components/OrderFulfillmentTreePanel.vue @@ -44,7 +44,7 @@ onMounted(() => { void load() }) - + diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts index 2945df33..95bb9409 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts @@ -92,9 +92,9 @@ describe('UnifiedOrdersView co-load group behavior', () => { await wrapper.get('[aria-label="查看同柜组 CLG-9001 详情"]').trigger('click') expect(wrapper.text()).toContain('2 票 · 3 个入仓片段 · 未锁定') expect(wrapper.text()).toContain('13 箱') - expect(wrapper.text()).toContain('7003') - expect(wrapper.findAll('[data-copy="7003"]')).toHaveLength(1) - expect(wrapper.findAll('[data-copy="5001"]')).toHaveLength(2) + expect(wrapper.text()).toContain('入仓码待补充') + expect(wrapper.text()).not.toContain('7003') + expect(wrapper.text()).not.toContain('5001') await wrapper.findAll('button').find((button) => button.text().includes('取消同柜组'))!.trigger('click') await flushPromises() expect(cancelPlatformCoLoadGroup).toHaveBeenCalledWith('9001', 0, '计划调整') diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts index 9458f6c2..f40ce574 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts @@ -183,10 +183,12 @@ describe('UnifiedOrdersView contract', () => { expect(source).toContain('coLoadGroupOrderCount(coLoadGroup(row as UnifiedOrderResource)!)') expect(source).not.toContain('—\n @@ -1354,6 +1364,7 @@ onBeforeUnmount(() => { - + - + @@ -1384,7 +1395,7 @@ onBeforeUnmount(() => { -
{{ row.orderNo || row.inboundCode || row.orderId }}
+
{{ row.orderNo || row.inboundCode || '业务编码待补充' }}
@@ -1392,8 +1403,8 @@ onBeforeUnmount(() => {
{{ coLoadGroupOrderCount(coLoadDetailGroup) }} 票 · {{ coLoadDetailGroup.members.length }} 个入仓片段 · {{ coLoadGroupStatusLabel(coLoadDetailGroup) }}共 {{ coLoadDetailGroup.fullPackageCount }} 箱
- - + +
入仓码 / 入仓 ID订单编码 / ID箱数
{{ visibleInboundCode(member.inboundId) || member.inboundId }}ID {{ member.inboundId }}{{ visibleOrderNo(member.orderId) || member.orderId }}ID {{ member.orderId }}{{ member.fullPackageCount }} 箱
入仓码订单编码箱数
{{ visibleInboundCode(member.inboundId) || '入仓码待补充' }}{{ visibleOrderNo(member.orderId) || '订单编码待补充' }}{{ member.fullPackageCount }} 箱
@@ -1408,6 +1419,7 @@ onBeforeUnmount(() => { :inbound-code="relationOrder.inboundCode" /> + diff --git a/src/modules/unified-orders/workflows/components/SmartLoadingWorkbench.vue b/src/modules/unified-orders/workflows/components/SmartLoadingWorkbench.vue index c14f2a31..fd1a6d94 100644 --- a/src/modules/unified-orders/workflows/components/SmartLoadingWorkbench.vue +++ b/src/modules/unified-orders/workflows/components/SmartLoadingWorkbench.vue @@ -275,7 +275,7 @@ onMounted(() => { void load() }) From 0e828cd44a2b6077df7bec1d00f08798cc17b9b6 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 31 Aug 2026 10:09:17 +0800 Subject: [PATCH 34/57] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unified-orders/api/coLoadGroups.test.ts | 13 +++--- .../unified-orders/api/coLoadGroups.ts | 1 + .../pages/UnifiedOrdersView.coLoad.test.ts | 34 ++++++++++---- .../pages/UnifiedOrdersView.test.ts | 29 ++++++------ .../pages/UnifiedOrdersView.vue | 44 +++++++++---------- 5 files changed, 70 insertions(+), 51 deletions(-) diff --git a/src/modules/unified-orders/api/coLoadGroups.test.ts b/src/modules/unified-orders/api/coLoadGroups.test.ts index 3e0a1039..c50e9f18 100644 --- a/src/modules/unified-orders/api/coLoadGroups.test.ts +++ b/src/modules/unified-orders/api/coLoadGroups.test.ts @@ -7,8 +7,8 @@ const group = { warehouseId: 2093000000000000002n, channelCode: 'NY-EXX', lockedPlanId: null, version: 0, fullPackageCount: 18, createdAt: '2026-08-29T10:00:00', updatedAt: '2026-08-29T10:00:00', members: [ - { tenantId: 2093000000000000004n, orderId: 2093000000000000005n, inboundId: 2093000000000000006n, fullPackageCount: 8 }, - { tenantId: 2093000000000000007n, orderId: 2093000000000000008n, inboundId: 2093000000000000009n, fullPackageCount: 10 }, + { tenantId: 2093000000000000004n, orderId: 2093000000000000005n, inboundId: 2093000000000000006n, inboundCode: 'NYEXX2608310001', fullPackageCount: 8 }, + { tenantId: 2093000000000000007n, orderId: 2093000000000000008n, inboundId: 2093000000000000009n, inboundCode: 'NYEXX2608310002', fullPackageCount: 10 }, ], } @@ -23,6 +23,7 @@ describe('co-load group API', () => { expect(result.groupId).toBe('2093000000000000001') expect(result.members.map((member) => member.orderId)).toEqual(orderIds) expect(result.members.map((member) => member.tenantId)).toEqual(['2093000000000000004', '2093000000000000007']) + expect(result.members.map((member) => member.inboundCode)).toEqual(['NYEXX2608310001', 'NYEXX2608310002']) }) it('cancels only the whole group with optimistic version and an optional reason', async () => { @@ -48,7 +49,7 @@ describe('co-load group API', () => { }) it('keeps every inbound fragment when one order has multiple current inbounds', async () => { - const fragment = { ...group.members[0], inboundId: 2093000000000000012n, fullPackageCount: 3 } + const fragment = { ...group.members[0], inboundId: 2093000000000000012n, inboundCode: 'NYEXX2608310001-B', fullPackageCount: 3 } const multiInboundGroup = { ...group, fullPackageCount: 21, members: [group.members[0], fragment, group.members[1]] } const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: [ { orderId: group.members[0].orderId, group: multiInboundGroup }, @@ -57,9 +58,9 @@ describe('co-load group API', () => { const result = await batchQueryPlatformCoLoadGroups(['2093000000000000005', '2093000000000000008']) expect(post).toHaveBeenCalledTimes(1) expect(result[0].group.members).toEqual([ - expect.objectContaining({ orderId: '2093000000000000005', inboundId: '2093000000000000006', fullPackageCount: 8 }), - expect.objectContaining({ orderId: '2093000000000000005', inboundId: '2093000000000000012', fullPackageCount: 3 }), - expect.objectContaining({ orderId: '2093000000000000008', inboundId: '2093000000000000009', fullPackageCount: 10 }), + expect.objectContaining({ orderId: '2093000000000000005', inboundId: '2093000000000000006', inboundCode: 'NYEXX2608310001', fullPackageCount: 8 }), + expect.objectContaining({ orderId: '2093000000000000005', inboundId: '2093000000000000012', inboundCode: 'NYEXX2608310001-B', fullPackageCount: 3 }), + expect.objectContaining({ orderId: '2093000000000000008', inboundId: '2093000000000000009', inboundCode: 'NYEXX2608310002', fullPackageCount: 10 }), ]) }) }) diff --git a/src/modules/unified-orders/api/coLoadGroups.ts b/src/modules/unified-orders/api/coLoadGroups.ts index 86039995..5f16fee2 100644 --- a/src/modules/unified-orders/api/coLoadGroups.ts +++ b/src/modules/unified-orders/api/coLoadGroups.ts @@ -6,6 +6,7 @@ export interface CoLoadGroupMember { tenantId: string orderId: string inboundId: string + inboundCode: string fullPackageCount: number } diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts index 95bb9409..abf60f4e 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts @@ -33,7 +33,7 @@ const rows = ['5001', '5002'].map((orderId, index) => ({ })) const group = { groupId: '9001', groupNo: 'CLG-9001', status: 'ACTIVE' as const, warehouseId: '8', channelCode: 'NY-EXX', lockedPlanId: null, version: 0, fullPackageCount: 10, - members: rows.map((row) => ({ tenantId: String(row.tenantId), orderId: String(row.orderId), inboundId: String(row.inboundId), fullPackageCount: 5 })), + members: rows.map((row) => ({ tenantId: String(row.tenantId), orderId: String(row.orderId), inboundId: String(row.inboundId), inboundCode: String(row.inboundCode), fullPackageCount: 5 })), createdAt: '2026-08-29T10:00:00', updatedAt: '2026-08-29T10:00:00', } @@ -43,7 +43,7 @@ function mountView() { AdminPage: { template: '
' }, ListPageLayout: { template: '
' }, QueryToolbar: { template: '
' }, - DataTable: { name: 'DataTable', props: ['data'], emits: ['selection-change'], template: '
' }, + DataTable: { name: 'DataTable', props: ['data'], emits: ['selection-change'], template: '
' }, AppDialog: { name: 'AppDialog', props: ['modelValue', 'confirmText'], emits: ['confirm', 'update:modelValue'], template: '
' }, ApiErrorMeta: { props: ['details'], template: '
{{ details.code }} {{ details.requestId }} {{ details.traceId }}
' }, CopyButton: { props: ['value', 'label'], template: '' }, @@ -76,23 +76,39 @@ describe('UnifiedOrdersView co-load group behavior', () => { expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(1) }) - it('shows group number and member count and cancels only the whole ACTIVE group', async () => { + it('uses inbound codes as the only row identifier and shows an explicit pending state', async () => { + vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: [rows[0], { ...rows[1], inboundCode: null }], page: 1, size: 20, total: 2, pages: 1 }) + const wrapper = mountView() + await flushPromises() + expect(wrapper.text()).toContain('IN-1') + expect(wrapper.text()).toContain('待生成入仓码') + expect(wrapper.text()).not.toContain('ORD-1') + expect(wrapper.text()).not.toContain('ORD-2') + expect(wrapper.text()).not.toContain('5001') + expect(wrapper.text()).not.toContain('7000') + }) + + it('shows a business-only group summary and all server-provided inbound codes in the dialog', async () => { const multiInboundGroup = { ...group, fullPackageCount: 13, - members: [group.members[0], { ...group.members[0], inboundId: '7003', fullPackageCount: 3 }, group.members[1]], + members: [group.members[0], { ...group.members[0], inboundId: '7003', inboundCode: 'IN-1-B', fullPackageCount: 3 }, group.members[1]], } vi.mocked(batchQueryPlatformCoLoadGroups).mockResolvedValue(rows.map((row) => ({ orderId: row.orderId, group: multiInboundGroup }))) vi.mocked(cancelPlatformCoLoadGroup).mockResolvedValue({ ...multiInboundGroup, status: 'CANCELLED', version: 1 }) vi.spyOn(ElMessageBox, 'prompt').mockResolvedValue({ value: '计划调整', action: 'confirm' } as never) const wrapper = mountView() await flushPromises() - expect(wrapper.text()).toContain('CLG-9001') - expect(wrapper.text()).toContain('CLG-9001 · 2 票 · 未锁定') - await wrapper.get('[aria-label="查看同柜组 CLG-9001 详情"]').trigger('click') - expect(wrapper.text()).toContain('2 票 · 3 个入仓片段 · 未锁定') + expect(wrapper.text()).toContain('同柜组 · 2 票 · 未锁定') + expect(wrapper.text()).not.toContain('CLG-9001') + await wrapper.get('[aria-label="查看同柜组 · 2 票 · 未锁定成员"]').trigger('click') + expect(wrapper.text()).toContain('3 个入仓码') expect(wrapper.text()).toContain('13 箱') - expect(wrapper.text()).toContain('入仓码待补充') + expect(wrapper.text()).toContain('IN-1') + expect(wrapper.text()).toContain('IN-1-B') + expect(wrapper.text()).toContain('IN-2') + expect(wrapper.text()).not.toContain('ORD-1') + expect(wrapper.text()).not.toContain('ORD-2') expect(wrapper.text()).not.toContain('7003') expect(wrapper.text()).not.toContain('5001') await wrapper.findAll('button').find((button) => button.text().includes('取消同柜组'))!.trigger('click') diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts index f40ce574..3fae982f 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts @@ -95,7 +95,7 @@ describe('UnifiedOrdersView contract', () => { expect(source).toContain('v-model:page-size="pageSize"') const columnSource = source.slice(source.indexOf('const columns ='), source.indexOf('function emptyFilters')) const expectedColumnKeys = [ - 'actions', 'orderNo', 'inboundCode', 'mainStatus', 'customerName', 'receivingWarehouseName', + 'actions', 'inboundCode', 'mainStatus', 'customerName', 'receivingWarehouseName', 'destinationWarehouseName', 'channelName', 'totalPackageCount', 'totalPalletCount', 'requestedPickupAt', 'locationCodes', 'measurementReviewStatus', 'actualGrossWeightKg', 'actualVolumeCbm', 'totalNetWeightKg', 'chargeableWeightKg', 'chargeableVolumeCbm', 'expectedShipDate', 'expectedArrivalAt', 'feeReviewStatus', 'skuNos', 'totalQuantity', 'expressCompany', 'expressTrackingNo', @@ -106,10 +106,10 @@ describe('UnifiedOrdersView contract', () => { for (const uncontractedKey of [ 'internalOrderStatus', 'holdCargo', 'companyName', 'pickupMethod', 'associatedFiles', 'createdBy', ]) expect(columnSource).not.toContain(`key: '${uncontractedKey}'`) - expect(columnSource.indexOf("key: 'actions'")).toBeLessThan(columnSource.indexOf("key: 'orderNo'")) - expect(columnSource.indexOf("key: 'orderNo'")).toBeLessThan(columnSource.indexOf("key: 'inboundCode'")) + expect(columnSource.indexOf("key: 'actions'")).toBeLessThan(columnSource.indexOf("key: 'inboundCode'")) expect(columnSource).not.toContain("key: 'coLoadGroup'") - expect(columnSource).toContain("key: 'orderNo', prop: 'orderNo', label: '订单号', minWidth: 230, slotName: 'orderNo'") + expect(columnSource).not.toContain("key: 'orderNo'") + expect(columnSource).toContain("key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 260, slotName: 'inboundCode'") expect(columnSource).toContain("{ key: 'mainStatus', prop: 'mainStatus', label: '订单主状态'") expect(columnSource).not.toContain("key: 'loadingStatus'") expect(columnSource).not.toContain("key: 'businessDate'") @@ -176,18 +176,21 @@ describe('UnifiedOrdersView contract', () => { expect(createSource).toContain('createPlatformCoLoadGroup(selectedOrders.value.map((row) => String(row.orderId)))') }) - it('embeds a clickable compact group tag under the order code without an empty placeholder column', () => { + it('uses inbound code as the only list identifier and opens a business-only group dialog', () => { expect(source).not.toContain("{ key: 'coLoadGroup', label: '同柜组'") - expect(source).toContain(' @@ -1501,7 +1499,7 @@ onBeforeUnmount(() => { .row-actions { display: flex; align-items: center; gap: 4px; white-space: nowrap; } .row-actions :deep(.el-button) { margin-left: 0; } .inbound-relation-cell { display: grid; min-width: 0; gap: 5px; } -.inbound-relation-cell > span { overflow-wrap: anywhere; } +.inbound-code-value { display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .order-relation-link { width: fit-content; max-width: 100%; height: auto; justify-content: flex-start; padding: 0; font-size: 12px; } .order-relation-link :deep(span) { min-width: 0; white-space: normal; text-align: left; } .primary-cell { display: grid; min-width: 0; gap: 4px; } From b4d5fb11706b1dc66745ba3dfeef86d8b4c7ff5e Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 31 Aug 2026 10:17:06 +0800 Subject: [PATCH 35/57] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile | 2 +- README.md | 2 +- docker-compose.development.yml | 2 +- docker-compose.test.yml | 6 +++--- src/deployment/nginxConfig.test.ts | 4 +++- .../container-loading/pages/GlobalSmartLoadingView.test.ts | 2 +- .../pages/SmartLoadingWorkbenchView.test.ts | 2 +- 7 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index c80b947f..9a398fc0 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -70,7 +70,7 @@ pipeline { docker build \ --build-arg NODE_IMAGE="$NODE_BASE_IMAGE" \ --build-arg NGINX_IMAGE="$NGINX_BASE_IMAGE" \ - --build-arg VITE_CLIENT_WEB_URL=http://8.163.49.32:9002 \ + --build-arg VITE_CLIENT_WEB_URL="${CLIENT_WEB_URL:-https://dev-app.buggoglobal.com/}" \ -t "$image" . export IMAGE_TAG="$SOURCE_TAG" docker compose -p "$DEVELOPMENT_COMPOSE_PROJECT" -f "$DEVELOPMENT_COMPOSE_FILE" up -d --remove-orphans --pull never diff --git a/README.md b/README.md index 0e8eca94..3cb6f085 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ VITE_CLIENT_WEB_URL=http://localhost:5174 切换开发或测试环境时,只修改未提交的 `.env.local` 中 `API_PROXY_TARGET`、`OVERSEAS_API_PROXY_TARGET` 和 `VITE_CLIENT_WEB_URL`,然后重启 `pnpm dev`;无需修改业务代码。真实环境地址不写入受版本控制的配置模板或说明。 -客户代登录打开的工作台地址是运行时配置,不需要为每个环境重新构建镜像。Docker 部署时设置容器环境变量 `CLIENT_WEB_URL`;容器启动时会生成 `/runtime-config.js`。本地 Vite 开发使用 `VITE_CLIENT_WEB_URL` 作为兜底。 +客户代登录打开的工作台地址是运行时配置,不需要为每个环境重新构建镜像。Docker 部署时为 Admin 容器设置当前环境的 `CLIENT_WEB_URL`;容器启动时会生成 `/runtime-config.js`。开发、测试和生产分别在各自部署任务中注入自己的 Client 根地址,禁止复用其他环境的域名。本地 Vite 开发使用 `VITE_CLIENT_WEB_URL` 作为兜底。部署后可读取 `/runtime-config.js` 核对最终生效值。 认证请求使用 `/api/auth/**`,平台 IAM 请求使用 `/api/platform/v1/iam/**`。登录后只从 JWT 读取平台身份,菜单和操作权限通过当前用户授权接口实时加载,不从 JWT 携带或解析角色权限。企业审核调用 `/reviews`,打开审核详情后按租户调用 `/tenants/{tenantId}/qualifications`;营业执照通过 `/api/platform/v1/files/qualifications/{tenantId}/{fileName}` 携带平台 Token 临时读取,关闭预览后释放本地对象地址。客户端自主注册企业审核通过后继续使用注册账号作为管理员,不展示新凭据;Admin 代建企业并由后端创建管理员时,页面必须展示不可通过遮罩、Esc 或关闭按钮跳过的一次性凭据弹窗,运营人员确认保存后才清除账号密码。平台角色查询、新增、编辑、启停、删除和权限全量保存均已按 `RbacController` 接入真实 `/roles` 契约,角色返回的 `permissionIds` 用于授权回显;系统内置角色及仍被成员使用的角色继续由服务端保护。权限资源查询和新增调用 `/permissions`,尚未提供的权限编辑、删除和启停不会模拟成功。一期真实权限资源只创建 `MENU` 与 `BUTTON`,不创建 `PAGE` 或 `API` 类型。其余业务页面需在对应服务契约确认后逐项替换,生产逻辑不得静默回退到模拟数据。 diff --git a/docker-compose.development.yml b/docker-compose.development.yml index f39efb2b..a677b495 100644 --- a/docker-compose.development.yml +++ b/docker-compose.development.yml @@ -6,7 +6,7 @@ services: ports: - "127.0.0.1:9001:80" environment: - CLIENT_WEB_URL: "http://8.163.49.32:9002" + CLIENT_WEB_URL: "${CLIENT_WEB_URL:-https://dev-app.buggoglobal.com/}" API_UPSTREAM: "http://firstleg-development-gateway:9000" OVERSEAS_API_UPSTREAM: "http://firstleg-development-gateway-overseas:9300" networks: diff --git a/docker-compose.test.yml b/docker-compose.test.yml index c8c35400..f522117c 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -12,8 +12,8 @@ services: ports: - "${HOST_PORT:-9001}:80" environment: - # The test client is exposed on the same host through port 9002. Allow - # deployments to override this, but never fall back to the server IP. - CLIENT_WEB_URL: "${CLIENT_WEB_URL:-http://test-admin.buggoglobal.com:9002}" + # Runtime URL used by the secure customer-workbench handoff. Each + # environment may override it without rebuilding the Admin image. + CLIENT_WEB_URL: "${CLIENT_WEB_URL:-https://test-app.buggoglobal.com/}" API_UPSTREAM: "http://test-api-cn.buggoglobal.com" OVERSEAS_API_UPSTREAM: "http://test-api-global.buggoglobal.com" diff --git a/src/deployment/nginxConfig.test.ts b/src/deployment/nginxConfig.test.ts index 2fb21fba..a26cf17b 100644 --- a/src/deployment/nginxConfig.test.ts +++ b/src/deployment/nginxConfig.test.ts @@ -40,8 +40,9 @@ describe('admin nginx deployment config', () => { it('declares an explicit overseas gateway upstream in container deployments', () => { expect(testCompose).toContain('API_UPSTREAM: "http://test-api-cn.buggoglobal.com"') expect(testCompose).toContain('OVERSEAS_API_UPSTREAM: "http://test-api-global.buggoglobal.com"') - expect(testCompose).toContain('CLIENT_WEB_URL: "${CLIENT_WEB_URL:-http://test-admin.buggoglobal.com:9002}"') + expect(testCompose).toContain('CLIENT_WEB_URL: "${CLIENT_WEB_URL:-https://test-app.buggoglobal.com/}"') expect(testCompose).not.toContain('CLIENT_WEB_URL: "http://8.163.') + expect(developmentCompose).toContain('CLIENT_WEB_URL: "${CLIENT_WEB_URL:-https://dev-app.buggoglobal.com/}"') expect(developmentCompose).toContain('OVERSEAS_API_UPSTREAM: "http://firstleg-development-gateway-overseas:9300"') expect(config).toContain('proxy_set_header Host $proxy_host;') }) @@ -64,5 +65,6 @@ describe('admin nginx deployment config', () => { expect(jenkinsfile).toContain("NGINX_BASE_IMAGE = '172.31.198.7:10082/java-project/nginx:1.27-alpine'") expect(jenkinsfile).toContain('withCredentials([usernamePassword(') expect(jenkinsfile).toContain('export DOCKER_CONFIG="${WORKSPACE}@tmp/docker-auth"') + expect(jenkinsfile).toContain('--build-arg VITE_CLIENT_WEB_URL="${CLIENT_WEB_URL:-https://dev-app.buggoglobal.com/}"') }) }) diff --git a/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts b/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts index 91ab3e26..7a230e81 100644 --- a/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts +++ b/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts @@ -252,7 +252,7 @@ describe('GlobalSmartLoadingView', () => { const groupedPlan = { ...plan, items } const group = { groupId: '2093539779795795969', groupNo: 'CLG-2093539779795795969', status: 'LOCKED' as const, warehouseId: '8', channelCode: 'ZBYHK', lockedPlanId: '99', version: 1, - members: items.map((item) => ({ tenantId: item.accountId, orderId: item.orderId, inboundId: item.inboundId, fullPackageCount: item.packageCount })), fullPackageCount: 13, createdAt: '2026-08-29T10:00:00', updatedAt: '2026-08-29T10:30:00', + members: items.map((item) => ({ tenantId: item.accountId, orderId: item.orderId, inboundId: item.inboundId, inboundCode: item.inboundCode, fullPackageCount: item.packageCount })), fullPackageCount: 13, createdAt: '2026-08-29T10:00:00', updatedAt: '2026-08-29T10:30:00', } vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValueOnce(groupedPlan).mockResolvedValue({ ...groupedPlan, version: 3, items: [] }) vi.mocked(batchQueryPlatformCoLoadGroups).mockResolvedValue([...new Set(items.map((item) => item.orderId))].map((orderId) => ({ orderId, group }))) diff --git a/src/modules/container-loading/pages/SmartLoadingWorkbenchView.test.ts b/src/modules/container-loading/pages/SmartLoadingWorkbenchView.test.ts index 4028d614..532429b3 100644 --- a/src/modules/container-loading/pages/SmartLoadingWorkbenchView.test.ts +++ b/src/modules/container-loading/pages/SmartLoadingWorkbenchView.test.ts @@ -189,7 +189,7 @@ describe('SmartLoadingWorkbenchView', () => { vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({ ...planResponse, items }) vi.mocked(batchQueryPlatformCoLoadGroups).mockResolvedValue([...new Set(items.map((item) => item.orderId))].map((orderId) => ({ orderId, group: { groupId: '9900', groupNo: 'CLG-9900', status: 'LOCKED', warehouseId: '8', channelCode: 'ZBYHK', lockedPlanId: '99', version: 1, - members: items.map((member) => ({ tenantId: member.accountId, orderId: member.orderId, inboundId: member.inboundId, fullPackageCount: member.packageCount })), fullPackageCount: 13, createdAt: '2026-08-29T10:00:00', updatedAt: '2026-08-29T10:30:00', + members: items.map((member) => ({ tenantId: member.accountId, orderId: member.orderId, inboundId: member.inboundId, inboundCode: member.inboundCode, fullPackageCount: member.packageCount })), fullPackageCount: 13, createdAt: '2026-08-29T10:00:00', updatedAt: '2026-08-29T10:30:00', } }))) vi.mocked(removeSmartLoadingGroup).mockResolvedValue({ ...planResponse, version: 3, items: [] }) vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as never) From 1548d444c6f33122dd603c9d785c92f4bed99df4 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 31 Aug 2026 10:31:53 +0800 Subject: [PATCH 36/57] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../unified-orders/api/coLoadGroups.test.ts | 16 +++ .../unified-orders/api/coLoadGroups.ts | 43 ++++-- .../unified-orders/api/unifiedOrders.ts | 14 ++ src/modules/unified-orders/index.ts | 5 +- .../pages/UnifiedOrdersView.coLoad.test.ts | 21 ++- .../pages/UnifiedOrdersView.test.ts | 9 ++ .../pages/UnifiedOrdersView.vue | 135 +++++++----------- 7 files changed, 144 insertions(+), 99 deletions(-) diff --git a/src/modules/unified-orders/api/coLoadGroups.test.ts b/src/modules/unified-orders/api/coLoadGroups.test.ts index c50e9f18..2aa2e6ec 100644 --- a/src/modules/unified-orders/api/coLoadGroups.test.ts +++ b/src/modules/unified-orders/api/coLoadGroups.test.ts @@ -63,4 +63,20 @@ describe('co-load group API', () => { expect.objectContaining({ orderId: '2093000000000000008', inboundId: '2093000000000000009', inboundCode: 'NYEXX2608310002', fullPackageCount: 10 }), ]) }) + + it('normalizes missing and blank member inbound codes without exposing technical ids as fallbacks', async () => { + const incompleteGroup = { + ...group, + members: [ + { ...group.members[0], inboundCode: undefined }, + { ...group.members[1], inboundCode: null }, + { ...group.members[1], inboundId: 2093000000000000012n, inboundCode: ' ' }, + { ...group.members[1], inboundId: 2093000000000000013n, inboundCode: ' NYEXX2608310003 ' }, + ], + } + vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: incompleteGroup }) + const result = await createPlatformCoLoadGroup(['2093000000000000005', '2093000000000000008']) + expect(result.members.map((member) => member.inboundCode)).toEqual(['', '', '', 'NYEXX2608310003']) + expect(result.members.map((member) => member.inboundCode)).not.toContain('2093000000000000012') + }) }) diff --git a/src/modules/unified-orders/api/coLoadGroups.ts b/src/modules/unified-orders/api/coLoadGroups.ts index 5f16fee2..df3844e0 100644 --- a/src/modules/unified-orders/api/coLoadGroups.ts +++ b/src/modules/unified-orders/api/coLoadGroups.ts @@ -6,7 +6,6 @@ export interface CoLoadGroupMember { tenantId: string orderId: string inboundId: string - inboundCode: string fullPackageCount: number } @@ -24,21 +23,31 @@ export interface CoLoadGroup { updatedAt: string } -export interface OrderCoLoadGroupInfo { - orderId: string - group: CoLoadGroup +export interface CoLoadBusinessMember { + inboundCode: string +} + +export interface CoLoadBusinessSummary { + status: CoLoadGroupStatus + memberCount: number + members: CoLoadBusinessMember[] } type RawId = string | number +type RawCoLoadGroupMember = Omit & { + tenantId: RawId + orderId: RawId + inboundId: RawId +} type RawCoLoadGroup = Omit & { groupId: RawId warehouseId: RawId lockedPlanId: RawId | null - members: Array & { tenantId: RawId; orderId: RawId; inboundId: RawId }> + members: RawCoLoadGroupMember[] } -type RawOrderCoLoadGroupInfo = Omit & { - orderId: RawId - group: RawCoLoadGroup +type RawCoLoadBusinessSummary = Omit & { + memberCount: number | string + members: Array<{ inboundCode?: string | null }> } const basePath = '/platform/v1/first-leg/coload-groups' @@ -81,13 +90,17 @@ export async function cancelPlatformCoLoadGroup(groupId: string, expectedVersion return normalizeGroup(unwrapApiResponse(response)) } -export async function batchQueryPlatformCoLoadGroups(orderIds: string[]) { - const response = await http.post, ApiEnvelope>( - `${basePath}/batch-query`, - { orderIds }, +export async function getPlatformCoLoadGroupByInboundCode(inboundCode: string) { + const response = await http.get, ApiEnvelope>( + `${basePath}/by-inbound-code/${encodeURIComponent(inboundCode)}`, ) - return unwrapApiResponse(response).map((item) => ({ - orderId: String(item.orderId), - group: normalizeGroup(item.group), + const raw = unwrapApiResponse(response) + const members = (raw.members || []).map((member) => ({ + inboundCode: typeof member.inboundCode === 'string' ? member.inboundCode.trim() : '', })) + return { + status: raw.status, + memberCount: Number(raw.memberCount), + members, + } satisfies CoLoadBusinessSummary } diff --git a/src/modules/unified-orders/api/unifiedOrders.ts b/src/modules/unified-orders/api/unifiedOrders.ts index 1ff047a0..7011ef89 100644 --- a/src/modules/unified-orders/api/unifiedOrders.ts +++ b/src/modules/unified-orders/api/unifiedOrders.ts @@ -75,6 +75,12 @@ export interface UnifiedOrderRelationSummary { childCount: number } +export interface UnifiedOrderCoLoadGroupSummary { + status: string + memberCount: number + memberInboundCodes: string[] +} + export interface UnifiedOrderResource { orderId: number | string tenantId: number | string @@ -103,6 +109,7 @@ export interface UnifiedOrderResource { loading: UnifiedOrderLoadingSummary | null delivery: UnifiedOrderDeliverySummary | null transport: UnifiedOrderTransportSummary | null + coLoadGroup: UnifiedOrderCoLoadGroupSummary | null createdAt: string | null updatedAt: string | null expectedArrivalAt: string | null @@ -368,6 +375,13 @@ function normalizeRow(raw: UnifiedOrderResource): UnifiedOrderResource { evidenceCount: nullableInteger(raw.loading.evidenceCount), } : null, transport: raw.transport ? { ...raw.transport } : null, + coLoadGroup: raw.coLoadGroup ? { + status: String(raw.coLoadGroup.status || ''), + memberCount: integer(raw.coLoadGroup.memberCount, 0), + memberInboundCodes: (raw.coLoadGroup.memberInboundCodes || []) + .map((value) => typeof value === 'string' ? value.trim() : '') + .filter(Boolean), + } : null, } } diff --git a/src/modules/unified-orders/index.ts b/src/modules/unified-orders/index.ts index bb658ed6..81057d42 100644 --- a/src/modules/unified-orders/index.ts +++ b/src/modules/unified-orders/index.ts @@ -5,13 +5,14 @@ export { type UnifiedOrderResource, } from './api/unifiedOrders' export { - batchQueryPlatformCoLoadGroups, cancelPlatformCoLoadGroup, createPlatformCoLoadGroup, + getPlatformCoLoadGroupByInboundCode, + type CoLoadBusinessMember, + type CoLoadBusinessSummary, type CoLoadGroup, type CoLoadGroupMember, type CoLoadGroupStatus, - type OrderCoLoadGroupInfo, } from './api/coLoadGroups' export { diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts index abf60f4e..364224ce 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts @@ -102,7 +102,7 @@ describe('UnifiedOrdersView co-load group behavior', () => { expect(wrapper.text()).toContain('同柜组 · 2 票 · 未锁定') expect(wrapper.text()).not.toContain('CLG-9001') await wrapper.get('[aria-label="查看同柜组 · 2 票 · 未锁定成员"]').trigger('click') - expect(wrapper.text()).toContain('3 个入仓码') + expect(wrapper.text()).toContain('3 个成员') expect(wrapper.text()).toContain('13 箱') expect(wrapper.text()).toContain('IN-1') expect(wrapper.text()).toContain('IN-1-B') @@ -116,4 +116,23 @@ describe('UnifiedOrdersView co-load group behavior', () => { expect(cancelPlatformCoLoadGroup).toHaveBeenCalledWith('9001', 0, '计划调整') expect(ElMessageBox.prompt).toHaveBeenCalledWith(expect.stringContaining('不支持单独移除成员'), '取消同柜不可拆组', expect.any(Object)) }) + + it.each([ + ['undefined', undefined], + ['null', null], + ['blank', ' '], + ])('keeps the member dialog readable when a normalized inbound code is %s', async (_case, inboundCode) => { + const incompleteGroup = { + ...group, + members: [{ ...group.members[0], inboundCode: inboundCode as unknown as string }, group.members[1]], + } + vi.mocked(batchQueryPlatformCoLoadGroups).mockResolvedValue(rows.map((row) => ({ orderId: row.orderId, group: incompleteGroup }))) + const wrapper = mountView() + await flushPromises() + await wrapper.get('[aria-label="查看同柜组 · 2 票 · 未锁定成员"]').trigger('click') + expect(wrapper.text()).toContain('同柜组成员入仓码尚未返回,请刷新后重试') + expect(wrapper.text()).not.toContain('CLG-9001') + expect(wrapper.text()).not.toContain('ORD-1') + expect(wrapper.text()).not.toContain('5001') + }) }) diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts index 3fae982f..5ce061d8 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts @@ -188,12 +188,21 @@ describe('UnifiedOrdersView contract', () => { expect(source).toContain('v-for="member in coLoadDetailMembers"') expect(source).toContain('入仓码箱数') expect(source).toContain('{{ member.inboundCode }}') + expect(source).toContain('同柜组成员入仓码尚未返回,请刷新后重试') + expect(source).toContain('coLoadDetailHasMissingInboundCodes') expect(source).not.toContain('visibleInboundCode') expect(source).not.toContain('visibleOrderNo') expect(source).not.toContain('订单编码') expect(source).not.toContain('客户 ID') }) + it('names the cross-domain relationship entry as fulfillment details', () => { + expect(source).toContain('>履约详情') + expect(source).toContain('aria-label="查看该入仓码的订单、装柜、尾程与库存关系"') + expect(source).toContain('title="查看该入仓码的订单、装柜、尾程与库存关系"') + expect(source).not.toContain('>关联数据') + }) + it('keeps editable query values separate from the applied list request', () => { expect(source).toContain('const appliedFilters = ref(emptyFilters())') expect(source).toContain('const activeFilters = appliedFilters.value') diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.vue b/src/modules/unified-orders/pages/UnifiedOrdersView.vue index 20232edd..49e9a1a4 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.vue +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.vue @@ -1,7 +1,7 @@ - + - {{ detail?.billOfLadingNo || '—' }} - {{ detail?.vesselName || '—' }} - {{ detail?.voyageNo || '—' }} - {{ detail?.estimatedSailingAt || '—' }} - {{ detail?.estimatedArrivalAt || '—' }} + {{ form.data.billOfLadingNo || '—' }} + {{ form.data.vesselName || '—' }} + {{ form.data.voyageNo || '—' }} + {{ form.data.estimatedSailingAt || '—' }} + {{ form.data.estimatedArrivalAt || '—' }} From cd290b8dc646fc253b3ae102709d1278aae0cc92 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 31 Aug 2026 14:54:57 +0800 Subject: [PATCH 43/57] feat(admin): validate and cancel co-load groups --- .../unified-orders/api/coLoadGroups.test.ts | 8 +- .../unified-orders/api/coLoadGroups.ts | 6 +- .../pages/UnifiedOrdersView.coLoad.test.ts | 79 ++++++++++++++++++- .../pages/UnifiedOrdersView.test.ts | 8 ++ .../pages/UnifiedOrdersView.vue | 65 +++++++++++++-- 5 files changed, 149 insertions(+), 17 deletions(-) diff --git a/src/modules/unified-orders/api/coLoadGroups.test.ts b/src/modules/unified-orders/api/coLoadGroups.test.ts index 5cf365b1..23242413 100644 --- a/src/modules/unified-orders/api/coLoadGroups.test.ts +++ b/src/modules/unified-orders/api/coLoadGroups.test.ts @@ -25,12 +25,12 @@ describe('co-load group API', () => { expect(result.members.map((member) => member.tenantId)).toEqual(['2093000000000000004', '2093000000000000007']) }) - it('cancels only the whole group with optimistic version and an optional reason', async () => { + it('cancels the whole group by its visible inbound code without exposing group identifiers', async () => { const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: { ...group, status: 'CANCELLED', version: 1 } }) - await cancelPlatformCoLoadGroup('2093000000000000001', 0, '路线调整') + await cancelPlatformCoLoadGroup('NYEXX2608310001', '路线调整') expect(post).toHaveBeenCalledWith( - '/platform/v1/first-leg/coload-groups/2093000000000000001/cancel', - { expectedVersion: 0, reason: '路线调整' }, + '/platform/v1/first-leg/coload-groups/by-inbound-code/NYEXX2608310001/cancel', + { reason: '路线调整' }, { headers: { 'Idempotency-Key': expect.any(String) } }, ) }) diff --git a/src/modules/unified-orders/api/coLoadGroups.ts b/src/modules/unified-orders/api/coLoadGroups.ts index df3844e0..051b290c 100644 --- a/src/modules/unified-orders/api/coLoadGroups.ts +++ b/src/modules/unified-orders/api/coLoadGroups.ts @@ -81,10 +81,10 @@ export async function createPlatformCoLoadGroup(orderIds: string[]) { return normalizeGroup(unwrapApiResponse(response)) } -export async function cancelPlatformCoLoadGroup(groupId: string, expectedVersion: number, reason?: string | null) { +export async function cancelPlatformCoLoadGroup(inboundCode: string, reason?: string | null) { const response = await http.post, ApiEnvelope>( - `${basePath}/${encodeURIComponent(groupId)}/cancel`, - { expectedVersion, reason: reason?.trim() || null }, + `${basePath}/by-inbound-code/${encodeURIComponent(inboundCode)}/cancel`, + { reason: reason?.trim() || null }, { headers: { 'Idempotency-Key': operationId('admin-coload-cancel') } }, ) return normalizeGroup(unwrapApiResponse(response)) diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts index 9778b627..75cacb90 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts @@ -1,8 +1,9 @@ import { flushPromises, shallowMount } from '@vue/test-utils' import { beforeEach, describe, expect, it, vi } from 'vitest' +import { ElMessageBox } from 'element-plus' import { ApiResponseError } from '@/shared/api/http' import { listPlatformUnifiedOrders } from '@/modules/unified-orders/api/unifiedOrders' -import { createPlatformCoLoadGroup, getPlatformCoLoadGroupByInboundCode } from '@/modules/unified-orders/api/coLoadGroups' +import { cancelPlatformCoLoadGroup, createPlatformCoLoadGroup, getPlatformCoLoadGroupByInboundCode } from '@/modules/unified-orders/api/coLoadGroups' import UnifiedOrdersView from './UnifiedOrdersView.vue' vi.mock('vue-router', () => ({ useRoute: () => ({ params: {}, query: {} }), useRouter: () => ({ push: vi.fn() }) })) @@ -17,7 +18,7 @@ vi.mock('@/modules/unified-orders/api/unifiedOrders', () => ({ unifiedOrderStatusOptions: [{ value: 'WAIT_SPLIT', label: '待分柜' }], })) vi.mock('@/modules/unified-orders/api/coLoadGroups', () => ({ - createPlatformCoLoadGroup: vi.fn(), getPlatformCoLoadGroupByInboundCode: vi.fn(), + cancelPlatformCoLoadGroup: vi.fn(), createPlatformCoLoadGroup: vi.fn(), getPlatformCoLoadGroupByInboundCode: vi.fn(), })) const rows = ['5001', '5002'].map((orderId, index) => ({ @@ -38,10 +39,10 @@ function mountView() { ListPageLayout: { template: '
' }, QueryToolbar: { template: '
' }, DataTable: { name: 'DataTable', props: ['data'], emits: ['selection-change'], template: '
' }, - AppDialog: { name: 'AppDialog', props: ['modelValue', 'confirmText'], emits: ['confirm', 'update:modelValue'], template: '
' }, + AppDialog: { name: 'AppDialog', props: ['modelValue', 'confirmText', 'confirmDisabled', 'confirmLoading'], emits: ['confirm', 'update:modelValue'], methods: { close() { this.$emit('update:modelValue', false) } }, template: '
' }, ApiErrorMeta: { props: ['details'], template: '
{{ details.code }} {{ details.requestId }} {{ details.traceId }}
' }, CopyButton: { props: ['value', 'label'], template: '' }, - 'el-button': { template: '' }, + 'el-button': { props: ['disabled', 'loading'], emits: ['click'], template: '' }, 'el-tag': { template: '' }, 'el-alert': { props: ['title'], template: '
{{ title }}
' }, } }, @@ -52,6 +53,7 @@ describe('UnifiedOrdersView co-load group behavior', () => { beforeEach(() => { vi.clearAllMocks() vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: rows, page: 1, size: 20, total: 2, pages: 1 }) + vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue({ action: 'confirm', value: '' } as never) }) it('keeps both selected orders and backend tracing evidence when create validation fails', async () => { @@ -69,6 +71,22 @@ describe('UnifiedOrdersView co-load group behavior', () => { expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(1) }) + it.each([ + ['平台收货仓不同', [{ ...rows[0], receivingWarehouseId: '8' }, { ...rows[1], receivingWarehouseId: '9' }], '所选订单的平台收货仓不同,不能设为同柜组'], + ['渠道不同', [{ ...rows[0], channelName: 'NY-EXX' }, { ...rows[1], channelName: 'LA-SEA' }], '所选订单的渠道不同,不能设为同柜组'], + ['已有同柜组', [rows[0], { ...rows[1], coLoadGroup: { status: 'ACTIVE', memberCount: 2, memberInboundCodes: ['IN-2', 'IN-3'] } }], '所选订单中已有同柜组,请先解除原同柜组'], + ])('blocks create immediately when %s', async (_case, selectedRows, reason) => { + vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: selectedRows, page: 1, size: 20, total: 2, pages: 1 }) + const wrapper = mountView() + await flushPromises() + await wrapper.get('[data-testid="select"]').trigger('click') + const createButton = wrapper.findAll('button').find((button) => button.text().includes('设为同柜不可拆组'))! + expect(createButton.attributes('disabled')).toBeDefined() + expect(wrapper.text()).toContain(reason) + await createButton.trigger('click') + expect(createPlatformCoLoadGroup).not.toHaveBeenCalled() + }) + it('uses inbound codes as the only row identifier and shows an explicit pending state', async () => { vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: [rows[0], { ...rows[1], inboundCode: null }], page: 1, size: 20, total: 2, pages: 1 }) const wrapper = mountView() @@ -104,6 +122,59 @@ describe('UnifiedOrdersView co-load group behavior', () => { expect(wrapper.text()).not.toContain('5001') }) + it('cancels an ACTIVE group by the clicked member inbound code and preserves dialog error evidence', async () => { + const rowsWithGroup = rows.map((row) => ({ ...row, coLoadGroup: { status: 'ACTIVE', memberCount: 2, memberInboundCodes: ['IN-1', 'IN-2'] } })) + vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: rowsWithGroup, page: 1, size: 20, total: 2, pages: 1 }) + vi.mocked(getPlatformCoLoadGroupByInboundCode).mockResolvedValue({ status: 'ACTIVE', memberCount: 2, members: [{ inboundCode: 'IN-1' }, { inboundCode: 'IN-2' }] }) + vi.mocked(cancelPlatformCoLoadGroup).mockRejectedValueOnce(new ApiResponseError('组已被并发锁定', 'A0409', 409, undefined, 'request-cancel', 'trace-cancel')) + const wrapper = mountView() + await flushPromises() + await wrapper.get('[aria-label="查看同柜组 · 2 票 · 未锁定成员"]').trigger('click') + await flushPromises() + const cancelButton = wrapper.findAll('button').find((button) => button.text() === '解除同柜组')! + expect(cancelButton.exists()).toBe(true) + await cancelButton.trigger('click') + await flushPromises() + + expect(ElMessageBox.confirm).toHaveBeenCalledWith(expect.stringContaining('解除整个同柜组'), '解除同柜组', expect.objectContaining({ confirmButtonText: '确认解除' })) + expect(cancelPlatformCoLoadGroup).toHaveBeenCalledWith('IN-1', '重新调整同柜组成员') + expect(wrapper.text()).toContain('组已被并发锁定') + expect(wrapper.text()).toContain('A0409 request-cancel trace-cancel') + expect(wrapper.findAllComponents({ name: 'AppDialog' }).some((dialog) => dialog.props('modelValue') === true)).toBe(true) + expect(wrapper.text()).not.toContain('CLG-') + expect(wrapper.text()).not.toContain('5001') + }) + + it('closes and refreshes after one successful whole-group cancellation request', async () => { + const rowsWithGroup = rows.map((row) => ({ ...row, coLoadGroup: { status: 'ACTIVE', memberCount: 2, memberInboundCodes: ['IN-1', 'IN-2'] } })) + vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: rowsWithGroup, page: 1, size: 20, total: 2, pages: 1 }) + vi.mocked(getPlatformCoLoadGroupByInboundCode).mockResolvedValue({ status: 'ACTIVE', memberCount: 2, members: [{ inboundCode: 'IN-1' }, { inboundCode: 'IN-2' }] }) + vi.mocked(cancelPlatformCoLoadGroup).mockResolvedValue({} as never) + const wrapper = mountView() + await flushPromises() + await wrapper.get('[aria-label="查看同柜组 · 2 票 · 未锁定成员"]').trigger('click') + await flushPromises() + await wrapper.findAll('button').find((button) => button.text() === '解除同柜组')!.trigger('click') + await flushPromises() + + expect(cancelPlatformCoLoadGroup).toHaveBeenCalledTimes(1) + expect(cancelPlatformCoLoadGroup).toHaveBeenCalledWith('IN-1', '重新调整同柜组成员') + expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(2) + expect(wrapper.findAllComponents({ name: 'AppDialog' }).every((dialog) => dialog.props('modelValue') !== true)).toBe(true) + }) + + it.each(['LOCKED', 'CONSUMED'])('does not offer group cancellation for %s', async (status) => { + const rowsWithGroup = rows.map((row) => ({ ...row, coLoadGroup: { status, memberCount: 2, memberInboundCodes: ['IN-1', 'IN-2'] } })) + vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: rowsWithGroup, page: 1, size: 20, total: 2, pages: 1 }) + vi.mocked(getPlatformCoLoadGroupByInboundCode).mockResolvedValue({ status: status as 'LOCKED' | 'CONSUMED', memberCount: 2, members: [{ inboundCode: 'IN-1' }, { inboundCode: 'IN-2' }] }) + const wrapper = mountView() + await flushPromises() + await wrapper.get(`[aria-label="查看同柜组 · 2 票 · ${status === 'LOCKED' ? '已锁定' : '已消费'}成员"]`).trigger('click') + await flushPromises() + expect(wrapper.findAll('button').some((button) => button.text() === '解除同柜组')).toBe(false) + expect(cancelPlatformCoLoadGroup).not.toHaveBeenCalled() + }) + it.each([ ['undefined', undefined], ['null', null], diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts index 94c1b52b..3fee5bae 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts @@ -174,6 +174,10 @@ describe('UnifiedOrdersView contract', () => { expect(createSource).not.toContain('splitStatus') expect(createSource).not.toContain('relation') expect(createSource).toContain('createPlatformCoLoadGroup(selectedOrders.value.map((row) => String(row.orderId)))') + expect(source).toContain(':disabled="loading || !canCreateCoLoadGroup"') + expect(source).toContain('所选订单的平台收货仓不同,不能设为同柜组') + expect(source).toContain('所选订单的渠道不同,不能设为同柜组') + expect(source).toContain('所选订单中已有同柜组,请先解除原同柜组') }) it('uses inbound code as the only list identifier and opens a business-only group dialog', () => { @@ -194,6 +198,10 @@ describe('UnifiedOrdersView contract', () => { expect(source).not.toContain('visibleOrderNo') expect(source).not.toContain('订单编码') expect(source).not.toContain('客户 ID') + expect(source).toContain("coLoadDetailSummary?.status === 'ACTIVE'") + expect(source).toContain('解除同柜组') + expect(source).toContain('cancelPlatformCoLoadGroup(inboundCode') + expect(source).toContain(' { diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.vue b/src/modules/unified-orders/pages/UnifiedOrdersView.vue index 8ce46415..4146c801 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.vue +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.vue @@ -16,7 +16,7 @@ import { type UnifiedOrderMainStatus, type UnifiedOrderResource, } from '@/modules/unified-orders/api/unifiedOrders' -import { createPlatformCoLoadGroup, getPlatformCoLoadGroupByInboundCode, type CoLoadBusinessSummary, type CoLoadGroup } from '@/modules/unified-orders/api/coLoadGroups' +import { cancelPlatformCoLoadGroup, createPlatformCoLoadGroup, getPlatformCoLoadGroupByInboundCode, type CoLoadBusinessSummary, type CoLoadGroup } from '@/modules/unified-orders/api/coLoadGroups' import OrderFulfillmentTreePanel from '@/modules/unified-orders/components/OrderFulfillmentTreePanel.vue' import InboundFeeDetailManager from '@/modules/unified-orders/components/InboundFeeDetailManager.vue' import { useAccessPreviewStore } from '@/core/access/accessPreview' @@ -131,6 +131,8 @@ const coLoadDetailInboundCode = ref('') const coLoadDetailSummary = ref(null) const coLoadDetailLoading = ref(false) const coLoadDetailError = ref(null) +const coLoadCancelError = ref(null) +const coLoadCancelSaving = ref(false) const coLoadSaving = ref(false) const coLoadError = ref(null) const approvalOrder = ref(null) @@ -813,6 +815,19 @@ function handleOrderSelectionChange(rows: UnifiedOrderResource[]) { selectedApprovalOrders.value = rows.filter(canApprove) } +const coLoadSelectionProblem = computed(() => { + if (selectedOrders.value.length < 2) return null + if (selectedOrders.value.some((row) => row.coLoadGroup)) return '所选订单中已有同柜组,请先解除原同柜组' + const warehouseIds = selectedOrders.value.map((row) => String(row.receivingWarehouseId || '').trim()) + if (warehouseIds.some((value) => !value)) return '所选订单缺少平台收货仓,请刷新后重试' + if (new Set(warehouseIds).size > 1) return '所选订单的平台收货仓不同,不能设为同柜组' + const channels = selectedOrders.value.map((row) => String(row.channelName || '').trim()) + if (channels.some((value) => !value)) return '所选订单缺少渠道,请刷新后重试' + if (new Set(channels).size > 1) return '所选订单的渠道不同,不能设为同柜组' + return null +}) +const canCreateCoLoadGroup = computed(() => selectedOrders.value.length >= 2 && !coLoadSelectionProblem.value) + function coLoadGroup(row: UnifiedOrderResource) { return row.coLoadGroup || null } @@ -860,18 +875,22 @@ function openCoLoadDetail(row: UnifiedOrderResource) { coLoadDetailInboundCode.value = row.inboundCode?.trim() || '' coLoadDetailOpen.value = true coLoadDetailSummary.value = null + coLoadCancelError.value = null coLoadDetailError.value = coLoadDetailInboundCode.value ? null : getApiErrorDetails(new Error('同柜组成员入仓码尚未返回'), '同柜组成员入仓码尚未返回,请刷新后重试') if (coLoadDetailInboundCode.value) void loadCoLoadDetail() } function openCoLoadCreate() { - if (selectedOrders.value.length < 2) return + if (!canCreateCoLoadGroup.value) { + if (coLoadSelectionProblem.value) ElMessage.warning(coLoadSelectionProblem.value) + return + } coLoadError.value = null coLoadCreateOpen.value = true } async function createCoLoadGroup() { - if (selectedOrders.value.length < 2 || coLoadSaving.value) return + if (!canCreateCoLoadGroup.value || coLoadSaving.value) return coLoadSaving.value = true coLoadError.value = null try { @@ -886,6 +905,33 @@ async function createCoLoadGroup() { } } +async function cancelCoLoadGroup() { + const inboundCode = coLoadDetailInboundCode.value + if (!inboundCode || coLoadDetailSummary.value?.status !== 'ACTIVE' || coLoadCancelSaving.value) return + try { + await ElMessageBox.confirm( + '确认解除整个同柜组吗?解除后这些订单可重新选择成员建组;不会拆分订单或子单。', + '解除同柜组', + { confirmButtonText: '确认解除', cancelButtonText: '取消', type: 'warning' }, + ) + } catch (error) { + if (error === 'cancel' || error === 'close') return + throw error + } + coLoadCancelSaving.value = true + coLoadCancelError.value = null + try { + await cancelPlatformCoLoadGroup(inboundCode, '重新调整同柜组成员') + coLoadDetailOpen.value = false + ElMessage.success('同柜组已整组解除') + await loadOrders() + } catch (error) { + coLoadCancelError.value = getApiErrorDetails(error, '解除同柜组失败') + } finally { + coLoadCancelSaving.value = false + } +} + async function batchApproveOrders() { const selected = [...selectedApprovalOrders.value] if (!selected.length || batchApprovalSaving.value) return @@ -1372,7 +1418,10 @@ onBeforeUnmount(() => { - +
{{ row.inboundCode || '待生成入仓码' }}
- + + @@ -1522,6 +1573,8 @@ onBeforeUnmount(() => { .coload-detail-table td strong,.coload-detail-table td span { display: block; overflow-wrap: anywhere; } .coload-detail-table td span { color: var(--cc-muted); } .coload-detail-table :deep(.el-button) { min-height: 24px; padding: 2px 0; } +.coload-toolbar-action { display: flex; min-width: 0; align-items: center; gap: 8px; } +.coload-toolbar-action small { max-width: 360px; overflow: hidden; color: var(--el-color-danger); font-size: 12px; text-overflow: ellipsis; white-space: nowrap; } .subsequent-fulfillment { min-height: 72px; margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--cc-border); } .subsequent-fulfillment h3 { margin: 0 0 10px; font-size: 15px; } .fulfillment-facts { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin: 0 0 10px; border: 1px solid var(--cc-border); border-radius: 6px; overflow: hidden; } From afbee83b5103a116db23b85ab6dae8602986a2c5 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 31 Aug 2026 15:36:02 +0800 Subject: [PATCH 44/57] style(admin): align list typography with orders --- src/shared/components/DataTable.test.ts | 32 ++++++++++++-- src/shared/components/DataTable.vue | 8 +++- src/shared/components/QueryToolbar.vue | 1 + src/styles/adminListTypography.test.ts | 55 +++++++++++++++++++++++++ src/styles/foundation.css | 12 ++++++ src/styles/index.css | 12 +++--- src/styles/shell.css | 4 +- 7 files changed, 111 insertions(+), 13 deletions(-) create mode 100644 src/styles/adminListTypography.test.ts diff --git a/src/shared/components/DataTable.test.ts b/src/shared/components/DataTable.test.ts index 96ef7460..0a8c1f87 100644 --- a/src/shared/components/DataTable.test.ts +++ b/src/shared/components/DataTable.test.ts @@ -6,14 +6,14 @@ import LayoutPreferenceList from './LayoutPreferenceList.vue' import { UI_PREFERENCE_SCOPE, preferenceStorageKey } from '@/shared/utils/uiLayoutPreference' const TableStub = defineComponent({ - props: { border: Boolean, stripe: Boolean, maxHeight: [String, Number] }, - template: '
', + props: { border: Boolean, stripe: Boolean, maxHeight: [String, Number], emptyText: String }, + template: '
', methods: { doLayout() {} }, }) const ColumnStub = defineComponent({ inheritAttrs: false, - props: { label: String, prop: String }, - template: '
', + props: { label: String, prop: String, fixed: [String, Boolean], showOverflowTooltip: Boolean }, + template: '
', }) const ButtonStub = defineComponent({ template: '' }) const PopoverStub = defineComponent({ template: '
' }) @@ -128,6 +128,30 @@ describe('DataTable column preferences', () => { expect(customTable.get('.table-stub').attributes('data-max-height')).toBe('420') }) + it('keeps fixed actions, long text, empty state, and pagination in the shared list contract', () => { + const wrapper = shallowMount(DataTable, { + props: { + data: [], + columns: [ + { key: 'actions', label: '操作', fixed: 'left', slotName: 'actions' }, + { key: 'description', label: '长文本', showOverflowTooltip: true }, + ], + emptyText: '暂无符合条件的数据', + total: 0, + }, + global: { + stubs: { ElTable: TableStub, ElTableColumn: ColumnStub, ElButton: ButtonStub, ElPopover: PopoverStub }, + directives: { loading: {} }, + }, + }) + + const columns = wrapper.findAll('.column-stub') + expect(columns[0].attributes('data-fixed')).toBe('left') + expect(columns[1].attributes('data-overflow-tooltip')).toBe('true') + expect(wrapper.get('.table-stub').attributes('data-empty-text')).toBe('暂无符合条件的数据') + expect(wrapper.get('.data-table__pagination').text()).toContain('共 0 条') + }) + it('uses key as the data property for ordinary columns only', () => { const wrapper = shallowMount(DataTable, { props: { diff --git a/src/shared/components/DataTable.vue b/src/shared/components/DataTable.vue index 510ed917..38d81ccd 100644 --- a/src/shared/components/DataTable.vue +++ b/src/shared/components/DataTable.vue @@ -358,6 +358,10 @@ function setPage(value: number) { diff --git a/src/modules/ocean-bookings/components/OceanBookingPrintDialog.test.ts b/src/modules/ocean-bookings/components/OceanBookingPrintDialog.test.ts new file mode 100644 index 00000000..801ff8a8 --- /dev/null +++ b/src/modules/ocean-bookings/components/OceanBookingPrintDialog.test.ts @@ -0,0 +1,57 @@ +import ElementPlus from 'element-plus' +import { flushPromises, mount } from '@vue/test-utils' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import OceanBookingPrintDialog from './OceanBookingPrintDialog.vue' +import { downloadOceanBookingPrint } from '../api/platformOceanBookings' +import { ApiResponseError } from '@/shared/api/http' + +vi.mock('../api/platformOceanBookings', () => ({ downloadOceanBookingPrint: vi.fn() })) + +describe('OceanBookingPrintDialog', () => { + beforeEach(() => { + vi.clearAllMocks() + document.body.innerHTML = '' + vi.stubGlobal('URL', { createObjectURL: vi.fn(() => 'blob:booking-print'), revokeObjectURL: vi.fn() }) + }) + + it('offers the two confirmed documents and previews the selected PDF', async () => { + vi.mocked(downloadOceanBookingPrint).mockResolvedValue(new Blob(['pdf'], { type: 'application/pdf' })) + const wrapper = mount(OceanBookingPrintDialog, { + props: { modelValue: true, bookingId: '123', bookingNo: 'OB-123', allowed: true }, + global: { plugins: [ElementPlus] }, + attachTo: document.body, + }) + await flushPromises() + + expect(document.body.textContent).toContain('出口货物托运单') + expect(document.body.textContent).toContain('提单确认件') + await wrapper.findAll('.booking-print-option')[0]!.trigger('click') + await flushPromises() + await Array.from(document.body.querySelectorAll('button')).find((button) => button.textContent?.includes('预览 PDF'))?.click() + await flushPromises() + + expect(downloadOceanBookingPrint).toHaveBeenCalledWith('123', 'SHIPPING_INSTRUCTION') + expect(document.body.querySelector('iframe')?.getAttribute('src')).toBe('blob:booking-print') + wrapper.unmount() + }) + + it('shows permission and API errors without attempting a download', async () => { + const denied = mount(OceanBookingPrintDialog, { props: { modelValue: true, bookingId: '123', allowed: false }, global: { plugins: [ElementPlus] }, attachTo: document.body }) + await flushPromises() + expect(document.body.textContent).toContain('暂无订舱单据打印权限') + expect(downloadOceanBookingPrint).not.toHaveBeenCalled() + denied.unmount() + + vi.mocked(downloadOceanBookingPrint).mockRejectedValue(new ApiResponseError('缺少提单号', 'B0409', 200, undefined, 'req-print', 'trace-print')) + const failed = mount(OceanBookingPrintDialog, { props: { modelValue: true, bookingId: '123', allowed: true }, global: { plugins: [ElementPlus] }, attachTo: document.body }) + await flushPromises() + await failed.findAll('.booking-print-option')[1]!.trigger('click') + await flushPromises() + await Array.from(document.body.querySelectorAll('button')).find((button) => button.textContent?.includes('预览 PDF'))?.click() + await flushPromises() + expect(document.body.textContent).toContain('缺少提单号') + expect(document.body.textContent).toContain('req-print') + expect(document.body.textContent).toContain('trace-print') + failed.unmount() + }) +}) diff --git a/src/modules/ocean-bookings/components/OceanBookingPrintDialog.vue b/src/modules/ocean-bookings/components/OceanBookingPrintDialog.vue new file mode 100644 index 00000000..4927085f --- /dev/null +++ b/src/modules/ocean-bookings/components/OceanBookingPrintDialog.vue @@ -0,0 +1,108 @@ + + +