From 50c3a7e1ba8b0777a5b62d53976497fe25ac8462 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 31 Aug 2026 10:48:23 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/GlobalSmartLoadingView.test.ts | 36 +----- .../pages/GlobalSmartLoadingView.vue | 29 ++--- .../pages/SmartLoadingWorkbenchView.test.ts | 30 +---- .../pages/SmartLoadingWorkbenchView.vue | 46 +++---- .../smartLoadingPlanGroups.test.ts | 36 ++++++ .../smartLoadingPlanGroups.ts | 47 +++++++ .../unified-orders/api/coLoadGroups.test.ts | 55 +++------ .../pages/UnifiedOrdersView.coLoad.test.ts | 42 +++---- .../UnifiedOrdersView.fulfillment.test.ts | 115 ++++++++++++++++++ .../pages/UnifiedOrdersView.requests.test.ts | 2 +- .../pages/UnifiedOrdersView.test.ts | 13 +- .../pages/UnifiedOrdersView.vue | 68 ++++++++++- src/shared/api/inboundCodeRelations.ts | 8 ++ 13 files changed, 343 insertions(+), 184 deletions(-) create mode 100644 src/modules/container-loading/smartLoadingPlanGroups.test.ts create mode 100644 src/modules/container-loading/smartLoadingPlanGroups.ts create mode 100644 src/modules/unified-orders/pages/UnifiedOrdersView.fulfillment.test.ts diff --git a/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts b/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts index 7a230e81..d104e424 100644 --- a/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts +++ b/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts @@ -17,7 +17,6 @@ import { removeSmartLoadingGroup, removeSmartLoadingItem, } from '../api/platformContainerLoading' -import { batchQueryPlatformCoLoadGroups } from '@/modules/unified-orders' const { routerPush } = vi.hoisted(() => ({ routerPush: vi.fn() })) @@ -31,9 +30,6 @@ vi.mock('@/core/access/accessPreview', () => ({ vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn().mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }), })) -vi.mock('@/modules/unified-orders', () => ({ - batchQueryPlatformCoLoadGroups: vi.fn().mockResolvedValue([]), -})) vi.mock('../api/platformContainerLoading', () => ({ listContainers: vi.fn(), getSmartLoadingPlanByBooking: vi.fn(), @@ -47,6 +43,7 @@ vi.mock('../api/platformContainerLoading', () => ({ removeSmartLoadingGroup: vi.fn(), pushSmartLoadingPreload: vi.fn(), })) +vi.mock('../smartLoadingPlanGroups', () => ({ loadSmartLoadingPlanCoLoadGroups: vi.fn().mockResolvedValue(new Map()) })) const container = { id: '9007199254740993', planId: '9007199254740994', bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', @@ -70,7 +67,6 @@ describe('GlobalSmartLoadingView', () => { vi.clearAllMocks() vi.mocked(listContainers).mockResolvedValue({ records: [container], page: 1, size: 100, total: 1, pages: 1 }) vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue(plan) - vi.mocked(batchQueryPlatformCoLoadGroups).mockResolvedValue([]) vi.mocked(listSmartLoadingCandidates).mockResolvedValue([{ accountId: '1', inboundId: '5', orderId: '7', orderNo: 'ORD-2', inboundCode: 'IN-2', warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '另一企业', goodsSummary: '鞋类', availablePackageCount: 12, unitGrossWeightKg: 10, unitNetWeightKg: 9, averageUnitVolumeCbm: 0.2, availableVolumeCbm: 2.4, coLoadGroupId: null, groupNo: null, memberCount: null, groupTotalBoxes: null, locked: null, blockReason: null }]) }) @@ -244,36 +240,6 @@ describe('GlobalSmartLoadingView', () => { expect(assignSmartLoadingGroup).not.toHaveBeenCalled() }) - it('aggregates assigned members into one locked group row and removes the group once', async () => { - const baseItems = ['201', '202'].map((inboundId, index) => ({ - ...plan.items[0], id: `70${index}`, accountId: `${index + 1}`, inboundId, inboundDetailId: `80${index}`, orderId: `30${index}`, orderNo: `ORD-${index}`, inboundCode: `NYEXX260829005${index + 1}`, customerName: `客户${index}`, packageCount: 5, grossWeightKg: 50, volumeCbm: 1, loadSequence: index + 1, - })) - const items = [baseItems[0], { ...baseItems[0], id: '703', inboundId: '203', inboundDetailId: '803', inboundCode: 'NYEXX2608290051-B', packageCount: 3, grossWeightKg: 30, volumeCbm: 0.6 }, baseItems[1]] - 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, 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 }))) - vi.mocked(removeSmartLoadingGroup).mockResolvedValue({ ...groupedPlan, version: 3, items: [] }) - vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as never) - const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } }) - await flushPromises() - - expect(wrapper.text()).toContain('CLG-2093539779795795969 · 2 票 · 3 个入仓片段') - expect(wrapper.text()).toContain('ORD-0:NYEXX2608290051、NYEXX2608290051-B') - expect(wrapper.text()).toContain('固定 13 箱 · 已锁定') - expect(wrapper.text()).toContain('整组固定全箱数,不允许单票调整') - expect(wrapper.findAllComponents(ElInputNumber).some((input) => input.props('modelValue') === 13)).toBe(false) - const removeButton = wrapper.findAll('button').find((button) => button.text().includes('移除整组'))! - await removeButton.trigger('click') - await flushPromises() - - expect(removeSmartLoadingGroup).toHaveBeenCalledWith(plan.id, group.groupId, plan.version) - expect(removeSmartLoadingItem).not.toHaveBeenCalled() - }) - it('keeps the package count editor inside its table cell without overflow ellipsis', () => { const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8') expect(source).toContain('
(), { embedded: false }) @@ -55,7 +55,7 @@ const assignOpen = ref(false) const selectedCandidate = ref(null) const assignForm = reactive({ packageCount: 1 }) const quantityDrafts = reactive>({}) -const planGroupsByInboundId = ref(new Map()) +const planGroupsByInboundId = ref(new Map()) const query = ref({ customerId: '', inboundCode: '' }) const appliedQuery = ref({ ...query.value }) let selectionRequest = 0 @@ -95,10 +95,10 @@ function memberOrderSummaries `${order.label}:${order.inboundCodes.join('、')}`) } -function groupOrderCount(group: CoLoadGroup) { return new Set(group.members.map((member) => member.orderId)).size } -function groupMemberLabel(group: CoLoadGroup) { - const orderCount = groupOrderCount(group) - return group.members.length === orderCount ? `${orderCount} 票` : `${orderCount} 票 · ${group.members.length} 个入仓片段` +function groupMemberLabel(group: SmartLoadingPlanCoLoadGroup) { + return group.inboundFragmentCount === group.memberOrderCount + ? `${group.memberOrderCount} 票` + : `${group.memberOrderCount} 票 · ${group.inboundFragmentCount} 个入仓片段` } const displayedCandidates = computed(() => { const rows: SmartLoadingCandidateRow[] = [] @@ -128,7 +128,7 @@ const displayedCandidates = computed(() => { } return rows }) -type SmartLoadingPlanRow = SmartLoadingInboundItem & { coLoadGroup: CoLoadGroup | null; memberInboundCodes: string[]; memberOrderSummaries: string[] } +type SmartLoadingPlanRow = SmartLoadingInboundItem & { coLoadGroup: SmartLoadingPlanCoLoadGroup | null; memberInboundCodes: string[]; memberOrderSummaries: string[] } const displayedPlanItems = computed(() => { const inboundRows = groupSmartLoadingItems(plan.value?.items || []) const result: SmartLoadingPlanRow[] = [] @@ -261,18 +261,6 @@ function acceptPlan(next: SmartLoadingPlan, preserved: Record = if (syncContainerStatus) container.planStatus = next.status } } -async function loadPlanGroups(request = selectionRequest) { - planGroupsByInboundId.value = new Map() - const currentPlan = plan.value - const orderIds = [...new Set((currentPlan?.items || []).map((item) => item.orderId))] - if (!currentPlan || !orderIds.length || !canAssign.value) return - const groups = await batchQueryPlatformCoLoadGroups(orderIds) - if (request !== selectionRequest || plan.value?.id !== currentPlan.id) return - const byInbound = new Map() - for (const info of groups) for (const member of info.group.members) byInbound.set(member.inboundId, info.group) - planGroupsByInboundId.value = byInbound - syncDrafts(currentPlan) -} function clearSelectionData() { plan.value = null candidates.value = [] @@ -303,7 +291,8 @@ async function loadSelectedData(preserved: Record = {}) { candidates.value = candidateResult if (planResult.value) { acceptPlan(planResult.value, preserved, false) - await loadPlanGroups(request) + planGroupsByInboundId.value = await loadSmartLoadingPlanCoLoadGroups(planResult.value) + syncDrafts(planResult.value, preserved) } else noDraft.value = planResult.missing } catch (error) { diff --git a/src/modules/container-loading/pages/SmartLoadingWorkbenchView.test.ts b/src/modules/container-loading/pages/SmartLoadingWorkbenchView.test.ts index 532429b3..3c3dbfb0 100644 --- a/src/modules/container-loading/pages/SmartLoadingWorkbenchView.test.ts +++ b/src/modules/container-loading/pages/SmartLoadingWorkbenchView.test.ts @@ -8,14 +8,13 @@ import { readFileSync } from 'node:fs' import { resolve } from 'node:path' import SmartLoadingWorkbenchView from './SmartLoadingWorkbenchView.vue' import { assignSmartLoadingGroup, assignSmartLoadingItem, getContainer, getSmartLoadingPlanByBooking, listSmartLoadingCandidates, recommendSmartLoading, removeSmartLoadingGroup } from '../api/platformContainerLoading' -import { batchQueryPlatformCoLoadGroups } from '@/modules/unified-orders' vi.mock('../api/platformContainerLoading', () => ({ getContainer: vi.fn(), getSmartLoadingPlanByBooking: vi.fn(), listSmartLoadingCandidates: vi.fn(), createSmartLoadingPlan: vi.fn(), assignSmartLoadingItem: vi.fn(), assignSmartLoadingGroup: vi.fn(), recommendSmartLoading: vi.fn(), adjustSmartLoadingItem: vi.fn(), removeSmartLoadingItem: vi.fn(), removeSmartLoadingGroup: vi.fn(), pushSmartLoadingPreload: vi.fn(), })) -vi.mock('@/modules/unified-orders', () => ({ batchQueryPlatformCoLoadGroups: vi.fn().mockResolvedValue([]) })) +vi.mock('../smartLoadingPlanGroups', () => ({ loadSmartLoadingPlanCoLoadGroups: vi.fn().mockResolvedValue(new Map()) })) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) })) const container = { @@ -178,31 +177,4 @@ describe('SmartLoadingWorkbenchView', () => { wrapper.unmount() }) - it('removes a locked group once instead of looping through member items', async () => { - const baseItems = ['201', '202'].map((inboundId, index) => ({ - id: `70${index}`, loadingPlanId: '99', accountId: `${index + 1}`, inboundId, inboundDetailId: `80${index}`, orderId: `30${index}`, - orderNo: `ORD-${index}`, inboundCode: `IN-${inboundId}`, customerName: `客户${index}`, destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', - skuCode: `SKU-${index}`, productNameZh: `货物${index}`, packageCount: 5, productQuantity: 50, grossWeightKg: 50, volumeCbm: 1, - loadSequence: index + 1, version: 1, pickedPackageCount: 0, minPackageCount: 1, canAdjust: true, canRemove: true, - })) - const items = [baseItems[0], { ...baseItems[0], id: '703', inboundId: '203', inboundDetailId: '803', inboundCode: 'IN-203', packageCount: 3, productQuantity: 30, grossWeightKg: 30, volumeCbm: 0.6 }, baseItems[1]] - 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, 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) - const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/container-loading/:containerId/smart-loading', component: SmartLoadingWorkbenchView }] }) - await router.push(`/container-loading/${container.id}/smart-loading`); await router.isReady() - const wrapper = mount(SmartLoadingWorkbenchView, { global: { plugins: [ElementPlus, router] } }) - await flushPromises() - expect(wrapper.text()).toContain('CLG-9900 · 2 票 · 3 个入仓片段') - expect(wrapper.text()).toContain('ORD-0:IN-201、IN-203') - expect(wrapper.text()).toContain('全部入仓片段联动,固定 13 箱') - await wrapper.findAll('button').find((button) => button.text().includes('移除整组'))!.trigger('click') - await flushPromises() - expect(removeSmartLoadingGroup).toHaveBeenCalledWith('99', '9900', 2) - expect(ElMessageBox.confirm).toHaveBeenCalledWith(expect.stringContaining('组内所有入仓片段将联动移除'), '移除同柜不可拆组', expect.any(Object)) - }) }) diff --git a/src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue b/src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue index 2c45c06b..af377aab 100644 --- a/src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue +++ b/src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue @@ -4,7 +4,6 @@ import { useRoute, useRouter } from 'vue-router' import { ArrowLeft, ListChecks, Plus, RefreshCw, Save, Sparkles, Trash2, Upload } from '@lucide/vue' import { ElMessage, ElMessageBox } from 'element-plus' import { useAccessPreviewStore } from '@/core/access/accessPreview' -import { batchQueryPlatformCoLoadGroups, type CoLoadGroup } from '@/modules/unified-orders' import AdminPage from '@/shared/components/AdminPage.vue' import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import AppDialog from '@/shared/components/AppDialog.vue' @@ -18,6 +17,7 @@ import { } from '../api/platformContainerLoading' import type { ContainerBookingResource, SmartLoadingCandidate, SmartLoadingPlan } from '../types' import { groupSmartLoadingItems, type SmartLoadingInboundItem } from '../smartLoadingItems' +import { loadSmartLoadingPlanCoLoadGroups, type SmartLoadingPlanCoLoadGroup } from '../smartLoadingPlanGroups' const route = useRoute() const router = useRouter() @@ -35,7 +35,7 @@ const assignOpen = ref(false) const selectedCandidate = ref(null) const assignForm = reactive({ packageCount: 1 }) const quantityDrafts = reactive>({}) -const planGroupsByInboundId = ref(new Map()) +const planGroupsByInboundId = ref(new Map()) const canCreate = computed(() => access.can('platform:firstleg:smart-loading:create')) const canAssign = computed(() => access.can('platform:firstleg:smart-loading:assign')) @@ -56,10 +56,10 @@ function memberOrderSummaries `${order.label}:${order.inboundCodes.join('、')}`) } -function groupOrderCount(group: CoLoadGroup) { return new Set(group.members.map((member) => member.orderId)).size } -function groupMemberLabel(group: CoLoadGroup) { - const orderCount = groupOrderCount(group) - return group.members.length === orderCount ? `${orderCount} 票` : `${orderCount} 票 · ${group.members.length} 个入仓片段` +function groupMemberLabel(group: SmartLoadingPlanCoLoadGroup) { + return group.inboundFragmentCount === group.memberOrderCount + ? `${group.memberOrderCount} 票` + : `${group.memberOrderCount} 票 · ${group.inboundFragmentCount} 个入仓片段` } const displayedCandidates = computed(() => { const rows: SmartLoadingCandidateRow[] = [] @@ -89,7 +89,7 @@ const displayedCandidates = computed(() => { } return rows }) -type SmartLoadingPlanRow = SmartLoadingInboundItem & { coLoadGroup: CoLoadGroup | null; memberInboundCodes: string[]; memberOrderSummaries: string[] } +type SmartLoadingPlanRow = SmartLoadingInboundItem & { coLoadGroup: SmartLoadingPlanCoLoadGroup | null; memberInboundCodes: string[]; memberOrderSummaries: string[] } const displayedPlanItems = computed(() => { const inboundRows = groupSmartLoadingItems(plan.value?.items || []) const result: SmartLoadingPlanRow[] = [] @@ -187,15 +187,10 @@ function syncDrafts(preserved: Record = {}) { for (const item of displayedPlanItems.value) quantityDrafts[item.id] = preserved[item.id] ?? item.packageCount } function acceptPlan(next: SmartLoadingPlan, preserved: Record = {}) { plan.value = next; noDraft.value = false; syncDrafts(preserved) } -async function loadPlanGroups() { - planGroupsByInboundId.value = new Map() - const orderIds = [...new Set((plan.value?.items || []).map((item) => item.orderId))] - if (!orderIds.length || !canAssign.value) return - const groups = await batchQueryPlatformCoLoadGroups(orderIds) - const byInbound = new Map() - for (const info of groups) for (const member of info.group.members) byInbound.set(member.inboundId, info.group) - planGroupsByInboundId.value = byInbound - syncDrafts() +async function acceptPlanWithGroups(next: SmartLoadingPlan, preserved: Record = {}) { + acceptPlan(next, preserved) + planGroupsByInboundId.value = await loadSmartLoadingPlanCoLoadGroups(next) + syncDrafts(preserved) } async function load() { @@ -208,7 +203,7 @@ async function load() { candidates.value = [] return } - try { acceptPlan(await getSmartLoadingPlanByBooking(containerId.value)); await loadPlanGroups() } + try { await acceptPlanWithGroups(await getSmartLoadingPlanByBooking(containerId.value)) } catch (error) { const detail = getApiErrorDetails(error) if (detail.code === 'A0404') { plan.value = null; noDraft.value = true } @@ -230,7 +225,7 @@ function openOperationalList() { async function createPlan() { mutating.value = true mutationError.value = null - try { acceptPlan(await createSmartLoadingPlan(containerId.value, null)); ElMessage.success('排柜计划已创建') } + try { await acceptPlanWithGroups(await createSmartLoadingPlan(containerId.value, null)); ElMessage.success('排柜计划已创建') } catch (error) { await handleMutationError(error, '创建排柜计划失败') } finally { mutating.value = false } } @@ -251,8 +246,7 @@ async function assign() { const next = selected.coLoadGroupId ? await assignSmartLoadingGroup(plan.value.id, selected.inboundId, plan.value.version) : await assignSmartLoadingItem(plan.value.id, { inboundId: selected.inboundId, packageCount: assignForm.packageCount, expectedVersion: plan.value.version }) - acceptPlan(next) - await loadPlanGroups() + await acceptPlanWithGroups(next) assignOpen.value = false ElMessage.success(selected.coLoadGroupId ? `同柜不可拆组 ${selected.groupNo} 已整组分配` : '货物已分配') } catch (error) { await handleMutationError(error, '分配失败') } @@ -269,13 +263,13 @@ async function recommend() { mutationError.value = null try { try { - acceptPlan(await recommendSmartLoading(plan.value.id, plan.value.version)) + await acceptPlanWithGroups(await recommendSmartLoading(plan.value.id, plan.value.version)) } catch (error) { const details = getApiErrorDetails(error, '智能推荐失败') if (details.code !== 'A0409' || !/(版本|并发|已被修改|已变化)/.test(details.message)) throw error await load() if (!plan.value) throw error - acceptPlan(await recommendSmartLoading(plan.value.id, plan.value.version)) + await acceptPlanWithGroups(await recommendSmartLoading(plan.value.id, plan.value.version)) } ElMessage.success('智能建议已生成,请人工核对后再推送') } @@ -289,7 +283,7 @@ async function adjust(source: object) { if (!Number.isInteger(count) || count < item.minPackageCount) { ElMessage.warning(`分配箱数不能低于已拣下限 ${item.minPackageCount} 箱`); return } mutating.value = true mutationError.value = null - try { acceptPlan(await adjustSmartLoadingItem(plan.value.id, item.id, count, plan.value.version, item.version)); ElMessage.success('箱数已调整') } + try { await acceptPlanWithGroups(await adjustSmartLoadingItem(plan.value.id, item.id, count, plan.value.version, item.version)); ElMessage.success('箱数已调整') } catch (error) { await handleMutationError(error, '调整失败') } finally { mutating.value = false } } @@ -306,7 +300,7 @@ async function remove(source: object) { mutationError.value = null try { if (row.coLoadGroup) { - acceptPlan(await removeSmartLoadingGroup(plan.value!.id, row.coLoadGroup.groupId, plan.value!.version)) + await acceptPlanWithGroups(await removeSmartLoadingGroup(plan.value!.id, row.coLoadGroup.groupId, plan.value!.version)) await load() ElMessage.success(`同柜不可拆组 ${row.coLoadGroup.groupNo} 已整组移除`) return @@ -316,7 +310,7 @@ async function remove(source: object) { const currentItem = current.items.find((value) => value.id === original.id) if (!currentItem) continue current = await removeSmartLoadingItem(current.id, currentItem.id, current.version, currentItem.version) - acceptPlan(current) + await acceptPlanWithGroups(current) } await load() ElMessage.success('该入仓码已从装柜方案移除') @@ -331,7 +325,7 @@ async function pushPreload() { if (!confirmed || !plan.value) return mutating.value = true mutationError.value = null - try { acceptPlan(await pushSmartLoadingPreload(plan.value.id, plan.value.version)); ElMessage.success('预装清单已生成,请继续完成集装箱号和预装确认') } + try { await acceptPlanWithGroups(await pushSmartLoadingPreload(plan.value.id, plan.value.version)); ElMessage.success('预装清单已生成,请继续完成集装箱号和预装确认') } catch (error) { await handleMutationError(error, '推送失败') } finally { mutating.value = false } } diff --git a/src/modules/container-loading/smartLoadingPlanGroups.test.ts b/src/modules/container-loading/smartLoadingPlanGroups.test.ts new file mode 100644 index 00000000..068c634d --- /dev/null +++ b/src/modules/container-loading/smartLoadingPlanGroups.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it, vi } from 'vitest' +import { loadSmartLoadingPlanCoLoadGroups } from './smartLoadingPlanGroups' +import type { SmartLoadingPlan } from './types' + +const item = { + id: '701', loadingPlanId: '99', accountId: '1', inboundId: '201', inboundDetailId: '801', orderId: '301', + orderNo: 'ORD-1', inboundCode: 'IN-201', customerName: '客户一', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', + skuCode: 'SKU-1', productNameZh: '货物一', packageCount: 5, productQuantity: 50, grossWeightKg: 50, volumeCbm: 1, + loadSequence: 1, version: 1, pickedPackageCount: 0, minPackageCount: 1, canAdjust: true, canRemove: true, +} + +const plan = { + id: '99', planNo: 'LP-99', status: 'EDITING', bookingId: '88', containerNo: null, voyageNo: null, version: 2, + canAssign: true, canEdit: true, blockedReason: null, finalCompletionStatus: 'NOT_STARTED', pdaTaskId: null, + pdaTaskStatus: null, pdaTaskVersion: null, + items: [item, { ...item, id: '702', inboundId: '202', inboundDetailId: '802', orderId: '302', orderNo: 'ORD-2', inboundCode: 'IN-202', packageCount: 7 }], +} satisfies SmartLoadingPlan + +describe('smart loading plan co-load groups', () => { + it('rebuilds the locked group from the current inbound association contract', async () => { + const lookup = vi.fn(async (inboundId: string) => ({ + inboundId, inboundCode: `IN-${inboundId}`, loadingPlanId: '99', coLoadGroupId: '9001', coLoadGroupNo: 'CLG-9001', items: [], + })) + const result = await loadSmartLoadingPlanCoLoadGroups(plan, lookup) + expect(lookup).toHaveBeenCalledTimes(2) + expect(result.get('201')).toEqual({ + groupId: '9001', groupNo: 'CLG-9001', lockedPlanId: '99', memberOrderCount: 2, inboundFragmentCount: 2, fullPackageCount: 12, + }) + expect(result.get('202')).toBe(result.get('201')) + }) + + it('leaves ordinary plan items ungrouped', async () => { + const result = await loadSmartLoadingPlanCoLoadGroups(plan, async (inboundId) => ({ inboundId, inboundCode: `IN-${inboundId}`, items: [] })) + expect(result.size).toBe(0) + }) +}) diff --git a/src/modules/container-loading/smartLoadingPlanGroups.ts b/src/modules/container-loading/smartLoadingPlanGroups.ts new file mode 100644 index 00000000..bf4fb008 --- /dev/null +++ b/src/modules/container-loading/smartLoadingPlanGroups.ts @@ -0,0 +1,47 @@ +import { getLoadingAssociationSummaryByInboundId, type LoadingAssociationSummary } from '@/shared/api/inboundCodeRelations' +import { groupSmartLoadingItems } from './smartLoadingItems' +import type { SmartLoadingPlan } from './types' + +export interface SmartLoadingPlanCoLoadGroup { + groupId: string + groupNo: string + lockedPlanId: string + memberOrderCount: number + inboundFragmentCount: number + fullPackageCount: number +} + +type AssociationLookup = (inboundId: string) => Promise + +export async function loadSmartLoadingPlanCoLoadGroups( + plan: SmartLoadingPlan, + lookup: AssociationLookup = getLoadingAssociationSummaryByInboundId, +) { + const inboundRows = groupSmartLoadingItems(plan.items || []) + if (!inboundRows.length) return new Map() + + const associations = await Promise.all(inboundRows.map(async (row) => ({ + row, + association: await lookup(row.inboundId), + }))) + const membersByGroupId = new Map() + for (const entry of associations) { + const groupId = String(entry.association.coLoadGroupId || '') + if (!groupId) continue + membersByGroupId.set(groupId, [...(membersByGroupId.get(groupId) || []), entry]) + } + + const groupsByInboundId = new Map() + for (const [groupId, members] of membersByGroupId) { + const group = { + groupId, + groupNo: members.find((entry) => entry.association.coLoadGroupNo)?.association.coLoadGroupNo || '同柜不可拆组', + lockedPlanId: members.find((entry) => entry.association.loadingPlanId)?.association.loadingPlanId || plan.id, + memberOrderCount: new Set(members.map((entry) => entry.row.orderId)).size, + inboundFragmentCount: members.length, + fullPackageCount: members.reduce((sum, entry) => sum + entry.row.packageCount, 0), + } satisfies SmartLoadingPlanCoLoadGroup + for (const { row } of members) groupsByInboundId.set(row.inboundId, group) + } + return groupsByInboundId +} diff --git a/src/modules/unified-orders/api/coLoadGroups.test.ts b/src/modules/unified-orders/api/coLoadGroups.test.ts index 2aa2e6ec..5cf365b1 100644 --- a/src/modules/unified-orders/api/coLoadGroups.test.ts +++ b/src/modules/unified-orders/api/coLoadGroups.test.ts @@ -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') }) diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts index 364224ce..9778b627 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.coLoad.test.ts @@ -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') diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.fulfillment.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.fulfillment.test.ts new file mode 100644 index 00000000..59cb5675 --- /dev/null +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.fulfillment.test.ts @@ -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: '
' }, InboundMeasurementDialog: { template: '
' }, MeasurementEvidenceThumbnail: { template: '
' }, +})) +vi.mock('@/modules/unified-orders/workflows', () => ({ UnifiedOrderWorkflowPanel: { template: '
' } })) +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: '
' }, + DetailPageLayout: { template: '
' }, + OrderFulfillmentTreePanel: { template: '
' }, + LoadErrorState: { props: ['title', 'message'], template: '
{{ title }} {{ message }}
' }, + ApiErrorMeta: true, + StatusBadge: true, + 'el-button': { template: '' }, + }, + }, + }) +} + +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') + }) +}) diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.requests.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.requests.test.ts index 4cfd0d2d..5b2888a4 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.requests.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.requests.test.ts @@ -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: '
' } })) 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(), diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts index 5ce061d8..94c1b52b 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts @@ -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('—\n