import ElementPlus, { ElCheckboxButton, ElInputNumber, ElMessage, ElMessageBox, ElOption, ElSelect } from 'element-plus' import { AxiosError } from 'axios' import { createPinia, setActivePinia } from 'pinia' import { DOMWrapper, enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { readFileSync } from 'node:fs' import { resolve } from 'node:path' import QueryToolbar from '@/shared/components/QueryToolbar.vue' import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam' import GlobalSmartLoadingView from './GlobalSmartLoadingView.vue' import AppDialog from '@/shared/components/AppDialog.vue' import SmartLoadingDialog from '../components/SmartLoadingDialog.vue' import type { ContainerBookingResource } from '../types' import { assignPendingReviewSmartLoadingOrder, assignSmartLoadingGroup, assignSmartLoadingItem, assignSmartLoadingPallet, getSmartLoadingPlanByBooking, listContainers, listSmartLoadingCandidates, listSmartLoadingWorkbenchContainers, recommendSmartLoading, adjustSmartLoadingItem, removeAllUnpickedSmartLoadingItems, removeSmartLoadingGroup, removeSmartLoadingItem, removeSmartLoadingPallet, } from '../api/platformContainerLoading' enableAutoUnmount(afterEach) const { routerPush } = vi.hoisted(() => ({ routerPush: vi.fn() })) vi.mock('vue-router', () => ({ useRouter: () => ({ push: routerPush }), })) vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }), })) vi.mock('@/modules/iam', async importOriginal => ({ ...await importOriginal(), listPlatformTenants: vi.fn().mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }), })) vi.mock('../api/platformContainerLoading', () => ({ listContainers: vi.fn(), getSmartLoadingPlanByBooking: vi.fn(), listSmartLoadingCandidates: vi.fn(), listSmartLoadingWorkbenchContainers: vi.fn(), createSmartLoadingPlan: vi.fn(), assignSmartLoadingItem: vi.fn(), assignSmartLoadingPallet: vi.fn(), assignPendingReviewSmartLoadingOrder: vi.fn(), assignSmartLoadingGroup: vi.fn(), recommendSmartLoading: vi.fn(), adjustSmartLoadingItem: vi.fn(), removeSmartLoadingItem: vi.fn(), removeSmartLoadingGroup: vi.fn(), removeSmartLoadingPallet: vi.fn(), removeAllUnpickedSmartLoadingItems: vi.fn(), 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()) })) const container = { id: '9007199254740993', planId: '9007199254740994', cabinetNo: 'CB-1', containerNo: 'MSCU-1', warehouseId: '8', warehouseName: '义乌仓', channelGroupCode: 'MATSON_CLX', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', containerType: '40HQ', planStatus: 'EDITING' as const, version: 1, billOfLadingNo: null, vesselName: null, voyageNo: null, shippingCompanyId: '31', shippingCompanyName: '美森轮船', departurePortCode: null, departurePortName: null, destinationPortCode: null, destinationPortName: null, destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', capacityCbm: 68, maxWeightKg: 19500, terminalTruckingCompanyId: null, terminalTruckingCompany: null, loadingAt: null, estimatedSailingAt: null, actualSailingAt: null, 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, canAssign: true, canEdit: true, blockedReason: null, finalCompletionStatus: 'NOT_STARTED' as const, pdaTaskId: null, pdaTaskStatus: null, pdaTaskVersion: null, coLoadGroups: [], palletMembers: [], 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 }], } 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, availableVolumeCbm: 2.4, estimatedVolumeCbm: 3.5, coLoadGroupId: null, groupNo: null, memberCount: null, groupTotalBoxes: null, locked: null, blockReason: null, } 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[] } return { customerId: candidate.customerId || candidate.accountId || '', groupMembers: [], unitType: 'PACKAGE', palletId: null, palletNo: null, indivisible: false, ...candidate } }), page, size, total, pages: total ? Math.ceil(total / size) : 0, } as Awaited> } function containerPage(records: ContainerBookingResource[] = [container]) { return { records: records.map((item) => ({ bookingId: item.id, 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, })), page: 1, size: 200, total: records.length, pages: records.length ? 1 : 0, } as Awaited> } function mockContainerSearch(records: ContainerBookingResource[]) { vi.mocked(listSmartLoadingWorkbenchContainers).mockImplementation(async (query) => containerPage(records.filter((item) => ( item.planStatus !== 'COMPLETED' && query.channelGroupCodes.includes(item.channelGroupCode) )))) } function assignmentDialog() { const root = document.body.querySelector('.smart-loading-assignment-dialog') if (!root) throw new Error('assignment dialog is missing') return new DOMWrapper(root) } describe('GlobalSmartLoadingView', () => { it('mounts assignment outside the parent workbench and keeps fullscreen and reopen states independent', async () => { const wrapper = mount(SmartLoadingDialog, { attachTo: document.body, props: { modelValue: true }, global: { plugins: [ElementPlus] } }) try { await flushPromises() const workbench = wrapper.getComponent(GlobalSmartLoadingView) const openAssignment = async () => { await workbench.get('.candidate-pane .el-table__body-wrapper').findAll('button').find(button => button.text() === '加入')!.trigger('click') await flushPromises() } await openAssignment() const popup = workbench.getComponent(AppDialog) const outer = document.querySelector('.smart-loading-dialog')! const child = assignmentDialog() expect(document.body.contains(child.element)).toBe(true) expect(outer.contains(child.element)).toBe(false) expect(child.element.closest('.global-loading-page')).toBeNull() expect(child.classes()).toEqual(expect.arrayContaining(['admin-density-dialog', 'admin-workbench', 'is-dialog'])) popup.getComponent(ElInputNumber).vm.$emit('update:modelValue', 4) await child.get('[aria-label="展开全屏"]').trigger('click') expect(child.classes()).toContain('is-fullscreen') expect(outer.classList.contains('is-fullscreen')).toBe(true) await child.findAll('button').find(button => button.text() === '取消')!.trigger('click') await flushPromises() expect(popup.props('modelValue')).toBe(false) expect(wrapper.getComponent(AppDialog).props('modelValue')).toBe(true) expect(workbench.get('.container-option.selected').attributes('aria-pressed')).toBe('true') await openAssignment() expect(popup.props('modelValue')).toBe(true) expect(assignmentDialog().classes()).not.toContain('is-fullscreen') expect(popup.getComponent(ElInputNumber).props('modelValue')).toBe(candidate.availablePackageCount) expect(outer.classList.contains('is-fullscreen')).toBe(true) expect(assignSmartLoadingItem).not.toHaveBeenCalled() } finally { wrapper.unmount() } }) it.each(['PACKAGE', 'SEALED_PALLET'] as const)('分配窗口全屏保留来源及输入,按%s原接口提交', async (unitType) => { if (unitType === 'SEALED_PALLET') { vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([{ ...candidate, unitType, palletId: '801', palletNo: 'DP-TEST-1', indivisible: true, groupMembers: [ { accountId: '1', inboundId: '180', orderId: '180', orderNo: 'O180', inboundCode: 'I180', customerName: '客户甲', availablePackageCount: 3, orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: null }, { accountId: '1', inboundId: '181', orderId: '181', orderNo: 'O181', inboundCode: 'I181', customerName: '客户甲', availablePackageCount: 9, orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: null }, ], }])) } vi.mocked(assignSmartLoadingItem).mockResolvedValue(plan) vi.mocked(assignSmartLoadingPallet).mockResolvedValue(plan) const wrapper = mount(GlobalSmartLoadingView, { attachTo: document.body, global: { plugins: [ElementPlus] } }) await flushPromises() await wrapper.get('.candidate-pane .el-table__body-wrapper').findAll('button').find(button => button.text() === '加入')!.trigger('click') await flushPromises() const popup = wrapper.getComponent(AppDialog) expect(assignmentDialog().get('table[aria-label="全局装柜分配来源"]').text()).toContain('CB-1') if (unitType === 'PACKAGE') { const input = popup.getComponent(ElInputNumber) expect(input.props('modelValue')).toBe(candidate.availablePackageCount) expect(input.props('max')).toBe(candidate.availablePackageCount) input.vm.$emit('update:modelValue', 4) } else { expect(popup.findComponent(ElInputNumber).exists()).toBe(false) expect(assignmentDialog().get('details').text()).toContain('I181:9 箱') expect(popup.props('title')).toBe('整托加入当前柜次 · DP-TEST-1') } await assignmentDialog().get('[aria-label="展开全屏"]').trigger('click') expect(assignmentDialog().classes()).toContain('is-fullscreen') if (unitType === 'PACKAGE') expect(popup.getComponent(ElInputNumber).props('modelValue')).toBe(4) popup.vm.$emit('confirm'); await flushPromises() if (unitType === 'PACKAGE') { expect(assignSmartLoadingItem).toHaveBeenCalledWith(plan.id, { inboundId: candidate.inboundId, packageCount: 4, expectedVersion: plan.version }) expect(assignSmartLoadingPallet).not.toHaveBeenCalled() } else { expect(assignSmartLoadingPallet).toHaveBeenCalledWith(plan.id, { palletId: '801', expectedVersion: plan.version }) expect(assignSmartLoadingItem).not.toHaveBeenCalled() } expect(popup.props('modelValue')).toBe(false) wrapper.unmount() }) 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('') expect(source).toContain('') expect(source).toContain("row.unitType === 'SEALED_PALLET'") 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(' { setActivePinia(createPinia()) vi.clearAllMocks() vi.mocked(listPlatformTenants).mockReset().mockResolvedValue({ records: [], page: 1, size: 50, total: 0, pages: 0 }) mockContainerSearch([container]) vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue(plan) vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([candidate])) }) 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(wrapper.get('.container-option.selected').attributes('aria-pressed')).toBe('true') expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(container.id) expect(listSmartLoadingCandidates).toHaveBeenCalledWith({ channelGroupCodes: ['MATSON_CLX'], page: 1, size: 20 }) expect(wrapper.text()).toContain('候选装柜明细') expect(wrapper.text()).toContain('美森正班') expect(wrapper.findAll('.channel-tags .el-checkbox-button')).toHaveLength(9) expect(wrapper.text()).not.toContain('美森正班海卡包税') expect(wrapper.text()).toContain('另一企业') expect(wrapper.text()).toContain('MSCU-1 装柜方案') expect(wrapper.text()).not.toContain('LP-99') expect(wrapper.text()).toContain('示例企业') expect(wrapper.text()).not.toContain('服装') expect(wrapper.text()).not.toContain('鞋类') expect(wrapper.text()).not.toContain('SKU-1') expect(wrapper.text()).toContain('IN-1') expect(wrapper.text()).toContain('IN-2') expect(wrapper.text()).toContain('预估体积') expect(wrapper.text()).toContain('3.5 m³') expect(wrapper.text()).not.toContain('ORD-1') expect(wrapper.text()).not.toContain('ORD-2') expect(wrapper.get('.candidate-pane').text()).toContain('120 kg') expect(wrapper.get('.candidate-pane').text()).toContain('2.4 m³') }) it('uses the top-level container channel snapshot without expecting a nested snapshot', async () => { vi.mocked(listContainers).mockResolvedValue({ records: [{ ...container, shippingCompanyId: '32', shippingCompanyName: 'EXX', channelCode: 'EXX_RAIL_NY', channelName: '纽约EXX海铁' }], page: 1, size: 100, total: 1, pages: 1, }) mockContainerSearch([container]) const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } }) await flushPromises() expect(wrapper.text()).toContain('美森正班') expect(wrapper.text()).not.toContain('美森正班海卡包税') expect(wrapper.text()).not.toContain('缺少渠道快照') expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(container.id) }) it('shows one user-facing container number and hides internal manual or split references', async () => { vi.mocked(listContainers).mockResolvedValue({ records: [{ ...container, cabinetNo: 'CAB-260814-0001', containerNo: 'SPLIT-814022117898chain1' }], page: 1, size: 100, total: 1, pages: 1, }) mockContainerSearch([{ ...container, cabinetNo: 'CAB-260814-0001', containerNo: 'SPLIT-814022117898chain1' }]) const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } }) await flushPromises() expect(wrapper.text()).toContain('CAB-260814-0001') expect(wrapper.text()).not.toContain('SPLIT-814022117898chain1') expect(wrapper.text()).not.toContain('LP-99') }) 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: '义乌国内仓', containerType: '40HQ', }], page: 1, size: 100, total: 1, pages: 1, }) mockContainerSearch([{ ...container, cabinetNo: '2088475410134773762', containerNo: null, channelName: '美森正班海卡包税', warehouseName: '义乌国内仓', containerType: '40HQ' }]) const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } }) await flushPromises() const option = wrapper.find('.container-option') expect(option.text()).toContain('模拟柜次') expect(option.get('.container-option__top strong').text()).toBe('待生成') expect(option.text()).not.toContain('空柜') expect(option.text()).toContain('集装箱:待补录') expect(option.text()).not.toContain('开始装柜前录入集装箱号') expect(option.text()).toContain('渠道:美森正班') expect(option.get('.container-option__meta').text()).toContain('义乌国内仓') expect(option.get('.container-option__meta').text()).toContain('40HQ') expect(option.text()).not.toContain('2088475410134773762') expect(wrapper.text()).not.toContain('真实柜号') }) it('keeps nine fixed channel tags and allows selecting multiple channels', async () => { const other = { ...container, id: '9007199254740998', planId: '9007199254740999', cabinetNo: 'CB-2', containerNo: 'MSCU-2', channelGroupCode: 'HEDE', 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() expect(wrapper.findAll('.container-option')).toHaveLength(1) expect(wrapper.text()).toContain('MSCU-1') expect(wrapper.text()).not.toContain('MSCU-2') expect(wrapper.text()).toContain('柜次1 个') 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) await flushPromises() expect(wrapper.findAll('.container-option')).toHaveLength(2) expect(wrapper.text()).toContain('MSCU-1') expect(wrapper.text()).toContain('MSCU-2') expect(wrapper.text()).toContain('柜次2 个') }) 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', 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') }) 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 }) mockContainerSearch([container, parcelContainer]) 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('美森正班海派') }) it('keeps the shipping warehouse as a candidate-only query filter', async () => { 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 }) mockContainerSearch([container, dongguanContainer]) vi.mocked(getSmartLoadingPlanByBooking).mockImplementation(async (bookingId) => ({ ...plan, bookingId })) const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } }) await flushPromises() expect(wrapper.text()).toContain('MSCU-1') expect(wrapper.text()).toContain('MSCU-DG') const containerRequestCount = vi.mocked(listSmartLoadingWorkbenchContainers).mock.calls.length wrapper.findComponent(QueryToolbar).vm.$emit('search', { warehouseId: '18', customerId: '', inboundCode: '', createdRange: [], orderStatuses: [] }) await flushPromises() expect(wrapper.text()).toContain('MSCU-DG') expect(wrapper.text()).toContain('MSCU-1') expect(listSmartLoadingCandidates).toHaveBeenLastCalledWith({ channelGroupCodes: ['MATSON_CLX'], warehouseId: dongguanContainer.warehouseId, page: 1, size: 20 }) expect(listSmartLoadingWorkbenchContainers).toHaveBeenCalledTimes(containerRequestCount) }) it('keeps channel cabinets unchanged when another search field has no matching cabinet', async () => { const dongguanOvertimeContainer = { ...container, id: '9007199254740998', planId: '9007199254740999', cabinetNo: 'CB-DG-MAX', containerNo: 'MSCU-DG-MAX', warehouseId: '18', warehouseName: '东莞仓', channelGroupCode: 'MATSON_MAX', channelCode: 'MATSON_MAX_TRUCK', channelName: '美森加班海卡', } vi.mocked(listContainers).mockResolvedValue({ records: [container, dongguanOvertimeContainer], page: 1, size: 100, total: 2, pages: 1 }) mockContainerSearch([container, dongguanOvertimeContainer]) const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } }) await flushPromises() const containerRequestCount = vi.mocked(listSmartLoadingWorkbenchContainers).mock.calls.length wrapper.findComponent(QueryToolbar).vm.$emit('search', { warehouseId: '18', customerId: '', inboundCode: '', createdRange: [], orderStatuses: [] }) await flushPromises() expect(wrapper.find('.channel-tags .el-checkbox-button.is-checked').text()).toBe('美森正班') expect(wrapper.findAll('.container-option')).toHaveLength(1) expect(wrapper.text()).toContain('MSCU-1') expect(wrapper.text()).not.toContain('MSCU-DG-MAX') expect(listSmartLoadingCandidates).toHaveBeenCalledTimes(2) expect(listSmartLoadingCandidates).toHaveBeenLastCalledWith({ channelGroupCodes: ['MATSON_CLX'], warehouseId: '18', page: 1, size: 20 }) 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', { warehouseId: '', customerId: '', inboundCode: 'NYEXX2609030002', createdRange: [], orderStatuses: [], }) 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) }) it('sends the active name-only customer selector to the server candidate query', () => { const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8') expect(source).toContain('useActiveCustomerOptions(') expect(source).toContain('remoteMethod: loadCustomers') expect(source).toContain("label: customer.tenantName") expect(source).toContain('params.customerId = customerId') expect(source).not.toContain('customer.tenantCode') }) it('searches customers remotely, loads more options, and applies the selected customer to candidates', async () => { const target = { id: '9007199254740999', tenantName: '柏悦物流', status: 'ACTIVE' } as PlatformTenantResource vi.mocked(listPlatformTenants).mockImplementation(async query => ({ records: query.keyword || query.page === 2 ? [target] : [{ id: '1', tenantName: '跨客户车次-测试客户', status: 'ACTIVE' } as PlatformTenantResource], page: query.page, size: 50, total: query.keyword ? 1 : 350, pages: query.keyword ? 1 : 7, })) const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } }) await flushPromises() const selector = wrapper.findAllComponents(ElSelect).find(item => item.props('placeholder') === '全部客户')! expect(selector.props('remote')).toBe(true) selector.vm.$emit('end-reached', 'bottom'); await flushPromises() expect(listPlatformTenants).toHaveBeenLastCalledWith({ page: 2, size: 50, status: 'ACTIVE' }) await selector.props('remoteMethod')?.('柏悦'); await flushPromises() expect(listPlatformTenants).toHaveBeenLastCalledWith({ page: 1, size: 50, status: 'ACTIVE', keyword: '柏悦' }) expect(selector.findAllComponents(ElOption).map(option => option.props('label'))).toContain('柏悦物流') selector.vm.$emit('update:modelValue', target.id); await flushPromises() const toolbar = wrapper.findComponent(QueryToolbar) toolbar.vm.$emit('search', toolbar.props('modelValue')); await flushPromises() expect(listSmartLoadingCandidates).toHaveBeenLastCalledWith(expect.objectContaining({ customerId: target.id })) wrapper.unmount() }) it('defaults ordinary candidate assignment to all available boxes', () => { const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8') expect(source).toContain(': candidate.availablePackageCount') }) it('filters candidates by creation, expected-arrival range and multiple order statuses', async () => { vi.mocked(listSmartLoadingCandidates) .mockResolvedValueOnce(candidatePage([ { ...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' }, ])) .mockResolvedValueOnce(candidatePage([{ ...candidate, inboundId: '51', inboundCode: 'IN-MATCH', orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-11T08:00:00' }])) const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } }) await flushPromises() const containerRequestCount = vi.mocked(listSmartLoadingWorkbenchContainers).mock.calls.length wrapper.getComponent(QueryToolbar).vm.$emit('search', { customerId: '', inboundCode: '', orderStatuses: ['WAIT_SPLIT', 'LOADING'], createdRange: ['2026-09-10', '2026-09-12'], expectedArrivalRange: ['2026-09-11', '2026-09-15'], }) 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') 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 }) expect(listSmartLoadingWorkbenchContainers).toHaveBeenCalledTimes(containerRequestCount) expect(listSmartLoadingWorkbenchContainers).toHaveBeenLastCalledWith({ channelGroupCodes: ['MATSON_CLX'], page: 1, size: 200 }) }) it('paginates large inbound candidate results with twenty rows by default', async () => { 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)) 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') expect(pane.text()).toContain('共 25 条 · 1/2 页') 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 () => { 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('.el-table__body-wrapper button').filter((button) => button.text() === '加入') expect(buttons).toHaveLength(1) await buttons[0].trigger('click') await flushPromises() expect(document.body.textContent).not.toContain('不能部分输入、单独分配或单独移除') const popup = wrapper.getComponent(AppDialog) expect(popup.props('title')).toBe('整组加入当前柜次 · CLG-2093539779795795969') const contextRows = assignmentDialog().findAll('table[aria-label="全局装柜分配来源"] tr') expect(contextRows[0]!.findAll('td').map(cell => cell.text())).toEqual(['CB-1', 'MSCU-1']) expect(contextRows[1]!.findAll('td').map(cell => cell.text())).toEqual(['义乌仓', '美森正班']) await assignmentDialog().get('[aria-label="展开全屏"]').trigger('click') expect(assignmentDialog().get('table[aria-label="全局装柜同柜组成员"]').text()).toContain('NYEXX2608290051-B') 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() }) it('allows a pending-review order to join through the provisional assignment command', async () => { vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([{ ...candidate, customerId: '', inboundId: null, orderStatusSnapshot: 'PENDING_REVIEW', inboundCode: 'EXXHK2609020361', channelCode: 'HEDE_TRUCK', channelName: '合德海卡', unitGrossWeightKg: null, averageUnitVolumeCbm: null, availableVolumeCbm: null, 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() const popup = wrapper.getComponent(AppDialog) expect(popup.props('title')).toBe('加入当前柜次 · EXXHK2609020361') const measurements = assignmentDialog().findAll('table[aria-label="全局装柜分配填写"] tr').find(row => row.text().includes('平均单箱毛重'))! expect(measurements.findAll('td').map(cell => cell.text())).toEqual(['待测量', '待测量']) const channel = assignmentDialog().findAll('table[aria-label="全局装柜分配来源"] tr').find(row => row.text().includes('订单渠道'))! expect(channel.findAll('td')[0]!.text()).toBe('合德海卡') expect(document.body.textContent).toContain('这是人工装柜分配,确认后仍会加入') expect(document.body.textContent).toContain('智能推荐不会跨渠道') ;([...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 () => { 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('.el-table__body-wrapper button').find((item) => item.text() === '加入')! expect(button.attributes('disabled')).toBeDefined() expect(assignSmartLoadingGroup).not.toHaveBeenCalled() }) it('shows the package summary first and opens the editor from an icon', () => { const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8') 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"') expect(source).toContain(':controls="false"') expect(source).toContain('@blur="adjust(row)" @keyup.enter="adjust(row)"') expect(source).not.toContain(' { vi.mocked(adjustSmartLoadingItem).mockResolvedValue({ ...plan, version: 3, items: [{ ...plan.items[0], packageCount: 4, version: 2 }], }) const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } }) await flushPromises() await wrapper.find('[aria-label="调整箱数"]').trigger('click') await flushPromises() 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) }) it('shows a shared pallet once with exact members and a text-only join action', async () => { vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([{ ...candidate, unitType: 'SEALED_PALLET', palletId: '801', palletNo: 'DP-0025', indivisible: true, availablePackageCount: 13, unitGrossWeightKg: 10, groupMembers: [ { accountId: '1', inboundId: '180', orderId: '180', orderNo: 'O180', inboundCode: 'I180', customerName: '客户', availablePackageCount: 3, orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: null }, { accountId: '1', inboundId: '181', orderId: '181', orderNo: 'O181', inboundCode: 'I181', customerName: '客户', availablePackageCount: 10, orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: null }, ], }])) const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } }) await flushPromises() const buttons = wrapper.findAll('.el-table__body-wrapper button').filter(button => button.text() === '加入') expect(buttons).toHaveLength(1) expect(buttons[0].find('svg').exists()).toBe(false) expect(wrapper.text()).toContain('I180:3 箱') expect(wrapper.text()).toContain('I181:10 箱') expect(wrapper.text()).toContain('130 kg') expect(wrapper.get('.candidate-pane').text()).toContain('整托加入 · 不可拆') expect(wrapper.get('.plan-pane').text()).toContain('按箱加入') expect(wrapper.text()).toContain('装柜规则') expect(wrapper.text()).not.toContain('分配单位') wrapper.unmount() }) it('uses one loading-rule column and expandable group details in both workbenches', () => { for (const page of ['GlobalSmartLoadingView.vue', 'SmartLoadingWorkbenchView.vue']) { const source = readFileSync(resolve(process.cwd(), `src/modules/container-loading/pages/${page}`), 'utf8') expect(source.match(/label: '装柜规则'/g)).toHaveLength(2) expect(source).not.toContain("label: '分配单位'") expect(source).not.toContain('