From 2d14568caf0d541de48de87db92abcdfde3265ba Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 4 Sep 2026 09:57:39 +0800 Subject: [PATCH] perf(admin): streamline smart loading workbench --- .../api/platformContainerLoading.ts | 10 +++ .../pages/GlobalSmartLoadingView.test.ts | 38 +++++++--- .../pages/GlobalSmartLoadingView.vue | 71 +++++++++---------- .../pages/SmartLoadingWorkbenchView.test.ts | 2 +- .../smartLoadingPlanGroups.test.ts | 16 ++--- .../smartLoadingPlanGroups.ts | 39 +++------- src/modules/container-loading/types.ts | 11 +++ 7 files changed, 97 insertions(+), 90 deletions(-) diff --git a/src/modules/container-loading/api/platformContainerLoading.ts b/src/modules/container-loading/api/platformContainerLoading.ts index 79ac0b16..9895e157 100644 --- a/src/modules/container-loading/api/platformContainerLoading.ts +++ b/src/modules/container-loading/api/platformContainerLoading.ts @@ -119,6 +119,14 @@ function plan(raw: SmartLoadingPlan): SmartLoadingPlan { blockedReason: raw.blockedReason || null, pdaTaskId: optionalId(raw.pdaTaskId), pdaTaskVersion: raw.pdaTaskVersion == null ? null : numberValue(raw.pdaTaskVersion), + coLoadGroups: (raw.coLoadGroups || []).map((group) => ({ + ...group, + groupId: id(group.groupId), lockedPlanId: optionalId(group.lockedPlanId), + memberOrderCount: numberValue(group.memberOrderCount), + inboundFragmentCount: numberValue(group.inboundFragmentCount), + fullPackageCount: numberValue(group.fullPackageCount), + memberInboundIds: (group.memberInboundIds || []).map(id), + })), items: (raw.items || []).map(item), } } @@ -280,6 +288,8 @@ export async function listSmartLoadingWorkbenchContainers(query: SmartLoadingCan total: numberValue(result.total), pages: numberValue(result.pages), records: result.records.map((raw) => ({ ...raw, bookingId: id(raw.bookingId), warehouseId: id(raw.warehouseId), planId: optionalId(raw.planId), + capacityCbm: raw.capacityCbm == null ? null : numberValue(raw.capacityCbm), + maxWeightKg: raw.maxWeightKg == null ? null : numberValue(raw.maxWeightKg), version: numberValue(raw.version), })), } diff --git a/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts b/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts index a481f868..1eb8bfbd 100644 --- a/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts +++ b/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts @@ -49,7 +49,13 @@ vi.mock('../api/platformContainerLoading', () => ({ removeSmartLoadingGroup: vi.fn(), pushSmartLoadingPreload: vi.fn(), })) -vi.mock('../smartLoadingPlanGroups', () => ({ loadSmartLoadingPlanCoLoadGroups: vi.fn().mockResolvedValue(new Map()) })) +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', bookingNo: 'BK-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', @@ -64,6 +70,7 @@ 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: [], 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: '服装', packageCount: 6, productQuantity: 6, grossWeightKg: 60, volumeCbm: 1.2, loadSequence: 1, version: 1, pickedPackageCount: 0, minPackageCount: 1, canAdjust: true, canRemove: true }], } const candidate = { @@ -89,7 +96,8 @@ function containerPage(records: ContainerBookingResource[] = [container]) { records: records.map((item) => ({ bookingId: item.id, bookingNo: item.bookingNo, cabinetNo: item.cabinetNo, containerNo: item.containerNo, warehouseId: item.warehouseId, warehouseName: item.warehouseName || '', channelCode: item.channelCode || '', channelName: item.channelName || '', - containerType: item.containerType, planId: item.planId, planNo: null, planStatus: item.planStatus, version: item.version, + 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> @@ -105,7 +113,6 @@ describe('GlobalSmartLoadingView', () => { beforeEach(() => { setActivePinia(createPinia()) vi.clearAllMocks() - vi.mocked(listContainers).mockResolvedValue({ records: [container], page: 1, size: 100, total: 1, pages: 1 }) mockContainerSearch([container]) vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue(plan) vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([candidate])) @@ -115,7 +122,6 @@ describe('GlobalSmartLoadingView', () => { const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } }) await flushPromises() - expect(listContainers).toHaveBeenCalledWith({ page: 1, size: 100 }) expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(container.id) expect(listSmartLoadingCandidates).toHaveBeenCalledWith({ channelGroupCodes: ['MATSON_CLX'], page: 1, size: 20 }) expect(wrapper.text()).toContain('候选装柜明细') @@ -145,13 +151,12 @@ describe('GlobalSmartLoadingView', () => { total: 1, pages: 1, }) - mockContainerSearch([{ ...container, shippingCompanyId: '32', shippingCompanyName: 'EXX', channelCode: 'EXX_RAIL_NY', channelName: '纽约EXX海铁' }]) - vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([{ accountId: '1', inboundId: '5', orderId: '7', orderNo: 'ORD-2', inboundCode: 'IN-2', warehouseId: '8', channelCode: 'EXX_RAIL_NY', channelName: '纽约EXX海铁', 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 }])) + mockContainerSearch([container]) const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } }) await flushPromises() - expect(wrapper.text()).toContain('EXX') - expect(wrapper.text()).not.toContain('纽约EXX海铁') + expect(wrapper.text()).toContain('美森正班') + expect(wrapper.text()).not.toContain('美森正班海卡包税') expect(wrapper.text()).not.toContain('缺少渠道快照') expect(getSmartLoadingPlanByBooking).toHaveBeenCalledWith(container.id) }) @@ -438,6 +443,8 @@ describe('GlobalSmartLoadingView', () => { 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() @@ -454,6 +461,9 @@ describe('GlobalSmartLoadingView', () => { inboundId: null, orderStatusSnapshot: 'PENDING_REVIEW', inboundCode: 'EXXHK2609020361', + unitGrossWeightKg: null, + averageUnitVolumeCbm: null, + availableVolumeCbm: null, blockReason: null, }])) vi.mocked(assignPendingReviewSmartLoadingOrder).mockResolvedValue(plan) @@ -466,6 +476,9 @@ describe('GlobalSmartLoadingView', () => { 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') ;([...document.body.querySelectorAll('button')] .find((button) => button.textContent?.includes('确认加入')) as HTMLButtonElement).click() await flushPromises() @@ -522,7 +535,10 @@ describe('GlobalSmartLoadingView', () => { const fields = wrapper.findComponent(QueryToolbar).props('fields') as Array<{ key: string; options?: Array<{ label: string; value: string }>; props?: Record }> expect(fields.map((field) => field.key)).toEqual(['warehouseId', 'customerId', 'inboundCode', 'createdRange', 'orderStatuses']) - expect(fields.find((field) => field.key === 'warehouseId')?.options).toEqual([{ label: '义乌仓', value: '8' }]) + expect(fields.find((field) => field.key === 'warehouseId')?.options).toEqual([ + { label: '义乌仓', value: '8' }, + { label: '东莞仓', value: '18' }, + ]) expect(fields.find((field) => field.key === 'orderStatuses')?.options).toEqual([ { value: 'PENDING_REVIEW', label: '待审核' }, { value: 'WAIT_MEASURE', label: '待测量数据' }, @@ -610,7 +626,7 @@ describe('GlobalSmartLoadingView', () => { expect(getSmartLoadingPlanByBooking).not.toHaveBeenCalled() expect(wrapper.text()).not.toContain('CB-3') - expect(wrapper.text()).toContain('当前柜次已完成装柜,不可继续编辑') + expect(wrapper.text()).toContain('暂无符合条件的货柜') expect(wrapper.text()).not.toContain('缺少渠道快照') }) @@ -661,7 +677,7 @@ describe('GlobalSmartLoadingView', () => { expect(recommendSmartLoading).toHaveBeenCalledWith(plan.id, 2) expect(recommendSmartLoading).toHaveBeenCalledWith(plan.id, 3) - expect(getSmartLoadingPlanByBooking).toHaveBeenCalledTimes(3) + expect(getSmartLoadingPlanByBooking).toHaveBeenCalledTimes(2) expect(warningMessage).not.toHaveBeenCalled() expect(wrapper.text()).not.toContain('装柜操作失败') expect(wrapper.text()).not.toContain('request-1') diff --git a/src/modules/container-loading/pages/GlobalSmartLoadingView.vue b/src/modules/container-loading/pages/GlobalSmartLoadingView.vue index 3dc99403..b41bf307 100644 --- a/src/modules/container-loading/pages/GlobalSmartLoadingView.vue +++ b/src/modules/container-loading/pages/GlobalSmartLoadingView.vue @@ -4,6 +4,7 @@ import { Plus, RefreshCw, Sparkles, Trash2 } from '@lucide/vue' import { ElMessage, ElMessageBox } from 'element-plus' import { useAccessPreviewStore } from '@/core/access/accessPreview' import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam' +import { listAllPlatformWarehouses, type PlatformWarehouseSummaryResource } from '@/modules/warehouses' import AdminPage from '@/shared/components/AdminPage.vue' import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import AppDialog from '@/shared/components/AppDialog.vue' @@ -18,7 +19,6 @@ import { assignSmartLoadingGroup, assignSmartLoadingItem, getSmartLoadingPlanByBooking, - listContainers, listSmartLoadingCandidates, listSmartLoadingWorkbenchContainers, recommendSmartLoading, @@ -26,7 +26,6 @@ import { removeSmartLoadingItem, } from '../api/platformContainerLoading' import type { - ContainerBookingResource, SmartLoadingCandidate, SmartLoadingPlan, SmartLoadingWorkbenchContainer, @@ -42,7 +41,7 @@ const props = withDefaults(defineProps<{ embedded?: boolean }>(), { embedded: fa const access = useAccessPreviewStore() const containers = ref([]) -const containerCatalog = ref([]) +const warehouses = ref([]) const customers = ref([]) const customersLoading = ref(false) const selectedContainerId = ref('') @@ -75,8 +74,8 @@ let containerRequest = 0 const canAssign = computed(() => access.can('platform:firstleg:smart-loading:assign')) const canDelete = computed(() => access.can('platform:firstleg:smart-loading:delete')) const selectedContainer = computed(() => containers.value.find((item) => item.bookingId === selectedContainerId.value) || null) -const orchestratableContainers = computed(() => containerCatalog.value.filter((item) => ['WAIT_LOADING', 'EDITING', 'LOADING'].includes(item.planStatus))) -const completedContainerCount = computed(() => containerCatalog.value.filter((item) => item.planStatus === 'COMPLETED').length) +const orchestratableContainers = computed(() => containers.value) +const completedContainerCount = computed(() => 0) const missingChannelContainerCount = computed(() => orchestratableContainers.value.filter((item) => !item.channelCode).length) const unmappedChannelContainerCount = computed(() => orchestratableContainers.value.filter((item) => ( item.channelCode && !resolveSmartLoadingChannelGroup(item.channelCode, item.channelName) @@ -86,17 +85,12 @@ const mappedWorkbenchContainerCount = computed(() => orchestratableContainers.va )).length) const channelOptions = computed(() => SMART_LOADING_CHANNEL_GROUPS) const warehouseOptions = computed(() => { - const options = new Map() - for (const item of orchestratableContainers.value) { - if (item.warehouseId && !options.has(item.warehouseId)) options.set(item.warehouseId, item.warehouseName?.trim() || '未命名发货仓') - } - return [...options].map(([value, label]) => ({ value, label })) + return warehouses.value.map((warehouse) => ({ value: String(warehouse.id), label: warehouse.warehouseName })) }) const editable = computed(() => plan.value?.canEdit === true) const assignable = computed(() => plan.value?.canAssign === true) -const selectedContainerResource = computed(() => containerCatalog.value.find((item) => item.id === selectedContainerId.value) || null) const assignedInboundIds = computed(() => new Set((plan.value?.items || []).map((item) => item.inboundId))) -type SmartLoadingCandidateRow = SmartLoadingCandidate & { memberInboundIds: string[]; memberInboundCodes: string[]; memberOrderSummaries: string[]; memberOrderCount: number; inboundFragmentCount: number; totalGrossWeightKg: number; totalVolumeCbm: number } +type SmartLoadingCandidateRow = SmartLoadingCandidate & { memberInboundIds: string[]; memberInboundCodes: string[]; memberOrderSummaries: string[]; memberOrderCount: number; inboundFragmentCount: number; totalGrossWeightKg: number | null; totalVolumeCbm: number | null } function memberOrderSummaries(members: T[]) { const orders = new Map() for (const member of members) { @@ -113,11 +107,11 @@ function groupMemberLabel(group: SmartLoadingPlanCoLoadGroup) { : `${group.memberOrderCount} 票 · ${group.inboundFragmentCount} 个入仓片段` } function candidateTotalGrossWeight(candidate: SmartLoadingCandidate) { - return Number(candidate.unitGrossWeightKg || 0) * Number(candidate.availablePackageCount || 0) + return candidate.unitGrossWeightKg == null ? null : Number(candidate.unitGrossWeightKg) * Number(candidate.availablePackageCount || 0) } function candidateTotalVolume(candidate: SmartLoadingCandidate) { return candidate.availableVolumeCbm == null - ? Number(candidate.averageUnitVolumeCbm || 0) * Number(candidate.availablePackageCount || 0) + ? candidate.averageUnitVolumeCbm == null ? null : Number(candidate.averageUnitVolumeCbm) * Number(candidate.availablePackageCount || 0) : Number(candidate.availableVolumeCbm) } const displayedCandidates = computed(() => { @@ -178,8 +172,8 @@ const displayedPlanItems = computed(() => { const totalPackages = computed(() => (plan.value?.items || []).reduce((sum, item) => sum + Number(item.packageCount || 0), 0)) const totalVolume = computed(() => (plan.value?.items || []).reduce((sum, item) => sum + Number(item.volumeCbm || 0), 0)) const totalWeight = computed(() => (plan.value?.items || []).reduce((sum, item) => sum + Number(item.grossWeightKg || 0), 0)) -const volumeLimit = computed(() => Number(selectedContainerResource.value?.capacityCbm || 0) * 1.5) -const weightLimit = computed(() => Number(selectedContainerResource.value?.maxWeightKg || 0) * 1.5) +const volumeLimit = computed(() => Number(selectedContainer.value?.capacityCbm || 0) * 1.5) +const weightLimit = computed(() => Number(selectedContainer.value?.maxWeightKg || 0) * 1.5) const volumePercent = computed(() => volumeLimit.value ? totalVolume.value / volumeLimit.value * 100 : 0) const weightPercent = computed(() => weightLimit.value ? totalWeight.value / weightLimit.value * 100 : 0) const displayedVolumePercent = computed(() => percentage(volumePercent.value)) @@ -242,6 +236,9 @@ const itemColumns: DataTableColumn[] = [ function formatNumber(value: number, digits = 3) { return Number.isFinite(value) ? value.toLocaleString('zh-CN', { maximumFractionDigits: digits }) : '0' } +function formatMeasurement(value: number | null | undefined, unit: string, digits = 3) { + return value == null ? '待测量' : `${formatNumber(Number(value), digits)} ${unit}` +} function percentage(value: number) { return Number.isFinite(value) ? Number(Math.max(value, 0).toFixed(2)) : 0 } function candidateRow(source: object) { return source as SmartLoadingCandidateRow } function itemRow(source: object) { return source as SmartLoadingInboundItem } @@ -250,17 +247,17 @@ function adjustmentMax(source: object) { const remaining = candidates.value.find((candidate) => candidate.inboundId === item.inboundId)?.availablePackageCount || 0 return Math.max(item.minPackageCount, item.packageCount + remaining) } -function displayCabinetNo(container: ContainerBookingResource | SmartLoadingWorkbenchContainer | null | undefined) { +function displayCabinetNo(container: SmartLoadingWorkbenchContainer | null | undefined) { if (!container) return '待生成' const cabinetReference = String(container.cabinetNo || '').trim() return /^\d{15,}$/.test(cabinetReference) ? '待生成' : cabinetReference || '待生成' } -function hasRealContainerNo(container: ContainerBookingResource | SmartLoadingWorkbenchContainer | null | undefined) { +function hasRealContainerNo(container: SmartLoadingWorkbenchContainer | null | undefined) { if (!container) return false const value = String(container.containerNo || '').trim() return Boolean(value) && !/^(?:MAN|SPLIT)-/i.test(value) } -function displayActualContainerNo(container: ContainerBookingResource | SmartLoadingWorkbenchContainer | null | undefined) { +function displayActualContainerNo(container: SmartLoadingWorkbenchContainer | null | undefined) { return hasRealContainerNo(container) ? String(container?.containerNo || '').trim() : '待补录' } function syncDrafts(next: SmartLoadingPlan, preserved: Record = {}) { @@ -269,6 +266,7 @@ function syncDrafts(next: SmartLoadingPlan, preserved: Record = } function acceptPlan(next: SmartLoadingPlan, preserved: Record = {}, syncContainerStatus = true) { plan.value = next + planGroupsByInboundId.value = loadSmartLoadingPlanCoLoadGroups(next) noDraft.value = false syncDrafts(next, preserved) const container = selectedContainer.value @@ -357,7 +355,6 @@ async function loadSelectedPlan(preserved: Record = {}) { if (request !== selectionRequest) return if (planResult.value) { acceptPlan(planResult.value, preserved, false) - planGroupsByInboundId.value = await loadSmartLoadingPlanCoLoadGroups(planResult.value) syncDrafts(planResult.value, preserved) } else noDraft.value = planResult.missing @@ -398,17 +395,11 @@ async function loadAll(preserved: Record = {}) { loading.value = true loadError.value = null try { - const firstPage = await listContainers({ page: 1, size: 100 }) - const remainingPages = firstPage.pages > 1 - ? await Promise.all(Array.from({ length: firstPage.pages - 1 }, (_, index) => listContainers({ page: index + 2, size: 100 }))) - : [] - containerCatalog.value = [firstPage, ...remainingPages].flatMap((page) => page.records) + warehouses.value = await listAllPlatformWarehouses({ warehouseType: 'DOMESTIC', status: 'ACTIVE' }) const availableChannelCodes = new Set(channelOptions.value.map((item) => item.code)) selectedChannelCodes.value = selectedChannelCodes.value.filter((code) => availableChannelCodes.has(code)) if (!selectedChannelCodes.value.length) { - selectedChannelCodes.value = [channelOptions.value.find((channel) => orchestratableContainers.value.some((item) => ( - resolveSmartLoadingChannelGroup(item.channelCode, item.channelName)?.code === channel.code - )))?.code || channelOptions.value[0]?.code || ''] + selectedChannelCodes.value = [channelOptions.value[0]?.code || ''] } await loadWorkbenchData(preserved) } catch (error) { @@ -497,7 +488,7 @@ async function assign() { expectedVersion: plan.value!.version, })) assignOpen.value = false - await loadWorkbenchData() + await loadCandidateData() ElMessage.success(selected.coLoadGroupId ? `同柜不可拆组 ${selected.groupNo} 已整组分配` : '明细已加入当前柜次') }, '分配明细失败', { preserveAssign: true }) } @@ -524,7 +515,7 @@ async function recommend() { if (!plan.value) throw error acceptPlan(await recommendSmartLoading(plan.value.id, plan.value.version)) } - await loadWorkbenchData() + await loadCandidateData() ElMessage.success('智能推荐已生成') }, '智能推荐失败') } @@ -540,7 +531,7 @@ async function adjust(source: object) { if (count > max) { ElMessage.warning(`该明细最多可分配 ${max} 箱`); return } await mutate(async () => { acceptPlan(await adjustSmartLoadingItem(plan.value!.id, item.id, count, plan.value!.version, item.version)) - await loadWorkbenchData() + await loadCandidateData() ElMessage.success('箱数已调整') }, '调整箱数失败', { preserveDrafts: { ...quantityDrafts } }) } @@ -557,7 +548,7 @@ async function remove(source: object) { await mutate(async () => { if (row.coLoadGroup) { acceptPlan(await removeSmartLoadingGroup(plan.value!.id, row.coLoadGroup.groupId, plan.value!.version)) - await loadWorkbenchData() + await loadCandidateData() ElMessage.success(`同柜不可拆组 ${row.coLoadGroup.groupNo} 已整组移除`) return } @@ -568,7 +559,7 @@ async function remove(source: object) { current = await removeSmartLoadingItem(current.id, currentItem.id, current.version, currentItem.version) acceptPlan(current) } - await loadWorkbenchData() + await loadCandidateData() ElMessage.success('该入仓码已从当前柜次移除') }, '移除装柜明细失败', { pickingConflict: true }) } @@ -595,7 +586,7 @@ async function removeAll() { acceptPlan(current) } } - await loadWorkbenchData() + await loadCandidateData() ElMessage.success(`已移除 ${itemCount} 个未拣入仓码`) }, '全部移除失败', { pickingConflict: true }) } @@ -703,10 +694,10 @@ onMounted(() => { void load(); void loadCustomers() }) - - - - + + + + @@ -786,13 +777,15 @@ onMounted(() => { void load(); void loadCustomers() })

{{ selectedCandidate.groupNo || selectedCandidate.customerName || '客户待补充' }}

+

目标柜次:{{ displayCabinetNo(selectedContainer) }} · 集装箱号:{{ displayActualContainerNo(selectedContainer) }}

+

发货仓:{{ selectedContainer?.warehouseName || '—' }} · 渠道:{{ resolveSmartLoadingChannelGroup(selectedContainer?.channelCode, selectedContainer?.channelName)?.name || '—' }}

diff --git a/src/modules/container-loading/pages/SmartLoadingWorkbenchView.test.ts b/src/modules/container-loading/pages/SmartLoadingWorkbenchView.test.ts index d2283fd9..056a73f1 100644 --- a/src/modules/container-loading/pages/SmartLoadingWorkbenchView.test.ts +++ b/src/modules/container-loading/pages/SmartLoadingWorkbenchView.test.ts @@ -29,7 +29,7 @@ const container = { const planResponse = { 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, items: [], + pdaTaskId: null, pdaTaskStatus: null, pdaTaskVersion: null, coLoadGroups: [], items: [], } const candidate = { 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: 12, unitGrossWeightKg: 10, unitNetWeightKg: 9, averageUnitVolumeCbm: 0.2, availableVolumeCbm: 2.4, coLoadGroupId: null, groupNo: null, memberCount: null, groupTotalBoxes: null, locked: null, blockReason: null } function candidatePage(records: unknown[]) { diff --git a/src/modules/container-loading/smartLoadingPlanGroups.test.ts b/src/modules/container-loading/smartLoadingPlanGroups.test.ts index 068c634d..cf4db946 100644 --- a/src/modules/container-loading/smartLoadingPlanGroups.test.ts +++ b/src/modules/container-loading/smartLoadingPlanGroups.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, vi } from 'vitest' +import { describe, expect, it } from 'vitest' import { loadSmartLoadingPlanCoLoadGroups } from './smartLoadingPlanGroups' import type { SmartLoadingPlan } from './types' @@ -13,24 +13,22 @@ const plan = { id: '99', planNo: 'LP-99', status: 'EDITING', bookingId: '88', containerNo: null, voyageNo: null, version: 2, canAssign: true, canEdit: true, blockedReason: null, finalCompletionStatus: 'NOT_STARTED', pdaTaskId: null, pdaTaskStatus: null, pdaTaskVersion: null, + coLoadGroups: [{ groupId: '9001', groupNo: 'CLG-9001', lockedPlanId: '99', memberOrderCount: 2, + inboundFragmentCount: 2, fullPackageCount: 12, memberInboundIds: ['201', '202'] }], items: [item, { ...item, id: '702', inboundId: '202', inboundDetailId: '802', orderId: '302', orderNo: 'ORD-2', inboundCode: 'IN-202', packageCount: 7 }], } satisfies SmartLoadingPlan describe('smart loading plan co-load groups', () => { - it('rebuilds the locked group from the current inbound association contract', async () => { - const lookup = vi.fn(async (inboundId: string) => ({ - inboundId, inboundCode: `IN-${inboundId}`, loadingPlanId: '99', coLoadGroupId: '9001', coLoadGroupNo: 'CLG-9001', items: [], - })) - const result = await loadSmartLoadingPlanCoLoadGroups(plan, lookup) - expect(lookup).toHaveBeenCalledTimes(2) + it('uses the group snapshot already returned with the plan without per-inbound requests', () => { + const result = loadSmartLoadingPlanCoLoadGroups(plan) expect(result.get('201')).toEqual({ groupId: '9001', groupNo: 'CLG-9001', lockedPlanId: '99', memberOrderCount: 2, inboundFragmentCount: 2, fullPackageCount: 12, }) expect(result.get('202')).toBe(result.get('201')) }) - it('leaves ordinary plan items ungrouped', async () => { - const result = await loadSmartLoadingPlanCoLoadGroups(plan, async (inboundId) => ({ inboundId, inboundCode: `IN-${inboundId}`, items: [] })) + it('leaves ordinary plan items ungrouped', () => { + const result = loadSmartLoadingPlanCoLoadGroups({ ...plan, coLoadGroups: [] }) expect(result.size).toBe(0) }) }) diff --git a/src/modules/container-loading/smartLoadingPlanGroups.ts b/src/modules/container-loading/smartLoadingPlanGroups.ts index bf4fb008..8a500191 100644 --- a/src/modules/container-loading/smartLoadingPlanGroups.ts +++ b/src/modules/container-loading/smartLoadingPlanGroups.ts @@ -1,5 +1,3 @@ -import { getLoadingAssociationSummaryByInboundId, type LoadingAssociationSummary } from '@/shared/api/inboundCodeRelations' -import { groupSmartLoadingItems } from './smartLoadingItems' import type { SmartLoadingPlan } from './types' export interface SmartLoadingPlanCoLoadGroup { @@ -11,37 +9,18 @@ export interface SmartLoadingPlanCoLoadGroup { fullPackageCount: number } -type AssociationLookup = (inboundId: string) => Promise - -export async function loadSmartLoadingPlanCoLoadGroups( - plan: SmartLoadingPlan, - lookup: AssociationLookup = getLoadingAssociationSummaryByInboundId, -) { - const inboundRows = groupSmartLoadingItems(plan.items || []) - if (!inboundRows.length) return new Map() - - const associations = await Promise.all(inboundRows.map(async (row) => ({ - row, - association: await lookup(row.inboundId), - }))) - const membersByGroupId = new Map() - for (const entry of associations) { - const groupId = String(entry.association.coLoadGroupId || '') - if (!groupId) continue - membersByGroupId.set(groupId, [...(membersByGroupId.get(groupId) || []), entry]) - } - +export function loadSmartLoadingPlanCoLoadGroups(plan: SmartLoadingPlan) { const groupsByInboundId = new Map() - for (const [groupId, members] of membersByGroupId) { + for (const source of plan.coLoadGroups || []) { const group = { - groupId, - groupNo: members.find((entry) => entry.association.coLoadGroupNo)?.association.coLoadGroupNo || '同柜不可拆组', - lockedPlanId: members.find((entry) => entry.association.loadingPlanId)?.association.loadingPlanId || plan.id, - memberOrderCount: new Set(members.map((entry) => entry.row.orderId)).size, - inboundFragmentCount: members.length, - fullPackageCount: members.reduce((sum, entry) => sum + entry.row.packageCount, 0), + groupId: source.groupId, + groupNo: source.groupNo || '同柜不可拆组', + lockedPlanId: source.lockedPlanId || plan.id, + memberOrderCount: source.memberOrderCount, + inboundFragmentCount: source.inboundFragmentCount, + fullPackageCount: source.fullPackageCount, } satisfies SmartLoadingPlanCoLoadGroup - for (const { row } of members) groupsByInboundId.set(row.inboundId, group) + for (const inboundId of source.memberInboundIds) groupsByInboundId.set(inboundId, group) } return groupsByInboundId } diff --git a/src/modules/container-loading/types.ts b/src/modules/container-loading/types.ts index 28e69baa..2cda96f1 100644 --- a/src/modules/container-loading/types.ts +++ b/src/modules/container-loading/types.ts @@ -302,6 +302,8 @@ export interface SmartLoadingWorkbenchContainer { channelCode: string channelName: string containerType: string + capacityCbm: number | null + maxWeightKg: number | null planId: string | null planNo: string | null planStatus: Exclude @@ -358,5 +360,14 @@ export interface SmartLoadingPlan { pdaTaskId: string | null pdaTaskStatus: 'LOADING' | 'COMPLETED' | null pdaTaskVersion: number | null + coLoadGroups: Array<{ + groupId: string + groupNo: string + lockedPlanId: string | null + memberOrderCount: number + inboundFragmentCount: number + fullPackageCount: number + memberInboundIds: string[] + }> items: SmartLoadingItem[] }