From ff25847b02c7f9c78998a496740660662edb3fa1 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Tue, 15 Sep 2026 11:54:19 +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 --- .../measurement/InboundMeasurementPanel.vue | 2 +- .../OceanBookingDetailView.release.test.ts | 11 +++- .../pages/OceanBookingDetailView.vue | 13 ++-- src/modules/overseas-warehouse/AGENTS.md | 6 +- .../api/customerVisits.test.ts | 8 +-- .../overseas-warehouse/api/customerVisits.ts | 8 --- .../overseas-warehouse/api/deliveryOrders.ts | 3 + .../overseas-warehouse/api/dispatches.test.ts | 2 +- .../overseas-warehouse/api/dispatches.ts | 3 +- .../overseas-warehouse/api/outbounds.test.ts | 2 +- .../overseas-warehouse/api/outbounds.ts | 2 +- .../DeliveryDocumentsDialog.test.ts | 4 +- .../components/DeliveryDocumentsDialog.vue | 3 +- .../pages/DeliveryOrderDetailView.test.ts | 3 +- .../pages/DeliveryOrderDetailView.vue | 9 +-- .../pages/DeliveryOrdersView.test.ts | 60 ++++++++----------- .../pages/DeliveryOrdersView.vue | 13 ++-- .../pages/OutboundDetailView.test.ts | 15 +++-- .../pages/OutboundDetailView.vue | 6 +- 19 files changed, 93 insertions(+), 80 deletions(-) diff --git a/src/modules/inbounds/measurement/InboundMeasurementPanel.vue b/src/modules/inbounds/measurement/InboundMeasurementPanel.vue index 06e3a55f..3b02e553 100644 --- a/src/modules/inbounds/measurement/InboundMeasurementPanel.vue +++ b/src/modules/inbounds/measurement/InboundMeasurementPanel.vue @@ -518,7 +518,7 @@ async function submitForReview() { } try { await ElMessageBox.confirm( - '提交后将校验测量图片和全部称重结果,并由后端准备托盘与库位占用。提交成功后本批次不能继续编辑,设备也不能再覆盖已提交数据。', + '提交成功后本批次不能继续编辑', '提交测量审核', { customClass: 'admin-density-confirm', type: 'warning', confirmButtonText: '确认提交', cancelButtonText: '取消' }, ) diff --git a/src/modules/ocean-bookings/pages/OceanBookingDetailView.release.test.ts b/src/modules/ocean-bookings/pages/OceanBookingDetailView.release.test.ts index fa788e53..8fd9fb34 100644 --- a/src/modules/ocean-bookings/pages/OceanBookingDetailView.release.test.ts +++ b/src/modules/ocean-bookings/pages/OceanBookingDetailView.release.test.ts @@ -37,7 +37,7 @@ function booking(status: OceanBookingStatus = 'DRAFT', version = 3, billOfLading const resource = { id: '123', bookingNo: 'OB-123', status, version, customerId: '456', customerName: '测试客户', billOfLadingNo, shipperName: '发货人', consigneeName: '收货人', containerCount: 2, - channelCode: 'SEA_FCL', channelName: '海运整箱', warehouseId: '8', warehouseName: '义乌仓', + channelCode: 'MATSON_CLX', channelName: '美森正班', warehouseId: '8', warehouseName: '义乌仓', shippingCompanyId: '10', shippingCompanyName: '测试船公司', carrierCode: 'MATSON' as const, bookingAgentId: '20', bookingAgentName: '订舱代理', vesselName: 'TEST VESSEL', voyageNo: 'V001', departurePortCode: 'CNSHA', departurePortName: '上海', destinationPortCode: 'USLAX', destinationPortName: '洛杉矶', @@ -97,10 +97,17 @@ describe('OceanBookingDetailView one-click release', () => { vi.mocked(listPlatformTenants).mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }) vi.mocked(listPlatformPartners).mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }) vi.mocked(listPlatformWarehouses).mockResolvedValue({ records: [], page: 1, size: 200, total: 0, pages: 0 }) - vi.mocked(listWarehouseLogisticsChannels).mockResolvedValue([{ channelCode: 'SEA_FCL', channelName: '海运整箱' }]) + vi.mocked(listWarehouseLogisticsChannels).mockResolvedValue([{ channelCode: 'MATSON_CLX_TRUCK', channelName: '美森正班海卡' }]) vi.mocked(listOceanBookingAgents).mockResolvedValue([{ supplierId: '20', name: '订舱代理', carrierCodes: ['MATSON'] }]) }) + it('shows the matching fixed logistics group instead of the underlying price-book channel', async () => { + const { wrapper } = await mountView() + expect(wrapper.text()).toContain('美森正班') + expect(wrapper.text()).not.toContain('美森正班海卡') + wrapper.unmount() + }) + it('saves dirty form data and releases with the returned latest version', async () => { vi.mocked(updateOceanBookingDraft).mockResolvedValue(booking('DRAFT', 4, 'NEW-BL')) vi.mocked(releaseOceanBooking).mockResolvedValue(booking('RELEASED', 5, 'NEW-BL')) diff --git a/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue b/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue index 49cefbdb..343c3c07 100644 --- a/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue +++ b/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue @@ -20,6 +20,7 @@ import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam' import { listPlatformPartners, type PlatformPartnerResource } from '@/modules/master-data' import { listPlatformWarehouses, type PlatformWarehouseResource } from '@/modules/warehouses' import { CarrierScheduleQueryPanel } from '@/modules/port-schedules' +import { SMART_LOADING_CHANNEL_GROUPS } from '@/modules/container-loading/smartLoadingChannelGroups' import { createOceanBooking, deleteOceanBookingDraft, @@ -87,7 +88,11 @@ const selectableCustomers = computed(() => customers.value.filter((customer) => customer.status === 'ACTIVE' || String(customer.id) === String(form.data.customerId) ))) const shippingCompanies = computed(() => partners.value.filter((partner) => partner.types.includes('CARRIER'))) -const channelUnavailable = computed(() => Boolean(form.data.warehouseId) && !channelsLoading.value && !channelsError.value && channels.value.length === 0) +const channelGroups = computed(() => { + const activeCodes = new Set(channels.value.map((channel) => channel.channelCode)) + return SMART_LOADING_CHANNEL_GROUPS.filter((group) => group.channelCodes.some((code) => activeCodes.has(code))) +}) +const channelUnavailable = computed(() => Boolean(form.data.warehouseId) && !channelsLoading.value && !channelsError.value && channelGroups.value.length === 0) function blankParties(): OceanBookingParty[] { return partyTypes.map(({ value }) => ({ partyType: value, partyName: '', contactName: '', contactPhone: '', address: '' })) @@ -187,9 +192,9 @@ async function loadChannels(warehouseId: string) { } } function selectChannel(channelCode: string) { - const channel = channels.value.find((item) => item.channelCode === channelCode) + const channel = SMART_LOADING_CHANNEL_GROUPS.find((item) => item.code === channelCode) form.data.channelCode = channelCode - selectedChannelName.value = channel?.channelName || '' + selectedChannelName.value = channel?.name || '' } function clearBookingAgent() { form.data.bookingAgentId = null @@ -397,7 +402,7 @@ onMounted(() => { void load(); void loadOptions() }) - {{ channel.channelName }}{{ channel.channelCode }}
{{ channelsError }}重新加载
该仓库暂无有效物流渠道,请更换仓库或联系管理员。
+
{{ channelsError }}重新加载
该仓库暂无有效物流渠道,请更换仓库或联系管理员。
重新加载
diff --git a/src/modules/overseas-warehouse/AGENTS.md b/src/modules/overseas-warehouse/AGENTS.md index 83e1e019..303d0e11 100644 --- a/src/modules/overseas-warehouse/AGENTS.md +++ b/src/modules/overseas-warehouse/AGENTS.md @@ -20,15 +20,15 @@ | 仓储转运审核、通过与驳回 | `pages/LastMileOrdersView.vue`、`components/WarehouseTransferReviewDialog.vue` | `api/warehouseTransfers.ts` 的列表、详情及 `/warehouse-transfers/{transferNo}/review`;审核按钮要求 `permissions.ts` 的 `warehouseTransferApprove` 且行类型为 `WAREHOUSE_TRANSFER`、状态为 `PENDING_REVIEW`。请求携带该行客户范围和转运单号,审核结论为 `APPROVE/REJECT`,驳回原因必填;这是尾程订单内的审核弹框,不是独立审核路由或排车转派送入口 | | 尾程订单控制与柜级入仓计划转换 | `pages/LastMileOrdersView.vue`、`pages/LastMileOrderDetailView.vue` | 柜子工作台按 `sourceContainerBookingId` 消费 `/orders/container-summaries`;服务端分页返回全部稳定来源柜,计划已经齐全的柜仍保留用于接柜仓查询和修改,并按冻结 `plannedArrivalAt` 升序、空值最后排序。详情返回全柜订单且不受列表分页截断;单票手工计划保存后,由服务端在整柜最后一票计划齐全时自动生成并绑定全柜托盘码,一票柜立即生成;`resourcePendingPlanCount` 只表示自动生成异常的重试入口,不是正常人工步骤。柜级确认弹框先调用 `/inbound-plans/container-preview`,按服务端 FBA/一件代发/私人地址分组展示头程已有、海外仓补充和最终托数以及 `readiness/blockers`;散货最终托数不得少于头程已有托盘码,差额连续补码且不阻断流程;一件代发必须严格按不可变头程封托托盘数整体入库,单票表单托数锁定且服务端再次失败关闭校验。前端不计算托数、不逐票建计划、不预占托盘,再把同一 `snapshotVersion/snapshotHash` 原样提交 `/inbound-plans/convert-container`,快照冲突后重新获取预览。服务端只为剩余订单逐票生成独立计划,并为异常遗留的人工计划补齐真实托盘,整批原子且重放不得产生第二批资源。单票完整入库计划、暂扣/解除和八行人工节点均从 `api/lastMileOrders.ts` 提交订单版本与幂等键;入仓码只读展示并提交订单冻结值,不允许人工改写,暂扣资格、接柜仓锁定原因和操作能力只消费服务端结果,人工节点一经保存不得回退柜级节点。转仓候选只取 `../iam/api/platformIam.ts` 的平台公共 `ACTIVE + FBA` 仓主档;Backend `lastleg-service/` | | 入仓单、入仓计划、FBA 预约 | `pages/InboundOrdersView.vue`、`pages/InboundOrderDetailView.vue` | 列表/预约 `api/inboundOrders.ts`,计划列表/详情 `api/inboundPlans.ts`;Backend `lastleg-service/inboundplan/` | -| 排车查询、智能预排与转派送 | `pages/DispatchesView.vue`、`pages/DispatchDetailView.vue`、`components/SmartDispatchDialog.vue` | `api/dispatches.ts`;Backend `lastleg-service/dispatch/`、`delivery/`;覆盖详情、候选、草稿柜、托盘锁、`pre-arrange` 和 `transfer-delivery`。页面能力必须由正式路由、API 调用和测试共同证明 | -| 海外出库与派送 | `pages/OutboundsView.vue`、`pages/OutboundDetailView.vue`、`pages/DeliveryOrdersView.vue`、`pages/DeliveryOrderDetailView.vue` | `api/outbounds.ts`、`api/deliveryOrders.ts`;出库列表按类型筛选并展示中文类型。一件代发新建弹窗直接跨客户加载服务端可用候选,不单独展示客户筛选,海外仓必选并优先默认洛杉矶仓;订单条件可按订单号、入库单号、客户、托盘号、SKU 或品名查询/重置,以托盘+SKU 行分页多选。提交时从所选行解析唯一客户,并校验所选行属于当前唯一海外仓,不要求先选客户。退货出库客户下拉同时展示客户名称与联系电话并支持手机号检索,客户和仓库选项独立容错加载;选择客户后须立即按当前默认/已选海外仓加载库存。SKU 与箱号查询均使用输入态/已查询态分离,只允许点击查询按钮后生效;支持英文逗号批量条件,查询时清除空格并按任一条件命中,左右列表分别提供每页 5/10/20/50 条选择。SKU 列表只展示 `availableQuantity > 0` 的未占用可用数量,合箱后已扣减为零的 SKU 不再作为可选库存显示,实际作业以可用箱唛选择和箱内 SKU 明细扣减为准。取消、派送列表/详情及退回入口对应 Backend `lastleg-service/delivery/`、`dropship/` | +| 排车查询、智能预排与转派送 | `pages/DispatchesView.vue`、`pages/DispatchDetailView.vue`、`components/SmartDispatchDialog.vue` | `api/dispatches.ts`;Backend `lastleg-service/dispatch/`、`delivery/`;覆盖详情、候选、草稿柜、托盘锁、`pre-arrange` 和 `transfer-delivery`。一次转派送仍生成一张车辆级出库单,响应的 `deliveries` 按目的 FBA 仓返回多张派送单。页面能力必须由正式路由、API 调用和测试共同证明 | +| 海外出库与派送 | `pages/OutboundsView.vue`、`pages/OutboundDetailView.vue`、`pages/DeliveryOrdersView.vue`、`pages/DeliveryOrderDetailView.vue` | `api/outbounds.ts`、`api/deliveryOrders.ts`;出库详情使用 `deliveries` 展示按目的 FBA 仓拆出的全部派送单,派送列表和详情必须显示目的 FBA 仓。人工状态入口不提供完成派送;签收资料保存成功后提示自动完成并刷新列表。出库列表按类型筛选并展示中文类型。一件代发新建弹窗直接跨客户加载服务端可用候选,不单独展示客户筛选,海外仓必选并优先默认洛杉矶仓;订单条件可按订单号、入库单号、客户、托盘号、SKU 或品名查询/重置,以托盘+SKU 行分页多选。提交时从所选行解析唯一客户,并校验所选行属于当前唯一海外仓,不要求先选客户。退货出库客户下拉同时展示客户名称与联系电话并支持手机号检索,客户和仓库选项独立容错加载;选择客户后须立即按当前默认/已选海外仓加载库存。SKU 与箱号查询均使用输入态/已查询态分离,只允许点击查询按钮后生效;支持英文逗号批量条件,查询时清除空格并按任一条件命中,左右列表分别提供每页 5/10/20/50 条选择。SKU 列表只展示 `availableQuantity > 0` 的未占用可用数量,合箱后已扣减为零的 SKU 不再作为可选库存显示,实际作业以可用箱唛选择和箱内 SKU 明细扣减为准。取消、派送列表/详情及退回入口对应 Backend `lastleg-service/delivery/`、`dropship/` | | 异常退回与客户回访 | `pages/ExceptionOrdersView.vue`、`pages/CustomerVisitsView.vue` | `api/exceptionOrders.ts`、`api/customerVisits.ts`;异常关系解析、异常建档、派送退回、完成/状态和回访附件对应 Backend `lastleg-service/exceptionorder/`、`customervisit/` | | 库存余额、库位与流水 | `pages/InventoryView.vue`、`pages/InventoryDetailView.vue`、`pages/InventoryTransactionsView.vue` | `api/inventory.ts`;业务库存归 `lastleg-service/inventory/`,实物资源归 `warehouse-overseas-service/` | | 托盘、箱唛、产品与换标 | `pages/PalletsView.vue`、`pages/CartonsView.vue`、`pages/ProductsView.vue`、`pages/RelabelOrdersView.vue` 及详情页 | `api/pallets.ts`、`api/relabelOrders.ts`;换标详情展示 PDA 完工照片与复核人,状态区分待客户确认和待出库;路由是否公开以 `routes.ts` 为准 | ## 履约硬边界 -- API 为 `api/dispatches.ts`、`api/outbounds.ts`、`api/deliveryOrders.ts`;Backend `lastleg-service/dispatch/`、`delivery/`、`dropship/`。一件代发独立出库,从服务端库存位置选择并携带幂等键;普通出库及换标单经头程排车转出库,按托盘快照绑定头程箱唛,PDA 按托盘作业,详情展示箱唛与已拣数量;排车转派送按稳定 `dispatchNo` 进入服务端命令,可在 `PENDING_DEPARTURE`、`PICKING`、`READY_TO_DEPART` 状态执行并须在 PDA 发车前生成待出库单 +- API 为 `api/dispatches.ts`、`api/outbounds.ts`、`api/deliveryOrders.ts`;Backend `lastleg-service/dispatch/`、`delivery/`、`dropship/`。一件代发独立出库,从服务端库存位置选择并携带幂等键;普通出库及换标单经头程排车转出库,按托盘快照绑定头程箱唛,PDA 按托盘作业,详情展示箱唛与已拣数量;排车转派送按稳定 `dispatchNo` 进入服务端命令,可在 `PENDING_DEPARTURE`、`PICKING`、`READY_TO_DEPART` 状态执行并须在 PDA 发车前生成一张待出库单及按目的 FBA 仓拆分的派送单 - API 为 `api/exceptionOrders.ts`、`api/customerVisits.ts`;Backend `lastleg-service/exceptionorder/`、`customervisit/`。异常处理只消费服务端 `allowedActions/version`;完成时至少上传一份异常证据。派送退回先从服务端原出库拣货事实选择入仓单和可退箱数,不允许自由填写 SKU/数量;退回海外仓时必须选择服务端校验的目标托盘和库位,库存恢复、异常完成与派送 `RETURNED` 由服务端原子推进。回访附件和派送完成/状态均走海外真实 API ## 验证 diff --git a/src/modules/overseas-warehouse/api/customerVisits.test.ts b/src/modules/overseas-warehouse/api/customerVisits.test.ts index 389a5bf2..dd45827e 100644 --- a/src/modules/overseas-warehouse/api/customerVisits.test.ts +++ b/src/modules/overseas-warehouse/api/customerVisits.test.ts @@ -27,10 +27,10 @@ describe('customer visit API', () => { expect(overseasHttp.get).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/customer-visits/9', { params: { customerId: '100' } }) }) - it('updates delivery status so selecting delivered can generate a visit', async () => { - vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', data: { deliveryStatus: 'DELIVERED' } }) - await updateDeliveryOrderStatus('DEL/1', '100', { status: 'DELIVERED', expectedVersion: 7, remark: '完成' }) - expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DEL%2F1/status', { status: 'DELIVERED', expectedVersion: 7, remark: '完成' }, { params: { customerId: '100' } }) + it('updates a delivery to an operator-managed nonterminal status', async () => { + vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', data: { deliveryStatus: 'DELIVERING' } }) + await updateDeliveryOrderStatus('DEL/1', '100', { status: 'DELIVERING', expectedVersion: 7, remark: '开始派送' }) + expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/last-leg/delivery-orders/DEL%2F1/status', { status: 'DELIVERING', expectedVersion: 7, remark: '开始派送' }, { params: { customerId: '100' } }) }) it('uploads and saves repeatable visit material', async () => { diff --git a/src/modules/overseas-warehouse/api/customerVisits.ts b/src/modules/overseas-warehouse/api/customerVisits.ts index 5e8674ff..df84a332 100644 --- a/src/modules/overseas-warehouse/api/customerVisits.ts +++ b/src/modules/overseas-warehouse/api/customerVisits.ts @@ -99,14 +99,6 @@ export async function uploadCustomerVisitAttachment(visitId: string, customerId: return unwrapApiResponse(response) } -export async function completeDeliveryOrder(deliveryNo: string, customerId: string) { - const response = await overseasHttp.post, ApiEnvelope<{ visitId: string; deliveryNo: string; deliveryStatus: string; visitStatus: string; replayed: boolean }>>( - `/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}/complete`, undefined, - { params: { customerId } }, - ) - return unwrapApiResponse(response) -} - export async function updateDeliveryOrderStatus(deliveryNo: string, customerId: string, payload: { status: string; expectedVersion: number; remark?: string }) { const response = await overseasHttp.put, ApiEnvelope<{ visitId?: string | null; deliveryNo: string; deliveryStatus: string; visitStatus?: string | null; replayed: boolean }>>( `/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}/status`, payload, diff --git a/src/modules/overseas-warehouse/api/deliveryOrders.ts b/src/modules/overseas-warehouse/api/deliveryOrders.ts index 9aa54e71..02a8eaa5 100644 --- a/src/modules/overseas-warehouse/api/deliveryOrders.ts +++ b/src/modules/overseas-warehouse/api/deliveryOrders.ts @@ -30,6 +30,9 @@ export interface DeliveryOrderRow { deliveryNo: string outboundNo: string dispatchNo: string + destinationWarehouseId?: string | null + destinationWarehouseCode?: string | null + destinationWarehouseName?: string | null overseasWarehouseId?: string | null overseasWarehouseName?: string | null status: string diff --git a/src/modules/overseas-warehouse/api/dispatches.test.ts b/src/modules/overseas-warehouse/api/dispatches.test.ts index 67500466..e822020d 100644 --- a/src/modules/overseas-warehouse/api/dispatches.test.ts +++ b/src/modules/overseas-warehouse/api/dispatches.test.ts @@ -86,7 +86,7 @@ describe('dispatch overseas API', () => { }) it('transfers to delivery only by dispatchNo without customerId', async () => { - vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', message: 'success', data: { dispatchNo: 'PC/1', outboundNo: 'OUT-1', deliveryNo: 'DEL-1' } }) + vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', message: 'success', data: { dispatchNo: 'PC/1', outboundNo: 'OUT-1', outboundStatus: 'PICKED', palletCount: 2, packageCount: 20, deliveries: [{ deliveryNo: 'DEL-1', deliveryStatus: 'WAIT_DELIVERY', destinationWarehouseCode: 'ACY2', palletCount: 2, packageCount: 20 }], replayed: false } }) const payload = { idempotencyKey: 'transfer-1', plannedDeliveryAt: '2026-09-01T10:00:00', deliveryMethod: 'TRUCK_PALLET', pickupMethod: 'WAREHOUSE_PICKUP', loadingType: 'PALLET', fees: [{ feeType: 'DELIVERY_FEE', feeName: '派送费用', direction: 'RECEIVABLE' as const, currency: 'USD' as const, amount: 10 }] } await transferDispatchToDelivery('PC/1', payload) expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/PC%2F1/transfer-delivery', payload) diff --git a/src/modules/overseas-warehouse/api/dispatches.ts b/src/modules/overseas-warehouse/api/dispatches.ts index 8411d55c..0b4cc59d 100644 --- a/src/modules/overseas-warehouse/api/dispatches.ts +++ b/src/modules/overseas-warehouse/api/dispatches.ts @@ -225,7 +225,8 @@ export async function releaseDispatchDraft(draftToken: string) { return unwrapApiResponse(response) } -export interface TransferDeliveryResult { dispatchNo: string; outboundNo: string; outboundStatus: string; deliveryNo: string; deliveryStatus: string; palletCount: number; packageCount: number; replayed: boolean } +export interface TransferDeliveryItem { deliveryNo: string; deliveryStatus: string; destinationWarehouseId?: string | null; destinationWarehouseCode?: string | null; destinationWarehouseName?: string | null; palletCount: number; packageCount: number } +export interface TransferDeliveryResult { dispatchNo: string; outboundNo: string; outboundStatus: string; palletCount: number; packageCount: number; deliveries: TransferDeliveryItem[]; replayed: boolean } export interface TransferDeliveryPayload { idempotencyKey: string plannedDeliveryAt: string diff --git a/src/modules/overseas-warehouse/api/outbounds.test.ts b/src/modules/overseas-warehouse/api/outbounds.test.ts index eca9bdf8..5255dc4f 100644 --- a/src/modules/overseas-warehouse/api/outbounds.test.ts +++ b/src/modules/overseas-warehouse/api/outbounds.test.ts @@ -14,7 +14,7 @@ describe('outbound overseas API', () => { }) it('loads detail and cancels only by outboundNo without customerId', async () => { - vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: { outbound: { id: '1', outboundNo: 'OUT/1', status: 'PICKED' }, delivery: null } }) + vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: { outbound: { id: '1', outboundNo: 'OUT/1', status: 'PICKED' }, deliveries: [] } }) vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', data: null }) await getOutbound('OUT/1') await cancelOutbound('OUT/1', '运营取消') diff --git a/src/modules/overseas-warehouse/api/outbounds.ts b/src/modules/overseas-warehouse/api/outbounds.ts index 484e495e..e8cca936 100644 --- a/src/modules/overseas-warehouse/api/outbounds.ts +++ b/src/modules/overseas-warehouse/api/outbounds.ts @@ -16,7 +16,7 @@ export interface OutboundPalletRow { pickedPackageCount?: number | null status?: string | null } -export interface OutboundDetail { outbound: OutboundRow; pallets?: OutboundPalletRow[]; delivery?: DeliveryOrderRow | null } +export interface OutboundDetail { outbound: OutboundRow; pallets?: OutboundPalletRow[]; deliveries?: DeliveryOrderRow[] } export interface OutboundQuery { page: number; size: number; documentNo?: string; lxCode?: string; warehouseId?: string; status?: string; outboundType?: string } export interface ReturnOutboundCandidate { returnNo?: string | null; overseasWarehouseId?: string | null; cartonMark?: string | null; fbaCartonMark?: string | null; destinationWarehouseCode?: string | null; destinationWarehouseName?: string | null; totalQuantity?: number | null; status?: string | null } export interface OneClickOutboundCandidate { inventoryPositionId: string; orderNo?: string | null; customerId?: string | null; customerName?: string | null; overseasWarehouseId?: string | null; overseasWarehouseName?: string | null; palletNo?: string | null; inboundCode?: string | null; skuCode?: string | null; productName?: string | null; availablePackages: number } diff --git a/src/modules/overseas-warehouse/components/DeliveryDocumentsDialog.test.ts b/src/modules/overseas-warehouse/components/DeliveryDocumentsDialog.test.ts index 29b92160..17b97822 100644 --- a/src/modules/overseas-warehouse/components/DeliveryDocumentsDialog.test.ts +++ b/src/modules/overseas-warehouse/components/DeliveryDocumentsDialog.test.ts @@ -1,4 +1,4 @@ -import ElementPlus from 'element-plus' +import ElementPlus, { ElMessage } from 'element-plus' import { defineComponent, h } from 'vue' import { DOMWrapper, flushPromises, mount } from '@vue/test-utils' import { beforeEach, describe, expect, it, vi } from 'vitest' @@ -51,6 +51,7 @@ describe('delivery document drafts', () => { wrapper.unmount() }) it('uses real upload controls to add, replace, delete and reload the saved three-category snapshot', async () => { + const success = vi.spyOn(ElMessage, 'success') const receipt = { ...originalFile, fileId: 'receipt-1', fileName: 'pod.pdf' } const courier = { ...originalFile, fileId: 'receipt-2', fileName: 'courier.pdf' } const replacement = { ...originalFile, fileId: 'bol-2', fileName: 'signed-bol.pdf' } @@ -72,6 +73,7 @@ describe('delivery document drafts', () => { await dialog.findAll('button').find(button => button.text() === '保存资料')!.trigger('click'); await flushPromises() expect(saveDeliveryDocuments).toHaveBeenCalledWith('DL/1', '100', { expectedVersion: 9, idempotencyKey: expect.any(String), bolNo: 'BOL-1', documents: [{ category: 'LOADING_PHOTO', fileIds: [] }, { category: 'BOL', fileIds: ['bol-2'] }, { category: 'RECEIPT', fileIds: ['receipt-2'] }] }) expect(wrapper.emitted('saved')).toHaveLength(1) + expect(success).toHaveBeenCalledWith('签收文件已保存,派送单已自动完成') const persisted = response() persisted.version = 10; persisted.uploadedCategoryCount = 2 persisted.categories = persisted.categories.map(group => ({ ...group, files: group.category === 'BOL' ? [replacement] : group.category === 'RECEIPT' ? [courier] : [], fileCount: group.category === 'LOADING_PHOTO' ? 0 : 1, status: group.category === 'LOADING_PHOTO' ? 'MISSING' : 'UPLOADED' })) diff --git a/src/modules/overseas-warehouse/components/DeliveryDocumentsDialog.vue b/src/modules/overseas-warehouse/components/DeliveryDocumentsDialog.vue index 314d537a..2829ace7 100644 --- a/src/modules/overseas-warehouse/components/DeliveryDocumentsDialog.vue +++ b/src/modules/overseas-warehouse/components/DeliveryDocumentsDialog.vue @@ -97,7 +97,8 @@ async function save() { try { await saveDeliveryDocuments(deliveryNo, customerId, { ...body, idempotencyKey: attempt.key }) if (!isCurrent(current, deliveryNo, customerId)) return - open.value = false; emit('saved'); ElMessage.success('派送资料已保存') + const hasReceipt = body.documents.some(item => item.category === 'RECEIPT' && item.fileIds.length > 0) + open.value = false; emit('saved'); ElMessage.success(hasReceipt ? '签收文件已保存,派送单已自动完成' : '派送资料已保存') } catch (cause) { if (isCurrent(current, deliveryNo, customerId)) { error.value = getApiErrorDetails(cause, '派送资料保存失败'); conflict.value = error.value.code === 'A0409' } } finally { if (current === sequence) saving.value = false } } diff --git a/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.test.ts b/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.test.ts index 615019d0..2811b2a6 100644 --- a/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.test.ts +++ b/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.test.ts @@ -15,7 +15,7 @@ describe('DeliveryOrderDetailView', () => { order: { version: 3, allowedActions: ['COMPLETE_DELIVERY'], id: '1', customerId: '100', deliveryNo: 'DEL/1', outboundNo: 'OUT-1', dispatchNo: 'PC-1', - status: 'DELIVERING', overseasWarehouseName: '洛杉矶仓', palletCount: 1, packageCount: 2, + status: 'DELIVERING', overseasWarehouseName: '洛杉矶仓', destinationWarehouseCode: 'ACY2', palletCount: 1, packageCount: 2, createdAt: '2026-09-01T10:00:00', }, skus: [], fees: [ @@ -32,6 +32,7 @@ describe('DeliveryOrderDetailView', () => { expect(wrapper.text()).toContain('DEL/1') expect(wrapper.text()).toContain('派送中') expect(wrapper.text()).toContain('洛杉矶仓') + expect(wrapper.text()).toContain('ACY2') await wrapper.find('.el-tabs__item:nth-child(3)').trigger('click') expect(wrapper.text()).toContain('私人仓尾程派送费') expect(wrapper.text()).toContain('派送费用') diff --git a/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.vue b/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.vue index 963a8abf..07468871 100644 --- a/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.vue +++ b/src/modules/overseas-warehouse/pages/DeliveryOrderDetailView.vue @@ -68,10 +68,11 @@ watch([deliveryNo, customerId], () => { void load() }, { immediate: true })
派送单号
{{ show(detail.order.deliveryNo) }}
状态
{{ statusLabels[detail.order.status] || detail.order.status }}
-
仓库
{{ show(detail.order.overseasWarehouseName) }}
运单号
{{ show(detail.order.waybillNo) }}
-
派送人
{{ show(detail.order.carrierName) }}
派送时间
{{ formatOverseasDateTime(detail.order.dispatchedAt || detail.order.plannedDeliveryAt) }}
-
提货方式
{{ pickupLabels[detail.order.pickupMethod || ''] || show(detail.order.pickupMethod) }}
装车类型
{{ loadingLabels[detail.order.loadingType || ''] || show(detail.order.loadingType) }}
-
派送方式
{{ methodLabels[detail.order.deliveryMethod || ''] || show(detail.order.deliveryMethod) }}
司机/车队
{{ show(detail.order.driverName) }}
+
仓库
{{ show(detail.order.overseasWarehouseName) }}
目的 FBA 仓
{{ show(detail.order.destinationWarehouseCode || detail.order.destinationWarehouseName) }}
+
运单号
{{ show(detail.order.waybillNo) }}
派送人
{{ show(detail.order.carrierName) }}
+
派送时间
{{ formatOverseasDateTime(detail.order.dispatchedAt || detail.order.plannedDeliveryAt) }}
提货方式
{{ pickupLabels[detail.order.pickupMethod || ''] || show(detail.order.pickupMethod) }}
+
装车类型
{{ loadingLabels[detail.order.loadingType || ''] || show(detail.order.loadingType) }}
派送方式
{{ methodLabels[detail.order.deliveryMethod || ''] || show(detail.order.deliveryMethod) }}
+
司机/车队
{{ show(detail.order.driverName) }}
车牌号
{{ show(detail.order.vehiclePlate) }}
总箱数
{{ quantity(detail.order.packageCount, '箱') }}
总重量
{{ quantity(detail.order.totalWeightKg, 'kg') }}
总体积
{{ quantity(detail.order.totalVolumeCbm, 'm³') }}
预计到达时间
{{ formatOverseasDateTime(detail.order.expectedArrivalAt) }}
客户确认
{{ customerConfirmation() }}
diff --git a/src/modules/overseas-warehouse/pages/DeliveryOrdersView.test.ts b/src/modules/overseas-warehouse/pages/DeliveryOrdersView.test.ts index d71e4e3e..df6205b1 100644 --- a/src/modules/overseas-warehouse/pages/DeliveryOrdersView.test.ts +++ b/src/modules/overseas-warehouse/pages/DeliveryOrdersView.test.ts @@ -1,6 +1,6 @@ import ElementPlus from 'element-plus' import { defineComponent, h, type PropType } from 'vue' -import { DOMWrapper, flushPromises, mount } from '@vue/test-utils' +import { flushPromises, mount } from '@vue/test-utils' import { beforeEach, describe, expect, it, vi } from 'vitest' import { getDeliveryOrder, listDeliveryOrders, listDeliveryReturnInboundOptions, markDeliveryReturn } from '../api/deliveryOrders' import { updateDeliveryOrderStatus } from '../api/customerVisits' @@ -54,10 +54,10 @@ describe('DeliveryOrdersView permissions and commands', () => { await wrapper.findAll('button').find(button => button.text().includes('编辑状态'))!.trigger('click') await flushPromises() - await wrapper.find('[data-dialog="编辑派送状态"] select').setValue('DELIVERED') + await wrapper.find('[data-dialog="编辑派送状态"] select').setValue('DELIVERING') await wrapper.find('[data-dialog="编辑派送状态"] .dialog-confirm').trigger('click') await flushPromises() - expect(updateDeliveryOrderStatus).toHaveBeenCalledWith('DEL/1', '100', { status: 'DELIVERED', expectedVersion: 7, remark: undefined }) + expect(updateDeliveryOrderStatus).toHaveBeenCalledWith('DEL/1', '100', { status: 'DELIVERING', expectedVersion: 7, remark: undefined }) await wrapper.findAll('button').find(button => button.text().includes('标记退货'))!.trigger('click') await flushPromises() @@ -86,7 +86,7 @@ describe('DeliveryOrdersView permissions and commands', () => { wrapper.unmount() }) - it('maps delivering separately from completion and blocks duplicate or conflicted submission', async () => { + it('excludes manual completion and blocks duplicate or conflicted status submission', async () => { canPermission.mockReturnValue(true) const wrapper = mountView() await flushPromises() @@ -94,7 +94,7 @@ describe('DeliveryOrdersView permissions and commands', () => { await flushPromises() const select = wrapper.find('[data-dialog="编辑派送状态"] select') expect(select.find('option[value="DELIVERING"]').text()).toBe('派送中') - expect(select.find('option[value="DELIVERED"]').text()).toBe('派送完成') + expect(select.find('option[value="DELIVERED"]').exists()).toBe(false) expect(select.find('option[value="WAIT_DELIVERY"]').exists()).toBe(false) await select.setValue('DELIVERING') await wrapper.find('[data-dialog="编辑派送状态"] textarea').setValue('123') @@ -114,7 +114,7 @@ describe('DeliveryOrdersView permissions and commands', () => { expect(updateDeliveryOrderStatus).toHaveBeenCalledTimes(1) vi.mocked(getDeliveryOrder).mockResolvedValue({ order: { ...row, version: 8, allowedActions: ['COMPLETE_DELIVERY'] }, skus: [], fees: [], operations: [] }) await state.refreshStatus(); await flushPromises() - expect(select.findAll('option').map(option => option.attributes('value'))).toEqual(['DELIVERED']) + expect(select.findAll('option')).toHaveLength(0) wrapper.unmount() }) @@ -192,37 +192,27 @@ describe('DeliveryOrdersView permissions and commands', () => { wrapper.unmount() }) - it('selects completion through the real dialog and select, then retries a conflict with the refreshed version', async () => { + it('hides status editing when completion is the only server action', async () => { canPermission.mockImplementation(code => code.endsWith(':status')) vi.mocked(getDeliveryOrder).mockResolvedValue({ order: { ...row, version: 7, status: 'DELIVERING', allowedActions: ['COMPLETE_DELIVERY'] }, skus: [], fees: [], operations: [] }) - vi.mocked(updateDeliveryOrderStatus).mockRejectedValueOnce({ isAxiosError: true, response: { status: 409, data: { code: 'A0409', message: '版本冲突' } } }) - const wrapper = mount(DeliveryOrdersView, { attachTo: document.body, global: { plugins: [ElementPlus], stubs: { ...commonPhase5Stubs, AppDialog: false, ElInput: false, ElSelect: false, ElOption: false } } }) - const body = new DOMWrapper(document.body) - try { - await flushPromises() - await wrapper.findAll('button').find(button => button.text().includes('编辑状态'))!.trigger('click') - await flushPromises() - const dialog = body.find('[role="dialog"][aria-label="编辑派送状态"]') - const save = () => dialog.findAll('button').find(button => button.text() === '保存状态')! - expect(save().attributes('disabled')).toBeDefined() - await dialog.find('.el-select__wrapper').trigger('click') - await flushPromises() - const options = body.findAll('.el-select-dropdown__item') - expect(options.map(option => option.text())).toEqual(['派送完成']) - await options[0]!.trigger('click') - await dialog.find('textarea').setValue('已交付') - await save().trigger('click'); await flushPromises() - expect(updateDeliveryOrderStatus).toHaveBeenLastCalledWith('DEL/1', '100', { status: 'DELIVERED', expectedVersion: 7, remark: '已交付' }) - expect(save().attributes('disabled')).toBeDefined() - expect(dialog.text()).toContain('请刷新最新状态并重新确认') - vi.mocked(getDeliveryOrder).mockResolvedValue({ order: { ...row, version: 8, status: 'DELIVERING', allowedActions: ['COMPLETE_DELIVERY'] }, skus: [], fees: [], operations: [] }) - await dialog.findAll('button').find(button => button.text().includes('重新加载'))!.trigger('click') - await flushPromises() - expect((dialog.find('textarea').element as HTMLTextAreaElement).value).toBe('已交付') - await save().trigger('click'); await flushPromises() - expect(updateDeliveryOrderStatus).toHaveBeenLastCalledWith('DEL/1', '100', { status: 'DELIVERED', expectedVersion: 8, remark: '已交付' }) - expect(listDeliveryOrders).toHaveBeenCalledTimes(2) - } finally { wrapper.unmount() } + vi.mocked(listDeliveryOrders).mockResolvedValue({ records: [{ ...row, status: 'DELIVERING', allowedActions: ['COMPLETE_DELIVERY'] }], page: 1, size: 10, total: 1, pages: 1 }) + const wrapper = mountView(); await flushPromises() + expect(wrapper.find('[data-testid="data-table"]').text()).not.toContain('编辑状态') + expect(getDeliveryOrder).not.toHaveBeenCalled() + expect(updateDeliveryOrderStatus).not.toHaveBeenCalled() + wrapper.unmount() + }) + + it('shows the destination FBA warehouse that identifies each split delivery', async () => { + vi.mocked(listDeliveryOrders).mockResolvedValue({ records: [{ ...row, destinationWarehouseCode: 'ACY2', destinationWarehouseName: 'Amazon ACY2' }], page: 1, size: 10, total: 1, pages: 1 }) + const table = defineComponent({ + props: { data: Array as PropType, columns: Array as PropType string }>> }, + setup(props) { return () => h('div', props.columns?.find(column => column.key === 'destinationWarehouse')?.formatter?.(props.data?.[0] || {}) || '') }, + }) + const wrapper = mount(DeliveryOrdersView, { global: { plugins: [ElementPlus], stubs: { ...commonPhase5Stubs, DataTable: table } } }) + await flushPromises() + expect(wrapper.text()).toContain('ACY2 · Amazon ACY2') + wrapper.unmount() }) it('renders category counts from the list without per-row document requests', async () => { diff --git a/src/modules/overseas-warehouse/pages/DeliveryOrdersView.vue b/src/modules/overseas-warehouse/pages/DeliveryOrdersView.vue index c3a82d53..3e7a618b 100644 --- a/src/modules/overseas-warehouse/pages/DeliveryOrdersView.vue +++ b/src/modules/overseas-warehouse/pages/DeliveryOrdersView.vue @@ -48,6 +48,7 @@ const columns: DataTableColumn[] = [ { key: 'documents', label: '所需资料', minWidth: 380, slotName: 'documents' }, { key: 'bolNo', prop: 'bolNo', label: 'BOL号', minWidth: 170 }, { key: 'customerName', prop: 'customerName', label: '客户', minWidth: 160 }, + { key: 'destinationWarehouse', label: '目的 FBA 仓', minWidth: 180, formatter: (row: DeliveryOrderRow) => destinationWarehouseLabel(row) }, { key: 'overseasWarehouseName', prop: 'overseasWarehouseName', label: '仓库', minWidth: 150 }, { key: 'dispatchNo', prop: 'dispatchNo', label: '排车单号', minWidth: 170 }, { key: 'waybillNo', prop: 'waybillNo', label: '运单号', minWidth: 150 }, @@ -71,6 +72,11 @@ const statusLabels: Record = { WAIT_DELIVERY: '待派送', DELIV function deliveryMethodLabel(value?: string | null) { return ({ FBA_PICKUP: 'FBA自提', CUSTOMER_PICKUP: '客户自提', TRUCK_PALLET: '卡派打托', TRUCK_FLOOR: '卡派地板', EXPRESS: '快递', OUTSOURCED: '外配', DEDICATED_TRUCK: '专车直送', LTL: '零担派送', SELF_DELIVERY: '自送', THIRD_PARTY: '第三方配送' } as Record)[String(value)] || value || '—' } function pickupMethodLabel(value?: string | null) { return ({ ONE_PICKUP_ONE_DROP: '一提一卸', ONE_PICKUP_MULTI_DROP: '一提多卸', MULTI_PICKUP_ONE_DROP: '多提一卸', MULTI_PICKUP_MULTI_DROP: '多提多卸', WAREHOUSE_PICKUP: '仓库自提', DOOR_PICKUP: '上门提货', SELF_PICKUP: '自提' } as Record)[String(value)] || value || '—' } function loadingTypeLabel(value?: string | null) { return ({ PALLET: '托盘', FLOOR: '地板', MIXED: '混装', CARTON: '散箱' } as Record)[String(value)] || value || '—' } +function destinationWarehouseLabel(row: DeliveryOrderRow) { + const code = row.destinationWarehouseCode?.trim() + const name = row.destinationWarehouseName?.trim() + return code && name && code !== name ? `${code} · ${name}` : code || name || '—' +} async function load(model: QueryModel = filters.value) { Object.assign(filters.value, model); loading.value = true; error.value = null; try { const range = Array.isArray(filters.value.plannedAt) ? filters.value.plannedAt.map(String) : []; const result = await listDeliveryOrders({ page: page.value, size: pageSize.value, documentNo: text('documentNo'), dispatchNo: text('dispatchNo'), waybillNo: text('waybillNo'), warehouseId: text('warehouseId'), status: text('status'), vehiclePlate: text('vehiclePlate'), driverName: text('driverName'), deliveryMethod: text('deliveryMethod'), plannedFrom: range[0] ? `${range[0]}T00:00:00` : undefined, plannedTo: range[1] ? `${range[1]}T23:59:59` : undefined }); rows.value = result.records; total.value = result.total } catch (cause) { rows.value = []; total.value = 0; error.value = getApiErrorDetails(cause, '派送单列表加载失败') } finally { loading.value = false } } function text(key: string) { return String(filters.value[key] || '').trim() || undefined } function open(row: DeliveryOrderRow) { void router.push({ name: 'overseas-delivery-order-detail', params: { deliveryNo: row.deliveryNo }, query: { customerId: row.customerId } }) } @@ -123,7 +129,6 @@ const statusForm = reactive({ status: '', remark: '' }) let statusSequence = 0 const statusTargets = [ { action: 'START_DELIVERY', value: 'DELIVERING', label: '派送中' }, - { action: 'COMPLETE_DELIVERY', value: 'DELIVERED', label: '派送完成' }, { action: 'REJECT_DELIVERY', value: 'REJECTED', label: '已拒签' }, { action: 'CANCEL_DELIVERY', value: 'CANCELLED', label: '已取消' }, ] @@ -153,9 +158,9 @@ async function submitStatus() { if (!statusOpen.value || !statusRow.value || statusLoading.value || statusSaving.value || statusConflict.value || !access.can(permission.deliveryStatus) || !statusOptions.value.some(option => option.value === statusForm.status)) return statusSaving.value = true; statusError.value = null try { - const result = await updateDeliveryOrderStatus(statusRow.value.deliveryNo, statusRow.value.customerId, { status: statusForm.status, expectedVersion: statusRow.value.version, remark: statusForm.remark.trim() || undefined }) + await updateDeliveryOrderStatus(statusRow.value.deliveryNo, statusRow.value.customerId, { status: statusForm.status, expectedVersion: statusRow.value.version, remark: statusForm.remark.trim() || undefined }) statusOpen.value = false - ElMessage.success(result.deliveryStatus === 'DELIVERED' ? '派送已完成,待回访记录已生成' : '派送状态已更新') + ElMessage.success('派送状态已更新') await load() } catch (cause) { const details = getApiErrorDetails(cause, '派送状态更新失败') @@ -168,6 +173,6 @@ function reset(model: QueryModel) { page.value = 1; void load(model) } async function loadWarehouses() { if (!access.can(permission.warehouseList)) return; warehousesLoading.value = true; try { warehouses.value = (await listReceivingOverseasWarehouses()).records } catch { warehouses.value = [] } finally { warehousesLoading.value = false } } onMounted(() => { void load(); void loadWarehouses() }) - + diff --git a/src/modules/overseas-warehouse/pages/OutboundDetailView.test.ts b/src/modules/overseas-warehouse/pages/OutboundDetailView.test.ts index 7ecda07b..26fd6738 100644 --- a/src/modules/overseas-warehouse/pages/OutboundDetailView.test.ts +++ b/src/modules/overseas-warehouse/pages/OutboundDetailView.test.ts @@ -15,7 +15,10 @@ describe('OutboundDetailView', () => { vi.mocked(getOutbound).mockReset().mockResolvedValue({ outbound: { id: '1', outboundNo: 'OUT/1', dispatchNo: 'PC-1', status: 'PICKED', palletCount: 2, packageCount: 20, createdAt: '2026-09-01T10:00:00' }, pallets: [{ palletNo: 'IP-ACY2-20260907-597057-P001', inboundCode: 'NYEXX2609060063', destinationWarehouseCode: 'ONT8', destinationWarehouseName: 'Amazon ONT8', skuCode: 'SKU-001', packageCount: 20 }], - delivery: { version: 2, allowedActions: ['START_DELIVERY'], id: '2', customerId: 'customer-1', deliveryNo: 'DEL-1', outboundNo: 'OUT/1', dispatchNo: 'PC-1', status: 'WAIT_DELIVERY', palletCount: 2, packageCount: 20, createdAt: '2026-09-01T10:00:00' }, + deliveries: [ + { version: 2, allowedActions: ['START_DELIVERY'], id: '2', customerId: 'customer-1', deliveryNo: 'DEL-ONT8', outboundNo: 'OUT/1', dispatchNo: 'PC-1', status: 'WAIT_DELIVERY', destinationWarehouseCode: 'ONT8', palletCount: 1, packageCount: 10, createdAt: '2026-09-01T10:00:00' }, + { version: 2, allowedActions: ['START_DELIVERY'], id: '3', customerId: 'customer-1', deliveryNo: 'DEL-ACY2', outboundNo: 'OUT/1', dispatchNo: 'PC-1', status: 'WAIT_DELIVERY', destinationWarehouseCode: 'ACY2', palletCount: 1, packageCount: 10, createdAt: '2026-09-01T10:00:00' }, + ], }) }) @@ -25,7 +28,9 @@ describe('OutboundDetailView', () => { expect(getOutbound).toHaveBeenCalledWith('OUT/1') expect(wrapper.text()).toContain('OUT/1') expect(wrapper.text()).toContain('PC-1') - expect(wrapper.text()).toContain('DEL-1') + expect(wrapper.text()).toContain('DEL-ONT8') + expect(wrapper.text()).toContain('DEL-ACY2') + expect(wrapper.text()).toContain('ACY2') expect(wrapper.text()).toContain('出库托盘') expect(wrapper.text()).toContain('IP-ACY2-20260907-597057-P001') expect(wrapper.text()).toContain('NYEXX2609060063') @@ -40,7 +45,7 @@ describe('OutboundDetailView', () => { vi.mocked(getOutbound).mockResolvedValueOnce({ outbound: { id: '1', outboundNo: 'OUT/1', status: 'OUTBOUNDED' }, pallets: [{ palletNo: null, inboundCode: null, destinationWarehouseCode: null, destinationWarehouseName: null, skuCode: null, packageCount: null }], - delivery: null, + deliveries: [], }) const wrapper = mount(OutboundDetailView, { global: { plugins: [ElementPlus] } }) await flushPromises() @@ -51,7 +56,7 @@ describe('OutboundDetailView', () => { vi.mocked(getOutbound).mockResolvedValueOnce({ outbound: { id: '1', outboundNo: 'OUT/1', status: 'OUTBOUNDED' }, pallets: [], - delivery: null, + deliveries: [], }) const emptyWrapper = mount(OutboundDetailView, { global: { plugins: [ElementPlus] } }) await flushPromises() @@ -74,7 +79,7 @@ describe('OutboundDetailView', () => { ])('uses a matching warehouse name and never displays internal IDs (%s, %s)', async (deliveryWarehouseId, name, expected) => { vi.mocked(getOutbound).mockResolvedValueOnce({ outbound: { id: '1', outboundNo: 'OUT/1', overseasWarehouseId: 'warehouse-internal-id' }, - delivery: { id: '2', customerId: 'customer-1', deliveryNo: 'DEL-1', outboundNo: 'OUT/1', dispatchNo: 'PC-1', status: 'WAIT_DELIVERY', palletCount: 2, packageCount: 20, createdAt: '2026-09-01T10:00:00', version: 1, allowedActions: [], overseasWarehouseId: deliveryWarehouseId, overseasWarehouseName: name }, + deliveries: [{ id: '2', customerId: 'customer-1', deliveryNo: 'DEL-1', outboundNo: 'OUT/1', dispatchNo: 'PC-1', status: 'WAIT_DELIVERY', palletCount: 2, packageCount: 20, createdAt: '2026-09-01T10:00:00', version: 1, allowedActions: [], overseasWarehouseId: deliveryWarehouseId, overseasWarehouseName: name }], }) const wrapper = mount(OutboundDetailView, { global: { plugins: [ElementPlus] } }) await flushPromises() diff --git a/src/modules/overseas-warehouse/pages/OutboundDetailView.vue b/src/modules/overseas-warehouse/pages/OutboundDetailView.vue index 4bb7340c..2e40acee 100644 --- a/src/modules/overseas-warehouse/pages/OutboundDetailView.vue +++ b/src/modules/overseas-warehouse/pages/OutboundDetailView.vue @@ -37,7 +37,7 @@ function show(value: unknown) { const text = String(value ?? '').trim(); return // A warehouse ID is only a join key; never render it as a warehouse name. const warehouseLabel = computed(() => { const outboundWarehouseId = detail.value?.outbound.overseasWarehouseId - const delivery = detail.value?.delivery + const delivery = detail.value?.deliveries?.find(item => item.overseasWarehouseId === outboundWarehouseId) return outboundWarehouseId && delivery?.overseasWarehouseId === outboundWarehouseId ? show(delivery.overseasWarehouseName) : '—' }) function statusLabel(value: unknown) { const code = String(value || ''); return statusLabels[code] || show(code) } @@ -54,11 +54,11 @@ watch(outboundNo, () => { void load() }, { immediate: true }) - + -