Files
admin-web/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts
T

987 lines
54 KiB
TypeScript
Raw Normal View History

2026-09-03 11:47:32 +08:00
import ElementPlus, { ElCheckboxButton, ElInputNumber, ElMessage, ElMessageBox } from 'element-plus'
2026-08-10 17:42:17 +08:00
import { AxiosError } from 'axios'
import { createPinia, setActivePinia } from 'pinia'
import { flushPromises, mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
2026-08-12 18:00:26 +08:00
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
2026-08-11 11:52:30 +08:00
import QueryToolbar from '@/shared/components/QueryToolbar.vue'
2026-08-10 17:42:17 +08:00
import GlobalSmartLoadingView from './GlobalSmartLoadingView.vue'
2026-09-03 15:00:58 +08:00
import type { ContainerBookingResource } from '../types'
2026-08-10 17:42:17 +08:00
import {
2026-09-03 21:25:56 +08:00
assignPendingReviewSmartLoadingOrder,
assignSmartLoadingGroup,
assignSmartLoadingItem,
2026-09-08 17:24:49 +08:00
assignSmartLoadingPallet,
2026-08-10 17:42:17 +08:00
getSmartLoadingPlanByBooking,
listContainers,
listSmartLoadingCandidates,
2026-09-03 15:00:58 +08:00
listSmartLoadingWorkbenchContainers,
2026-08-10 17:42:17 +08:00
recommendSmartLoading,
2026-09-04 12:18:39 +08:00
adjustSmartLoadingItem,
2026-09-04 10:51:31 +08:00
removeAllUnpickedSmartLoadingItems,
removeSmartLoadingGroup,
2026-08-11 11:52:30 +08:00
removeSmartLoadingItem,
removeSmartLoadingPallet,
2026-08-10 17:42:17 +08:00
} from '../api/platformContainerLoading'
const { routerPush } = vi.hoisted(() => ({ routerPush: vi.fn() }))
vi.mock('vue-router', () => ({
useRouter: () => ({ push: routerPush }),
}))
2026-08-10 17:42:17 +08:00
vi.mock('@/core/access/accessPreview', () => ({
useAccessPreviewStore: () => ({ can: () => true }),
}))
2026-08-14 12:50:50 +08:00
vi.mock('@/modules/iam', () => ({
listPlatformTenants: vi.fn().mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }),
}))
2026-08-10 17:42:17 +08:00
vi.mock('../api/platformContainerLoading', () => ({
listContainers: vi.fn(),
getSmartLoadingPlanByBooking: vi.fn(),
listSmartLoadingCandidates: vi.fn(),
2026-09-03 15:00:58 +08:00
listSmartLoadingWorkbenchContainers: vi.fn(),
2026-08-10 17:42:17 +08:00
createSmartLoadingPlan: vi.fn(),
assignSmartLoadingItem: vi.fn(),
2026-09-08 17:24:49 +08:00
assignSmartLoadingPallet: vi.fn(),
2026-09-03 21:25:56 +08:00
assignPendingReviewSmartLoadingOrder: vi.fn(),
assignSmartLoadingGroup: vi.fn(),
2026-08-10 17:42:17 +08:00
recommendSmartLoading: vi.fn(),
adjustSmartLoadingItem: vi.fn(),
removeSmartLoadingItem: vi.fn(),
removeSmartLoadingGroup: vi.fn(),
removeSmartLoadingPallet: vi.fn(),
2026-09-04 10:51:31 +08:00
removeAllUnpickedSmartLoadingItems: vi.fn(),
2026-08-10 17:42:17 +08:00
pushSmartLoadingPreload: vi.fn(),
}))
vi.mock('@/modules/warehouses', () => ({
listAllPlatformWarehouses: vi.fn().mockResolvedValue([
{ id: '8', warehouseName: '义乌仓', warehouseType: 'DOMESTIC', status: 'ACTIVE' },
{ id: '18', warehouseName: '东莞仓', warehouseType: 'DOMESTIC', status: 'ACTIVE' },
]),
}))
vi.mock('../smartLoadingPlanGroups', () => ({ loadSmartLoadingPlanCoLoadGroups: vi.fn().mockReturnValue(new Map()) }))
2026-08-10 17:42:17 +08:00
const container = {
id: '9007199254740993', planId: '9007199254740994', bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1',
warehouseId: '8', warehouseName: '义乌仓', channelGroupCode: 'MATSON_CLX', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', containerType: '40HC', planStatus: 'EDITING' as const, version: 1,
2026-09-03 11:47:32 +08:00
billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: '31', shippingCompanyName: '美森轮船',
2026-08-10 17:42:17 +08:00
departurePortCode: null, departurePortName: null, destinationPortCode: null, destinationPortName: null,
2026-08-17 12:31:27 +08:00
destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', capacityCbm: 76, maxWeightKg: 28000,
2026-08-25 19:56:37 +08:00
terminalTruckingCompanyId: null, terminalTruckingCompany: null, loadingAt: null, estimatedSailingAt: null, actualSailingAt: null,
2026-08-10 17:42:17 +08:00
estimatedPortArrivalAt: null, actualPortArrivalAt: null, estimatedWarehouseArrivalAt: null, actualWarehouseArrivalAt: null,
}
const plan = {
id: '99', bookingId: container.id, planNo: 'LP-99', status: 'EDITING' as const, containerNo: 'MSCU-1', voyageNo: null, version: 2,
2026-08-17 16:45:46 +08:00
canAssign: true, canEdit: true, blockedReason: null, finalCompletionStatus: 'NOT_STARTED' as const,
pdaTaskId: null, pdaTaskStatus: null, pdaTaskVersion: null,
coLoadGroups: [],
2026-09-08 17:24:49 +08:00
items: [{ id: '101', loadingPlanId: '99', accountId: '1', inboundId: '2', inboundDetailId: '3', orderId: '4', orderNo: 'ORD-1', inboundCode: 'IN-1', customerName: '示例企业', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', skuCode: 'SKU-1', productNameZh: '服装', unitType: 'PACKAGE' as const, palletId: null, palletNo: null, indivisible: false, packageCount: 6, productQuantity: 6, grossWeightKg: 60, volumeCbm: 1.2, loadSequence: 1, version: 1, pickedPackageCount: 0, minPackageCount: 1, canAdjust: true, canRemove: true }],
2026-08-10 17:42:17 +08:00
}
2026-09-01 21:09:39 +08:00
const candidate = {
accountId: '1', inboundId: '5', orderId: '7', orderNo: 'ORD-2', inboundCode: 'IN-2',
orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-10T09:30:00',
warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9',
destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '另一企业', goodsSummary: '鞋类',
availablePackageCount: 12, unitGrossWeightKg: 10, unitNetWeightKg: 9, averageUnitVolumeCbm: 0.2,
2026-09-04 18:47:43 +08:00
availableVolumeCbm: 2.4, estimatedVolumeCbm: 3.5, coLoadGroupId: null, groupNo: null, memberCount: null, groupTotalBoxes: null,
2026-09-01 21:09:39 +08:00
locked: null, blockReason: null,
}
2026-09-03 15:00:58 +08:00
function candidatePage(records: unknown[], page = 1, size = 20, total = records.length) {
return {
records: records.map((record) => {
const candidate = record as { accountId?: string; customerId?: string; groupMembers?: unknown[] }
2026-09-08 17:24:49 +08:00
return { customerId: candidate.customerId || candidate.accountId || '', groupMembers: [], unitType: 'PACKAGE', palletId: null, palletNo: null, indivisible: false, ...candidate }
2026-09-03 15:00:58 +08:00
}),
page, size, total, pages: total ? Math.ceil(total / size) : 0,
} as Awaited<ReturnType<typeof listSmartLoadingCandidates>>
}
function containerPage(records: ContainerBookingResource[] = [container]) {
return {
records: records.map((item) => ({
bookingId: item.id, bookingNo: item.bookingNo, cabinetNo: item.cabinetNo, containerNo: item.containerNo,
warehouseId: item.warehouseId, warehouseName: item.warehouseName || '', channelGroupCode: item.channelGroupCode,
channelCode: item.channelCode, channelName: item.channelName,
containerType: item.containerType, capacityCbm: item.capacityCbm, maxWeightKg: item.maxWeightKg,
planId: item.planId, planNo: null, planStatus: item.planStatus, version: item.version,
2026-09-03 15:00:58 +08:00
})),
page: 1, size: 200, total: records.length, pages: records.length ? 1 : 0,
} as Awaited<ReturnType<typeof listSmartLoadingWorkbenchContainers>>
}
function mockContainerSearch(records: ContainerBookingResource[]) {
vi.mocked(listSmartLoadingWorkbenchContainers).mockImplementation(async (query) => containerPage(records.filter((item) => (
item.planStatus !== 'COMPLETED'
&& query.channelGroupCodes.includes(item.channelGroupCode)
2026-09-03 15:00:58 +08:00
))))
}
2026-08-10 17:42:17 +08:00
describe('GlobalSmartLoadingView', () => {
2026-09-04 10:12:41 +08:00
it('uses one-line shared text clamps for co-load and assigned-count summaries', () => {
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
expect(source).toContain('<LineClampText :content="row.memberOrderSummaries.join(\';\')" />')
2026-09-08 17:24:49 +08:00
expect(source).toContain('<LineClampText v-else-if="row.coLoadGroup" :content="`${row.packageCount} 箱(整组固定)`" />')
expect(source).toContain("row.unitType === 'SEALED_PALLET'")
2026-09-04 10:12:41 +08:00
expect(source).toContain('.candidate-group { display: grid; width: 100%; min-width: 0;')
expect(source).toContain('.candidate-group :deep(.line-clamp-text) { width: 100%; }')
expect(source).not.toContain('<span v-for="summary in row.memberOrderSummaries"')
})
2026-08-10 17:42:17 +08:00
beforeEach(() => {
setActivePinia(createPinia())
vi.clearAllMocks()
2026-09-03 15:00:58 +08:00
mockContainerSearch([container])
2026-08-10 17:42:17 +08:00
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue(plan)
2026-09-03 15:00:58 +08:00
vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([candidate]))
2026-08-10 17:42:17 +08:00
})
it('loads real containers, candidates and the selected plan into the three-column workbench', async () => {
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(container.id)
2026-09-03 15:00:58 +08:00
expect(listSmartLoadingCandidates).toHaveBeenCalledWith({ channelGroupCodes: ['MATSON_CLX'], page: 1, size: 20 })
2026-08-18 16:09:43 +08:00
expect(wrapper.text()).toContain('候选装柜明细')
2026-09-03 10:27:31 +08:00
expect(wrapper.text()).toContain('美森正班')
2026-09-03 11:47:32 +08:00
expect(wrapper.findAll('.channel-tags .el-checkbox-button')).toHaveLength(9)
2026-09-03 10:27:31 +08:00
expect(wrapper.text()).not.toContain('美森正班海卡包税')
2026-08-18 16:09:43 +08:00
expect(wrapper.text()).toContain('另一企业')
2026-08-14 19:59:32 +08:00
expect(wrapper.text()).toContain('MSCU-1 装柜方案')
expect(wrapper.text()).not.toContain('LP-99')
2026-08-18 16:09:43 +08:00
expect(wrapper.text()).toContain('示例企业')
expect(wrapper.text()).not.toContain('服装')
expect(wrapper.text()).not.toContain('鞋类')
expect(wrapper.text()).not.toContain('SKU-1')
2026-08-24 18:07:38 +08:00
expect(wrapper.text()).toContain('IN-1')
2026-08-21 17:07:37 +08:00
expect(wrapper.text()).toContain('IN-2')
2026-09-04 18:47:43 +08:00
expect(wrapper.text()).toContain('预估体积')
expect(wrapper.text()).toContain('3.5 m³')
2026-08-14 19:59:32 +08:00
expect(wrapper.text()).not.toContain('ORD-1')
expect(wrapper.text()).not.toContain('ORD-2')
2026-09-03 14:12:42 +08:00
expect(wrapper.get('.candidate-pane').text()).toContain('120 kg')
expect(wrapper.get('.candidate-pane').text()).toContain('2.4 m³')
2026-08-14 19:59:32 +08:00
})
2026-08-17 18:32:41 +08:00
it('uses the top-level container channel snapshot without expecting a nested snapshot', async () => {
vi.mocked(listContainers).mockResolvedValue({
2026-09-03 11:47:32 +08:00
records: [{ ...container, shippingCompanyId: '32', shippingCompanyName: 'EXX', channelCode: 'EXX_RAIL_NY', channelName: '纽约EXX海铁' }],
2026-08-17 18:32:41 +08:00
page: 1,
size: 100,
total: 1,
pages: 1,
})
mockContainerSearch([container])
2026-08-17 18:32:41 +08:00
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.text()).toContain('美森正班')
expect(wrapper.text()).not.toContain('美森正班海卡包税')
2026-08-17 18:32:41 +08:00
expect(wrapper.text()).not.toContain('缺少渠道快照')
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(container.id)
})
2026-08-14 19:59:32 +08:00
it('shows one user-facing container number and hides internal manual or split references', async () => {
vi.mocked(listContainers).mockResolvedValue({
2026-08-19 09:14:35 +08:00
records: [{ ...container, cabinetNo: 'CAB-260814-0001', containerNo: 'SPLIT-814022117898chain1' }],
2026-08-14 19:59:32 +08:00
page: 1,
size: 100,
total: 1,
pages: 1,
})
2026-09-03 15:00:58 +08:00
mockContainerSearch([{ ...container, cabinetNo: 'CAB-260814-0001', containerNo: 'SPLIT-814022117898chain1' }])
2026-08-14 19:59:32 +08:00
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
2026-08-19 09:14:35 +08:00
expect(wrapper.text()).toContain('CAB-260814-0001')
2026-08-14 19:59:32 +08:00
expect(wrapper.text()).not.toContain('SPLIT-814022117898chain1')
expect(wrapper.text()).not.toContain('LP-99')
2026-08-10 17:42:17 +08:00
})
2026-08-15 12:12:38 +08:00
it('explains simulated empty cabinets instead of presenting an internal id as a container number', async () => {
vi.mocked(listContainers).mockResolvedValue({
records: [{
...container,
cabinetNo: '2088475410134773762',
containerNo: null,
channelName: '美森正班海卡包税',
warehouseName: '义乌国内仓',
2026-08-17 12:31:27 +08:00
containerType: '40HC',
2026-08-15 12:12:38 +08:00
}],
page: 1,
size: 100,
total: 1,
pages: 1,
})
2026-09-03 15:00:58 +08:00
mockContainerSearch([{ ...container, cabinetNo: '2088475410134773762', containerNo: null, channelName: '美森正班海卡包税', warehouseName: '义乌国内仓', containerType: '40HC' }])
2026-08-15 12:12:38 +08:00
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const option = wrapper.find('.container-option')
expect(option.text()).toContain('模拟柜次')
expect(option.text()).toContain('柜次号:待生成')
expect(option.text()).toContain('集装箱号:待补录')
2026-08-18 16:09:43 +08:00
expect(option.text()).not.toContain('开始装柜前录入集装箱号')
2026-09-03 10:27:31 +08:00
expect(option.text()).toContain('渠道:美森正班')
2026-08-15 12:12:38 +08:00
expect(option.text()).toContain('仓库:义乌国内仓')
2026-08-17 12:31:27 +08:00
expect(option.text()).toContain('柜型:40HC')
2026-08-15 12:12:38 +08:00
expect(option.text()).not.toContain('2088475410134773762')
expect(wrapper.text()).not.toContain('真实柜号')
2026-08-15 12:12:38 +08:00
})
2026-09-03 11:47:32 +08:00
it('keeps nine fixed channel tags and allows selecting multiple channels', async () => {
2026-08-14 19:02:34 +08:00
const other = {
...container,
id: '9007199254740998',
planId: '9007199254740999',
cabinetNo: 'CB-2',
2026-08-14 19:59:32 +08:00
containerNo: 'MSCU-2',
channelGroupCode: 'HEDE',
2026-08-14 19:02:34 +08:00
channelCode: 'HDHK',
channelName: '合德海卡包税',
}
vi.mocked(listContainers).mockResolvedValue({ records: [container, other], page: 1, size: 100, total: 2, pages: 1 })
2026-09-03 15:00:58 +08:00
mockContainerSearch([container, other])
2026-08-14 19:02:34 +08:00
vi.mocked(getSmartLoadingPlanByBooking).mockImplementation(async (bookingId) => ({ ...plan, bookingId }))
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.findAll('.container-option')).toHaveLength(1)
2026-08-14 19:59:32 +08:00
expect(wrapper.text()).toContain('MSCU-1')
expect(wrapper.text()).not.toContain('MSCU-2')
2026-09-03 11:47:32 +08:00
expect(wrapper.text()).toContain('柜子数量1 个')
2026-08-14 19:02:34 +08:00
2026-09-03 11:47:32 +08:00
expect(wrapper.findAll('.channel-tags .el-checkbox-button').map((item) => item.text())).toEqual([
'美森正班', '美森加班', 'EXX', '合德', '洛杉矶普船', '芝加哥普船', '休斯顿普船', '萨凡纳普船', '纽约普船海卡',
])
const hedeChannel = wrapper.findAllComponents(ElCheckboxButton).find((item) => item.text() === '合德')!
await hedeChannel.find('input').setValue(true)
2026-08-14 19:02:34 +08:00
await flushPromises()
2026-09-03 11:47:32 +08:00
expect(wrapper.findAll('.container-option')).toHaveLength(2)
expect(wrapper.text()).toContain('MSCU-1')
2026-08-14 19:59:32 +08:00
expect(wrapper.text()).toContain('MSCU-2')
2026-09-03 11:47:32 +08:00
expect(wrapper.text()).toContain('柜子数量2 个')
2026-08-14 19:02:34 +08:00
})
2026-09-04 14:17:36 +08:00
it('selects only the double-clicked channel as a shortcut', async () => {
const other = {
...container,
id: '9007199254740998',
planId: '9007199254740999',
cabinetNo: 'CB-2',
containerNo: 'MSCU-2',
channelGroupCode: 'HEDE',
2026-09-04 14:17:36 +08:00
channelCode: 'HDHK',
channelName: '合德海卡包税',
}
vi.mocked(listContainers).mockResolvedValue({ records: [container, other], page: 1, size: 100, total: 2, pages: 1 })
mockContainerSearch([container, other])
vi.mocked(getSmartLoadingPlanByBooking).mockImplementation(async (bookingId) => ({ ...plan, bookingId }))
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const hedeChannel = wrapper.findAllComponents(ElCheckboxButton).find((item) => item.text() === '合德')!
await hedeChannel.find('input').setValue(true)
await flushPromises()
expect(wrapper.findAll('.channel-tags .el-checkbox-button.is-checked')).toHaveLength(2)
await hedeChannel.trigger('dblclick')
await flushPromises()
expect(wrapper.findAll('.channel-tags .el-checkbox-button.is-checked').map((item) => item.text())).toEqual(['合德'])
expect(listSmartLoadingCandidates).toHaveBeenLastCalledWith(expect.objectContaining({ channelGroupCodes: ['HEDE'] }))
expect(listSmartLoadingWorkbenchContainers).toHaveBeenLastCalledWith({ channelGroupCodes: ['HEDE'], page: 1, size: 200 })
expect(wrapper.text()).toContain('MSCU-2')
expect(wrapper.text()).not.toContain('MSCU-1')
})
2026-09-03 10:27:31 +08:00
it('merges detailed pricing channels into the same confirmed display group', async () => {
const parcelContainer = {
...container,
id: '9007199254740998',
planId: '9007199254740999',
cabinetNo: 'CB-2',
containerNo: 'MSCU-2',
channelCode: 'MATSON_CLX_PARCEL',
channelName: '美森正班海派',
}
vi.mocked(listContainers).mockResolvedValue({ records: [container, parcelContainer], page: 1, size: 100, total: 2, pages: 1 })
2026-09-03 15:00:58 +08:00
mockContainerSearch([container, parcelContainer])
2026-09-03 10:27:31 +08:00
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.findAll('.container-option')).toHaveLength(2)
expect(wrapper.text()).toContain('MSCU-1')
expect(wrapper.text()).toContain('MSCU-2')
expect(wrapper.text()).not.toContain('美森正班海卡包税')
expect(wrapper.text()).not.toContain('美森正班海派')
})
2026-09-03 20:24:54 +08:00
it('keeps the shipping warehouse as a candidate-only query filter', async () => {
2026-09-03 10:27:31 +08:00
const dongguanContainer = {
...container,
id: '9007199254740998',
planId: '9007199254740999',
cabinetNo: 'CB-DG',
containerNo: 'MSCU-DG',
warehouseId: '18',
warehouseName: '东莞仓',
}
vi.mocked(listContainers).mockResolvedValue({ records: [container, dongguanContainer], page: 1, size: 100, total: 2, pages: 1 })
2026-09-03 15:00:58 +08:00
mockContainerSearch([container, dongguanContainer])
2026-09-03 10:27:31 +08:00
vi.mocked(getSmartLoadingPlanByBooking).mockImplementation(async (bookingId) => ({ ...plan, bookingId }))
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.text()).toContain('MSCU-1')
2026-09-03 11:47:32 +08:00
expect(wrapper.text()).toContain('MSCU-DG')
2026-09-03 20:24:54 +08:00
const containerRequestCount = vi.mocked(listSmartLoadingWorkbenchContainers).mock.calls.length
2026-09-03 10:27:31 +08:00
2026-09-03 22:15:25 +08:00
wrapper.findComponent(QueryToolbar).vm.$emit('search', { warehouseId: '18', customerId: '', inboundCode: '', createdRange: [], orderStatuses: [] })
2026-09-03 10:27:31 +08:00
await flushPromises()
expect(wrapper.text()).toContain('MSCU-DG')
2026-09-03 20:24:54 +08:00
expect(wrapper.text()).toContain('MSCU-1')
2026-09-03 15:00:58 +08:00
expect(listSmartLoadingCandidates).toHaveBeenLastCalledWith({ channelGroupCodes: ['MATSON_CLX'], warehouseId: dongguanContainer.warehouseId, page: 1, size: 20 })
2026-09-03 20:24:54 +08:00
expect(listSmartLoadingWorkbenchContainers).toHaveBeenCalledTimes(containerRequestCount)
2026-09-03 10:27:31 +08:00
})
2026-09-03 20:24:54 +08:00
it('keeps channel cabinets unchanged when another search field has no matching cabinet', async () => {
2026-09-03 10:41:09 +08:00
const dongguanOvertimeContainer = {
...container,
id: '9007199254740998',
planId: '9007199254740999',
cabinetNo: 'CB-DG-MAX',
containerNo: 'MSCU-DG-MAX',
warehouseId: '18',
warehouseName: '东莞仓',
channelGroupCode: 'MATSON_MAX',
2026-09-03 10:41:09 +08:00
channelCode: 'MATSON_MAX_TRUCK',
channelName: '美森加班海卡',
}
vi.mocked(listContainers).mockResolvedValue({ records: [container, dongguanOvertimeContainer], page: 1, size: 100, total: 2, pages: 1 })
2026-09-03 15:00:58 +08:00
mockContainerSearch([container, dongguanOvertimeContainer])
2026-09-03 10:41:09 +08:00
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
2026-09-03 20:24:54 +08:00
const containerRequestCount = vi.mocked(listSmartLoadingWorkbenchContainers).mock.calls.length
2026-09-03 10:41:09 +08:00
2026-09-03 22:15:25 +08:00
wrapper.findComponent(QueryToolbar).vm.$emit('search', { warehouseId: '18', customerId: '', inboundCode: '', createdRange: [], orderStatuses: [] })
2026-09-03 10:41:09 +08:00
await flushPromises()
2026-09-03 11:47:32 +08:00
expect(wrapper.find('.channel-tags .el-checkbox-button.is-checked').text()).toBe('美森正班')
2026-09-03 20:24:54 +08:00
expect(wrapper.findAll('.container-option')).toHaveLength(1)
expect(wrapper.text()).toContain('MSCU-1')
2026-09-03 10:41:09 +08:00
expect(wrapper.text()).not.toContain('MSCU-DG-MAX')
2026-09-03 15:00:58 +08:00
expect(listSmartLoadingCandidates).toHaveBeenCalledTimes(2)
expect(listSmartLoadingCandidates).toHaveBeenLastCalledWith({ channelGroupCodes: ['MATSON_CLX'], warehouseId: '18', page: 1, size: 20 })
2026-09-03 20:24:54 +08:00
expect(listSmartLoadingWorkbenchContainers).toHaveBeenCalledTimes(containerRequestCount)
})
it('filters candidate details by inbound code without filtering channel cabinets', async () => {
const secondContainer = {
...container,
id: '9007199254740998',
planId: '9007199254740999',
cabinetNo: 'CB-2',
containerNo: 'MSCU-2',
}
vi.mocked(listContainers).mockResolvedValue({ records: [container, secondContainer], page: 1, size: 100, total: 2, pages: 1 })
mockContainerSearch([container, secondContainer])
vi.mocked(getSmartLoadingPlanByBooking).mockImplementation(async (bookingId) => ({ ...plan, bookingId }))
vi.mocked(listSmartLoadingCandidates)
.mockResolvedValueOnce(candidatePage([candidate]))
.mockResolvedValueOnce(candidatePage([{ ...candidate, inboundCode: 'NYEXX2609030002' }]))
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const containerRequestCount = vi.mocked(listSmartLoadingWorkbenchContainers).mock.calls.length
wrapper.findComponent(QueryToolbar).vm.$emit('search', {
2026-09-03 22:15:25 +08:00
warehouseId: '', customerId: '', inboundCode: 'NYEXX2609030002', createdRange: [], orderStatuses: [],
2026-09-03 20:24:54 +08:00
})
await flushPromises()
expect(wrapper.get('.candidate-pane').text()).toContain('NYEXX2609030002')
expect(wrapper.findAll('.container-option')).toHaveLength(2)
expect(wrapper.text()).toContain('MSCU-1')
expect(wrapper.text()).toContain('MSCU-2')
expect(listSmartLoadingCandidates).toHaveBeenLastCalledWith({ channelGroupCodes: ['MATSON_CLX'], inboundCode: 'NYEXX2609030002', page: 1, size: 20 })
expect(listSmartLoadingWorkbenchContainers).toHaveBeenCalledTimes(containerRequestCount)
2026-09-03 10:41:09 +08:00
})
2026-09-03 15:00:58 +08:00
it('sends the active name-only customer selector to the server candidate query', () => {
2026-08-14 13:03:11 +08:00
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
expect(source).toContain("listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' })")
2026-08-14 14:27:03 +08:00
expect(source).toContain("label: customer.tenantName")
2026-09-03 15:00:58 +08:00
expect(source).toContain('params.customerId = customerId')
2026-08-14 13:03:11 +08:00
expect(source).not.toContain('customer.tenantCode')
})
it('defaults ordinary candidate assignment to all available boxes', () => {
2026-08-18 16:09:43 +08:00
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
expect(source).toContain(': candidate.availablePackageCount')
})
2026-09-04 18:47:43 +08:00
it('filters candidates by creation, expected-arrival range and multiple order statuses', async () => {
2026-09-03 15:00:58 +08:00
vi.mocked(listSmartLoadingCandidates)
.mockResolvedValueOnce(candidatePage([
2026-09-01 21:09:39 +08:00
{ ...candidate, inboundId: '51', inboundCode: 'IN-MATCH', orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-11T08:00:00' },
{ ...candidate, inboundId: '52', inboundCode: 'IN-WRONG-STATUS', orderStatusSnapshot: 'LOADING', expectedArrivalAt: '2026-09-11T08:00:00' },
{ ...candidate, inboundId: '53', inboundCode: 'IN-WRONG-DATE', orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-20T08:00:00' },
2026-09-03 15:00:58 +08:00
]))
.mockResolvedValueOnce(candidatePage([{ ...candidate, inboundId: '51', inboundCode: 'IN-MATCH', orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-11T08:00:00' }]))
2026-09-01 21:09:39 +08:00
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
2026-09-03 20:24:54 +08:00
const containerRequestCount = vi.mocked(listSmartLoadingWorkbenchContainers).mock.calls.length
2026-09-01 21:09:39 +08:00
wrapper.getComponent(QueryToolbar).vm.$emit('search', {
2026-09-03 22:15:25 +08:00
customerId: '', inboundCode: '', orderStatuses: ['WAIT_SPLIT', 'LOADING'], createdRange: ['2026-09-10', '2026-09-12'],
2026-09-04 18:47:43 +08:00
expectedArrivalRange: ['2026-09-11', '2026-09-15'],
2026-09-01 21:09:39 +08:00
})
await flushPromises()
const candidates = wrapper.get('.candidate-pane').text()
expect(candidates).toContain('IN-MATCH')
expect(candidates).not.toContain('IN-WRONG-STATUS')
expect(candidates).not.toContain('IN-WRONG-DATE')
2026-09-04 18:47:43 +08:00
expect(listSmartLoadingCandidates).toHaveBeenLastCalledWith({ channelGroupCodes: ['MATSON_CLX'], createdStart: '2026-09-10', createdEnd: '2026-09-12', expectedArrivalStart: '2026-09-11', expectedArrivalEnd: '2026-09-15', orderStatuses: ['WAIT_SPLIT', 'LOADING'], page: 1, size: 20 })
2026-09-03 20:24:54 +08:00
expect(listSmartLoadingWorkbenchContainers).toHaveBeenCalledTimes(containerRequestCount)
expect(listSmartLoadingWorkbenchContainers).toHaveBeenLastCalledWith({ channelGroupCodes: ['MATSON_CLX'], page: 1, size: 200 })
2026-09-01 21:09:39 +08:00
})
it('paginates large inbound candidate results with twenty rows by default', async () => {
2026-09-03 15:00:58 +08:00
vi.mocked(listSmartLoadingCandidates)
.mockResolvedValueOnce(candidatePage(Array.from({ length: 20 }, (_, index) => ({
...candidate,
inboundId: String(100 + index),
orderId: String(200 + index),
inboundCode: `IN-${String(index + 1).padStart(3, '0')}`,
})), 1, 20, 25))
.mockResolvedValueOnce(candidatePage(Array.from({ length: 5 }, (_, index) => ({
...candidate,
inboundId: String(120 + index),
orderId: String(220 + index),
inboundCode: `IN-${String(index + 21).padStart(3, '0')}`,
})), 2, 20, 25))
2026-09-01 21:09:39 +08:00
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const pane = wrapper.get('.candidate-pane')
expect(pane.text()).toContain('IN-020')
expect(pane.text()).not.toContain('IN-021')
2026-09-03 13:49:40 +08:00
expect(pane.text()).toContain('共 25 条 · 1/2 页')
2026-09-01 21:09:39 +08:00
await pane.findAll('button').find((button) => button.attributes('aria-label') === '下一页')!.trigger('click')
await flushPromises()
expect(pane.text()).toContain('IN-021')
expect(pane.text()).not.toContain('IN-001')
})
it('aggregates a two-order candidate group and calls only whole-group assignment', async () => {
2026-09-03 15:00:58 +08:00
vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([{
accountId: '1', inboundId: '201', orderId: '301', orderNo: 'ORD-201', inboundCode: 'NYEXX2608290051', 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: '2093539779795795969', groupNo: 'CLG-2093539779795795969', memberCount: 3, groupTotalBoxes: 12, locked: false, blockReason: null,
groupMembers: [
{ accountId: '1', inboundId: '201', orderId: '301', orderNo: 'ORD-201', inboundCode: 'NYEXX2608290051', customerName: '客户一', availablePackageCount: 4, orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: null },
{ accountId: '1', inboundId: '203', orderId: '301', orderNo: 'ORD-201', inboundCode: 'NYEXX2608290051-B', customerName: '客户一', availablePackageCount: 2, orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: null },
{ accountId: '2', inboundId: '202', orderId: '302', orderNo: 'ORD-202', inboundCode: 'NYEXX2608290052', customerName: '客户二', availablePackageCount: 6, orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: null },
],
}]))
vi.mocked(assignSmartLoadingGroup).mockResolvedValue(plan)
const wrapper = mount(GlobalSmartLoadingView, { attachTo: document.body, global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.text()).toContain('CLG-2093539779795795969 · 2 票')
expect(wrapper.text()).toContain('3 个入仓片段')
expect(wrapper.text()).toContain('ORD-201:NYEXX2608290051、NYEXX2608290051-B')
const buttons = wrapper.findAll('button').filter((button) => button.text().includes('整组分配'))
expect(buttons).toHaveLength(1)
await buttons[0].trigger('click')
await flushPromises()
expect(document.body.textContent).toContain('不能部分输入、单独分配或单独移除')
expect(document.body.textContent).toContain('目标柜次:CB-1 · 集装箱号:MSCU-1')
expect(document.body.textContent).toContain('发货仓:义乌仓 · 柜次渠道:美森正班')
expect(document.body.textContent).not.toContain('本次分配箱数')
;([...document.body.querySelectorAll('button')].find((button) => button.textContent?.includes('确认整组分配')) as HTMLButtonElement).click()
await flushPromises()
expect(assignSmartLoadingGroup).toHaveBeenCalledWith(plan.id, '201', plan.version)
expect(assignSmartLoadingItem).not.toHaveBeenCalled()
wrapper.unmount()
})
2026-09-03 21:25:56 +08:00
it('allows a pending-review order to join through the provisional assignment command', async () => {
vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([{
...candidate,
2026-09-04 10:51:31 +08:00
customerId: '',
2026-09-03 21:25:56 +08:00
inboundId: null,
orderStatusSnapshot: 'PENDING_REVIEW',
inboundCode: 'EXXHK2609020361',
channelCode: 'HEDE_TRUCK',
channelName: '合德海卡',
unitGrossWeightKg: null,
averageUnitVolumeCbm: null,
availableVolumeCbm: null,
2026-09-03 21:25:56 +08:00
blockReason: null,
}]))
vi.mocked(assignPendingReviewSmartLoadingOrder).mockResolvedValue(plan)
const wrapper = mount(GlobalSmartLoadingView, { attachTo: document.body, global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.get('.candidate-pane').text()).toContain('待审核')
const add = wrapper.get('.candidate-pane .el-table__body-wrapper').findAll('button')
.find((button) => button.text().includes('加入'))!
expect(add.classes()).not.toContain('is-disabled')
await add.trigger('click')
await flushPromises()
expect(document.body.textContent).toContain('平均单箱毛重 待测量')
expect(document.body.textContent).toContain('平均单箱体积 待测量')
expect(document.body.textContent).not.toContain('平均单箱毛重 0 kg')
expect(document.body.textContent).toContain('订单渠道:合德海卡')
2026-09-04 15:54:36 +08:00
expect(document.body.textContent).toContain('这是人工装柜分配,确认后仍会加入')
2026-09-04 10:51:31 +08:00
expect(document.body.textContent).toContain('智能推荐不会跨渠道')
2026-09-03 21:25:56 +08:00
;([...document.body.querySelectorAll('button')]
.find((button) => button.textContent?.includes('确认加入')) as HTMLButtonElement).click()
await flushPromises()
expect(assignPendingReviewSmartLoadingOrder).toHaveBeenCalledWith(plan.id, {
customerId: '1', orderId: '7', inboundCode: 'EXXHK2609020361',
packageCount: 12, expectedVersion: plan.version,
})
expect(assignSmartLoadingItem).not.toHaveBeenCalled()
wrapper.unmount()
})
it('blocks an incomplete three-member candidate group without exposing member assignment', async () => {
2026-09-03 15:00:58 +08:00
const members = ['401', '402', '403'].map((inboundId, index) => ({ accountId: `${index + 1}`, inboundId, orderId: `50${index}`, orderNo: `ORD-${index}`, inboundCode: `IN-${inboundId}`, customerName: `客户${index}`, availablePackageCount: 6, orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: null }))
vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([{
...candidate, inboundId: '401', orderId: '500', inboundCode: 'IN-401', coLoadGroupId: '9900', groupNo: 'CLG-9900', memberCount: 3, groupTotalBoxes: 18, availablePackageCount: 18, locked: false, blockReason: '同柜不可拆组成员候选不齐,缺少 1 票', groupMembers: members,
}]))
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.text()).toContain('CLG-9900 · 3 票')
expect(wrapper.text()).toContain('同柜不可拆组成员候选不齐,缺少 1 票')
const button = wrapper.findAll('button').find((item) => item.text().includes('整组分配'))!
expect(button.attributes('disabled')).toBeDefined()
expect(assignSmartLoadingGroup).not.toHaveBeenCalled()
})
2026-09-04 14:17:36 +08:00
it('shows the package summary first and opens the editor from an icon', () => {
2026-08-27 21:48:15 +08:00
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
2026-09-04 14:17:36 +08:00
expect(source).toContain('class="package-count-summary"')
expect(source).toContain('aria-label="调整箱数"')
expect(source).toContain('v-if="(row.remainingPackageCount ?? 0) > 0"')
expect(source).toContain('v-if="editingQuantityId === row.id"')
2026-09-04 12:18:39 +08:00
expect(source).toContain(':controls="false"')
expect(source).toContain('@blur="adjust(row)" @keyup.enter="adjust(row)"')
2026-09-03 16:48:25 +08:00
expect(source).not.toContain('<Save')
2026-08-27 21:48:15 +08:00
expect(source).toContain('.plan-pane :deep(.package-count-editor .el-input-number) { width: 100%; min-width: 0; }')
})
2026-09-04 12:18:39 +08:00
it('saves a typed package count through the adjustment API when editing finishes', async () => {
vi.mocked(adjustSmartLoadingItem).mockResolvedValue({
...plan,
version: 3,
items: [{ ...plan.items[0], packageCount: 4, version: 2 }],
})
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
2026-09-04 14:17:36 +08:00
await wrapper.find('[aria-label="调整箱数"]').trigger('click')
await flushPromises()
2026-09-04 12:18:39 +08:00
const quantityInput = wrapper.findAllComponents(ElInputNumber).find((input) => input.props('modelValue') === 6)
expect(quantityInput?.props('controls')).toBe(false)
quantityInput?.vm.$emit('update:modelValue', 4)
await flushPromises()
quantityInput?.vm.$emit('blur')
await flushPromises()
expect(adjustSmartLoadingItem).toHaveBeenCalledTimes(1)
expect(adjustSmartLoadingItem).toHaveBeenCalledWith(plan.id, plan.items[0].id, 4, plan.version, plan.items[0].version)
})
2026-08-11 20:02:38 +08:00
it('rounds capacity percentages and hides column settings in embedded tables', async () => {
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({
...plan,
items: [{ ...plan.items[0], volumeCbm: 0.187, grossWeightKg: 13.6 }],
})
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
2026-09-03 21:52:28 +08:00
expect(wrapper.text()).toContain('0.16%')
expect(wrapper.text()).toContain('0.03%')
2026-08-11 20:02:38 +08:00
expect(wrapper.text()).not.toContain('0.169999999')
expect(wrapper.findAll('button[aria-label="列设置"]')).toHaveLength(0)
})
2026-09-01 21:09:39 +08:00
it('keeps the confirmed candidate filters without unrelated customs fields', async () => {
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
2026-08-10 17:42:17 +08:00
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
2026-09-03 22:15:25 +08:00
const fields = wrapper.findComponent(QueryToolbar).props('fields') as Array<{ key: string; options?: Array<{ label: string; value: string }>; props?: Record<string, unknown> }>
2026-09-04 18:47:43 +08:00
expect(fields.map((field) => field.key)).toEqual(['warehouseId', 'customerId', 'inboundCode', 'createdRange', 'expectedArrivalRange', 'orderStatuses'])
expect(fields.find((field) => field.key === 'warehouseId')?.options).toEqual([
{ label: '义乌仓', value: '8' },
{ label: '东莞仓', value: '18' },
])
2026-09-03 22:15:25 +08:00
expect(fields.find((field) => field.key === 'orderStatuses')?.options).toEqual([
2026-09-03 11:54:20 +08:00
{ value: 'PENDING_REVIEW', label: '待审核' },
{ value: 'WAIT_MEASURE', label: '待测量数据' },
{ value: 'WAIT_SPLIT', label: '待分柜' },
{ value: 'LOADING', label: '装柜中' },
])
2026-09-03 22:15:25 +08:00
expect(fields.find((field) => field.key === 'orderStatuses')?.props).toMatchObject({ multiple: true, collapseTags: true, maxCollapseTags: 2 })
expect(source).not.toContain('目的仓')
2026-08-11 11:52:30 +08:00
2026-08-10 17:42:17 +08:00
expect(wrapper.text()).not.toContain('税率')
expect(wrapper.text()).not.toContain('是否退税')
})
2026-09-03 15:00:58 +08:00
it('relies on the server contract to exclude completed candidates', async () => {
vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([candidate]))
2026-09-03 11:54:20 +08:00
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.get('.candidate-pane').text()).toContain('IN-2')
expect(wrapper.get('.candidate-pane').text()).not.toContain('IN-LOADED')
expect(wrapper.get('.candidate-pane').text()).not.toContain('IN-COMPLETED')
2026-09-03 15:00:58 +08:00
expect(listSmartLoadingCandidates).toHaveBeenCalledWith({ channelGroupCodes: ['MATSON_CLX'], page: 1, size: 20 })
2026-09-03 11:54:20 +08:00
})
2026-08-17 16:45:46 +08:00
it('prefers an editing container on first load while keeping loading containers available', async () => {
const pushed = { ...container, id: '9007199254740995', planStatus: 'LOADING' as const }
vi.mocked(listContainers).mockResolvedValue({ records: [pushed, container], page: 1, size: 100, total: 2, pages: 1 })
2026-09-03 15:00:58 +08:00
mockContainerSearch([pushed, container])
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(container.id)
2026-08-14 19:59:32 +08:00
expect(wrapper.text()).toContain('MSCU-1 装柜方案')
})
2026-08-14 19:59:32 +08:00
it('keeps waiting and editing empty containers without exposing their status labels', async () => {
const waiting = { ...container, id: '9007199254740995', planId: '9007199254740996', cabinetNo: 'CB-WAIT', containerNo: null, planStatus: 'WAIT_LOADING' as const }
2026-08-11 14:23:46 +08:00
vi.mocked(listContainers).mockResolvedValue({ records: [container, waiting], page: 1, size: 100, total: 2, pages: 1 })
2026-09-03 15:00:58 +08:00
mockContainerSearch([container, waiting])
2026-08-11 14:23:46 +08:00
vi.mocked(getSmartLoadingPlanByBooking).mockImplementation(async (bookingId) => (
bookingId === waiting.id
? { ...plan, bookingId: waiting.id, status: 'LOADING' as const }
: plan
))
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const waitingOption = wrapper.findAll('.container-option').find((item) => item.text().includes('CB-WAIT'))!
2026-08-14 19:59:32 +08:00
expect(waitingOption.text()).toContain('CB-WAIT')
expect(waitingOption.text()).not.toContain('待装柜')
2026-08-11 14:23:46 +08:00
2026-09-03 15:00:58 +08:00
const candidateCalls = vi.mocked(listSmartLoadingCandidates).mock.calls.length
2026-08-11 14:23:46 +08:00
await waitingOption.trigger('click')
await flushPromises()
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(waiting.id)
2026-09-03 15:00:58 +08:00
expect(listSmartLoadingCandidates).toHaveBeenCalledTimes(candidateCalls)
2026-08-14 19:59:32 +08:00
expect(waitingOption.text()).toContain('CB-WAIT')
2026-08-11 14:23:46 +08:00
expect(waitingOption.text()).not.toContain('拣货中')
})
2026-08-17 16:45:46 +08:00
it('keeps picking containers available when the plan capability allows increments', async () => {
const pushed = { ...container, id: '9007199254740995', planId: '9007199254740996', cabinetNo: 'CB-2', planStatus: 'LOADING' as const }
vi.mocked(listContainers).mockResolvedValue({ records: [pushed], page: 1, size: 100, total: 1, pages: 1 })
2026-09-03 15:00:58 +08:00
mockContainerSearch([pushed])
2026-08-17 16:45:46 +08:00
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({ ...plan, bookingId: pushed.id, status: 'LOADING', pdaTaskId: '700', pdaTaskStatus: 'LOADING', pdaTaskVersion: 4 })
2026-08-10 20:53:48 +08:00
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
2026-08-17 16:45:46 +08:00
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(pushed.id)
2026-09-03 15:00:58 +08:00
expect(listSmartLoadingCandidates).toHaveBeenCalledWith({ channelGroupCodes: ['MATSON_CLX'], page: 1, size: 20 })
2026-08-17 16:45:46 +08:00
expect(wrapper.findAll('.container-option')).toHaveLength(1)
expect(wrapper.text()).toContain('CB-2')
2026-08-18 16:09:43 +08:00
expect(wrapper.text()).not.toContain('已下发 PDA,仍可维护未完成的装柜明细')
expect(wrapper.text()).not.toContain('刷新 PDA 明细与版本')
expect(wrapper.text()).toContain('示例企业')
})
2026-08-11 14:36:08 +08:00
it('hides completed containers from the smart loading workbench', async () => {
const completed = { ...container, id: '9007199254740997', planId: '9007199254740998', cabinetNo: 'CB-3', planStatus: 'COMPLETED' as const }
vi.mocked(listContainers).mockResolvedValue({ records: [completed], page: 1, size: 100, total: 1, pages: 1 })
2026-09-03 15:00:58 +08:00
mockContainerSearch([completed])
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(getSmartLoadingPlanByBooking).not.toHaveBeenCalled()
2026-08-11 14:36:08 +08:00
expect(wrapper.text()).not.toContain('CB-3')
expect(wrapper.text()).toContain('暂无符合条件的货柜')
2026-08-17 18:32:41 +08:00
expect(wrapper.text()).not.toContain('缺少渠道快照')
})
it('isolates a missing-channel row without hiding another valid container', async () => {
const missingChannel = { ...container, id: '9007199254740997', cabinetNo: 'CB-MISSING', channelGroupCode: '', channelCode: null, channelName: null }
2026-08-17 18:32:41 +08:00
vi.mocked(listContainers).mockResolvedValue({ records: [missingChannel, container], page: 1, size: 100, total: 2, pages: 1 })
2026-09-03 15:00:58 +08:00
mockContainerSearch([missingChannel, container])
2026-08-17 18:32:41 +08:00
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.findAll('.container-option')).toHaveLength(1)
expect(wrapper.text()).toContain('CB-1')
expect(wrapper.text()).not.toContain('CB-MISSING')
expect(wrapper.text()).not.toContain('缺少渠道快照')
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(container.id)
})
2026-09-03 15:00:58 +08:00
it('renders the channel-filtered candidate page returned by the server', async () => {
vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([
{ accountId: '1', inboundId: '5', orderId: '7', orderNo: 'ORD-2', inboundCode: 'IN-MATCH', 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 },
2026-09-03 15:00:58 +08:00
]))
2026-08-17 18:32:41 +08:00
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
2026-08-18 16:09:43 +08:00
expect(wrapper.text()).toContain('匹配企业')
2026-08-17 18:32:41 +08:00
expect(wrapper.text()).not.toContain('IN-OTHER')
expect(wrapper.text()).not.toContain('其他货物')
2026-09-03 15:00:58 +08:00
expect(listSmartLoadingCandidates).toHaveBeenCalledWith({ channelGroupCodes: ['MATSON_CLX'], page: 1, size: 20 })
2026-08-10 20:53:48 +08:00
})
2026-08-10 17:42:17 +08:00
it('refreshes the latest plan after A0409 without losing the selected container', async () => {
2026-08-14 19:59:32 +08:00
const warningMessage = vi.spyOn(ElMessage, 'warning').mockImplementation(() => undefined as never)
2026-08-10 17:42:17 +08:00
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({ ...plan, version: 3 })
2026-08-22 10:02:13 +08:00
vi.mocked(recommendSmartLoading).mockRejectedValueOnce(new AxiosError(
2026-08-10 17:42:17 +08:00
'conflict',
'ERR_BAD_REQUEST',
undefined,
undefined,
{ data: { code: 'A0409', message: '数据已被修改', requestId: 'request-1', traceId: 'trace-1' }, status: 200, statusText: 'OK', headers: {}, config: {} as never },
2026-08-22 10:02:13 +08:00
)).mockResolvedValueOnce({ ...plan, version: 4 })
2026-08-10 17:42:17 +08:00
const button = wrapper.findAll('button').find((item) => item.text().includes('智能推荐'))
expect(button).toBeDefined()
await button!.trigger('click')
await flushPromises()
expect(recommendSmartLoading).toHaveBeenCalledWith(plan.id, 2)
2026-08-22 10:02:13 +08:00
expect(recommendSmartLoading).toHaveBeenCalledWith(plan.id, 3)
expect(getSmartLoadingPlanByBooking).toHaveBeenCalledTimes(2)
2026-08-22 10:02:13 +08:00
expect(warningMessage).not.toHaveBeenCalled()
2026-08-14 19:59:32 +08:00
expect(wrapper.text()).not.toContain('装柜操作失败')
expect(wrapper.text()).not.toContain('request-1')
2026-08-10 17:42:17 +08:00
})
2026-08-11 11:52:30 +08:00
it('shows a picking-specific message when a picked order cannot be removed', async () => {
vi.mocked(removeSmartLoadingItem).mockRejectedValue(new AxiosError(
'conflict',
'ERR_BAD_REQUEST',
undefined,
undefined,
{ data: { code: 'A0409', message: '已有拣货记录的明细不能删除', requestId: 'request-remove-1', traceId: 'trace-remove-1' }, status: 200, statusText: 'OK', headers: {}, config: {} as never },
2026-08-11 11:52:30 +08:00
))
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as never)
const errorMessage = vi.spyOn(ElMessage, 'error').mockImplementation(() => undefined as never)
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const removeButton = wrapper.find('.row-actions .el-button--danger')
expect(removeButton.exists()).toBe(true)
await removeButton.trigger('click')
await flushPromises()
expect(removeSmartLoadingItem).toHaveBeenCalled()
expect(errorMessage).toHaveBeenCalledWith('该订单在拣货中不许删除')
expect(wrapper.text()).toContain('装柜操作失败')
expect(wrapper.text()).toContain('request-remove-1')
expect(wrapper.text()).toContain('trace-remove-1')
2026-08-14 19:59:32 +08:00
})
2026-09-04 10:51:31 +08:00
it('removes every unpicked item with one bulk request', async () => {
2026-08-14 19:59:32 +08:00
const secondItem = { ...plan.items[0], id: '102', inboundDetailId: '4', inboundCode: 'IN-2' }
const initialPlan = { ...plan, items: [plan.items[0], secondItem] }
const emptyPlan = { ...plan, version: 4, items: [] }
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValueOnce(initialPlan).mockResolvedValue(emptyPlan)
2026-09-04 10:51:31 +08:00
vi.mocked(removeAllUnpickedSmartLoadingItems).mockResolvedValueOnce(emptyPlan)
2026-08-14 19:59:32 +08:00
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as never)
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
2026-08-17 16:45:46 +08:00
const removeAllButton = wrapper.findAll('button').find((button) => button.text().includes('移除未拣'))
2026-08-14 19:59:32 +08:00
expect(removeAllButton).toBeDefined()
await removeAllButton!.trigger('click')
await flushPromises()
2026-09-04 10:51:31 +08:00
expect(removeAllUnpickedSmartLoadingItems).toHaveBeenCalledTimes(1)
expect(removeAllUnpickedSmartLoadingItems).toHaveBeenCalledWith(plan.id, 2)
expect(removeSmartLoadingItem).not.toHaveBeenCalled()
2026-08-18 16:09:43 +08:00
expect(wrapper.text()).toContain('当前柜次暂无装柜明细')
2026-08-14 19:59:32 +08:00
})
2026-08-24 18:07:38 +08:00
it('shows one row per inbound code and removes all internal specification rows', async () => {
const secondDetail = {
...plan.items[0], id: '102', inboundDetailId: '4', skuCode: 'SKU-2', productNameZh: '鞋类',
packageCount: 4, grossWeightKg: 40, volumeCbm: 0.8, loadSequence: 2,
}
const groupedPlan = { ...plan, items: [plan.items[0], secondDetail] }
const afterFirst = { ...plan, version: 3, items: [secondDetail] }
const emptyPlan = { ...plan, version: 4, items: [] }
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValueOnce(groupedPlan).mockResolvedValue(emptyPlan)
vi.mocked(removeSmartLoadingItem).mockResolvedValueOnce(afterFirst).mockResolvedValueOnce(emptyPlan)
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as never)
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const inboundRows = wrapper.findAll('tr').filter((row) => row.text().includes('IN-1'))
expect(inboundRows).toHaveLength(1)
2026-09-03 16:48:25 +08:00
expect(inboundRows[0].text()).toContain('多规格,需删除后重加')
2026-08-24 18:07:38 +08:00
expect(inboundRows[0].text()).toContain('100 kg')
expect(inboundRows[0].text()).toContain('2 m³')
await inboundRows[0].find('.el-button--danger').trigger('click')
await flushPromises()
expect(removeSmartLoadingItem).toHaveBeenNthCalledWith(1, plan.id, plan.items[0].id, 2, 1)
expect(removeSmartLoadingItem).toHaveBeenNthCalledWith(2, plan.id, secondDetail.id, 3, 1)
})
2026-08-17 16:45:46 +08:00
it('uses item capabilities to protect picked quantities and removal', async () => {
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({
...plan,
status: 'LOADING',
pdaTaskId: '700',
pdaTaskStatus: 'LOADING',
pdaTaskVersion: 4,
items: [{ ...plan.items[0], pickedPackageCount: 3, minPackageCount: 3, canAdjust: true, canRemove: false }],
})
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.text()).toContain('已拣 3 箱')
expect(wrapper.findAll('.row-actions .el-button--danger')).toHaveLength(0)
2026-09-04 14:17:36 +08:00
await wrapper.find('[aria-label="调整箱数"]').trigger('click')
await flushPromises()
2026-08-17 16:45:46 +08:00
const quantityInput = wrapper.findAllComponents(ElInputNumber).find((input) => input.props('modelValue') === 6)
expect(quantityInput?.props('min')).toBe(3)
expect(wrapper.findAll('button').some((button) => button.text().includes('移除未拣'))).toBe(false)
})
it('removes a sealed pallet row through the whole-pallet command', async () => {
const palletItem = {
...plan.items[0], unitType: 'SEALED_PALLET' as const, palletId: '801', palletNo: 'DP-260914-0021',
indivisible: true, packageCount: 10, pickedPackageCount: 0, minPackageCount: 10,
canAdjust: false, canRemove: true,
}
const palletPlan = { ...plan, items: [palletItem] }
const emptyPlan = { ...palletPlan, version: 3, items: [] }
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue(palletPlan)
vi.mocked(removeSmartLoadingPallet).mockResolvedValue(emptyPlan)
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
const row = wrapper.findAll('tr').find((candidateRow) => candidateRow.text().includes('DP-260914-0021'))
expect(row).toBeDefined()
await row!.find('.el-button--danger').trigger('click')
await flushPromises()
expect(removeSmartLoadingPallet).toHaveBeenCalledWith(plan.id, '801', 2)
expect(removeSmartLoadingItem).not.toHaveBeenCalled()
})
2026-08-17 16:45:46 +08:00
it('renders final completion capability as read-only with the server reason', async () => {
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({
...plan,
canAssign: false,
canEdit: false,
blockedReason: '正在校验装柜证据',
finalCompletionStatus: 'IN_PROGRESS',
})
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
expect(wrapper.text()).toContain('正在最终确认,装柜方案暂时只读')
expect(wrapper.text()).toContain('正在校验装柜证据')
expect(wrapper.text()).not.toContain('请先完成拖车')
expect(wrapper.findAll('.row-actions button')).toHaveLength(0)
expect(wrapper.findAll('button').some((button) => button.text().includes('加入'))).toBe(false)
})
2026-09-04 14:17:36 +08:00
it('uses the server supplied total package count as the adjustment maximum', async () => {
2026-09-03 15:00:58 +08:00
vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([{ accountId: '1', inboundId: '2', orderId: '4', orderNo: 'ORD-1', inboundCode: 'IN-1', warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '示例企业', goodsSummary: '服装', availablePackageCount: 4, unitGrossWeightKg: 10, unitNetWeightKg: 9, averageUnitVolumeCbm: 0.2, availableVolumeCbm: 0.8, coLoadGroupId: null, groupNo: null, memberCount: null, groupTotalBoxes: null, locked: null, blockReason: null }]))
2026-09-04 14:17:36 +08:00
vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({
...plan,
items: [{ ...plan.items[0], totalPackageCount: 10, remainingPackageCount: 4 }],
})
2026-08-14 19:59:32 +08:00
const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } })
await flushPromises()
2026-09-04 14:17:36 +08:00
expect(wrapper.text()).toContain('使用:6')
expect(wrapper.text()).toContain('未用:4')
expect(wrapper.text()).not.toContain('共 10 箱')
await wrapper.find('[aria-label="调整箱数"]').trigger('click')
await flushPromises()
2026-08-14 19:59:32 +08:00
const maxima = wrapper.findAllComponents(ElInputNumber).map((input) => input.props('max'))
expect(maxima).toContain(10)
2026-08-11 11:52:30 +08:00
})
2026-08-12 18:00:26 +08:00
it('uses bordered assignment and adjustment actions at narrow widths', () => {
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
expect(source).toContain('size="small"')
expect(source).toContain('type="danger" plain')
2026-08-14 19:59:32 +08:00
expect(source).toContain(':max="adjustmentMax(row)"')
2026-09-03 21:25:56 +08:00
expect(source).toContain("{ key: 'customerName', prop: 'customerName', label: '客户', minWidth: 70, slotName: 'customerName' }")
2026-08-12 18:00:26 +08:00
expect(source).not.toContain('link type="danger"')
expect(source).toContain('@media (max-width: 720px)')
2026-08-12 18:16:44 +08:00
expect(source.match(/<DataTable[\s\S]*?\bstripe\b/g)).toHaveLength(2)
2026-08-12 18:00:26 +08:00
})
2026-08-14 19:59:32 +08:00
it('bounds all three workbench panes and keeps their content independently scrollable', () => {
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
expect(source).toContain('height: clamp(520px, 68vh, 720px); min-height: 0;')
2026-09-03 16:48:25 +08:00
expect(source).toContain('grid-template-columns: minmax(0, 5fr) minmax(0, 2fr) minmax(0, 3fr);')
2026-08-14 19:59:32 +08:00
expect(source).toContain('.dispatch-pane { display: flex; min-width: 0; min-height: 0; flex-direction: column; }')
expect(source).toContain('.candidate-pane :deep(.data-table),.plan-pane :deep(.data-table) { min-height: 0; flex: 1; }')
2026-09-03 13:49:40 +08:00
expect(source).toContain(".candidate-pane :deep(.data-table__page-button[aria-label^='第']),.candidate-pane :deep(.data-table__page-ellipsis) { display: none; }")
expect(source).toContain('.candidate-pane :deep(.data-table__page-size .el-select) { width: 68px; }')
2026-08-14 19:59:32 +08:00
expect(source).toContain('.container-list { display: grid; min-height: 0; flex: 1;')
expect(source).toContain('grid-auto-rows: max-content;')
expect(source).toContain('overflow-wrap: anywhere;')
expect(source).toContain('.candidate-pane,.plan-pane { height: min(68vh, 560px); min-height: 420px; }')
expect(source).toContain('.container-pane { max-height: min(58vh, 460px); }')
2026-08-15 12:12:38 +08:00
expect(source).toContain('min-height: 112px')
2026-09-03 21:25:56 +08:00
expect(source).toContain("label: '创建日期'")
2026-09-04 18:47:43 +08:00
expect(source).toContain("label: '预计到货时间'")
2026-09-01 21:09:39 +08:00
expect(source).toContain("label: '订单状态'")
expect(source).toContain('.smart-loading-query .query-toolbar__filters) { flex-wrap: nowrap; gap: 8px; overflow-x: auto; }')
expect(source).toContain('.smart-loading-query .query-toolbar__actions) { flex-wrap: nowrap; gap: 6px; }')
2026-09-01 21:09:39 +08:00
expect(source).toContain(':page-sizes="[20, 50, 100]"')
2026-08-14 19:59:32 +08:00
})
2026-09-03 11:47:32 +08:00
it('uses nine fixed multi-select channel tags with a compact right-aligned summary', () => {
2026-08-14 19:59:32 +08:00
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8')
2026-09-03 11:47:32 +08:00
expect(source).toContain('<el-checkbox-group :model-value="selectedChannelCodes"')
2026-09-04 14:41:29 +08:00
expect(source).toContain('v-for="channel in channelOptions"')
expect(source).toContain('@dblclick.stop.prevent="selectOnlyChannel(channel.code)"')
2026-09-03 11:47:32 +08:00
expect(source).toContain('{{ channel.name }}')
expect(source).toContain("ElMessage.warning('至少选择一个渠道')")
expect(source).toContain('class="compact-summary" aria-label="已选渠道装柜统计"')
2026-08-18 16:09:43 +08:00
expect(source).toContain('<dt>候选明细</dt>')
2026-09-03 11:47:32 +08:00
expect(source).toContain('<dt>柜子数量</dt>')
2026-08-14 19:59:32 +08:00
expect(source).toContain('<dt>已装</dt>')
expect(source).toContain('<dt>总重量</dt>')
expect(source).toContain('<dt>总体积</dt>')
2026-09-03 11:47:32 +08:00
expect(source).toContain('.channel-tags :deep(.el-checkbox-button__inner)')
expect(source).toContain('flex-wrap: nowrap; gap: 6px;')
expect(source).toContain('height: 34px; padding: 0 10px;')
expect(source).toContain('font-size: 13px; line-height: 32px;')
2026-08-14 19:59:32 +08:00
expect(source).not.toContain('class="summary-band"')
expect(source).toContain('class="capacity-grid"')
expect(source).toContain('智能推荐')
2026-09-01 20:07:40 +08:00
expect(source).not.toContain('推送预装')
expect(source).toContain('缺少自动生成的预装计划')
2026-08-27 21:48:15 +08:00
expect(source).not.toContain('关联并放舱真实订舱')
expect(source).not.toContain('订舱放舱')
expect(source).not.toContain("canPush && plan?.status === 'EDITING' && selectedContainer?.containerNo")
2026-08-14 19:59:32 +08:00
})
2026-08-10 17:42:17 +08:00
})