优化
This commit is contained in:
@@ -1,14 +1,14 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { http } from '@/shared/api/http'
|
||||
import { batchQueryPlatformCoLoadGroups, cancelPlatformCoLoadGroup, createPlatformCoLoadGroup } from './coLoadGroups'
|
||||
import { cancelPlatformCoLoadGroup, createPlatformCoLoadGroup, getPlatformCoLoadGroupByInboundCode } from './coLoadGroups'
|
||||
|
||||
const group = {
|
||||
groupId: 2093000000000000001n, groupNo: 'CLG-2093000000000000001', status: 'ACTIVE',
|
||||
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, inboundCode: 'NYEXX2608310001', fullPackageCount: 8 },
|
||||
{ tenantId: 2093000000000000007n, orderId: 2093000000000000008n, inboundId: 2093000000000000009n, inboundCode: 'NYEXX2608310002', fullPackageCount: 10 },
|
||||
{ tenantId: 2093000000000000004n, orderId: 2093000000000000005n, inboundId: 2093000000000000006n, fullPackageCount: 8 },
|
||||
{ tenantId: 2093000000000000007n, orderId: 2093000000000000008n, inboundId: 2093000000000000009n, fullPackageCount: 10 },
|
||||
],
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ 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 () => {
|
||||
@@ -36,46 +35,26 @@ describe('co-load group API', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('batch-enriches three list rows and normalizes every long id without numeric coercion', async () => {
|
||||
const third = { ...group.members[0], orderId: 2093000000000000010n, inboundId: 2093000000000000011n }
|
||||
const threeMemberGroup = { ...group, fullPackageCount: 23, members: [...group.members, third] }
|
||||
const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: threeMemberGroup.members.map((member) => ({ orderId: member.orderId, group: threeMemberGroup })) })
|
||||
const orderIds = ['2093000000000000005', '2093000000000000008', '2093000000000000010']
|
||||
const result = await batchQueryPlatformCoLoadGroups(orderIds)
|
||||
expect(post).toHaveBeenCalledWith('/platform/v1/first-leg/coload-groups/batch-query', { orderIds })
|
||||
expect(result).toHaveLength(3)
|
||||
expect(result[2]).toMatchObject({ orderId: '2093000000000000010', group: { groupId: '2093000000000000001', fullPackageCount: 23 } })
|
||||
expect(result[2]).not.toHaveProperty('inboundId')
|
||||
})
|
||||
|
||||
it('keeps every inbound fragment when one order has multiple current inbounds', async () => {
|
||||
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 },
|
||||
{ orderId: group.members[1].orderId, group: multiInboundGroup },
|
||||
] })
|
||||
const result = await batchQueryPlatformCoLoadGroups(['2093000000000000005', '2093000000000000008'])
|
||||
expect(post).toHaveBeenCalledTimes(1)
|
||||
expect(result[0].group.members).toEqual([
|
||||
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 }),
|
||||
])
|
||||
it('loads the business-only group summary by inbound code without platform batch-query', async () => {
|
||||
const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'ok', data: {
|
||||
status: 'LOCKED', memberCount: '2', members: [{ inboundCode: ' NYEXX2608310001 ' }, { inboundCode: 'NYEXX2608310002' }],
|
||||
} })
|
||||
const post = vi.spyOn(http, 'post')
|
||||
const result = await getPlatformCoLoadGroupByInboundCode('NYEXX2608310001')
|
||||
expect(get).toHaveBeenCalledWith('/platform/v1/first-leg/coload-groups/by-inbound-code/NYEXX2608310001')
|
||||
expect(result).toEqual({ status: 'LOCKED', memberCount: 2, members: [{ inboundCode: 'NYEXX2608310001' }, { inboundCode: 'NYEXX2608310002' }] })
|
||||
expect(post).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('normalizes missing and blank member inbound codes without exposing technical ids as fallbacks', async () => {
|
||||
const incompleteGroup = {
|
||||
...group,
|
||||
const incompleteSummary = {
|
||||
status: 'ACTIVE', memberCount: 4,
|
||||
members: [
|
||||
{ ...group.members[0], inboundCode: undefined },
|
||||
{ ...group.members[1], inboundCode: null },
|
||||
{ ...group.members[1], inboundId: 2093000000000000012n, inboundCode: ' ' },
|
||||
{ ...group.members[1], inboundId: 2093000000000000013n, inboundCode: ' NYEXX2608310003 ' },
|
||||
{ inboundCode: undefined }, { inboundCode: null }, { inboundCode: ' ' }, { inboundCode: ' NYEXX2608310003 ' },
|
||||
],
|
||||
}
|
||||
vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: incompleteGroup })
|
||||
const result = await createPlatformCoLoadGroup(['2093000000000000005', '2093000000000000008'])
|
||||
vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'ok', data: incompleteSummary })
|
||||
const result = await getPlatformCoLoadGroupByInboundCode('NYEXX2608310001')
|
||||
expect(result.members.map((member) => member.inboundCode)).toEqual(['', '', '', 'NYEXX2608310003'])
|
||||
expect(result.members.map((member) => member.inboundCode)).not.toContain('2093000000000000012')
|
||||
})
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { flushPromises, shallowMount } from '@vue/test-utils'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ApiResponseError } from '@/shared/api/http'
|
||||
import { listPlatformUnifiedOrders } from '@/modules/unified-orders/api/unifiedOrders'
|
||||
import { batchQueryPlatformCoLoadGroups, cancelPlatformCoLoadGroup, createPlatformCoLoadGroup } from '@/modules/unified-orders/api/coLoadGroups'
|
||||
import { createPlatformCoLoadGroup, getPlatformCoLoadGroupByInboundCode } from '@/modules/unified-orders/api/coLoadGroups'
|
||||
import UnifiedOrdersView from './UnifiedOrdersView.vue'
|
||||
|
||||
vi.mock('vue-router', () => ({ useRoute: () => ({ params: {}, query: {} }), useRouter: () => ({ push: vi.fn() }) }))
|
||||
@@ -18,7 +17,7 @@ vi.mock('@/modules/unified-orders/api/unifiedOrders', () => ({
|
||||
unifiedOrderStatusOptions: [{ value: 'WAIT_SPLIT', label: '待分柜' }],
|
||||
}))
|
||||
vi.mock('@/modules/unified-orders/api/coLoadGroups', () => ({
|
||||
batchQueryPlatformCoLoadGroups: vi.fn(), cancelPlatformCoLoadGroup: vi.fn(), createPlatformCoLoadGroup: vi.fn(),
|
||||
createPlatformCoLoadGroup: vi.fn(), getPlatformCoLoadGroupByInboundCode: vi.fn(),
|
||||
}))
|
||||
|
||||
const rows = ['5001', '5002'].map((orderId, index) => ({
|
||||
@@ -30,13 +29,8 @@ const rows = ['5001', '5002'].map((orderId, index) => ({
|
||||
createdAt: null, updatedAt: null, expectedArrivalAt: null, feeReviewStatus: null, requestedPickupAt: null, totalPalletCount: null,
|
||||
chargeableVolumeCbm: null, volumetricWeightKg: null, totalNetWeightKg: null, measurementCompletedAt: null, inboundCompletedAt: null,
|
||||
expressCompany: null, expressTrackingNo: null, skuNos: null, totalQuantity: null, shipmentId: null, referenceId: null,
|
||||
coLoadGroup: null,
|
||||
}))
|
||||
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), inboundCode: String(row.inboundCode), fullPackageCount: 5 })),
|
||||
createdAt: '2026-08-29T10:00:00', updatedAt: '2026-08-29T10:00:00',
|
||||
}
|
||||
|
||||
function mountView() {
|
||||
return shallowMount(UnifiedOrdersView, {
|
||||
global: { directives: { loading: () => undefined }, stubs: {
|
||||
@@ -58,7 +52,6 @@ describe('UnifiedOrdersView co-load group behavior', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: rows, page: 1, size: 20, total: 2, pages: 1 })
|
||||
vi.mocked(batchQueryPlatformCoLoadGroups).mockResolvedValue([])
|
||||
})
|
||||
|
||||
it('keeps both selected orders and backend tracing evidence when create validation fails', async () => {
|
||||
@@ -86,24 +79,22 @@ describe('UnifiedOrdersView co-load group behavior', () => {
|
||||
expect(wrapper.text()).not.toContain('ORD-2')
|
||||
expect(wrapper.text()).not.toContain('5001')
|
||||
expect(wrapper.text()).not.toContain('7000')
|
||||
expect(wrapper.text()).not.toContain('履约详情')
|
||||
expect(wrapper.text()).not.toContain('关联数据')
|
||||
})
|
||||
|
||||
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', 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 rowsWithGroup = rows.map((row) => ({ ...row, coLoadGroup: { status: 'ACTIVE', memberCount: 2, memberInboundCodes: ['IN-1', 'IN-1-B', 'IN-2'] } }))
|
||||
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: rowsWithGroup, page: 1, size: 20, total: 2, pages: 1 })
|
||||
vi.mocked(getPlatformCoLoadGroupByInboundCode).mockResolvedValue({ status: 'ACTIVE', memberCount: 3, members: [{ inboundCode: 'IN-1' }, { inboundCode: 'IN-1-B' }, { inboundCode: 'IN-2' }] })
|
||||
const wrapper = mountView()
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('同柜组 · 2 票 · 未锁定')
|
||||
expect(wrapper.text()).not.toContain('CLG-9001')
|
||||
await wrapper.get('[aria-label="查看同柜组 · 2 票 · 未锁定成员"]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(getPlatformCoLoadGroupByInboundCode).toHaveBeenCalledWith('IN-1')
|
||||
expect(wrapper.text()).toContain('3 个成员')
|
||||
expect(wrapper.text()).toContain('13 箱')
|
||||
expect(wrapper.text()).toContain('IN-1')
|
||||
expect(wrapper.text()).toContain('IN-1-B')
|
||||
expect(wrapper.text()).toContain('IN-2')
|
||||
@@ -111,10 +102,6 @@ describe('UnifiedOrdersView co-load group behavior', () => {
|
||||
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')
|
||||
await flushPromises()
|
||||
expect(cancelPlatformCoLoadGroup).toHaveBeenCalledWith('9001', 0, '计划调整')
|
||||
expect(ElMessageBox.prompt).toHaveBeenCalledWith(expect.stringContaining('不支持单独移除成员'), '取消同柜不可拆组', expect.any(Object))
|
||||
})
|
||||
|
||||
it.each([
|
||||
@@ -122,14 +109,13 @@ describe('UnifiedOrdersView co-load group behavior', () => {
|
||||
['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 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: inboundCode as unknown as string }, { inboundCode: 'IN-2' }] })
|
||||
const wrapper = mountView()
|
||||
await flushPromises()
|
||||
await wrapper.get('[aria-label="查看同柜组 · 2 票 · 未锁定成员"]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('同柜组成员入仓码尚未返回,请刷新后重试')
|
||||
expect(wrapper.text()).not.toContain('CLG-9001')
|
||||
expect(wrapper.text()).not.toContain('ORD-1')
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import { flushPromises, shallowMount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getLoadingAssociationSummary, getOverseasInboundCodeRelationship } from '@/shared/api/inboundCodeRelations'
|
||||
import { getPlatformUnifiedOrder } from '@/modules/unified-orders/api/unifiedOrders'
|
||||
import UnifiedOrdersView from './UnifiedOrdersView.vue'
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
useRoute: () => ({ params: { orderId: '5001' }, query: { tenantId: '6001' } }),
|
||||
useRouter: () => ({ push: vi.fn() }),
|
||||
}))
|
||||
vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) }))
|
||||
vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn().mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }) }))
|
||||
vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn().mockResolvedValue([]) }))
|
||||
vi.mock('@/modules/inbounds', () => ({
|
||||
downloadWeightImage: vi.fn(), getInboundMeasurement: vi.fn(),
|
||||
InboundFeePanel: { template: '<div />' }, InboundMeasurementDialog: { template: '<div />' }, MeasurementEvidenceThumbnail: { template: '<div />' },
|
||||
}))
|
||||
vi.mock('@/modules/unified-orders/workflows', () => ({ UnifiedOrderWorkflowPanel: { template: '<div />' } }))
|
||||
vi.mock('@/modules/unified-orders/api/coLoadGroups', () => ({
|
||||
createPlatformCoLoadGroup: vi.fn(), getPlatformCoLoadGroupByInboundCode: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/shared/api/inboundCodeRelations', () => ({
|
||||
getLoadingAssociationSummary: vi.fn(), getOverseasInboundCodeRelationship: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/modules/unified-orders/api/unifiedOrders', () => ({
|
||||
addPlatformUnifiedOrderNote: vi.fn(), approvePlatformUnifiedOrder: vi.fn(), batchApprovePlatformUnifiedOrders: vi.fn(),
|
||||
getPlatformUnifiedOrder: vi.fn(), listPlatformUnifiedOrderFeeDetails: vi.fn(), listPlatformUnifiedOrders: vi.fn(),
|
||||
unifiedOrderStatusOptions: [],
|
||||
}))
|
||||
|
||||
const detail = {
|
||||
summary: {
|
||||
orderId: '5001', tenantId: '6001', orderNo: 'ORD-HIDDEN', mainStatus: 'WAIT_SPLIT', nextAction: null, relation: null,
|
||||
internalOrderStatus: 'WAIT_SPLIT', customerId: '6001', customerName: '测试客户', channelName: '纽约渠道',
|
||||
receivingWarehouseId: '8', receivingWarehouseName: '义乌仓', destinationWarehouseId: '9', destinationWarehouseName: 'ONT8',
|
||||
inboundId: '7001', inboundVersion: 1, inboundCode: 'NYEXX2608310001', expectedShipDate: null, totalPackageCount: 2,
|
||||
locationCodes: null, actualGrossWeightKg: null, actualVolumeCbm: null, measurement: null, preload: null, loading: null, delivery: null, transport: null,
|
||||
coLoadGroup: null, createdAt: null, updatedAt: null, expectedArrivalAt: null, feeReviewStatus: null, requestedPickupAt: null,
|
||||
totalPalletCount: null, chargeableVolumeCbm: null, volumetricWeightKg: null, totalNetWeightKg: null,
|
||||
measurementCompletedAt: null, inboundCompletedAt: null, expressCompany: null, expressTrackingNo: null, skuNos: null,
|
||||
totalQuantity: null, shipmentId: null, referenceId: null,
|
||||
},
|
||||
order: { order: null, cargoes: [], parties: [], internalNotes: [], statusHistory: [] },
|
||||
timeline: [],
|
||||
}
|
||||
|
||||
function mountDetail() {
|
||||
return shallowMount(UnifiedOrdersView, {
|
||||
global: {
|
||||
directives: { loading: () => undefined },
|
||||
stubs: {
|
||||
AdminPage: { template: '<main><slot /></main>' },
|
||||
DetailPageLayout: { template: '<section><slot /></section>' },
|
||||
OrderFulfillmentTreePanel: { template: '<div data-testid="fulfillment-tree" />' },
|
||||
LoadErrorState: { props: ['title', 'message'], template: '<div>{{ title }} {{ message }}<slot name="details" /></div>' },
|
||||
ApiErrorMeta: true,
|
||||
StatusBadge: true,
|
||||
'el-button': { template: '<button @click="$emit(\'click\')"><slot /></button>' },
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
describe('UnifiedOrdersView downstream fulfillment detail', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(getPlatformUnifiedOrder).mockResolvedValue(detail as never)
|
||||
vi.mocked(getLoadingAssociationSummary).mockResolvedValue({
|
||||
inboundId: '7001', inboundCode: 'NYEXX2608310001', loadingPlanId: null, loadingPlanNo: null,
|
||||
containerBookingId: null, cabinetNo: null, containerNo: null, loadingStatus: null, taskId: null,
|
||||
pdaTaskStatus: null, coLoadGroupId: null, coLoadGroupNo: null, items: [],
|
||||
})
|
||||
vi.mocked(getOverseasInboundCodeRelationship).mockResolvedValue({
|
||||
inboundCode: 'NYEXX2608310001', found: false, customer: null, overseasWarehouse: null,
|
||||
overseasReceivingStatus: null, taillegOrders: [], inboundPlans: [], palletCodes: [], inventory: [],
|
||||
})
|
||||
})
|
||||
|
||||
it('shows a compact empty state in order detail when no downstream relationship exists', async () => {
|
||||
const wrapper = mountDetail()
|
||||
await flushPromises()
|
||||
await wrapper.findAll('.detail-tabs button').find((button) => button.text().includes('拆分履约'))!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(getLoadingAssociationSummary).toHaveBeenCalledWith('NYEXX2608310001')
|
||||
expect(getOverseasInboundCodeRelationship).toHaveBeenCalledWith('NYEXX2608310001')
|
||||
expect(wrapper.text()).toContain('暂无后续履约数据')
|
||||
expect(wrapper.text()).not.toContain('新增关联')
|
||||
})
|
||||
|
||||
it('renders actual loading and overseas business identifiers inside order detail', async () => {
|
||||
vi.mocked(getLoadingAssociationSummary).mockResolvedValue({
|
||||
inboundId: '7001', inboundCode: 'NYEXX2608310001', loadingPlanId: '9001', loadingPlanNo: 'LP-260831-001',
|
||||
containerBookingId: '9002', cabinetNo: 'CAB-260831-001', containerNo: 'MSCU1234567', loadingStatus: 'LOADING',
|
||||
taskId: '9003', pdaTaskStatus: 'IN_PROGRESS', coLoadGroupId: null, coLoadGroupNo: null, items: [],
|
||||
})
|
||||
vi.mocked(getOverseasInboundCodeRelationship).mockResolvedValue({
|
||||
inboundCode: 'NYEXX2608310001', found: true, customer: null,
|
||||
overseasWarehouse: { warehouseId: '9004', warehouseCode: 'LAX', warehouseName: '洛杉矶海外仓' },
|
||||
overseasReceivingStatus: 'RECEIVING', taillegOrders: [{ orderNo: 'LL-260831-001' }],
|
||||
inboundPlans: [{ planNo: 'IP-260831-001' }], palletCodes: ['PLT-LAX-001'],
|
||||
inventory: [{ skuCode: 'SKU-001', packageCount: 2 }],
|
||||
})
|
||||
const wrapper = mountDetail()
|
||||
await flushPromises()
|
||||
await wrapper.findAll('.detail-tabs button').find((button) => button.text().includes('拆分履约'))!.trigger('click')
|
||||
await flushPromises()
|
||||
expect(wrapper.text()).toContain('LP-260831-001')
|
||||
expect(wrapper.text()).toContain('MSCU1234567')
|
||||
expect(wrapper.text()).toContain('LL-260831-001')
|
||||
expect(wrapper.text()).toContain('IP-260831-001')
|
||||
expect(wrapper.text()).toContain('PLT-LAX-001')
|
||||
expect(wrapper.text()).not.toContain('9001')
|
||||
expect(wrapper.text()).not.toContain('9004')
|
||||
})
|
||||
})
|
||||
@@ -13,9 +13,9 @@ vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn().mock
|
||||
vi.mock('@/modules/inbounds', () => ({ downloadWeightImage: vi.fn(), getInboundMeasurement: vi.fn() }))
|
||||
vi.mock('@/modules/unified-orders/workflows', () => ({ UnifiedOrderWorkflowPanel: { template: '<div />' } }))
|
||||
vi.mock('@/modules/unified-orders/api/coLoadGroups', () => ({
|
||||
batchQueryPlatformCoLoadGroups: vi.fn().mockResolvedValue([]),
|
||||
cancelPlatformCoLoadGroup: vi.fn(),
|
||||
createPlatformCoLoadGroup: vi.fn(),
|
||||
getPlatformCoLoadGroupByInboundCode: vi.fn(),
|
||||
}))
|
||||
vi.mock('@/modules/unified-orders/api/unifiedOrders', () => ({
|
||||
addPlatformUnifiedOrderNote: vi.fn(),
|
||||
|
||||
@@ -183,10 +183,10 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).toContain("inboundCode || '待生成入仓码'")
|
||||
expect(source).toContain('class="coload-tag-trigger"')
|
||||
expect(source).toContain('coLoadGroupSummary(coLoadGroup(row as UnifiedOrderResource)!)')
|
||||
expect(source).not.toContain('coLoadDetailGroup.groupNo')
|
||||
expect(source).not.toContain('coLoadDetailGroup')
|
||||
expect(source).not.toContain('<span v-else>—</span></template>\n <template #orderStatus')
|
||||
expect(source).toContain('v-for="member in coLoadDetailMembers"')
|
||||
expect(source).toContain('<th>入仓码</th><th>箱数</th>')
|
||||
expect(source).toContain('<thead><tr><th>入仓码</th></tr></thead>')
|
||||
expect(source).toContain('{{ member.inboundCode }}')
|
||||
expect(source).toContain('同柜组成员入仓码尚未返回,请刷新后重试')
|
||||
expect(source).toContain('coLoadDetailHasMissingInboundCodes')
|
||||
@@ -196,11 +196,12 @@ describe('UnifiedOrdersView contract', () => {
|
||||
expect(source).not.toContain('客户 ID')
|
||||
})
|
||||
|
||||
it('names the cross-domain relationship entry as fulfillment details', () => {
|
||||
expect(source).toContain('>履约详情</el-button>')
|
||||
expect(source).toContain('aria-label="查看该入仓码的订单、装柜、尾程与库存关系"')
|
||||
expect(source).toContain('title="查看该入仓码的订单、装柜、尾程与库存关系"')
|
||||
it('does not expose a generic relationship probe in list rows', () => {
|
||||
expect(source).not.toContain('>履约详情</el-button>')
|
||||
expect(source).not.toContain('>关联数据</el-button>')
|
||||
expect(source).not.toContain('openInboundRelationship')
|
||||
expect(source).not.toContain('inboundRelationshipDialogOpen')
|
||||
expect(source).not.toContain('<InboundCodeRelationshipDialog')
|
||||
})
|
||||
|
||||
it('keeps editable query values separate from the applied list request', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch, type Component } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Eye, GitBranch, History, ListChecks, MoreHorizontal, PackageOpen, Ruler, SlidersHorizontal, StickyNote, WalletCards } from '@lucide/vue'
|
||||
import { ArrowLeft, Box, ChevronDown, ChevronUp, CircleCheck, Eye, GitBranch, History, Link2, ListChecks, MoreHorizontal, PackageOpen, Ruler, SlidersHorizontal, StickyNote, WalletCards } from '@lucide/vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
getPlatformUnifiedOrder,
|
||||
@@ -23,6 +23,7 @@ import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam'
|
||||
import { listAllPlatformWarehouses, type PlatformWarehouseSummaryResource } from '@/modules/warehouses'
|
||||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||||
import { getLoadingAssociationSummary, getOverseasInboundCodeRelationship, type LoadingAssociationSummary, type OverseasInboundCodeRelationship } from '@/shared/api/inboundCodeRelations'
|
||||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
@@ -153,10 +154,16 @@ const cargoImagePreviewUrls = new Set<string>()
|
||||
const measurementDialogOpen = ref(false)
|
||||
const smartLoadingOpen = ref(false)
|
||||
const measurementOrder = ref<UnifiedOrderResource | null>(null)
|
||||
const loadingAssociation = ref<LoadingAssociationSummary | null>(null)
|
||||
const overseasRelationship = ref<OverseasInboundCodeRelationship | null>(null)
|
||||
const loadingAssociationError = ref<ApiErrorDetails | null>(null)
|
||||
const overseasRelationshipError = ref<ApiErrorDetails | null>(null)
|
||||
const subsequentFulfillmentLoading = ref(false)
|
||||
let detailLoadingKey = ''
|
||||
let detailRequestSerial = 0
|
||||
let measurementRequestSerial = 0
|
||||
let measurementPreviewRequestSerial = 0
|
||||
let subsequentFulfillmentRequestSerial = 0
|
||||
const showAdvancedFilters = ref(false)
|
||||
const filters = ref<FilterModel>(emptyFilters())
|
||||
const appliedFilters = ref<FilterModel>(emptyFilters())
|
||||
@@ -588,6 +595,36 @@ async function loadMeasurementDetails() {
|
||||
function selectDetailTab(key: string) {
|
||||
activeDetailTab.value = key
|
||||
if (key === 'measurement') void loadMeasurementDetails()
|
||||
if (key === 'fulfillment') void loadSubsequentFulfillment()
|
||||
}
|
||||
|
||||
const hasLoadingAssociation = computed(() => Boolean(loadingAssociation.value
|
||||
&& (loadingAssociation.value.loadingPlanNo || loadingAssociation.value.containerNo || loadingAssociation.value.cabinetNo
|
||||
|| loadingAssociation.value.loadingStatus || loadingAssociation.value.pdaTaskStatus)))
|
||||
const hasOverseasRelationship = computed(() => Boolean(overseasRelationship.value?.found
|
||||
&& (overseasRelationship.value.overseasWarehouse || overseasRelationship.value.overseasReceivingStatus
|
||||
|| overseasRelationship.value.taillegOrders.length || overseasRelationship.value.inboundPlans.length
|
||||
|| overseasRelationship.value.palletCodes.length || overseasRelationship.value.inventory.length)))
|
||||
const hasSubsequentFulfillment = computed(() => hasLoadingAssociation.value || hasOverseasRelationship.value)
|
||||
|
||||
async function loadSubsequentFulfillment() {
|
||||
const inboundCode = detail.value?.summary.inboundCode?.trim() || ''
|
||||
const serial = ++subsequentFulfillmentRequestSerial
|
||||
loadingAssociation.value = null
|
||||
overseasRelationship.value = null
|
||||
loadingAssociationError.value = null
|
||||
overseasRelationshipError.value = null
|
||||
if (!inboundCode) return
|
||||
subsequentFulfillmentLoading.value = true
|
||||
const tasks: Promise<void>[] = []
|
||||
if (can('platform:firstleg:smart-loading:list')) tasks.push(getLoadingAssociationSummary(inboundCode)
|
||||
.then((value) => { if (serial === subsequentFulfillmentRequestSerial) loadingAssociation.value = value })
|
||||
.catch((error) => { if (serial === subsequentFulfillmentRequestSerial) loadingAssociationError.value = getApiErrorDetails(error, '装柜与PDA履约数据加载失败') }))
|
||||
if (can('platform:overseas-warehouse:inventory:list')) tasks.push(getOverseasInboundCodeRelationship(inboundCode)
|
||||
.then((value) => { if (serial === subsequentFulfillmentRequestSerial) overseasRelationship.value = value })
|
||||
.catch((error) => { if (serial === subsequentFulfillmentRequestSerial) overseasRelationshipError.value = getApiErrorDetails(error, '尾程与库存履约数据加载失败') }))
|
||||
await Promise.all(tasks)
|
||||
if (serial === subsequentFulfillmentRequestSerial) subsequentFulfillmentLoading.value = false
|
||||
}
|
||||
|
||||
function toggleMeasurementSection(section: keyof typeof measurementCollapsed) {
|
||||
@@ -989,6 +1026,7 @@ watch(detailRouteKey, (value) => {
|
||||
void loadDetail()
|
||||
} else {
|
||||
detailRequestSerial += 1
|
||||
subsequentFulfillmentRequestSerial += 1
|
||||
detail.value = null
|
||||
detailError.value = null
|
||||
detailLoading.value = false
|
||||
@@ -1170,6 +1208,26 @@ onBeforeUnmount(() => {
|
||||
<template v-else-if="activeDetailTab === 'fulfillment'">
|
||||
<div class="panel-title"><div><h2>拆分履约</h2><p>根单、已完成分片与当前余量</p></div><ListChecks :size="19" /></div>
|
||||
<OrderFulfillmentTreePanel :tenant-id="String(detail.summary.tenantId)" :order-id="String(detail.summary.orderId)" :inbound-code="detail.summary.inboundCode" />
|
||||
<section v-loading="subsequentFulfillmentLoading" class="subsequent-fulfillment" aria-label="后续履约数据">
|
||||
<h3>后续履约数据</h3>
|
||||
<LoadErrorState v-if="loadingAssociationError" title="装柜与PDA履约数据加载失败" :message="loadingAssociationError.message" @retry="loadSubsequentFulfillment"><template #details><ApiErrorMeta :details="loadingAssociationError" /></template></LoadErrorState>
|
||||
<LoadErrorState v-if="overseasRelationshipError" title="尾程与库存履约数据加载失败" :message="overseasRelationshipError.message" @retry="loadSubsequentFulfillment"><template #details><ApiErrorMeta :details="overseasRelationshipError" /></template></LoadErrorState>
|
||||
<dl v-if="hasLoadingAssociation" class="fulfillment-facts">
|
||||
<div><dt>装柜计划</dt><dd>{{ loadingAssociation?.loadingPlanNo || '—' }}</dd></div>
|
||||
<div><dt>柜号</dt><dd>{{ loadingAssociation?.containerNo || loadingAssociation?.cabinetNo || '—' }}</dd></div>
|
||||
<div><dt>装柜状态</dt><dd>{{ loadingAssociation?.loadingStatus || '—' }}</dd></div>
|
||||
<div><dt>PDA状态</dt><dd>{{ loadingAssociation?.pdaTaskStatus || '—' }}</dd></div>
|
||||
</dl>
|
||||
<dl v-if="hasOverseasRelationship" class="fulfillment-facts">
|
||||
<div><dt>海外仓</dt><dd>{{ overseasRelationship?.overseasWarehouse?.warehouseName || '尚未分配海外仓' }}</dd></div>
|
||||
<div><dt>海外收货状态</dt><dd>{{ overseasRelationship?.overseasReceivingStatus || '—' }}</dd></div>
|
||||
<div class="wide"><dt>尾程订单</dt><dd>{{ overseasRelationship?.taillegOrders.map((item) => item.orderNo).join('、') || '—' }}</dd></div>
|
||||
<div class="wide"><dt>入库计划</dt><dd>{{ overseasRelationship?.inboundPlans.map((item) => item.planNo).join('、') || '—' }}</dd></div>
|
||||
<div class="wide"><dt>托盘码</dt><dd>{{ overseasRelationship?.palletCodes.join('、') || '—' }}</dd></div>
|
||||
<div><dt>库存SKU</dt><dd>{{ overseasRelationship?.inventory.length || 0 }} 个</dd></div>
|
||||
</dl>
|
||||
<p v-if="!subsequentFulfillmentLoading && !loadingAssociationError && !overseasRelationshipError && !hasSubsequentFulfillment" class="fulfillment-empty">暂无后续履约数据</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<template v-else-if="activeDetailTab === 'workflow'">
|
||||
@@ -1464,6 +1522,14 @@ 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; }
|
||||
.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; }
|
||||
.fulfillment-facts > div { min-width: 0; padding: 9px 10px; border-right: 1px solid var(--cc-border); background: #fff; }
|
||||
.fulfillment-facts > div.wide { grid-column: span 2; }
|
||||
.fulfillment-facts dt { color: var(--cc-muted); font-size: 12px; }
|
||||
.fulfillment-facts dd { min-width: 0; margin: 4px 0 0; overflow-wrap: anywhere; font-size: 13px; }
|
||||
.fulfillment-empty { margin: 0; padding: 12px; border: 1px dashed var(--cc-border); border-radius: 6px; color: var(--cc-muted); text-align: center; }
|
||||
.table-card :deep(.el-table) { --el-table-border-color: #e5e7eb; --el-table-header-bg-color: #f9fafb; --el-table-row-hover-bg-color: #f3f7fb; --el-table-tr-bg-color: #fff; color: #374151; font-size: 12px; }
|
||||
.table-card :deep(.el-table th.el-table__cell) { height: 42px; padding: 0; background: #f9fafb; color: #374151; font-size: 12px; font-weight: 600; }
|
||||
.table-card :deep(.el-table td.el-table__cell) { min-height: 42px; padding: 9px 0; }
|
||||
|
||||
Reference in New Issue
Block a user