From 1f3e14a6cd26ea5ce2e9169f27490ca5a1b7dd71 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 24 Aug 2026 18:07:38 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E6=99=BA=E8=83=BD=E8=A3=85?= =?UTF-8?q?=E6=9F=9C=E5=85=A5=E4=BB=93=E7=A0=81=E9=87=8D=E5=A4=8D=E5=B1=95?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pages/GlobalSmartLoadingView.test.ts | 30 ++++++++++- .../pages/GlobalSmartLoadingView.vue | 42 +++++++++------ .../pages/SmartLoadingWorkbenchView.vue | 26 +++++++--- .../smartLoadingItems.test.ts | 42 +++++++++++++++ .../container-loading/smartLoadingItems.ts | 51 +++++++++++++++++++ 5 files changed, 167 insertions(+), 24 deletions(-) create mode 100644 src/modules/container-loading/smartLoadingItems.test.ts create mode 100644 src/modules/container-loading/smartLoadingItems.ts diff --git a/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts b/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts index 16bcb9c4..77c2c289 100644 --- a/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts +++ b/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts @@ -80,7 +80,7 @@ describe('GlobalSmartLoadingView', () => { expect(wrapper.text()).not.toContain('服装') expect(wrapper.text()).not.toContain('鞋类') expect(wrapper.text()).not.toContain('SKU-1') - expect(wrapper.text()).not.toContain('IN-1') + expect(wrapper.text()).toContain('IN-1') expect(wrapper.text()).toContain('IN-2') expect(wrapper.text()).not.toContain('ORD-1') expect(wrapper.text()).not.toContain('ORD-2') @@ -378,6 +378,34 @@ describe('GlobalSmartLoadingView', () => { expect(wrapper.text()).toContain('当前柜次暂无装柜明细') }) + 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) + expect(inboundRows[0].text()).toContain('多规格;改箱数请移除后重加') + 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) + }) + it('uses item capabilities to protect picked quantities and removal', async () => { vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue({ ...plan, diff --git a/src/modules/container-loading/pages/GlobalSmartLoadingView.vue b/src/modules/container-loading/pages/GlobalSmartLoadingView.vue index d38e9abf..a0381e8d 100644 --- a/src/modules/container-loading/pages/GlobalSmartLoadingView.vue +++ b/src/modules/container-loading/pages/GlobalSmartLoadingView.vue @@ -26,9 +26,9 @@ import { import type { ContainerBookingResource, SmartLoadingCandidate, - SmartLoadingItem, SmartLoadingPlan, } from '../types' +import { groupSmartLoadingItems, type SmartLoadingInboundItem } from '../smartLoadingItems' const props = withDefaults(defineProps<{ embedded?: boolean }>(), { embedded: false }) @@ -79,6 +79,7 @@ const editable = computed(() => plan.value?.canEdit === true) const assignable = computed(() => plan.value?.canAssign === true) const selectedPlanStatus = computed(() => selectedContainer.value?.planStatus || null) const assignedInboundIds = computed(() => new Set((plan.value?.items || []).map((item) => item.inboundId))) +const displayedPlanItems = computed(() => groupSmartLoadingItems(plan.value?.items || [])) 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)) @@ -127,6 +128,7 @@ const candidateColumns: DataTableColumn[] = [ ] const itemColumns: DataTableColumn[] = [ { key: 'actions', label: '操作', width: 170, fixed: 'left', slotName: 'actions' }, + { key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 190, fixed: 'left', showOverflowTooltip: true }, { key: 'customerName', prop: 'customerName', label: '客户', minWidth: 140 }, { key: 'packageCount', label: '分配箱数', width: 140, slotName: 'packageCount' }, { key: 'grossWeightKg', label: '毛重', width: 110, slotName: 'weight' }, @@ -138,7 +140,7 @@ function formatNumber(value: number, digits = 3) { } function percentage(value: number) { return Number.isFinite(value) ? Number(Math.max(value, 0).toFixed(2)) : 0 } function candidateRow(source: object) { return source as SmartLoadingCandidate } -function itemRow(source: object) { return source as SmartLoadingItem } +function itemRow(source: object) { return source as SmartLoadingInboundItem } function adjustmentMax(source: object) { const item = itemRow(source) const remaining = candidates.value.find((candidate) => candidate.inboundId === item.inboundId)?.availablePackageCount || 0 @@ -159,7 +161,7 @@ function displayActualContainerNo(container: ContainerBookingResource | null | u } function syncDrafts(next: SmartLoadingPlan, preserved: Record = {}) { for (const key of Object.keys(quantityDrafts)) delete quantityDrafts[key] - for (const item of next.items || []) quantityDrafts[item.id] = preserved[item.id] ?? item.packageCount + for (const item of groupSmartLoadingItems(next.items || [])) quantityDrafts[item.id] = preserved[item.id] ?? item.packageCount } function acceptPlan(next: SmartLoadingPlan, preserved: Record = {}, syncContainerStatus = true) { plan.value = next @@ -328,34 +330,42 @@ async function adjust(source: object) { async function remove(source: object) { const item = itemRow(source) if (!plan.value || !item.canRemove) return - const confirmed = await ElMessageBox.confirm(`确认从当前柜次移除“${item.customerName || '当前客户'}”的 ${item.packageCount} 箱明细吗?`, '移除装柜明细', { + const confirmed = await ElMessageBox.confirm(`确认从当前柜次移除入仓码“${item.inboundCode || '—'}”的 ${item.packageCount} 箱吗?`, '移除装柜明细', { type: 'warning', confirmButtonText: '移除', cancelButtonText: '取消', }).catch(() => false) if (!confirmed || !plan.value) return await mutate(async () => { - acceptPlan(await removeSmartLoadingItem(plan.value!.id, item.id, plan.value!.version, item.version)) + let current = plan.value! + for (const original of item.sourceItems) { + const currentItem = current.items.find((value) => value.id === original.id) + if (!currentItem) continue + current = await removeSmartLoadingItem(current.id, currentItem.id, current.version, currentItem.version) + acceptPlan(current) + } await loadSelectedData() - ElMessage.success('装柜明细已移除') + ElMessage.success('该入仓码已从当前柜次移除') }, '移除装柜明细失败', { pickingConflict: true }) } async function removeAll() { - const removableItems = plan.value?.items.filter((item) => item.canRemove) || [] + const removableItems = displayedPlanItems.value.filter((item) => item.canRemove) if (!removableItems.length) return const itemCount = removableItems.length - const confirmed = await ElMessageBox.confirm(`确认移除当前方案的 ${itemCount} 条未拣明细吗?已拣明细会保留。`, '移除未拣明细', { + const confirmed = await ElMessageBox.confirm(`确认移除当前方案的 ${itemCount} 个未拣入仓码吗?已拣入仓码会保留。`, '移除未拣明细', { type: 'warning', confirmButtonText: '确认移除', cancelButtonText: '取消', }).catch(() => false) if (!confirmed || !plan.value) return await mutate(async () => { let current = plan.value! - for (const original of removableItems) { - const item = current.items.find((value) => value.id === original.id) - if (!item) continue - current = await removeSmartLoadingItem(current.id, item.id, current.version, item.version) - acceptPlan(current) + for (const inboundItem of removableItems) { + for (const original of inboundItem.sourceItems) { + const item = current.items.find((value) => value.id === original.id) + if (!item) continue + current = await removeSmartLoadingItem(current.id, item.id, current.version, item.version) + acceptPlan(current) + } } await loadSelectedData() - ElMessage.success(`已移除 ${itemCount} 条未拣明细`) + ElMessage.success(`已移除 ${itemCount} 个未拣入仓码`) }, '全部移除失败', { pickingConflict: true }) } async function pushPreload() { @@ -528,8 +538,8 @@ onMounted(() => { void load(); void loadCustomers() })
体积{{ formatNumber(totalVolume) }} / {{ formatNumber(volumeLimit) }} m³
重量{{ formatNumber(totalWeight, 1) }} / {{ formatNumber(weightLimit, 1) }} kg
- - + + diff --git a/src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue b/src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue index b86791b7..def71293 100644 --- a/src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue +++ b/src/modules/container-loading/pages/SmartLoadingWorkbenchView.vue @@ -15,7 +15,8 @@ import { getSmartLoadingPlanByBooking, listSmartLoadingCandidates, pushSmartLoadingPreload, recommendSmartLoading, removeSmartLoadingItem, } from '../api/platformContainerLoading' -import type { ContainerBookingResource, SmartLoadingCandidate, SmartLoadingItem, SmartLoadingPlan } from '../types' +import type { ContainerBookingResource, SmartLoadingCandidate, SmartLoadingPlan } from '../types' +import { groupSmartLoadingItems, type SmartLoadingInboundItem } from '../smartLoadingItems' const route = useRoute() const router = useRouter() @@ -42,6 +43,7 @@ const canViewLoadingList = computed(() => access.can('platform:firstleg:loading- const editable = computed(() => plan.value?.canEdit === true) const assignable = computed(() => plan.value?.canAssign === true) const assignedInboundIds = computed(() => new Set((plan.value?.items || []).map((item) => item.inboundId))) +const displayedPlanItems = computed(() => groupSmartLoadingItems(plan.value?.items || [])) 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(container.value?.capacityCbm || 0) * 1.1) @@ -75,7 +77,7 @@ const candidateColumns: DataTableColumn[] = [ function formatNumber(value: number, digits = 3) { return Number.isFinite(value) ? value.toLocaleString('zh-CN', { maximumFractionDigits: digits }) : '0' } function percentage(value: number) { return Number.isFinite(value) ? Number(Math.max(value, 0).toFixed(2)) : 0 } function candidateRow(source: object) { return source as SmartLoadingCandidate } -function itemRow(source: object) { return source as SmartLoadingItem } +function itemRow(source: object) { return source as SmartLoadingInboundItem } function explainCandidate(source: object) { const candidate = candidateRow(source) const volume = Number(candidate.averageUnitVolumeCbm || 0) @@ -95,7 +97,7 @@ function displayContainerNo(value: ContainerBookingResource | null | undefined) } function syncDrafts(preserved: Record = {}) { for (const key of Object.keys(quantityDrafts)) delete quantityDrafts[key] - for (const item of plan.value?.items || []) quantityDrafts[item.id] = preserved[item.id] ?? item.packageCount + for (const item of displayedPlanItems.value) quantityDrafts[item.id] = preserved[item.id] ?? item.packageCount } function acceptPlan(next: SmartLoadingPlan, preserved: Record = {}) { plan.value = next; noDraft.value = false; syncDrafts(preserved) } @@ -178,11 +180,21 @@ async function adjust(source: object) { async function remove(source: object) { const item = itemRow(source) if (!plan.value || !item.canRemove) return - await ElMessageBox.confirm(`确认从计划移除“${item.productNameZh || item.inboundCode || '该货物'}”吗?`, '移除装柜明细', { type: 'warning', confirmButtonText: '移除', cancelButtonText: '取消' }).catch(() => false).then(async (confirmed) => { + await ElMessageBox.confirm(`确认从计划移除入仓码“${item.inboundCode || '—'}”的 ${item.packageCount} 箱吗?`, '移除装柜明细', { type: 'warning', confirmButtonText: '移除', cancelButtonText: '取消' }).catch(() => false).then(async (confirmed) => { if (!confirmed || !plan.value) return mutating.value = true mutationError.value = null - try { acceptPlan(await removeSmartLoadingItem(plan.value.id, item.id, plan.value.version, item.version)); ElMessage.success('装柜明细已移除') } + try { + let current = plan.value! + for (const original of item.sourceItems) { + const currentItem = current.items.find((value) => value.id === original.id) + if (!currentItem) continue + current = await removeSmartLoadingItem(current.id, currentItem.id, current.version, currentItem.version) + acceptPlan(current) + } + await load() + ElMessage.success('该入仓码已从装柜方案移除') + } catch (error) { await handleMutationError(error, '移除失败', true) } finally { mutating.value = false } }) @@ -251,9 +263,9 @@ onMounted(load)