From ae48f46c9fd2a2b089da93c22cf3a26d1bc0afab Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Fri, 28 Aug 2026 14:33:57 +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/core/navigation/adminNavigation.ts | 2 + .../api/lastMileOrders.test.ts | 18 ++- .../overseas-warehouse/api/lastMileOrders.ts | 23 ++++ src/modules/overseas-warehouse/api/pallets.ts | 4 +- .../overseas-warehouse/api/relabelOrders.ts | 1 + .../components/SmartDispatchDialog.vue | 3 +- .../inventoryPresentation.test.ts | 1 + .../inventoryPresentation.ts | 3 +- .../lastMilePresentation.test.ts | 15 ++- .../lastMilePresentation.ts | 22 +++- .../pages/DispatchesView.vue | 4 +- .../pages/InboundOrdersView.vue | 2 +- .../pages/LastMileOrdersView.vue | 54 +++++++- .../pages/OutboundsView.vue | 3 +- .../pages/PalletDetailView.vue | 3 +- .../overseas-warehouse/pages/PalletsView.vue | 1 - .../pages/RelabelOrdersView.test.ts | 13 ++ .../pages/RelabelOrdersView.vue | 5 +- src/modules/overseas-warehouse/types.ts | 2 + .../overseas-delivery/AcceptanceWorkbench.vue | 119 ++++++++++++++++++ src/prototype/routes.ts | 8 ++ 21 files changed, 282 insertions(+), 24 deletions(-) create mode 100644 src/prototype/modules/overseas-delivery/AcceptanceWorkbench.vue diff --git a/src/core/navigation/adminNavigation.ts b/src/core/navigation/adminNavigation.ts index 957847b9..0186a35a 100644 --- a/src/core/navigation/adminNavigation.ts +++ b/src/core/navigation/adminNavigation.ts @@ -1,6 +1,7 @@ import type { Component } from 'vue' import { Building2, + ClipboardCheck, ContactRound, ClipboardList, CircleDollarSign, @@ -128,6 +129,7 @@ export const adminNavigation: AdminNavigationNode[] = [ { key: 'fba-relabel-order', title: '换标单', icon: ClipboardList, path: '/overseas-warehouse/relabel-orders', permission: 'platform:overseas-warehouse:relabel-order:list' }, { key: 'fba-pallet', title: '托盘管理', icon: WarehouseIcon, path: '/overseas-warehouse/pallets', permission: 'platform:overseas-warehouse:pallet:list' }, { key: 'overseas-inventory-list', title: '库存管理', icon: WarehouseIcon, path: '/overseas-warehouse/inventory', permission: 'platform:overseas-warehouse:inventory:list' }, + { key: 'overseas-acceptance', title: '尾程流程验收', icon: ClipboardCheck, path: '/prototype/overseas-delivery/acceptance-workbench' }, ], }, { diff --git a/src/modules/overseas-warehouse/api/lastMileOrders.test.ts b/src/modules/overseas-warehouse/api/lastMileOrders.test.ts index 018f9bb0..f56f36b6 100644 --- a/src/modules/overseas-warehouse/api/lastMileOrders.test.ts +++ b/src/modules/overseas-warehouse/api/lastMileOrders.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { overseasHttp } from '@/shared/api/overseasHttp' -import { convertLastMileOrderToInboundPlan, getLastMileOrder, listLastMileOrders, transferLastMileOrders } from './lastMileOrders' +import { convertLastMileOrderToInboundPlan, getLastMileOrder, listLastMileOrders, setLastMileOrderDetained, setLastMileOrderPalletMergeRequired, transferLastMileOrders } from './lastMileOrders' vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), put: vi.fn(), post: vi.fn() } })) @@ -47,6 +47,22 @@ describe('last-mile order overseas API', () => { expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/last-leg/orders/destination', expect.objectContaining({ orderIds: ['190000000000000099'] })) }) + it('uses the order hold endpoint with the required operation remark', async () => { + vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', message: 'success', data: null }) + await setLastMileOrderDetained('190000000000000099', { detained: true, remark: '等待质检确认' }) + expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/last-leg/orders/190000000000000099/detained', { + detained: true, remark: '等待质检确认', + }) + }) + + it('marks an order for a PDA pallet-merge reminder', async () => { + vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', message: 'success', data: null }) + await setLastMileOrderPalletMergeRequired('190000000000000099', { palletMergeRequired: true }) + expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/last-leg/orders/190000000000000099/pallet-merge-required', { + palletMergeRequired: true, + }) + }) + it('uses the inbound-plan conversion endpoint without customerId', async () => { vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', message: 'success', data: {} }) await convertLastMileOrderToInboundPlan({ orderId: '190000000000000099' }) diff --git a/src/modules/overseas-warehouse/api/lastMileOrders.ts b/src/modules/overseas-warehouse/api/lastMileOrders.ts index 18dad487..b1057285 100644 --- a/src/modules/overseas-warehouse/api/lastMileOrders.ts +++ b/src/modules/overseas-warehouse/api/lastMileOrders.ts @@ -152,6 +152,15 @@ export interface ConvertInboundPlanPayload { orderId: string } +export interface SetLastMileOrderDetainedPayload { + detained: boolean + remark: string +} + +export interface SetLastMileOrderPalletMergePayload { + palletMergeRequired: boolean +} + export interface LastMileOrderDetailDto { order: LastMileOrderDto cargo: LastMileCargoDto[] @@ -195,3 +204,17 @@ export async function convertLastMileOrderToInboundPlan(payload: ConvertInboundP ) return unwrapApiResponse(response) } + +export async function setLastMileOrderDetained(orderId: string, payload: SetLastMileOrderDetainedPayload) { + const response = await overseasHttp.put, ApiEnvelope>( + `/platform/v1/last-leg/orders/${encodeURIComponent(orderId)}/detained`, payload, + ) + return unwrapApiResponse(response) +} + +export async function setLastMileOrderPalletMergeRequired(orderId: string, payload: SetLastMileOrderPalletMergePayload) { + const response = await overseasHttp.put, ApiEnvelope>( + `/platform/v1/last-leg/orders/${encodeURIComponent(orderId)}/pallet-merge-required`, payload, + ) + return unwrapApiResponse(response) +} diff --git a/src/modules/overseas-warehouse/api/pallets.ts b/src/modules/overseas-warehouse/api/pallets.ts index 5e478508..8d00b442 100644 --- a/src/modules/overseas-warehouse/api/pallets.ts +++ b/src/modules/overseas-warehouse/api/pallets.ts @@ -51,9 +51,9 @@ export async function listCustomerWarehouses() { return normalizeOverseasPage(unwrapApiResponse(response)) } -export async function listPallets(warehouseId: string, query: { page: number; size: number; keyword?: string; palletType?: string; status?: string }) { +export async function listPallets(warehouseId: string | undefined, query: { page: number; size: number; keyword?: string; palletType?: string; status?: string }) { const response = await overseasHttp.get>, ApiEnvelope>>( - `/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets`, + warehouseId ? `/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets` : '/platform/v1/warehouse-admin/pallets', { params: { ...query } }, ) return normalizeOverseasPage(unwrapApiResponse(response)) diff --git a/src/modules/overseas-warehouse/api/relabelOrders.ts b/src/modules/overseas-warehouse/api/relabelOrders.ts index f6a0b2a6..982aaf06 100644 --- a/src/modules/overseas-warehouse/api/relabelOrders.ts +++ b/src/modules/overseas-warehouse/api/relabelOrders.ts @@ -5,6 +5,7 @@ import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrder export interface RelabelOrderQuery { page: number size: number + relabelNo?: string skuCode?: string status?: string } diff --git a/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue b/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue index e1a414e0..cad83bd3 100644 --- a/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue +++ b/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue @@ -9,6 +9,7 @@ import AppDialog from '@/shared/components/AppDialog.vue' import LoadErrorState from '@/shared/components/LoadErrorState.vue' import OperationErrorToast from '@/shared/components/OperationErrorToast.vue' import { createDispatchDraftContainer, deleteDispatchDraftContainer, listDispatchCandidates, listDispatchDrafts, lockDispatchDraftPallet, preArrangeDispatch, releaseDispatchDraft, unlockDispatchDraftPallet, type DispatchPalletCandidateDto } from '../api/dispatches' +import { formatOverseasDateTime } from '../lastMilePresentation' import type { InboundOrderListItem } from '../types' type VehicleSpec = { value: string; label: string; capacity: number; maxWeightLb: number } @@ -77,7 +78,7 @@ const containerWeightLb = (container: LoadingContainer) => container.palletIds.r function palletId(item: DispatchPalletCandidateDto) { return String(item.planPalletId) } function text(value: unknown) { const normalized = String(value ?? '').trim(); return normalized || '—' } function number(value: unknown, digits = 0) { const normalized = Number(value); return Number.isFinite(normalized) ? normalized.toFixed(digits) : '0' } -function dateTime(value: unknown) { const raw = String(value || '').trim(); return raw ? raw.replace('T', ' ').slice(0, 16) : '—' } +function dateTime(value: unknown) { return formatOverseasDateTime(value) } function orderType(value: unknown) { return ({ FIRST_LEG: '头程订单', RELABEL_APPLICATION: '换标下单', RETURN_RELABEL: '退货换标', LOCAL_LOOSEBOARD: '本土散板', WAREHOUSE_TRANSFER: '仓储转运', ONE_CLICK_DROPSHIP: '一件代发', REJECTED: '拒收订单', EXPRESS: '快递订单' } as Record)[String(value || '')] || text(value) } function cartonMarks(item: DispatchPalletCandidateDto) { return String(item.cartonMarks || '').split(',').map((value) => value.trim()).filter(Boolean) } function containerFor(id: string) { return containers.value.find((item) => item.palletIds.includes(id)) } diff --git a/src/modules/overseas-warehouse/inventoryPresentation.test.ts b/src/modules/overseas-warehouse/inventoryPresentation.test.ts index 6de47bde..25a47a1f 100644 --- a/src/modules/overseas-warehouse/inventoryPresentation.test.ts +++ b/src/modules/overseas-warehouse/inventoryPresentation.test.ts @@ -36,6 +36,7 @@ describe('overseas inventory presentation adapter', () => { available: 18, held: 2, status: '存在暂扣', + updatedAt: '2026-08-18 17:54:11', }) }) diff --git a/src/modules/overseas-warehouse/inventoryPresentation.ts b/src/modules/overseas-warehouse/inventoryPresentation.ts index bd962467..3b8c839c 100644 --- a/src/modules/overseas-warehouse/inventoryPresentation.ts +++ b/src/modules/overseas-warehouse/inventoryPresentation.ts @@ -1,5 +1,6 @@ import type { InventoryBalanceDto } from './api/inventory' import type { InventoryListItem } from './types' +import { formatOverseasDateTime } from './lastMilePresentation' function text(value: unknown) { const normalized = String(value ?? '').trim() @@ -40,6 +41,6 @@ export function toInventoryListItem(dto: InventoryBalanceDto): InventoryListItem inTransit: count(dto.inTransitPackages), purchase: count(dto.procurementPackages), status: inventoryStatusLabel(dto), - updatedAt: text(dto.updatedAt), + updatedAt: formatOverseasDateTime(dto.updatedAt), } } diff --git a/src/modules/overseas-warehouse/lastMilePresentation.test.ts b/src/modules/overseas-warehouse/lastMilePresentation.test.ts index 04fa6365..1415c320 100644 --- a/src/modules/overseas-warehouse/lastMilePresentation.test.ts +++ b/src/modules/overseas-warehouse/lastMilePresentation.test.ts @@ -1,15 +1,24 @@ import { describe, expect, it } from 'vitest' -import { toLastMileOrderListItem } from './lastMilePresentation' +import { formatOverseasDateTime, toLastMileOrderListItem } from './lastMilePresentation' describe('last-mile order presentation adapter', () => { + it('formats overseas timestamps with zero-padded seconds', () => { + expect(formatOverseasDateTime('2026-08-27T16:19:46')).toBe('2026-08-27 16:19:46') + expect(formatOverseasDateTime('2026-08-07T06:09:04')).toBe('2026-08-07 06:09:04') + expect(formatOverseasDateTime('2026-08-07')).toMatch(/^2026-08-07 /) + expect(formatOverseasDateTime(new Date(2026, 7, 7, 6, 9, 4))).toBe('2026-08-07 06:09:04') + expect(formatOverseasDateTime('')).toBe('—') + expect(formatOverseasDateTime('not-a-date')).toBe('not-a-date') + }) + it('maps backend codes, associations and long ids without inventing data', () => { const row = toLastMileOrderListItem({ id: '190000000000000099', customerId: '190000000000000001', orderNo: 'LL-001', orderType: 'ONE_CLICK_DROPSHIP', currentStatus: 'PENDING_ASSIGNMENT', - abnormal: true, detained: false, packageCount: 12, deliveryOrderNo: 'DEL-READ-ONLY', outboundOrderNo: 'OUT-READ-ONLY', + abnormal: true, detained: false, packageCount: 12, palletMergeFlag: 'REQUIRED', deliveryOrderNo: 'DEL-READ-ONLY', outboundOrderNo: 'OUT-READ-ONLY', }) expect(row).toMatchObject({ id: '190000000000000099', customerId: '190000000000000001', orderType: '一件代发', status: '待分配', - warning: '异常', abnormal: '是', held: '否', cartons: 12, dispatchOrderNo: 'DEL-READ-ONLY', outboundOrderNo: 'OUT-READ-ONLY', + warning: '异常', abnormal: '是', held: '否', cartons: 12, consolidated: '待合托', palletMergeFlag: 'REQUIRED', dispatchOrderNo: 'DEL-READ-ONLY', outboundOrderNo: 'OUT-READ-ONLY', }) expect(row.rejectedAt).toBe('—') }) diff --git a/src/modules/overseas-warehouse/lastMilePresentation.ts b/src/modules/overseas-warehouse/lastMilePresentation.ts index f2f9a6a1..d01d1ba8 100644 --- a/src/modules/overseas-warehouse/lastMilePresentation.ts +++ b/src/modules/overseas-warehouse/lastMilePresentation.ts @@ -48,8 +48,22 @@ export function lastMileOrderTypeLabel(value: unknown) { export function formatOverseasDateTime(value: unknown) { const raw = String(value ?? '').trim() if (!raw) return '—' - const date = new Date(raw) - return Number.isNaN(date.getTime()) ? raw : date.toLocaleString('zh-CN', { hour12: false }) + const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(raw) + const date = value instanceof Date + ? new Date(value.getTime()) + : typeof value === 'number' + ? new Date(value) + : dateOnly + ? new Date(Number(dateOnly[1]), Number(dateOnly[2]) - 1, Number(dateOnly[3])) + : new Date(raw) + if (Number.isNaN(date.getTime())) return raw + const pad = (part: number) => String(part).padStart(2, '0') + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` +} + +export function palletMergeLabel(value: unknown) { + const code = String(value ?? '').trim() + return ({ REQUIRED: '待合托', MERGED: '已合托' } as Record)[code] || (code ? text(code) : '未标记') } export function toLastMileOrderListItem(dto: LastMileOrderSummaryDto): LastMileOrderListItem { @@ -69,6 +83,7 @@ export function toLastMileOrderListItem(dto: LastMileOrderSummaryDto): LastMileO containerStatus: text(dto.containerStatus), abnormal: yesNo(dto.abnormal), held: yesNo(dto.detained), + detained: dto.detained === true, pallets: count(dto.palletCount), cartons: count(dto.packageCount), volume: count(dto.volumeCbm), @@ -82,7 +97,8 @@ export function toLastMileOrderListItem(dto: LastMileOrderSummaryDto): LastMileO dispatchOrderNo: text(dto.deliveryOrderNo), relabelOrderNo: text(dto.relabelOrderNo), relabelStatus: lastMileStatusLabel(dto.relabelStatus), - consolidated: text(dto.palletMergeFlag), + consolidated: palletMergeLabel(dto.palletMergeFlag), + palletMergeFlag: String(dto.palletMergeFlag ?? '').trim(), status: lastMileStatusLabel(dto.currentStatus), internalRemark: text(dto.privateRemark), customerRemark: text(dto.customerRemark), diff --git a/src/modules/overseas-warehouse/pages/DispatchesView.vue b/src/modules/overseas-warehouse/pages/DispatchesView.vue index 1573ee4a..70804933 100644 --- a/src/modules/overseas-warehouse/pages/DispatchesView.vue +++ b/src/modules/overseas-warehouse/pages/DispatchesView.vue @@ -118,10 +118,10 @@ function toListItem(dto: DispatchSummaryDto): DispatchListItem { orderNos: text(dto.orderNos), route: text(dto.route), overseasWarehouse: text(dto.overseasWarehouseName || dto.overseasWarehouseId), - departAt: text(dto.departAt), + departAt: formatOverseasDateTime(dto.departAt), fbaAppointment: appointmentLabel(dto.fbaAppointmentNo, dto.fbaAppointmentStatus), vehicleAppointment: text(dto.vehicleAppointmentStatus), - estimatedArrivalAt: text(dto.estimatedArrivalAt), + estimatedArrivalAt: formatOverseasDateTime(dto.estimatedArrivalAt), status: statusLabel(dto.status), palletCount: number(dto.palletCount), packageCount: number(dto.packageCount), diff --git a/src/modules/overseas-warehouse/pages/InboundOrdersView.vue b/src/modules/overseas-warehouse/pages/InboundOrdersView.vue index 6505201c..9eccc863 100644 --- a/src/modules/overseas-warehouse/pages/InboundOrdersView.vue +++ b/src/modules/overseas-warehouse/pages/InboundOrdersView.vue @@ -92,7 +92,7 @@ function toListItem(dto: InboundOrderSummaryDto): InboundOrderListItem { orderNo: text(dto.planNo), inboundCode: text(dto.inboundCode), type: typeLabel(dto.orderType), warehouse: text(dto.warehouseName || dto.warehouseCode), fbaWarehouse: text(dto.fbaWarehouseCode), owner: text(dto.customerName), cartons: number(dto.packageCount), pickedCartons: number(dto.pickedPackageCount), volume: number(dto.volumeCbm), pallets: number(dto.palletCount), status: planStatusLabel(dto.status), appointmentNo: text(dto.appointmentNo), appointmentStatus: appointmentStatusLabel(dto.appointmentStatus), - appointmentAt: text(dto.appointmentAt), expectedLoadingAt: text(dto.expectedLoadingAt), dispatchStatus: dto.dispatchStatus === 'SCHEDULED' ? '已排车' : '未排车', + appointmentAt: formatOverseasDateTime(dto.appointmentAt), expectedLoadingAt: formatOverseasDateTime(dto.expectedLoadingAt), dispatchStatus: dto.dispatchStatus === 'SCHEDULED' ? '已排车' : '未排车', createdAt: formatOverseasDateTime(dto.createdAt), } } diff --git a/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue b/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue index 4e5b1966..110d4a16 100644 --- a/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue +++ b/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue @@ -1,7 +1,7 @@ + + + + + diff --git a/src/prototype/routes.ts b/src/prototype/routes.ts index 905e5410..a12b9194 100644 --- a/src/prototype/routes.ts +++ b/src/prototype/routes.ts @@ -8,6 +8,7 @@ const moduleViews = { customerManagement: () => import('@/prototype/modules/customer-management/ModuleView.vue'), domesticWarehouse: () => import('@/prototype/modules/domestic-warehouse/ModuleView.vue'), overseasDelivery: () => import('@/prototype/modules/overseas-delivery/ModuleView.vue'), + overseasAcceptance: () => import('@/prototype/modules/overseas-delivery/AcceptanceWorkbench.vue'), overseasInventory: () => import('@/prototype/modules/overseas-inventory/ModuleView.vue'), serviceCenter: () => import('@/prototype/modules/service-center/ModuleView.vue'), supplierCenter: () => import('@/prototype/modules/supplier-center/ModuleView.vue'), @@ -35,6 +36,13 @@ function createRoutes(view: ModuleView, routes: PrototypeRoute[]): RouteRecordRa } export const prototypeModuleRoutes: RouteRecordRaw[] = [ + { + path: 'prototype/overseas-delivery/acceptance-workbench', + name: 'overseas-acceptance-workbench', + component: moduleViews.overseasAcceptance, + props: { title: '尾程业务全流程验收', description: '覆盖尾程订单、海外入库、排车、出库与库存验收' }, + meta: { title: '尾程业务全流程验收', description: '覆盖尾程订单、海外入库、排车、出库与库存验收' }, + }, ...createRoutes(moduleViews.customerManagement, [ ['prototype/customer-management/warehouse-tier-fee', 'warehouse-tier-fee', '海外仓阶梯费用'], ]),