diff --git a/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts b/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts index b559a619..ee368c49 100644 --- a/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts +++ b/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts @@ -8,12 +8,16 @@ import { resolve } from 'node:path' import QueryToolbar from '@/shared/components/QueryToolbar.vue' import GlobalSmartLoadingView from './GlobalSmartLoadingView.vue' import { + assignSmartLoadingGroup, + assignSmartLoadingItem, getSmartLoadingPlanByBooking, listContainers, listSmartLoadingCandidates, recommendSmartLoading, + removeSmartLoadingGroup, removeSmartLoadingItem, } from '../api/platformContainerLoading' +import { batchQueryPlatformCoLoadGroups } from '@/modules/unified-orders' const { routerPush } = vi.hoisted(() => ({ routerPush: vi.fn() })) @@ -27,15 +31,20 @@ vi.mock('@/core/access/accessPreview', () => ({ vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn().mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }), })) +vi.mock('@/modules/unified-orders', () => ({ + batchQueryPlatformCoLoadGroups: vi.fn().mockResolvedValue([]), +})) vi.mock('../api/platformContainerLoading', () => ({ listContainers: vi.fn(), getSmartLoadingPlanByBooking: vi.fn(), listSmartLoadingCandidates: vi.fn(), createSmartLoadingPlan: vi.fn(), assignSmartLoadingItem: vi.fn(), + assignSmartLoadingGroup: vi.fn(), recommendSmartLoading: vi.fn(), adjustSmartLoadingItem: vi.fn(), removeSmartLoadingItem: vi.fn(), + removeSmartLoadingGroup: vi.fn(), pushSmartLoadingPreload: vi.fn(), })) @@ -61,6 +70,7 @@ describe('GlobalSmartLoadingView', () => { vi.clearAllMocks() vi.mocked(listContainers).mockResolvedValue({ records: [container], page: 1, size: 100, total: 1, pages: 1 }) vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValue(plan) + vi.mocked(batchQueryPlatformCoLoadGroups).mockResolvedValue([]) vi.mocked(listSmartLoadingCandidates).mockResolvedValue([{ accountId: '1', inboundId: '5', orderId: '7', orderNo: 'ORD-2', inboundCode: 'IN-2', warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '另一企业', goodsSummary: '鞋类', availablePackageCount: 12, unitGrossWeightKg: 10, unitNetWeightKg: 9, averageUnitVolumeCbm: 0.2, availableVolumeCbm: 2.4, coLoadGroupId: null, groupNo: null, memberCount: null, groupTotalBoxes: null, locked: null, blockReason: null }]) }) @@ -184,18 +194,85 @@ describe('GlobalSmartLoadingView', () => { const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8') expect(source).toContain("listPlatformTenants({ page: 1, size: 200, status: 'ACTIVE' })") expect(source).toContain("label: customer.tenantName") - expect(source).toContain('item.accountId !== customerId') + expect(source).toContain('member.accountId === customerId') expect(source).not.toContain('customer.tenantCode') }) - it('defaults candidate assignment to all available boxes', () => { + 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('assignForm.packageCount = candidate.availablePackageCount') + expect(source).toContain(': candidate.availablePackageCount') + }) + + it('aggregates a two-order candidate group and calls only whole-group assignment', async () => { + vi.mocked(listSmartLoadingCandidates).mockResolvedValue([ + { 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: 4, unitGrossWeightKg: 10, unitNetWeightKg: 9, averageUnitVolumeCbm: 0.2, availableVolumeCbm: 0.8, coLoadGroupId: '2093539779795795969', groupNo: 'CLG-2093539779795795969', memberCount: 2, groupTotalBoxes: 10, locked: false, blockReason: null }, + { accountId: '2', inboundId: '202', orderId: '302', orderNo: 'ORD-202', inboundCode: 'NYEXX2608290052', warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: '客户二', goodsSummary: '货物二', availablePackageCount: 6, unitGrossWeightKg: 10, unitNetWeightKg: 9, averageUnitVolumeCbm: 0.2, availableVolumeCbm: 1.2, coLoadGroupId: '2093539779795795969', groupNo: 'CLG-2093539779795795969', memberCount: 2, groupTotalBoxes: 10, locked: false, blockReason: 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('NYEXX2608290051、NYEXX2608290052') + 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).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('blocks an incomplete three-member candidate group without exposing member assignment', async () => { + vi.mocked(listSmartLoadingCandidates).mockResolvedValue(['401', '402', '403'].map((inboundId, index) => ({ + accountId: `${index + 1}`, inboundId, orderId: `50${index}`, orderNo: `ORD-${index}`, inboundCode: `IN-${inboundId}`, warehouseId: '8', channelCode: 'ZBYHK', channelName: '美森正班海卡包税', destinationWarehouseId: '9', destinationWarehouseCode: 'LAX9', destinationWarehouseName: 'LAX9', customerName: `客户${index}`, goodsSummary: `货物${index}`, availablePackageCount: 6, unitGrossWeightKg: 10, unitNetWeightKg: 9, averageUnitVolumeCbm: 0.2, availableVolumeCbm: 1.2, coLoadGroupId: '9900', groupNo: 'CLG-9900', memberCount: 3, groupTotalBoxes: 18, locked: false, blockReason: '同柜不可拆组成员候选不齐,缺少 1 票', + }))) + 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() + }) + + it('aggregates assigned members into one locked group row and removes the group once', async () => { + const items = ['201', '202'].map((inboundId, index) => ({ + ...plan.items[0], id: `70${index}`, accountId: `${index + 1}`, inboundId, inboundDetailId: `80${index}`, orderId: `30${index}`, orderNo: `ORD-${index}`, inboundCode: `NYEXX260829005${index + 1}`, customerName: `客户${index}`, packageCount: 5, grossWeightKg: 50, volumeCbm: 1, loadSequence: index + 1, + })) + const groupedPlan = { ...plan, items } + const group = { + groupId: '2093539779795795969', groupNo: 'CLG-2093539779795795969', status: 'LOCKED' as const, warehouseId: '8', channelCode: 'ZBYHK', destinationType: 'WAREHOUSE', destinationWarehouseId: '9', destinationCode: 'LAX9', lockedPlanId: '99', version: 1, + members: items.map((item) => ({ tenantId: item.accountId, orderId: item.orderId, inboundId: item.inboundId, fullPackageCount: 5 })), fullPackageCount: 10, createdAt: '2026-08-29T10:00:00', updatedAt: '2026-08-29T10:30:00', + } + vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValueOnce(groupedPlan).mockResolvedValue({ ...groupedPlan, version: 3, items: [] }) + vi.mocked(batchQueryPlatformCoLoadGroups).mockResolvedValue(items.map((item) => ({ orderId: item.orderId, inboundId: item.inboundId, group }))) + vi.mocked(removeSmartLoadingGroup).mockResolvedValue({ ...groupedPlan, version: 3, items: [] }) + vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as never) + const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } }) + await flushPromises() + + expect(wrapper.text()).toContain('CLG-2093539779795795969 · 2 票') + expect(wrapper.text()).toContain('固定 10 箱 · 已锁定') + expect(wrapper.text()).toContain('整组固定全箱数,不允许单票调整') + expect(wrapper.findAllComponents(ElInputNumber).some((input) => input.props('modelValue') === 10)).toBe(false) + const removeButton = wrapper.findAll('button').find((button) => button.text().includes('移除整组'))! + await removeButton.trigger('click') + await flushPromises() + + expect(removeSmartLoadingGroup).toHaveBeenCalledWith(plan.id, group.groupId, plan.version) + expect(removeSmartLoadingItem).not.toHaveBeenCalled() }) it('keeps the package count editor inside its table cell without overflow ellipsis', () => { const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/pages/GlobalSmartLoadingView.vue'), 'utf8') - expect(source).toContain('
{ 'ERR_BAD_REQUEST', undefined, undefined, - { data: { code: 'A0409', message: '已有拣货记录的明细不能删除' }, status: 200, statusText: 'OK', headers: {}, config: {} as never }, + { data: { code: 'A0409', message: '已有拣货记录的明细不能删除', requestId: 'request-remove-1', traceId: 'trace-remove-1' }, status: 200, statusText: 'OK', headers: {}, config: {} as never }, )) vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as never) const errorMessage = vi.spyOn(ElMessage, 'error').mockImplementation(() => undefined as never) @@ -359,8 +436,9 @@ describe('GlobalSmartLoadingView', () => { expect(removeSmartLoadingItem).toHaveBeenCalled() expect(errorMessage).toHaveBeenCalledWith('该订单在拣货中不许删除') - expect(wrapper.text()).not.toContain('装柜操作失败') - expect(wrapper.text()).not.toContain('该订单在拣货中不许删除') + expect(wrapper.text()).toContain('装柜操作失败') + expect(wrapper.text()).toContain('request-remove-1') + expect(wrapper.text()).toContain('trace-remove-1') }) it('removes every item with the latest returned plan version', async () => { diff --git a/src/modules/container-loading/pages/GlobalSmartLoadingView.vue b/src/modules/container-loading/pages/GlobalSmartLoadingView.vue index fa3828db..829c5826 100644 --- a/src/modules/container-loading/pages/GlobalSmartLoadingView.vue +++ b/src/modules/container-loading/pages/GlobalSmartLoadingView.vue @@ -5,6 +5,7 @@ import { Plus, RefreshCw, Save, Sparkles, Trash2, Upload } from '@lucide/vue' import { ElMessage, ElMessageBox } from 'element-plus' import { useAccessPreviewStore } from '@/core/access/accessPreview' import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam' +import { batchQueryPlatformCoLoadGroups, type CoLoadGroup } from '@/modules/unified-orders' import AdminPage from '@/shared/components/AdminPage.vue' import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue' import AppDialog from '@/shared/components/AppDialog.vue' @@ -14,6 +15,7 @@ import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/compone import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import { adjustSmartLoadingItem, + assignSmartLoadingGroup, assignSmartLoadingItem, createSmartLoadingPlan, getSmartLoadingPlanByBooking, @@ -21,6 +23,7 @@ import { listSmartLoadingCandidates, pushSmartLoadingPreload, recommendSmartLoading, + removeSmartLoadingGroup, removeSmartLoadingItem, } from '../api/platformContainerLoading' import type { @@ -46,11 +49,13 @@ const selectionLoading = ref(false) const mutating = ref(false) const loadError = ref(null) const selectionError = ref(null) +const mutationError = ref(null) const noDraft = ref(false) const assignOpen = ref(false) -const selectedCandidate = ref(null) +const selectedCandidate = ref(null) const assignForm = reactive({ packageCount: 1 }) const quantityDrafts = reactive>({}) +const planGroupsByInboundId = ref(new Map()) const query = ref({ customerId: '', inboundCode: '' }) const appliedQuery = ref({ ...query.value }) let selectionRequest = 0 @@ -79,7 +84,69 @@ 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 || [])) +type SmartLoadingCandidateRow = SmartLoadingCandidate & { sourceCandidates: SmartLoadingCandidate[]; memberInboundCodes: string[] } +const displayedCandidates = computed(() => { + const rows: SmartLoadingCandidateRow[] = [] + const groups = new Map() + for (const candidate of candidates.value) { + if (!candidate.coLoadGroupId) { + rows.push({ ...candidate, sourceCandidates: [candidate], memberInboundCodes: [candidate.inboundCode || '入仓码暂缺'] }) + continue + } + groups.set(candidate.coLoadGroupId, [...(groups.get(candidate.coLoadGroupId) || []), candidate]) + } + for (const members of groups.values()) { + const first = members[0] + rows.push({ + ...first, + inboundCode: members.map((member) => member.inboundCode).filter(Boolean).join('、'), + customerName: [...new Set(members.map((member) => member.customerName).filter(Boolean))].join('、'), + goodsSummary: [...new Set(members.map((member) => member.goodsSummary).filter(Boolean))].join('、'), + availablePackageCount: Number(first.groupTotalBoxes || members.reduce((sum, member) => sum + member.availablePackageCount, 0)), + availableVolumeCbm: members.reduce((sum, member) => sum + Number(member.availableVolumeCbm || 0), 0), + sourceCandidates: members, + memberInboundCodes: members.map((member) => member.inboundCode || '入仓码暂缺'), + }) + } + return rows +}) +type SmartLoadingPlanRow = SmartLoadingInboundItem & { coLoadGroup: CoLoadGroup | null; memberInboundCodes: string[] } +const displayedPlanItems = computed(() => { + const inboundRows = groupSmartLoadingItems(plan.value?.items || []) + const result: SmartLoadingPlanRow[] = [] + const grouped = new Map() + for (const row of inboundRows) { + const group = planGroupsByInboundId.value.get(row.inboundId) + if (!group) { + result.push({ ...row, coLoadGroup: null, memberInboundCodes: [row.inboundCode || '入仓码暂缺'] }) + continue + } + grouped.set(group.groupId, [...(grouped.get(group.groupId) || []), row]) + } + for (const rows of grouped.values()) { + const first = rows[0] + const group = planGroupsByInboundId.value.get(first.inboundId)! + result.push({ + ...first, + id: `coload:${group.groupId}`, + inboundCode: rows.map((row) => row.inboundCode).filter(Boolean).join('、'), + customerName: [...new Set(rows.map((row) => row.customerName).filter(Boolean))].join('、'), + packageCount: rows.reduce((sum, row) => sum + row.packageCount, 0), + productQuantity: rows.reduce((sum, row) => sum + Number(row.productQuantity || 0), 0), + grossWeightKg: rows.reduce((sum, row) => sum + Number(row.grossWeightKg || 0), 0), + volumeCbm: rows.reduce((sum, row) => sum + Number(row.volumeCbm || 0), 0), + pickedPackageCount: rows.reduce((sum, row) => sum + row.pickedPackageCount, 0), + minPackageCount: rows.reduce((sum, row) => sum + row.minPackageCount, 0), + canAdjust: false, + canRemove: rows.every((row) => row.canRemove), + sourceItems: rows.flatMap((row) => row.sourceItems), + detailCount: rows.reduce((sum, row) => sum + row.detailCount, 0), + coLoadGroup: group, + memberInboundCodes: rows.map((row) => row.inboundCode || '入仓码暂缺'), + }) + } + return result +}) 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)) @@ -103,10 +170,10 @@ const filteredCandidates = computed(() => { const customerId = String(appliedQuery.value.customerId || '') const inboundCode = String(appliedQuery.value.inboundCode || '').trim().toLowerCase() const channelCode = selectedContainer.value?.channelCode || activeChannelCode.value - return candidates.value.filter((item) => { + return displayedCandidates.value.filter((item) => { if (!channelCode || item.channelCode !== channelCode) return false - if (customerId && item.accountId !== customerId) return false - return !inboundCode || String(item.inboundCode || '').toLowerCase().includes(inboundCode) + if (customerId && !item.sourceCandidates.some((member) => member.accountId === customerId)) return false + return !inboundCode || item.memberInboundCodes.some((code) => code.toLowerCase().includes(inboundCode)) }) }) @@ -120,6 +187,7 @@ function preferredContainerId() { const candidateColumns: DataTableColumn[] = [ { key: 'actions', label: '操作', width: 92, fixed: 'left', slotName: 'actions' }, { key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 190, fixed: 'left', showOverflowTooltip: true }, + { key: 'coLoadGroup', label: '同柜不可拆', minWidth: 210, slotName: 'coLoadGroup' }, { key: 'customerName', prop: 'customerName', label: '客户', minWidth: 140 }, { key: 'availablePackageCount', prop: 'availablePackageCount', label: '可用箱数', width: 100 }, { key: 'cartonIdentityStatus', label: '箱号归属', width: 105, slotName: 'cartonIdentityStatus' }, @@ -129,6 +197,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: 'coLoadGroup', label: '同柜不可拆', minWidth: 210, slotName: 'coLoadGroup' }, { key: 'customerName', prop: 'customerName', label: '客户', minWidth: 140 }, { key: 'packageCount', label: '分配箱数', width: 140, slotName: 'packageCount' }, { key: 'grossWeightKg', label: '毛重', width: 110, slotName: 'weight' }, @@ -139,7 +208,7 @@ 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 candidateRow(source: object) { return source as SmartLoadingCandidateRow } function itemRow(source: object) { return source as SmartLoadingInboundItem } function adjustmentMax(source: object) { const item = itemRow(source) @@ -173,11 +242,25 @@ function acceptPlan(next: SmartLoadingPlan, preserved: Record = if (syncContainerStatus) container.planStatus = next.status } } +async function loadPlanGroups(request = selectionRequest) { + planGroupsByInboundId.value = new Map() + const currentPlan = plan.value + const orderIds = [...new Set((currentPlan?.items || []).map((item) => item.orderId))] + if (!currentPlan || !orderIds.length || !canAssign.value) return + const groups = await batchQueryPlatformCoLoadGroups(orderIds) + if (request !== selectionRequest || plan.value?.id !== currentPlan.id) return + const byInbound = new Map() + for (const info of groups) for (const member of info.group.members) byInbound.set(member.inboundId, info.group) + planGroupsByInboundId.value = byInbound + syncDrafts(currentPlan) +} function clearSelectionData() { plan.value = null candidates.value = [] noDraft.value = false selectionError.value = null + mutationError.value = null + planGroupsByInboundId.value = new Map() for (const key of Object.keys(quantityDrafts)) delete quantityDrafts[key] } @@ -199,7 +282,10 @@ async function loadSelectedData(preserved: Record = {}) { ]) if (request !== selectionRequest) return candidates.value = candidateResult - if (planResult.value) acceptPlan(planResult.value, preserved, false) + if (planResult.value) { + acceptPlan(planResult.value, preserved, false) + await loadPlanGroups(request) + } else noDraft.value = planResult.missing } catch (error) { if (request === selectionRequest) selectionError.value = getApiErrorDetails(error, '柜次装柜数据加载失败') @@ -274,27 +360,39 @@ async function createPlan() { } function openAssign(source: object) { const candidate = candidateRow(source) + if (candidateBlocked(candidate)) return selectedCandidate.value = candidate - assignForm.packageCount = candidate.availablePackageCount + assignForm.packageCount = candidate.coLoadGroupId + ? Number(candidate.groupTotalBoxes || candidate.availablePackageCount) + : candidate.availablePackageCount assignOpen.value = true } async function assign() { if (!plan.value || !selectedCandidate.value || assignForm.packageCount < 1) return - if (assignForm.packageCount > selectedCandidate.value.availablePackageCount) { + if (selectedCandidate.value.coLoadGroupId && (selectedCandidate.value.blockReason || selectedCandidate.value.locked)) return + if (!selectedCandidate.value.coLoadGroupId && assignForm.packageCount > selectedCandidate.value.availablePackageCount) { ElMessage.warning(`本次最多可分配 ${selectedCandidate.value.availablePackageCount} 箱`) return } await mutate(async () => { - acceptPlan(await assignSmartLoadingItem(plan.value!.id, { - inboundId: selectedCandidate.value!.inboundId, - packageCount: assignForm.packageCount, - expectedVersion: plan.value!.version, - })) + const selected = selectedCandidate.value! + acceptPlan(selected.coLoadGroupId + ? await assignSmartLoadingGroup(plan.value!.id, selected.inboundId, plan.value!.version) + : await assignSmartLoadingItem(plan.value!.id, { + inboundId: selected.inboundId, + packageCount: assignForm.packageCount, + expectedVersion: plan.value!.version, + })) assignOpen.value = false await loadSelectedData() - ElMessage.success('明细已加入当前柜次') + ElMessage.success(selected.coLoadGroupId ? `同柜不可拆组 ${selected.groupNo} 已整组分配` : '明细已加入当前柜次') }, '分配明细失败', { preserveAssign: true }) } +function candidateBlocked(candidate: SmartLoadingCandidateRow) { + return Boolean(candidate.blockReason) || candidate.locked === true + || candidate.sourceCandidates.some((member) => assignedInboundIds.value.has(member.inboundId)) + || candidate.availablePackageCount < 1 +} async function recommend() { if (!plan.value) return await mutate(async () => { @@ -330,11 +428,20 @@ 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.inboundCode || '—'}”的 ${item.packageCount} 箱吗?`, '移除装柜明细', { - type: 'warning', confirmButtonText: '移除', cancelButtonText: '取消', + const row = source as SmartLoadingPlanRow + const confirmed = await ElMessageBox.confirm(row.coLoadGroup + ? `确认整组移除 ${row.coLoadGroup.groupNo}(${row.coLoadGroup.members.length} 票、${row.packageCount} 箱)吗?组内成员不能单独移除。` + : `确认从当前柜次移除入仓码“${item.inboundCode || '—'}”的 ${item.packageCount} 箱吗?`, row.coLoadGroup ? '移除同柜不可拆组' : '移除装柜明细', { + type: 'warning', confirmButtonText: row.coLoadGroup ? '移除整组' : '移除', cancelButtonText: '取消', }).catch(() => false) if (!confirmed || !plan.value) return await mutate(async () => { + if (row.coLoadGroup) { + acceptPlan(await removeSmartLoadingGroup(plan.value!.id, row.coLoadGroup.groupId, plan.value!.version)) + await loadSelectedData() + ElMessage.success(`同柜不可拆组 ${row.coLoadGroup.groupNo} 已整组移除`) + return + } let current = plan.value! for (const original of item.sourceItems) { const currentItem = current.items.find((value) => value.id === original.id) @@ -357,6 +464,11 @@ async function removeAll() { await mutate(async () => { let current = plan.value! for (const inboundItem of removableItems) { + if (inboundItem.coLoadGroup) { + current = await removeSmartLoadingGroup(current.id, inboundItem.coLoadGroup.groupId, current.version) + acceptPlan(current) + continue + } for (const original of inboundItem.sourceItems) { const item = current.items.find((value) => value.id === original.id) if (!item) continue @@ -386,6 +498,7 @@ async function mutate( recovery: { preserveDrafts?: Record; preserveAssign?: boolean; pickingConflict?: boolean } = {}, ) { mutating.value = true + mutationError.value = null try { await action() } catch (error) { @@ -402,6 +515,7 @@ async function mutate( } else { ElMessage.error(details.message) } + mutationError.value = details } finally { mutating.value = false } @@ -440,6 +554,9 @@ onMounted(() => { void load(); void loadCustomers() }) + @@ -539,8 +657,9 @@ onMounted(() => { void load(); void loadCustomers() })
重量{{ formatNumber(totalWeight, 1) }} / {{ formatNumber(weightLimit, 1) }} kg
- - + + + @@ -548,12 +667,19 @@ onMounted(() => { void load(); void loadCustomers() }) - +
-

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

-

入仓码 {{ selectedCandidate.inboundCode || '—' }} · 可用 {{ selectedCandidate.availablePackageCount }} 箱 · 平均单箱毛重 {{ formatNumber(Number(selectedCandidate.unitGrossWeightKg || 0), 1) }} kg · 平均单箱体积 {{ formatNumber(Number(selectedCandidate.averageUnitVolumeCbm || 0)) }} m³

- 本次只记录该入仓码的计划装柜箱数,具体箱号归属为未核验。 - +

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

+ +
@@ -607,6 +733,8 @@ onMounted(() => { void load(); void loadCustomers() }) .plan-pane :deep(.package-count-editor .el-input-number) { width: 100%; min-width: 0; } .assign-dialog p { margin: 0 0 10px; } .assign-dialog :deep(.el-input-number) { width: 100%; } +.candidate-group { display: grid; gap: 4px; } +.candidate-block { color: var(--el-color-danger) !important; } @media (max-width: 1180px) { .dispatch-grid { grid-template-columns: 1fr; height: auto; min-height: 0; overflow: visible; } .embedded-smart-loading .dispatch-grid { height: auto; }