From 9c9cb915933f3c570082e0a33a851a4a3e5dd7ee Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Thu, 17 Sep 2026 13:36:31 +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 --- src/modules/overseas-warehouse/AGENTS.md | 2 + .../overseas-warehouse/api/deliveryOrders.ts | 4 + .../overseas-warehouse/api/dispatches.ts | 7 +- .../overseas-warehouse/api/lastMileOrders.ts | 2 + .../overseas-warehouse/api/outbounds.ts | 10 ++ .../overseas-warehouse/api/relabelOrders.ts | 8 ++ .../components/SmartDispatchDialog.vue | 23 ++++- .../lastMilePresentation.test.ts | 28 +++++- .../lastMilePresentation.ts | 23 ++++- .../pages/LastMileOrderDetailView.test.ts | 30 ++++++ .../pages/LastMileOrderDetailView.vue | 18 +++- .../pages/LastMileOrdersView.behavior.test.ts | 15 +++ .../pages/LastMileOrdersView.vue | 45 ++++++++- .../pages/OutboundDetailView.vue | 10 +- .../pages/OutboundsView.test.ts | 25 +++++ .../pages/OutboundsView.vue | 93 ++++++++++++++++--- .../pages/prototypeContract.test.ts | 4 +- src/modules/overseas-warehouse/types.ts | 2 + 18 files changed, 317 insertions(+), 32 deletions(-) diff --git a/src/modules/overseas-warehouse/AGENTS.md b/src/modules/overseas-warehouse/AGENTS.md index 56b1b3ca..c446b016 100644 --- a/src/modules/overseas-warehouse/AGENTS.md +++ b/src/modules/overseas-warehouse/AGENTS.md @@ -28,6 +28,8 @@ ## 履约硬边界 +- 智能排车来源支持 `RELABEL`(待出库换标单),切换后不传已选入库计划 ID,按客户已确认、卡车派送且未排车的换标库存查询。提交换标号从候选 `orderRefs` 读取,不能把展示的原入库单号 `orderNo` 当换标号;沿用来源库存、托盘与箱唛,不生成新入库单。 + - 托盘管理单张打印:`pages/PalletsView.vue` 每行“打印”经 `api/pallets.ts` 的 `getPalletLabelPdf` 下载当前托盘一页 PDF;使用该行仓库 ID 与托盘 ID,权限复用托盘列表。Backend `warehouse-overseas-service` 的 `PalletLabelPrintService` 读取公共托盘并调用现有 Print `INBOUND_MARK_V1`,不在前端生成标签、改码或维护第二套模板。 - 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 仓拆分的派送单 diff --git a/src/modules/overseas-warehouse/api/deliveryOrders.ts b/src/modules/overseas-warehouse/api/deliveryOrders.ts index 02a8eaa5..339f62ae 100644 --- a/src/modules/overseas-warehouse/api/deliveryOrders.ts +++ b/src/modules/overseas-warehouse/api/deliveryOrders.ts @@ -33,6 +33,10 @@ export interface DeliveryOrderRow { destinationWarehouseId?: string | null destinationWarehouseCode?: string | null destinationWarehouseName?: string | null + destinationType?: 'FBA' | 'PRIVATE' | null + deliveryAddress?: string | null + contactName?: string | null + contactPhone?: string | null overseasWarehouseId?: string | null overseasWarehouseName?: string | null status: string diff --git a/src/modules/overseas-warehouse/api/dispatches.ts b/src/modules/overseas-warehouse/api/dispatches.ts index b5df3545..2f20f1dc 100644 --- a/src/modules/overseas-warehouse/api/dispatches.ts +++ b/src/modules/overseas-warehouse/api/dispatches.ts @@ -39,13 +39,18 @@ export interface DispatchPalletCandidateDto { sourceStage?: 'PLAN' | 'RECEIVED' | 'RELABEL' | null cartonMarks?: string | null relabelCartonMarks?: string | null + destinationType?: 'FBA' | 'PRIVATE' | null + deliveryAddress?: string | null + postalCode?: string | null + contactName?: string | null + contactPhone?: string | null } export interface DispatchCandidateQuery { planIds?: string orderNo?: string fbaWarehouseCode?: string - sourceStage?: 'PLAN' | 'RECEIVED' + sourceStage?: 'PLAN' | 'RECEIVED' | 'RELABEL' draftToken?: string } export interface DispatchDraftContainerDto { id: string; capacity: number; palletIds: string[] } diff --git a/src/modules/overseas-warehouse/api/lastMileOrders.ts b/src/modules/overseas-warehouse/api/lastMileOrders.ts index f766721c..d854c4db 100644 --- a/src/modules/overseas-warehouse/api/lastMileOrders.ts +++ b/src/modules/overseas-warehouse/api/lastMileOrders.ts @@ -71,6 +71,8 @@ export interface LastMileOrderSummaryDto { version?: number | null palletCount?: number | null packageCount?: number | null + receivedPackageCount?: number | null + packageDifference?: number | null volumeCbm?: number | null productQuantity?: number | null volumeWeightKg?: number | null diff --git a/src/modules/overseas-warehouse/api/outbounds.ts b/src/modules/overseas-warehouse/api/outbounds.ts index e8cca936..858d97fa 100644 --- a/src/modules/overseas-warehouse/api/outbounds.ts +++ b/src/modules/overseas-warehouse/api/outbounds.ts @@ -20,6 +20,8 @@ export interface OutboundDetail { outbound: OutboundRow; pallets?: OutboundPalle 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 } +export interface WarehouseTransferOutboundCandidate { customerId: string; customerName?: string | null; transferNo: string; overseasWarehouseId: string; transferStatus: string; needRelabel: boolean; relabelNo?: string | null; packageCount: number; grossWeightKg?: number | null; volumeCbm?: number | null } +export interface WarehouseTransferDestinationPayload { transfers: Array<{ customerId: string; transferNo: string }>; destinationType: 'FBA' | 'PRIVATE'; destinationWarehouseId?: string; destinationWarehouseCode?: string; destinationWarehouseName?: string; privateDestinationAddress?: string; privateDestinationPostalCode?: string; privateDestinationContactName?: string; privateDestinationContactPhone?: string; idempotencyKey: string } export async function listOutbounds(query: OutboundQuery) { const response = await overseasHttp.get>, ApiEnvelope>>('/platform/v1/last-leg/outbound-orders', { params: query }) return normalizeOverseasPage(unwrapApiResponse(response)) @@ -44,6 +46,14 @@ export async function createReturnOutbound(customerId: string, payload: { recipi const response = await overseasHttp.post, ApiEnvelope>('/platform/v1/last-leg/outbound-orders/return', payload, { params: { customerId } }) return unwrapApiResponse(response) } +export async function listWarehouseTransferOutboundCandidates(query: { customerId?: string; warehouseId?: string } = {}) { + const response = await overseasHttp.get, ApiEnvelope>('/platform/v1/last-leg/outbound-orders/warehouse-transfer/candidates', { params: query }) + return unwrapApiResponse(response).map(item => ({ ...item, packageCount: Number(item.packageCount || 0) })) +} +export async function bindWarehouseTransferDestination(payload: WarehouseTransferDestinationPayload) { + const response = await overseasHttp.post, ApiEnvelope>('/platform/v1/last-leg/warehouse-transfers/bind-destination', payload) + return unwrapApiResponse(response) +} export async function cancelOutbound(outboundNo: string, reason: string) { const response = await overseasHttp.post, ApiEnvelope>(`/platform/v1/last-leg/outbound-orders/${encodeURIComponent(outboundNo)}/cancel`, { reason }) return unwrapApiResponse(response) diff --git a/src/modules/overseas-warehouse/api/relabelOrders.ts b/src/modules/overseas-warehouse/api/relabelOrders.ts index 2a284697..a55bd281 100644 --- a/src/modules/overseas-warehouse/api/relabelOrders.ts +++ b/src/modules/overseas-warehouse/api/relabelOrders.ts @@ -167,6 +167,14 @@ export async function returnRelabelForRework(relabelNo: string, expectedVersion: return unwrapApiResponse(response) } +export async function confirmRelabelCompletion(relabelNo: string, remark?: string) { + const response = await overseasHttp.post, ApiEnvelope>( + `/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/confirm-completion`, + { idempotencyKey: crypto.randomUUID(), remark }, + ) + return unwrapApiResponse(response) +} + export async function uploadRelabelPdf(relabelNo: string, file: File, scope: 'TOTAL' | 'ITEM', itemId?: string) { const body = new FormData() body.append('file', file) diff --git a/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue b/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue index 8aef39a3..f36410e8 100644 --- a/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue +++ b/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue @@ -47,7 +47,7 @@ const selectedVehicle = ref('') const query = reactive({ orderNo: '', orderType: '', - sourceStage: 'RECEIVED' as '' | 'PLAN' | 'RECEIVED', + sourceStage: 'RECEIVED' as '' | 'PLAN' | 'RECEIVED' | 'RELABEL', fbaWarehouseCode: '', }) const warehouseTab = ref<'ALL' | 'LAX' | 'NYC'>('ALL') @@ -105,6 +105,10 @@ function number(value: unknown, digits = 0) { function dateTime(value: unknown) { return formatOverseasDateTime(value) } +function privateWarehouse(item: DispatchPalletCandidateDto) { + if (item.destinationType !== 'PRIVATE') return '—' + return [item.deliveryAddress, item.postalCode].map((value) => String(value || '').trim()).filter(Boolean).join(' · ') || '—' +} function orderType(value: unknown) { return ( ( @@ -158,7 +162,7 @@ async function load() { loadError.value = null try { candidates.value = await listDispatchCandidates({ - planIds: planIds.value.length ? planIds.value.join(',') : undefined, + planIds: query.sourceStage !== 'RELABEL' && planIds.value.length ? planIds.value.join(',') : undefined, orderNo: query.orderNo.trim() || undefined, fbaWarehouseCode: query.fbaWarehouseCode.trim() || undefined, sourceStage: query.sourceStage || undefined, @@ -295,7 +299,11 @@ async function submit() { return } const relabelOrders = [...new Set(assigned.value.filter((item) => item.sourceStage === 'RELABEL') - .map((item) => String(item.orderNo || '').trim()).filter(Boolean))] + .map((item) => String(item.orderRefs || '').trim()).filter(Boolean))] + if (assigned.value.some((item) => item.sourceStage === 'RELABEL' && !String(item.orderRefs || '').trim())) { + ElMessage.warning('换标来源单号缺失,请刷新后重新选择') + return + } if (relabelOrders.length && (relabelOrders.length !== 1 || assigned.value.some((item) => item.sourceStage !== 'RELABEL'))) { ElMessage.warning('一张换标单的托盘需要单独排车') return @@ -363,13 +371,15 @@ watch([warehouseTab, () => query.orderType], () => {
- + - + 查询重置
+

仅展示客户已确认、卡车派送且尚未排车的换标单。沿用原入库库存和托盘,按本次换标箱唛及新目的仓排车,无需重新生成入库单。

+