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 入仓码箱数 | ')
+ expect(source).toContain('| 入仓码 |
')
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('>履约详情')
- expect(source).toContain('aria-label="查看该入仓码的订单、装柜、尾程与库存关系"')
- expect(source).toContain('title="查看该入仓码的订单、装柜、尾程与库存关系"')
+ it('does not expose a generic relationship probe in list rows', () => {
+ expect(source).not.toContain('>履约详情')
expect(source).not.toContain('>关联数据')
+ expect(source).not.toContain('openInboundRelationship')
+ expect(source).not.toContain('inboundRelationshipDialogOpen')
+ expect(source).not.toContain(' {
diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.vue b/src/modules/unified-orders/pages/UnifiedOrdersView.vue
index 49e9a1a4..8ce46415 100644
--- a/src/modules/unified-orders/pages/UnifiedOrdersView.vue
+++ b/src/modules/unified-orders/pages/UnifiedOrdersView.vue
@@ -1,7 +1,7 @@