From 44d05e77cb452c82dfe517c5e3e4785d060db712 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 27 Aug 2026 17:49:05 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../API_INTEGRATION_MATRIX.md | 4 +- .../pages/LastMileOrdersView.behavior.test.ts | 142 +++++++++------ .../pages/LastMileOrdersView.vue | 162 ++++++++++-------- 3 files changed, 182 insertions(+), 126 deletions(-) diff --git a/src/modules/overseas-warehouse/API_INTEGRATION_MATRIX.md b/src/modules/overseas-warehouse/API_INTEGRATION_MATRIX.md index e6cae4f1..2a1ebc29 100644 --- a/src/modules/overseas-warehouse/API_INTEGRATION_MATRIX.md +++ b/src/modules/overseas-warehouse/API_INTEGRATION_MATRIX.md @@ -7,8 +7,8 @@ | 尾程列表/详情 | `GET /platform/v1/last-leg/orders`;`GET /orders/{id}` | list query:`page,size,orderNo,orderType,warehouse,inboundCode,sourceOrderNo,containerNo,currentStatus,transferWarehouseCode,abnormal,detained,createdFrom,createdTo`;平台跨客户,不传 `customerId`;货主只消费响应 `customerName`,空值显示 `—` | `platform:overseas-warehouse:last-mile-order:list`;真实分页、空态、错误及 request/trace 元数据;详情只读展示离仓关联事实 | | 尾程转目的仓 | `PUT /platform/v1/last-leg/orders/destination` | body:`orderIds:string[],warehouseId,warehouseCode,warehouseName,remark?` | `...:last-mile-order:edit`;失败保留弹框,不提示成功 | | 尾程转入仓计划 | `POST /platform/v1/last-leg/inbound-plans/convert` | body:`orderId:string,customerId:string`,客户作用域精确取自所选尾程行;缺失时前端失败关闭 | `...:inbound-order:create`;A0404/A0409 保留 code/requestId/traceId;不接补托/PDA | -| 尾程整柜转入仓计划 | `POST /platform/v1/last-leg/inbound-plans/convert-container` | body:`sourceContainerBookingId:string`;只调用一次柜级原子接口,不在前端循环逐票;返回一个计划数组,每个尾程订单/入仓码对应一个独立计划 | `...:inbound-order:create`;成功刷新待分配列表;计划数与整柜订单数不一致时不提示成功;错误保留 code/requestId/traceId | -| 待分配整柜摘要分页 | `GET /platform/v1/last-leg/orders/container-summaries` | DTO:`sourceContainerBookingId,containerNo?,orderCount,inboundCodeCount,customerCount,totalPackageCount,totalPalletCount,totalVolumeCbm,eligible,ineligibleReason?`;query:`page,size,containerNo?,createdFrom?,createdTo?` | 服务端一柜一行并负责全量聚合;前端只消费摘要分页,不从订单分页本地聚合 | +| 尾程整柜转入仓计划 | `POST /platform/v1/last-leg/inbound-plans/convert-container` | body:`sourceContainerBookingId:string`;只调用一次柜级原子接口,不在前端循环逐票;返回一个计划数组,每个尾程订单/入仓码对应一个独立计划 | `...:inbound-order:create`;成功按接口返回计划数提示并刷新原待分配订单列表,不用当前订单页行数校验整柜总数;错误保留 code/requestId/traceId | +| 待分配整柜摘要分页 | `GET /platform/v1/last-leg/orders/container-summaries` | DTO:`sourceContainerBookingId,containerNo?,orderCount,inboundCodeCount,customerCount,totalPackageCount,totalPalletCount,totalVolumeCbm,eligible,ineligibleReason?`;query:`page,size,containerNo?,createdFrom?,createdTo?` | 仅供原待分配订单页的“按柜批量生成”选择弹框加载柜候选;不替换原订单表,也不从订单分页本地聚合 | | 入库列表 | `GET /platform/v1/last-leg/inbound-plans` | query:`page,size,planNo,inboundCode,status,warehouse,fbaWarehouseCode,appointmentStatus,dispatchStatus,createdFrom,createdTo`;平台跨客户 | `...:inbound-order:list`;`lineId/planId/orderId/customerId` 均为字符串 | | 入库详情 | `GET /platform/v1/last-leg/inbound-plans/{planNo}` | query:显式 `customerId`;path 编码 | 同 list;缺客户范围时页面明确报错;`sourceOrderId` 字符串化 | | FBA 预约新增 | `POST /platform/v1/last-leg/inbound-plans/fba-appointments/batch` | body:`customerId,lineIds:string[],appointmentNo,appointmentAt,expectedLoadingAt,appointmentStatus,appointmentRemark?` | `...:inbound-order:fba-appointment`;只允许同客户、同 FBA 仓勾选 | diff --git a/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts b/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts index acb5986c..c60b391f 100644 --- a/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts +++ b/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts @@ -3,14 +3,20 @@ import { createPinia } from 'pinia' import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { ApiResponseError } from '@/shared/api/http' -import { convertLastMileContainerToInboundPlans, convertLastMileOrderToInboundPlan, listLastMileOrders, listPendingContainerSummaries } from '../api/lastMileOrders' +import DataTable from '@/shared/components/DataTable.vue' +import { + convertLastMileContainerToInboundPlans, + convertLastMileOrderToInboundPlan, + listLastMileOrders, + listPendingContainerSummaries, +} from '../api/lastMileOrders' import LastMileOrdersView from './LastMileOrdersView.vue' vi.mock('@/core/access/accessPreview', () => ({ useAccessPreviewStore: () => ({ can: () => true }) })) vi.mock('vue-router', () => ({ useRouter: () => ({ push: vi.fn() }) })) vi.mock('../api/lastMileOrders', () => ({ listLastMileOrders: vi.fn(), listPendingContainerSummaries: vi.fn(), - convertLastMileContainerToInboundPlans: vi.fn(), convertLastMileOrderToInboundPlan: vi.fn(), transferLastMileOrders: vi.fn(), + convertLastMileOrderToInboundPlan: vi.fn(), convertLastMileContainerToInboundPlans: vi.fn(), transferLastMileOrders: vi.fn(), })) class ResizeObserverStub implements ResizeObserver { observe() {} unobserve() {} disconnect() {} } @@ -22,94 +28,126 @@ function memoryStorage(): Storage { } } +const bookingId = '2092873463568597001' +const customerId = '2092618717861048321' const summary = { - sourceContainerBookingId: '2092873463568597001', containerNo: 'CONT-15', orderCount: 15, + sourceContainerBookingId: bookingId, containerNo: 'CONT-15', orderCount: 15, inboundCodeCount: 15, customerCount: 3, totalPackageCount: 150, totalPalletCount: 30, totalVolumeCbm: 45.5, eligible: true, ineligibleReason: null, } -function plans(count = 15) { - return Array.from({ length: count }, (_, index) => ({ - id: String(index + 1), planNo: `LIP-${index + 1}`, customerId: String(index % 3 + 1), sourceOrderId: String(index + 1), - containerNo: 'CONT-15', status: 'PENDING_ASSIGNMENT', lineCount: 1, palletCount: 1, totalVolumeCbm: 1, - lines: [], pallets: [], allocations: [], - })) +function selectedRow(overrides: Record = {}) { + return { id: '2092873463568596994', customerId, sourceContainerBookingId: bookingId, orderNo: 'LMO-260827-0001', ...overrides } } enableAutoUnmount(afterEach) afterAll(() => vi.unstubAllGlobals()) -async function openPending() { +async function mountView() { const wrapper = mount(LastMileOrdersView, { attachTo: document.body, global: { plugins: [createPinia(), ElementPlus] } }) await flushPromises() - const pendingTab = wrapper.findAll('.el-radio-button').find((item) => item.text().includes('待分配')) + return wrapper +} + +async function openPending() { + const wrapper = await mountView() + const pendingTab = wrapper.findAll('.el-radio-button').find((item) => item.text() === '待分配') await pendingTab!.trigger('click') await flushPromises() return wrapper } -describe('LastMileOrdersView server-side container summaries', () => { +async function selectRows(wrapper: Awaited>, rows: object[]) { + wrapper.findComponent(DataTable).vm.$emit('selection-change', rows) + await wrapper.vm.$nextTick() +} + +function button(wrapper: Awaited>, text: string) { + return wrapper.findAll('button').find((item) => item.text().includes(text)) +} + +async function openContainerDialog(wrapper: Awaited>) { + await button(wrapper, '按柜批量生成')!.trigger('click') + await flushPromises() +} + +async function chooseContainer(wrapper: Awaited>) { + const selects = wrapper.findAllComponents({ name: 'ElSelect' }) + const containerSelect = selects[selects.length - 1] + expect(containerSelect).toBeDefined() + containerSelect.vm.$emit('update:modelValue', bookingId) + await wrapper.vm.$nextTick() +} + +describe('LastMileOrdersView pending order and container conversion', () => { beforeEach(() => { vi.stubGlobal('ResizeObserver', ResizeObserverStub) vi.stubGlobal('localStorage', memoryStorage()) vi.mocked(listLastMileOrders).mockReset().mockResolvedValue({ records: [], page: 1, size: 10, total: 0, pages: 0 }) - vi.mocked(listPendingContainerSummaries).mockReset().mockResolvedValue({ records: [summary], page: 1, size: 10, total: 21, pages: 3 }) - vi.mocked(convertLastMileContainerToInboundPlans).mockReset().mockResolvedValue(plans()) - vi.mocked(convertLastMileOrderToInboundPlan).mockReset() + vi.mocked(listPendingContainerSummaries).mockReset().mockResolvedValue({ records: [summary], page: 1, size: 50, total: 1, pages: 1 }) + vi.mocked(convertLastMileOrderToInboundPlan).mockReset().mockResolvedValue({}) + vi.mocked(convertLastMileContainerToInboundPlans).mockReset().mockResolvedValue([{ planNo: 'LIP-1' }, { planNo: 'LIP-2' }] as never) }) - it('uses one server summary for a 15-order container that could span order pages', async () => { + it('keeps the original pending order table and does not render a view switch or container table', async () => { const wrapper = await openPending() - expect(listPendingContainerSummaries).toHaveBeenCalledWith({ page: 1, size: 10, containerNo: undefined, createdFrom: undefined, createdTo: undefined }) - expect(wrapper.text()).toContain('CONT-15') - expect(wrapper.text()).toContain('15') - expect(wrapper.text()).toContain('将为 15 个尾程订单生成 15 个独立入库计划') - expect(listLastMileOrders).toHaveBeenCalledTimes(1) + expect(wrapper.text()).toContain('尾程单号') + expect(wrapper.text()).toContain('海外仓') + expect(wrapper.text()).toContain('转仓') + expect(wrapper.text()).toContain('生成入库计划') + expect(wrapper.text()).toContain('按柜批量生成') + expect(wrapper.text()).not.toContain('按订单') + expect(wrapper.text()).not.toContain('待分配整柜') + expect(listLastMileOrders).toHaveBeenLastCalledWith(expect.objectContaining({ page: 1, size: 10, currentStatus: 'PENDING_ASSIGNMENT' })) + expect(listPendingContainerSummaries).not.toHaveBeenCalled() }) - it('requests the next server summary page instead of deriving containers from order rows', async () => { + it('keeps single-order conversion isolated with long order and customer ids', async () => { const wrapper = await openPending() - const nextPage = wrapper.find('button[aria-label="第 2 页"]') - expect(nextPage.exists()).toBe(true) - await nextPage.trigger('click') + await selectRows(wrapper, [selectedRow()]) + await button(wrapper, '生成入库计划')!.trigger('click') await flushPromises() - expect(listPendingContainerSummaries).toHaveBeenLastCalledWith({ - page: 2, size: 10, containerNo: undefined, createdFrom: undefined, createdTo: undefined, - }) - expect(listLastMileOrders).toHaveBeenCalledTimes(1) + expect(convertLastMileOrderToInboundPlan).toHaveBeenCalledTimes(1) + expect(convertLastMileOrderToInboundPlan).toHaveBeenCalledWith({ orderId: '2092873463568596994', customerId }) + expect(convertLastMileContainerToInboundPlans).not.toHaveBeenCalled() + expect(listPendingContainerSummaries).not.toHaveBeenCalled() }) - it('calls convert-container once, validates N plans and refreshes the server summary', async () => { + it('opens a cabinet selector without requiring order selection and loads server summaries only then', async () => { const wrapper = await openPending() - const button = wrapper.findAll('button').find((item) => item.text().includes('生成整柜入库计划')) - await button!.trigger('click') - await flushPromises() - expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledTimes(1) - expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledWith({ sourceContainerBookingId: '2092873463568597001' }) - expect(convertLastMileOrderToInboundPlan).not.toHaveBeenCalled() - expect(listPendingContainerSummaries).toHaveBeenCalledTimes(2) - expect(document.body.textContent).toContain('已为15个尾程订单生成15个入库计划') - }) - - it('does not report success or refresh when N returned plans do not match N orders', async () => { - vi.mocked(convertLastMileContainerToInboundPlans).mockResolvedValueOnce(plans(14)) - const wrapper = await openPending() - const button = wrapper.findAll('button').find((item) => item.text().includes('生成整柜入库计划')) - await button!.trigger('click') - await flushPromises() - expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledTimes(1) + await openContainerDialog(wrapper) expect(listPendingContainerSummaries).toHaveBeenCalledTimes(1) - expect(wrapper.text()).toContain('返回 14 个计划') - expect(document.body.textContent).not.toContain('已为15个尾程订单生成14个入库计划') + expect(listPendingContainerSummaries).toHaveBeenCalledWith({ page: 1, size: 50, containerNo: undefined }) + expect(document.body.textContent).toContain('选择待分配柜') + expect(document.body.textContent).toContain('将为所选柜内所有待分配尾程订单分别生成独立入库计划') + expect(convertLastMileContainerToInboundPlans).not.toHaveBeenCalled() }) - it.each(['A0404', 'A0409'])('keeps %s request evidence visible', async (code) => { + it('selects one cabinet and sends one long-id convert-container request without using current-page row counts', async () => { + const wrapper = await openPending() + await openContainerDialog(wrapper) + await chooseContainer(wrapper) + const confirm = [...document.body.querySelectorAll('button')].find((item) => item.textContent?.includes('确认按柜生成')) as HTMLButtonElement + expect(confirm.disabled).toBe(false) + confirm.click() + await flushPromises() + expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledTimes(1) + expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledWith({ sourceContainerBookingId: bookingId }) + expect(convertLastMileOrderToInboundPlan).not.toHaveBeenCalled() + expect(document.body.textContent).toContain('已按柜生成 2 个独立入库计划') + expect(listLastMileOrders).toHaveBeenCalledTimes(3) + }) + + it.each(['A0404', 'A0409'])('keeps %s request evidence visible after cabinet conversion failure', async (code) => { vi.mocked(convertLastMileContainerToInboundPlans).mockRejectedValueOnce(new ApiResponseError( '柜级生成失败', code, 200, undefined, `request-${code}`, `trace-${code}`, )) const wrapper = await openPending() - const button = wrapper.findAll('button').find((item) => item.text().includes('生成整柜入库计划')) - await button!.trigger('click') + await openContainerDialog(wrapper) + await chooseContainer(wrapper) + const confirm = [...document.body.querySelectorAll('button')].find((item) => item.textContent?.includes('确认按柜生成')) as HTMLButtonElement + confirm.click() await flushPromises() + expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledTimes(1) expect(wrapper.text()).toContain(code) expect(wrapper.text()).toContain(`request-${code}`) expect(wrapper.text()).toContain(`trace-${code}`) diff --git a/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue b/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue index aac84027..971e536b 100644 --- a/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue +++ b/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue @@ -12,7 +12,7 @@ import ListPageLayout from '@/shared/components/ListPageLayout.vue' import LoadErrorState from '@/shared/components/LoadErrorState.vue' import OperationErrorToast from '@/shared/components/OperationErrorToast.vue' import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue' -import { convertLastMileContainerToInboundPlans, listLastMileOrders, listPendingContainerSummaries, transferLastMileOrders, type PendingContainerSummaryDto } from '../api/lastMileOrders' +import { convertLastMileContainerToInboundPlans, convertLastMileOrderToInboundPlan, listLastMileOrders, listPendingContainerSummaries, transferLastMileOrders, type PendingContainerSummaryDto } from '../api/lastMileOrders' import OrderReferenceText from '../components/OrderReferenceText.vue' import { useAccessPreviewStore } from '@/core/access/accessPreview' import { overseasWarehousePermissions as permission } from '../permissions' @@ -27,13 +27,17 @@ const router = useRouter() const page = ref(1) const pageSize = ref(10) const total = ref(0) -const containerTotal = ref(0) const selectedRows = ref([]) -const containerRows = ref([]) const actionLoading = ref(false) const actionError = ref(null) const transferOpen = ref(false) const transferForm = reactive({ warehouseId: '', warehouseCode: '', warehouseName: '', remark: '' }) +const containerConvertOpen = ref(false) +const containerOptions = ref([]) +const containerOptionsLoading = ref(false) +const containerLoadError = ref(null) +const selectedContainerBookingId = ref('') +let containerOptionRequest = 0 const statusTab = ref<'全部' | '待分配'>('全部') const filters = ref({ orderNo: '', orderType: '', warehouse: '', inboundCode: '', sourceOrderNo: '', containerNo: '', status: '', transferWarehouseCode: '', abnormal: '', detained: '', createdAt: [] }) const allFields: QueryField[] = [ @@ -60,9 +64,8 @@ const allFields: QueryField[] = [ { key: 'detained', type: 'select', label: '是否暂扣', placeholder: '全部', options: [{ label: '是', value: 'true' }, { label: '否', value: 'false' }] }, { key: 'createdAt', type: 'daterange', label: '创建时间' }, ] -const fields = computed(() => statusTab.value === '待分配' - ? allFields.filter((field) => ['containerNo', 'createdAt'].includes(field.key)) - : allFields) +const fields = computed(() => allFields) +const selectedContainer = computed(() => containerOptions.value.find((row) => row.sourceContainerBookingId === selectedContainerBookingId.value)) const columns: DataTableColumn[] = [ { key: 'actions', label: '操作', width: 110, fixed: 'left', slotName: 'actions' }, { key: 'selection', type: 'selection', width: 48, fixed: 'left' }, @@ -98,45 +101,10 @@ const columns: DataTableColumn[] = [ { key: 'customerRemark', prop: 'customerRemark', label: '备注(客户)', minWidth: 160 }, { key: 'warehouseRemark', prop: 'warehouseRemark', label: '备注(仓库)', minWidth: 160 }, ] -const containerColumns: DataTableColumn[] = [ - { key: 'actions', label: '操作', width: 190, fixed: 'left', slotName: 'actions' }, - { key: 'containerNo', prop: 'containerNo', label: '柜号', minWidth: 180 }, - { key: 'orderCount', prop: 'orderCount', label: '订单数', width: 90, align: 'right' }, - { key: 'inboundCodeCount', prop: 'inboundCodeCount', label: '入仓码数', width: 100, align: 'right' }, - { key: 'customerCount', prop: 'customerCount', label: '客户数', width: 90, align: 'right' }, - { key: 'totalPackageCount', prop: 'totalPackageCount', label: '总箱数', width: 100, align: 'right' }, - { key: 'totalPalletCount', prop: 'totalPalletCount', label: '总托数', width: 100, align: 'right' }, - { key: 'totalVolumeCbm', prop: 'totalVolumeCbm', label: '总体积(m³)', width: 120, align: 'right' }, - { key: 'eligibility', prop: 'eligibility', label: '资格', minWidth: 160, slotName: 'eligibility' }, - { key: 'planNotice', label: '生成说明', minWidth: 320, slotName: 'planNotice' }, -] - async function load(model: QueryModel = filters.value) { Object.assign(filters.value, model) loading.value = true loadError.value = null - if (statusTab.value === '待分配') { - rows.value = [] - const dateRange = Array.isArray(filters.value.createdAt) ? filters.value.createdAt.map(String) : [] - try { - const result = await listPendingContainerSummaries({ - page: page.value, - size: pageSize.value, - containerNo: String(filters.value.containerNo || '').trim() || undefined, - createdFrom: dateRange[0] ? `${dateRange[0]}T00:00:00` : undefined, - createdTo: dateRange[1] ? `${dateRange[1]}T23:59:59` : undefined, - }) - containerRows.value = result.records - containerTotal.value = result.total - } catch (error) { - containerRows.value = [] - containerTotal.value = 0 - loadError.value = getApiErrorDetails(error, '柜级待分配摘要加载失败,请重试') - } finally { - loading.value = false - } - return - } try { const dateRange = Array.isArray(filters.value.createdAt) ? filters.value.createdAt.map(String) : [] const booleanValue = (value: unknown) => value === 'true' ? true : value === 'false' ? false : undefined @@ -149,7 +117,7 @@ async function load(model: QueryModel = filters.value) { inboundCode: String(filters.value.inboundCode || '').trim() || undefined, sourceOrderNo: String(filters.value.sourceOrderNo || '').trim() || undefined, containerNo: String(filters.value.containerNo || '').trim() || undefined, - currentStatus: String(filters.value.status || '').trim() || undefined, + currentStatus: statusTab.value === '待分配' ? 'PENDING_ASSIGNMENT' : String(filters.value.status || '').trim() || undefined, transferWarehouseCode: String(filters.value.transferWarehouseCode || '').trim() || undefined, abnormal: booleanValue(filters.value.abnormal), detained: booleanValue(filters.value.detained), @@ -171,7 +139,11 @@ async function load(model: QueryModel = filters.value) { function search(model: QueryModel) { page.value = 1; void load(model) } function reset(model: QueryModel) { page.value = 1; void load(model) } -function changeStatusTab() { selectedRows.value = []; page.value = 1; void load() } +function changeStatusTab() { + selectedRows.value = [] + page.value = 1 + void load() +} function onSelectionChange(selection: object[]) { selectedRows.value = selection as LastMileOrderListItem[] } function openTransfer() { if (!selectedRows.value.length) { ElMessage.warning('请至少选择 1 条尾程订单'); return } @@ -191,24 +163,69 @@ async function submitTransfer() { } catch (error) { actionError.value = getApiErrorDetails(error, '转仓失败,请重试') } finally { actionLoading.value = false } } -async function generateContainerInboundPlans(row: PendingContainerSummaryDto) { - if (!row.eligible) { - actionError.value = { message: row.ineligibleReason || '当前整柜不满足生成资格' } - return - } - if (!row.sourceContainerBookingId) { - actionError.value = { message: '所选整柜缺少稳定来源柜身份,无法安全生成入库计划' } +async function convertInboundPlan() { + if (selectedRows.value.length !== 1) { ElMessage.warning('生成入库计划时请选择 1 条尾程订单'); return } + const selected = selectedRows.value[0] + const customerId = typeof selected.customerId === 'string' ? selected.customerId.trim() : '' + if (!customerId) { + actionError.value = { message: '所选尾程订单缺少客户标识,无法安全生成入库计划' } return } actionLoading.value = true actionError.value = null try { - const plans = await convertLastMileContainerToInboundPlans({ sourceContainerBookingId: row.sourceContainerBookingId }) - if (plans.length !== row.orderCount) { - actionError.value = { message: `柜级接口返回 ${plans.length} 个计划,与所选整柜 ${row.orderCount} 个尾程订单不一致` } - return - } - ElMessage.success(`已为${row.orderCount}个尾程订单生成${plans.length}个入库计划`) + await convertLastMileOrderToInboundPlan({ orderId: String(selected.id), customerId }) + selectedRows.value = [] + ElMessage.success('入库计划生成成功') + await load() + } catch (error) { + actionError.value = getApiErrorDetails(error, '生成入库计划失败,请重试') + } finally { + actionLoading.value = false + } +} +async function loadContainerOptions(containerNo = '') { + const request = ++containerOptionRequest + containerOptionsLoading.value = true + containerLoadError.value = null + try { + const result = await listPendingContainerSummaries({ + page: 1, + size: 50, + containerNo: containerNo.trim() || undefined, + }) + if (request !== containerOptionRequest) return + containerOptions.value = result.records + } catch (error) { + if (request !== containerOptionRequest) return + containerOptions.value = [] + containerLoadError.value = getApiErrorDetails(error, '待分配柜列表加载失败,请重试') + } finally { + if (request === containerOptionRequest) containerOptionsLoading.value = false + } +} +function openContainerConversion() { + selectedContainerBookingId.value = '' + containerOptions.value = [] + containerLoadError.value = null + actionError.value = null + containerConvertOpen.value = true + void loadContainerOptions() +} +async function convertContainerInboundPlans() { + const container = selectedContainer.value + if (!container?.sourceContainerBookingId) { ElMessage.warning('请选择一个待分配柜'); return } + if (!container.eligible) { + actionError.value = { message: container.ineligibleReason || '所选柜当前不满足生成资格' } + return + } + actionLoading.value = true + actionError.value = null + try { + const plans = await convertLastMileContainerToInboundPlans({ sourceContainerBookingId: container.sourceContainerBookingId }) + containerConvertOpen.value = false + selectedContainerBookingId.value = '' + ElMessage.success(`已按柜生成 ${plans.length} 个独立入库计划`) await load() } catch (error) { actionError.value = getApiErrorDetails(error, '生成整柜入库计划失败,请重试') @@ -244,21 +261,8 @@ onMounted(() => { void load() }) -
- - - - - - - -
-
- +
+ @@ -272,10 +276,24 @@ onMounted(() => { void load() }) + +

将为所选柜内所有待分配尾程订单分别生成独立入库计划。

+ + + + + + + + + + + +