diff --git a/AGENTS.md b/AGENTS.md index ec19a163..92732c75 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -50,7 +50,7 @@ | 财务与柜级公共费用分摊 | `modules/finance/`,继续读该目录 `AGENTS.md`;柜费入口 `modules/loading-lists/components/ContainerFeesPanel.vue`、`api/containerFees.ts` | `finance/`、`firstleg/`、`file/`、`docs/财务管理/`;跨客户公共柜费只按服务端柜内客户/订单事实分摊,金额必须守恒,创建与审核、驳回、调整、作废均整组原子处理 | | 货柜、排柜、预装/装柜清单、真实柜号后补与整柜报关 | `modules/container-loading/`、`modules/loading-lists/`;预装详情 `pages/PreloadPlanDetailView.vue`,货柜详情 `pages/ContainerDetailView.vue`,资料/PDA 证据 `components/ContainerOverviewPanel.vue`,报关 `components/ContainerCustomsPanel.vue`、`api/platformContainerCustoms.ts` | 创建柜次时自动生成唯一预装计划,加入入仓码即同步进入该计划,页面不提供创建计划或推送预装操作。预装详情先调用 `/loading-plans/{planId}/lifecycle`。订舱关联和预装确认允许真实集装箱号为空;首次补录可在 `EDITING/LOADING` 进行,PDA 最终完成前必须补齐;已有柜号纠正只允许 `EDITING` 且任务未领取、无物理作业事实。整柜报关逐资料完成,全买单柜使用 `complete-all-buy` | | 合柜报关 / 同柜不可拆组 | `modules/unified-orders/api/coLoadGroups.ts`、`pages/UnifiedOrdersView.vue`;装柜消费 `modules/container-loading/pages/SmartLoadingWorkbenchView.vue` | 前端只提交订单 ID;Backend `firstleg/loading/coload/` 展开并冻结每票全部可用入仓片段。成员数是入仓片段数,不等于订单票数;只要求同国内仓、同渠道,允许跨客户及不同目的仓港;分配、移除、取消均整组处理 | -| 全局智能装柜工作台、入仓码候选与箱数分配 | `modules/container-loading/components/SmartLoadingDialog.vue` 内嵌 `pages/GlobalSmartLoadingView.vue`,柜次页 `SmartLoadingWorkbenchView.vue`,以及 `smartLoadingChannelGroups.ts`、`api/platformContainerLoading.ts`;全局页当前不注册独立路由 | Backend `firstleg/` 的 `/inbound-candidates`、`/containers` 和计划分配接口;渠道分组选项与服务端映射必须一致,同柜不可拆组作为单个候选卡片整组分配,前端不得拆成员分页或自行拼组 | +| 全局智能装柜工作台、入仓码候选与箱数分配 | `modules/container-loading/components/SmartLoadingDialog.vue` 内嵌 `pages/GlobalSmartLoadingView.vue`,柜次页 `SmartLoadingWorkbenchView.vue`,以及 `smartLoadingChannelGroups.ts`、`api/platformContainerLoading.ts`;全局页当前不注册独立路由 | Backend `firstleg/` 的 `/inbound-candidates`、`/containers`、`/inbound-assignments`、`/pending-review-assignments`、`/group-assignments`、`/unpicked-items`。候选状态多选覆盖待审核、待测量、待分柜、装柜中,状态只用于筛选展示,按钮资格以服务端 `canAssign` 为准;待审核/待测量可提前占位但不得提前确认预装或启动 PDA。人工加入允许跨渠道,智能推荐仍按柜次渠道。柜次容量和限重取服务端字段;同柜组作为单卡整组分页、分配和移除,任一成员已拣时整组保留 | | 三期订舱资料、打印、关联候选与出仓前船期 | `modules/ocean-bookings/` 的 `components/OceanBookingDocumentsPanel.vue`、`OceanBookingPrintDialog.vue`,以及 `modules/container-loading/`、`modules/port-schedules/` | 订舱草稿按当前版本维护资料,放舱后资料与表单只读;打印只对 `RELEASED` 开放。`platformOceanBookings.ts` 的 `/link-candidates` 只消费服务端分页,`containerCount` 是柜型需求 `quantity` 总和;Backend `firstleg/oceanbooking/`、`file/`、`print/`、`firstleg/portschedule/` | | 订舱级拖车预约 | `modules/trucking/api/platformTrucking.ts` 的 `/candidates`、`components/TruckAppointmentForm.vue`、`pages/TruckAppointmentCreateView.vue`、`TruckAppointmentsView.vue` | 表单只选择服务端返回的 `RELEASED` 订舱与拖车供应商,不选择或提交客户、柜次、装柜计划或真实集装箱号;提单号和仓库由订舱冻结事实带入。一个订舱最多一张非终态拖车单,重复创建转入现有单继续编辑;PDA 完成不自动推进拖车。附件为平台作用域且不重复提单文件;Backend `TruckAppointmentService`、`truck-appointments-v1.yaml` | | 历史报关协同与国内出仓 | `modules/customs/`、`modules/outbound-dispatch/` | `modules/customs/` 是历史报关案件/清单/代理提交与回执入口;装柜完成后的整柜退税/买单资料从货柜详情进入,不在旧模块复制;Backend `firstleg/outbound/dispatch/`、`firstleg/customs/`、`file/` | diff --git a/src/modules/container-loading/api/platformContainerLoading.test.ts b/src/modules/container-loading/api/platformContainerLoading.test.ts index 3582800e..8487b6a3 100644 --- a/src/modules/container-loading/api/platformContainerLoading.test.ts +++ b/src/modules/container-loading/api/platformContainerLoading.test.ts @@ -4,7 +4,7 @@ import { resolve } from 'node:path' import { http } from '@/shared/api/http' import { adjustSmartLoadingItem, assignPendingReviewSmartLoadingOrder, assignSmartLoadingGroup, assignSmartLoadingItem, getContainer, - listContainers, listSmartLoadingCandidates, listSmartLoadingWorkbenchContainers, removeSmartLoadingGroup, removeSmartLoadingItem, + listContainers, listSmartLoadingCandidates, listSmartLoadingWorkbenchContainers, removeAllUnpickedSmartLoadingItems, removeSmartLoadingGroup, removeSmartLoadingItem, getContainerClearance, downloadContainerClearance, uploadContainerClearance, completeContainerClearance, getContainerOverview, markContainerException, cancelContainerException, uploadContainerDocument, downloadContainerDocument, downloadContainerPdaPhoto, getCustomerContainerTransitSummary, @@ -66,7 +66,7 @@ describe('platform container loading API', () => { it('normalizes every long id in candidates and plans', async () => { vi.mocked(http.get) .mockResolvedValueOnce({ code: '200', data: container }) - .mockResolvedValueOnce({ code: '200', data: { records: [{ accountId: 1, customerId: 1, inboundId: 2, orderId: 4, orderNo: 'ORD-1', inboundCode: 'IN-1', orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-10T09:30:00', warehouseId: 5, destinationWarehouseId: 6, channelCode: 'NY_EXX', channelName: '纽约EXX海铁', customerName: '示例企业', goodsSummary: '服装、鞋类', availablePackageCount: '7', unitGrossWeightKg: '12.5', unitNetWeightKg: '11.8', averageUnitVolumeCbm: '0.187', availableVolumeCbm: '1.309', coLoadGroupId: 9007199254740999n, groupNo: 'CLG-9007199254740999', memberCount: '3', groupTotalBoxes: '21', locked: false, blockReason: null, groupMembers: [] }], page: '1', size: '20', total: '1', pages: '1' } }) + .mockResolvedValueOnce({ code: '200', data: { records: [{ accountId: 1, inboundId: 2, orderId: 4, orderNo: 'ORD-1', inboundCode: 'IN-1', orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-10T09:30:00', warehouseId: 5, destinationWarehouseId: 6, channelCode: 'NY_EXX', channelName: '纽约EXX海铁', customerName: '示例企业', goodsSummary: '服装、鞋类', availablePackageCount: '7', unitGrossWeightKg: '12.5', unitNetWeightKg: '11.8', averageUnitVolumeCbm: '0.187', availableVolumeCbm: '1.309', coLoadGroupId: 9007199254740999n, groupNo: 'CLG-9007199254740999', memberCount: '3', groupTotalBoxes: '21', locked: false, blockReason: null, groupMembers: [] }], page: '1', size: '20', total: '1', pages: '1' } }) expect((await getContainer('9')).id).toBe(String(container.id)) expect(await listSmartLoadingCandidates({ channelGroupCodes: ['EXX', 'HEDE'], warehouseId: '5', orderStatuses: ['PENDING_REVIEW', 'WAIT_MEASURE'], page: 1, size: 20 })).toMatchObject({ records: [{ accountId: '1', customerId: '1', inboundId: '2', inboundCode: 'IN-1', orderStatusSnapshot: 'WAIT_SPLIT', expectedArrivalAt: '2026-09-10T09:30:00', destinationWarehouseId: '6', channelCode: 'NY_EXX', channelName: '纽约EXX海铁', goodsSummary: '服装、鞋类', availablePackageCount: 7, unitGrossWeightKg: 12.5, unitNetWeightKg: 11.8, averageUnitVolumeCbm: 0.187, availableVolumeCbm: 1.309, coLoadGroupId: '9007199254740999', groupNo: 'CLG-9007199254740999', memberCount: 3, groupTotalBoxes: 21, locked: false, blockReason: null }], total: 1 }) const candidateParams = vi.mocked(http.get).mock.calls.at(-1)?.[1]?.params as URLSearchParams @@ -144,6 +144,17 @@ describe('platform container loading API', () => { ) }) + it('uses one endpoint to remove all unpicked plan items', async () => { + vi.mocked(http.delete).mockResolvedValue({ code: '200', data: plan }) + + await removeAllUnpickedSmartLoadingItems('10', 6) + + expect(http.delete).toHaveBeenCalledWith( + '/platform/v1/first-leg/smart-loading/plans/10/unpicked-items', + { data: { expectedPlanVersion: 6, operationId: expect.stringContaining('remove-unpicked-') } }, + ) + }) + it('uses stable container id and versioned clearance document actions', async () => { vi.spyOn(http, 'get').mockResolvedValue({ code: '200', data: { id: 4, documentType: 'CUSTOMS_CLEARANCE', fileId: 8, fileName: 'clearance.pdf', version: 2 } }) vi.spyOn(http, 'post').mockResolvedValue({ code: '200', data: { id: 4, documentType: 'CUSTOMS_CLEARANCE', fileId: 8, fileName: 'clearance.pdf', version: 3 } }) diff --git a/src/modules/container-loading/api/platformContainerLoading.ts b/src/modules/container-loading/api/platformContainerLoading.ts index 9895e157..2c0947a3 100644 --- a/src/modules/container-loading/api/platformContainerLoading.ts +++ b/src/modules/container-loading/api/platformContainerLoading.ts @@ -259,23 +259,26 @@ export async function listSmartLoadingCandidates(query: SmartLoadingCandidateQue ...result, page: numberValue(result.page, query.page), size: numberValue(result.size, query.size), total: numberValue(result.total), pages: numberValue(result.pages), - records: result.records.map((raw) => ({ - ...raw, accountId: id(raw.accountId), customerId: id(raw.customerId), inboundId: optionalId(raw.inboundId), - orderId: id(raw.orderId), warehouseId: id(raw.warehouseId), destinationWarehouseId: optionalId(raw.destinationWarehouseId), - coLoadGroupId: optionalId(raw.coLoadGroupId), - memberCount: raw.memberCount == null ? null : numberValue(raw.memberCount), - groupTotalBoxes: raw.groupTotalBoxes == null ? null : numberValue(raw.groupTotalBoxes), - locked: raw.locked == null ? null : Boolean(raw.locked), - availablePackageCount: numberValue(raw.availablePackageCount), - unitGrossWeightKg: raw.unitGrossWeightKg == null ? null : numberValue(raw.unitGrossWeightKg), - unitNetWeightKg: raw.unitNetWeightKg == null ? null : numberValue(raw.unitNetWeightKg), - averageUnitVolumeCbm: raw.averageUnitVolumeCbm == null ? null : numberValue(raw.averageUnitVolumeCbm), - availableVolumeCbm: raw.availableVolumeCbm == null ? null : numberValue(raw.availableVolumeCbm), - groupMembers: raw.groupMembers.map((member) => ({ - ...member, accountId: id(member.accountId), inboundId: id(member.inboundId), orderId: id(member.orderId), - availablePackageCount: numberValue(member.availablePackageCount), - })), - })), + records: result.records.map((raw) => { + const accountId = id(raw.accountId) + return { + ...raw, accountId, customerId: accountId, inboundId: optionalId(raw.inboundId), + orderId: id(raw.orderId), warehouseId: id(raw.warehouseId), destinationWarehouseId: optionalId(raw.destinationWarehouseId), + coLoadGroupId: optionalId(raw.coLoadGroupId), + memberCount: raw.memberCount == null ? null : numberValue(raw.memberCount), + groupTotalBoxes: raw.groupTotalBoxes == null ? null : numberValue(raw.groupTotalBoxes), + locked: raw.locked == null ? null : Boolean(raw.locked), + availablePackageCount: numberValue(raw.availablePackageCount), + unitGrossWeightKg: raw.unitGrossWeightKg == null ? null : numberValue(raw.unitGrossWeightKg), + unitNetWeightKg: raw.unitNetWeightKg == null ? null : numberValue(raw.unitNetWeightKg), + averageUnitVolumeCbm: raw.averageUnitVolumeCbm == null ? null : numberValue(raw.averageUnitVolumeCbm), + availableVolumeCbm: raw.availableVolumeCbm == null ? null : numberValue(raw.availableVolumeCbm), + groupMembers: raw.groupMembers.map((member) => ({ + ...member, accountId: id(member.accountId), inboundId: id(member.inboundId), orderId: id(member.orderId), + availablePackageCount: numberValue(member.availablePackageCount), + })), + } + }), } } @@ -348,3 +351,11 @@ export async function removeSmartLoadingGroup(planId: string, groupId: string, e return plan(unwrapApiResponse(response)) } +export async function removeAllUnpickedSmartLoadingItems(planId: string, expectedPlanVersion: number) { + const response = await http.delete, ApiEnvelope>( + `${smartLoadingPath}/plans/${encodeURIComponent(planId)}/unpicked-items`, + { data: { expectedPlanVersion, operationId: operationId('remove-unpicked') } }, + ) + return plan(unwrapApiResponse(response)) +} + diff --git a/src/modules/container-loading/components/SmartLoadingDialog.test.ts b/src/modules/container-loading/components/SmartLoadingDialog.test.ts index 64f4c3e0..26cc574e 100644 --- a/src/modules/container-loading/components/SmartLoadingDialog.test.ts +++ b/src/modules/container-loading/components/SmartLoadingDialog.test.ts @@ -8,7 +8,7 @@ import SmartLoadingDialog from './SmartLoadingDialog.vue' describe('SmartLoadingDialog', () => { afterEach(() => { document.body.innerHTML = '' }) - it('expands to the viewport and can return to the normal dialog size', async () => { + it('opens fullscreen by default, can return to normal size, and reopens fullscreen', async () => { const wrapper = mount(SmartLoadingDialog, { attachTo: document.body, props: { modelValue: true }, @@ -21,14 +21,19 @@ describe('SmartLoadingDialog', () => { expect(wrapper.find('.smart-loading-dialog__warehouses').exists()).toBe(false) expect(document.querySelector('.smart-loading-dialog .el-dialog__body')?.classList.contains('el-dialog__body')).toBe(true) - const expand = wrapper.get('button[aria-label="展开全屏"]') - await expand.trigger('click') expect(document.querySelector('.smart-loading-dialog.is-fullscreen')).not.toBeNull() expect(wrapper.find('button[aria-label="退出全屏"]').exists()).toBe(true) await wrapper.get('button[aria-label="退出全屏"]').trigger('click') expect(document.querySelector('.smart-loading-dialog.is-fullscreen')).toBeNull() expect(wrapper.find('button[aria-label="展开全屏"]').exists()).toBe(true) + + await wrapper.setProps({ modelValue: false }) + await flushPromises() + await wrapper.setProps({ modelValue: true }) + await flushPromises() + expect(document.querySelector('.smart-loading-dialog.is-fullscreen')).not.toBeNull() + expect(wrapper.find('button[aria-label="退出全屏"]').exists()).toBe(true) }) it('keeps the outer dialog fixed so only workbench data panes scroll', () => { diff --git a/src/modules/container-loading/components/SmartLoadingDialog.vue b/src/modules/container-loading/components/SmartLoadingDialog.vue index 045685cc..33b5b4f2 100644 --- a/src/modules/container-loading/components/SmartLoadingDialog.vue +++ b/src/modules/container-loading/components/SmartLoadingDialog.vue @@ -5,12 +5,10 @@ import AppDialog from '@/shared/components/AppDialog.vue' import GlobalSmartLoadingView from '../pages/GlobalSmartLoadingView.vue' const open = defineModel({ default: false }) -const fullscreen = ref(false) +const fullscreen = ref(true) watch(open, (value) => { - if (!value) { - fullscreen.value = false - } + if (value) fullscreen.value = true }) diff --git a/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts b/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts index 7a648fec..6b79bb66 100644 --- a/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts +++ b/src/modules/container-loading/pages/GlobalSmartLoadingView.test.ts @@ -18,6 +18,7 @@ import { listSmartLoadingCandidates, listSmartLoadingWorkbenchContainers, recommendSmartLoading, + removeAllUnpickedSmartLoadingItems, removeSmartLoadingGroup, removeSmartLoadingItem, } from '../api/platformContainerLoading' @@ -47,6 +48,7 @@ vi.mock('../api/platformContainerLoading', () => ({ adjustSmartLoadingItem: vi.fn(), removeSmartLoadingItem: vi.fn(), removeSmartLoadingGroup: vi.fn(), + removeAllUnpickedSmartLoadingItems: vi.fn(), pushSmartLoadingPreload: vi.fn(), })) vi.mock('@/modules/warehouses', () => ({ @@ -453,7 +455,7 @@ describe('GlobalSmartLoadingView', () => { 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).toContain('发货仓:义乌仓 · 柜次渠道:美森正班海卡包税') expect(document.body.textContent).not.toContain('本次分配箱数') ;([...document.body.querySelectorAll('button')].find((button) => button.textContent?.includes('确认整组分配')) as HTMLButtonElement).click() await flushPromises() @@ -466,10 +468,12 @@ describe('GlobalSmartLoadingView', () => { it('allows a pending-review order to join through the provisional assignment command', async () => { vi.mocked(listSmartLoadingCandidates).mockResolvedValue(candidatePage([{ ...candidate, - customerId: '1', + customerId: '', inboundId: null, orderStatusSnapshot: 'PENDING_REVIEW', inboundCode: 'EXXHK2609020361', + channelCode: 'SLOW', + channelName: '慢船', unitGrossWeightKg: null, averageUnitVolumeCbm: null, availableVolumeCbm: null, @@ -488,6 +492,9 @@ describe('GlobalSmartLoadingView', () => { expect(document.body.textContent).toContain('平均单箱毛重 待测量') expect(document.body.textContent).toContain('平均单箱体积 待测量') expect(document.body.textContent).not.toContain('平均单箱毛重 0 kg') + expect(document.body.textContent).toContain('订单渠道:慢船') + expect(document.body.textContent).toContain('这是人工排柜,确认后仍会加入') + expect(document.body.textContent).toContain('智能推荐不会跨渠道') ;([...document.body.querySelectorAll('button')] .find((button) => button.textContent?.includes('确认加入')) as HTMLButtonElement).click() await flushPromises() @@ -717,13 +724,12 @@ describe('GlobalSmartLoadingView', () => { expect(wrapper.text()).toContain('trace-remove-1') }) - it('removes every item with the latest returned plan version', async () => { + it('removes every unpicked item with one bulk request', async () => { const secondItem = { ...plan.items[0], id: '102', inboundDetailId: '4', inboundCode: 'IN-2' } const initialPlan = { ...plan, items: [plan.items[0], secondItem] } - const afterFirst = { ...plan, version: 3, items: [secondItem] } const emptyPlan = { ...plan, version: 4, items: [] } vi.mocked(getSmartLoadingPlanByBooking).mockResolvedValueOnce(initialPlan).mockResolvedValue(emptyPlan) - vi.mocked(removeSmartLoadingItem).mockResolvedValueOnce(afterFirst).mockResolvedValueOnce(emptyPlan) + vi.mocked(removeAllUnpickedSmartLoadingItems).mockResolvedValueOnce(emptyPlan) vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as never) const wrapper = mount(GlobalSmartLoadingView, { global: { plugins: [ElementPlus] } }) @@ -733,8 +739,9 @@ describe('GlobalSmartLoadingView', () => { await removeAllButton!.trigger('click') await flushPromises() - expect(removeSmartLoadingItem).toHaveBeenNthCalledWith(1, plan.id, plan.items[0].id, 2, 1) - expect(removeSmartLoadingItem).toHaveBeenNthCalledWith(2, plan.id, secondItem.id, 3, 1) + expect(removeAllUnpickedSmartLoadingItems).toHaveBeenCalledTimes(1) + expect(removeAllUnpickedSmartLoadingItems).toHaveBeenCalledWith(plan.id, 2) + expect(removeSmartLoadingItem).not.toHaveBeenCalled() expect(wrapper.text()).toContain('当前柜次暂无装柜明细') }) diff --git a/src/modules/container-loading/pages/GlobalSmartLoadingView.vue b/src/modules/container-loading/pages/GlobalSmartLoadingView.vue index a63fa8d4..22edc45b 100644 --- a/src/modules/container-loading/pages/GlobalSmartLoadingView.vue +++ b/src/modules/container-loading/pages/GlobalSmartLoadingView.vue @@ -22,6 +22,7 @@ import { listSmartLoadingCandidates, listSmartLoadingWorkbenchContainers, recommendSmartLoading, + removeAllUnpickedSmartLoadingItems, removeSmartLoadingGroup, removeSmartLoadingItem, } from '../api/platformContainerLoading' @@ -74,6 +75,11 @@ 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 crossChannelAssignment = computed(() => Boolean( + selectedCandidate.value?.channelCode + && selectedContainer.value?.channelCode + && selectedCandidate.value.channelCode !== selectedContainer.value.channelCode, +)) const orchestratableContainers = computed(() => containers.value) const completedContainerCount = computed(() => 0) const missingChannelContainerCount = computed(() => orchestratableContainers.value.filter((item) => !item.channelCode).length) @@ -211,7 +217,7 @@ function preferredContainerId() { } const candidateColumns: DataTableColumn[] = [ - { key: 'actions', label: '操作', width: 92, fixed: 'left', slotName: 'actions' }, + { key: 'actions', label: '操作', width: 90, fixed: 'left', slotName: 'actions' }, { key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 140, fixed: 'left', showOverflowTooltip: true }, { key: 'coLoadGroup', label: '同柜', minWidth: 80, slotName: 'coLoadGroup' }, { key: 'customerName', prop: 'customerName', label: '客户', minWidth: 70, slotName: 'customerName' }, @@ -224,13 +230,13 @@ const candidateColumns: DataTableColumn[] = [ { key: 'totalVolumeCbm', label: '体积', width: 90, slotName: 'candidateTotalVolume' }, ] const itemColumns: DataTableColumn[] = [ - { key: 'actions', label: '操作', width: 80, fixed: 'left', slotName: 'actions' }, + { key: 'actions', label: '操作', width: 90, fixed: 'left', slotName: 'actions' }, { key: 'inboundCode', prop: 'inboundCode', label: '入仓码', minWidth: 140, fixed: 'left', showOverflowTooltip: true }, { key: 'coLoadGroup', label: '同柜', minWidth: 80, slotName: 'coLoadGroup' }, { key: 'customerName', prop: 'customerName', label: '客户', minWidth: 70, slotName: 'customerName' }, { key: 'packageCount', label: '分配箱数', width: 100, slotName: 'packageCount' }, - { key: 'grossWeightKg', label: '毛重', width: 80, slotName: 'weight' }, - { key: 'volumeCbm', label: '体积', width: 80, slotName: 'volume' }, + { key: 'grossWeightKg', label: '毛重', width: 90, slotName: 'weight' }, + { key: 'volumeCbm', label: '体积', width: 90, slotName: 'volume' }, ] function formatNumber(value: number, digits = 3) { @@ -474,7 +480,7 @@ async function assign() { const selected = selectedCandidate.value! acceptPlan(selected.orderStatusSnapshot === 'PENDING_REVIEW' ? await assignPendingReviewSmartLoadingOrder(plan.value!.id, { - customerId: selected.customerId, + customerId: selected.accountId, orderId: selected.orderId, inboundCode: selected.inboundCode || '', packageCount: assignForm.packageCount, @@ -495,7 +501,7 @@ async function assign() { function candidateBlocked(candidate: SmartLoadingCandidateRow) { if (candidate.orderStatusSnapshot === 'PENDING_REVIEW') { return Boolean(candidate.blockReason) || candidate.locked === true - || !candidate.inboundCode || candidate.availablePackageCount < 1 + || !candidate.accountId || !candidate.inboundCode || candidate.availablePackageCount < 1 } return Boolean(candidate.blockReason) || candidate.locked === true || !candidate.inboundId @@ -572,20 +578,7 @@ async function removeAll() { }).catch(() => false) if (!confirmed || !plan.value) return 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 - current = await removeSmartLoadingItem(current.id, item.id, current.version, item.version) - acceptPlan(current) - } - } + acceptPlan(await removeAllUnpickedSmartLoadingItems(plan.value!.id, plan.value!.version)) await loadCandidateData() ElMessage.success(`已移除 ${itemCount} 个未拣入仓码`) }, '全部移除失败', { pickingConflict: true }) @@ -685,7 +678,7 @@ onMounted(() => { void load(); void loadCustomers() }) { void load(); void loadCustomers() })
重量{{ formatNumber(totalWeight, 1) }} / {{ formatNumber(weightLimit, 1) }} kg
- + @@ -778,7 +771,9 @@ onMounted(() => { void load(); void loadCustomers() })

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

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

-

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

+

发货仓:{{ selectedContainer?.warehouseName || '—' }} · 柜次渠道:{{ selectedContainer?.channelName || '—' }}

+

订单渠道:{{ selectedCandidate.channelName || '—' }}

+