From 6e9bb6e00cc8a7dc3a921fd507846d829297e396 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Fri, 21 Aug 2026 11:20:09 +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 --- docker-compose.development.yml | 1 + docker-compose.test.yml | 1 + nginx/default.conf.template | 12 ++ src/app/router/index.test.ts | 11 +- src/core/navigation/adminNavigation.test.ts | 13 +- src/core/navigation/adminNavigation.ts | 11 +- src/deployment/nginxConfig.test.ts | 13 ++ .../API_INTEGRATION_MATRIX.md | 2 +- .../overseas-warehouse/api/dispatches.test.ts | 39 +++++ .../overseas-warehouse/api/dispatches.ts | 97 ++++++++++++ .../api/inboundOrders.test.ts | 6 +- .../overseas-warehouse/api/inboundOrders.ts | 4 +- .../overseas-warehouse/api/inventory.test.ts | 14 +- .../overseas-warehouse/api/inventory.ts | 13 +- .../api/lastMileOrders.test.ts | 24 +-- .../overseas-warehouse/api/lastMileOrders.ts | 36 ++++- .../overseas-warehouse/api/pallets.test.ts | 26 +-- src/modules/overseas-warehouse/api/pallets.ts | 26 +-- .../api/relabelOrders.test.ts | 24 ++- .../overseas-warehouse/api/relabelOrders.ts | 51 +++++- .../components/SmartDispatchDialog.vue | 129 +++++++++++++++ .../inventoryPresentation.test.ts | 50 ++++++ .../inventoryPresentation.ts | 45 ++++++ .../lastMilePresentation.test.ts | 4 +- .../lastMilePresentation.ts | 4 +- .../pages/DispatchesView.test.ts | 84 ++++++++++ .../pages/DispatchesView.vue | 148 ++++++++++++++++++ .../pages/InboundOrdersView.vue | 63 ++++---- .../pages/InventoryDetailView.vue | 8 +- .../pages/InventoryTransactionsView.vue | 17 +- .../pages/InventoryView.vue | 46 ++---- .../pages/LastMileOrderDetailView.vue | 13 +- .../pages/LastMileOrdersView.vue | 55 ++----- .../pages/PalletDetailView.vue | 7 +- .../overseas-warehouse/pages/PalletsView.vue | 70 ++++----- .../pages/RelabelOrderDetailView.vue | 5 +- .../pages/RelabelOrdersView.vue | 81 ++++++---- .../pages/prototypeContract.test.ts | 38 ++++- .../pages/realApiMount.test.ts | 2 +- src/modules/overseas-warehouse/permissions.ts | 47 +++--- src/modules/overseas-warehouse/routes.test.ts | 1 + src/modules/overseas-warehouse/routes.ts | 6 + src/modules/overseas-warehouse/types.test.ts | 2 +- src/modules/overseas-warehouse/types.ts | 20 +++ src/shared/api/overseasHttp.test.ts | 11 ++ 45 files changed, 1051 insertions(+), 329 deletions(-) create mode 100644 src/modules/overseas-warehouse/api/dispatches.test.ts create mode 100644 src/modules/overseas-warehouse/api/dispatches.ts create mode 100644 src/modules/overseas-warehouse/components/SmartDispatchDialog.vue create mode 100644 src/modules/overseas-warehouse/inventoryPresentation.test.ts create mode 100644 src/modules/overseas-warehouse/inventoryPresentation.ts create mode 100644 src/modules/overseas-warehouse/pages/DispatchesView.test.ts create mode 100644 src/modules/overseas-warehouse/pages/DispatchesView.vue diff --git a/docker-compose.development.yml b/docker-compose.development.yml index 2e126b7c..a8c9fafd 100644 --- a/docker-compose.development.yml +++ b/docker-compose.development.yml @@ -10,3 +10,4 @@ services: environment: CLIENT_WEB_URL: "http://8.163.49.32:9002" API_UPSTREAM: "http://host.docker.internal:9000" + OVERSEAS_API_UPSTREAM: "http://host.docker.internal:9300" diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 23ad5bdc..4310900a 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -16,3 +16,4 @@ services: # 测试环境固定走 Docker 宿主机网关,避免 Jenkins 全局环境变量或 # 工作区残留配置把上游覆盖成公网 IP,造成容器回连公网端口失败。 API_UPSTREAM: "http://host.docker.internal:9000" + OVERSEAS_API_UPSTREAM: "http://host.docker.internal:9300" diff --git a/nginx/default.conf.template b/nginx/default.conf.template index 4bbf5ed5..3ec922ae 100644 --- a/nginx/default.conf.template +++ b/nginx/default.conf.template @@ -30,6 +30,18 @@ server { proxy_set_header X-Forwarded-Proto $scheme; } + # 海外业务使用独立 Gateway;将浏览器前缀改写为 Gateway 的 /api 前缀。 + location /overseas-api/ { + client_max_body_size 12m; + rewrite ^/overseas-api/(.*)$ /api/$1 break; + proxy_pass ${OVERSEAS_API_UPSTREAM}; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + # HTML must be revalidated on every visit so a new deployment cannot keep # referencing hashed chunks that were removed with the previous image. location = /index.html { diff --git a/src/app/router/index.test.ts b/src/app/router/index.test.ts index 31f06b36..b4a68aeb 100644 --- a/src/app/router/index.test.ts +++ b/src/app/router/index.test.ts @@ -81,11 +81,12 @@ describe('admin router access metadata', () => { it('registers the confirmed phase-four routes with stable list permissions', () => { const expected = [ - ['overseas-last-mile-orders', '/overseas-warehouse/last-mile-orders', 'platform:lastleg:order:list'], - ['overseas-inbound-orders', '/overseas-warehouse/inbound-orders', 'platform:lastleg:inbound-plan:list'], - ['overseas-relabel-orders', '/overseas-warehouse/relabel-orders', 'platform:lastleg:relabel:list'], - ['overseas-pallets', '/overseas-warehouse/pallets', 'platform:pallet:list'], - ['overseas-inventory', '/overseas-warehouse/inventory', 'platform:lastleg:inventory:list'], + ['overseas-last-mile-orders', '/overseas-warehouse/last-mile-orders', 'platform:overseas-warehouse:last-mile-order:list'], + ['overseas-inbound-orders', '/overseas-warehouse/inbound-orders', 'platform:overseas-warehouse:inbound-order:list'], + ['overseas-dispatches', '/overseas-warehouse/dispatches', 'platform:overseas-warehouse:dispatch:list'], + ['overseas-relabel-orders', '/overseas-warehouse/relabel-orders', 'platform:overseas-warehouse:relabel-order:list'], + ['overseas-pallets', '/overseas-warehouse/pallets', 'platform:overseas-warehouse:pallet:list'], + ['overseas-inventory', '/overseas-warehouse/inventory', 'platform:overseas-warehouse:inventory:list'], ['overseas-business-purchases', '/overseas-business/purchases', 'platform:overseas-business:purchase:list'], ['overseas-business-trucks', '/overseas-business/trucks', 'platform:overseas-business:truck:list'], ['overseas-business-looseboards', '/overseas-business/looseboards', 'platform:overseas-business:looseboard:list'], diff --git a/src/core/navigation/adminNavigation.test.ts b/src/core/navigation/adminNavigation.test.ts index 871f046d..6a584e66 100644 --- a/src/core/navigation/adminNavigation.test.ts +++ b/src/core/navigation/adminNavigation.test.ts @@ -50,6 +50,7 @@ describe('adminNavigation', () => { expect(adminNavigation.find((item) => item.key === 'overseas-inventory')?.children?.map((item) => item.title)).toEqual([ '尾程订单', '入库单', + '排车列表', '换标单', '托盘管理', '库存管理', @@ -137,11 +138,12 @@ describe('adminNavigation', () => { it('shows only the confirmed phase-four pages with their list permissions', () => { const filtered = filterAdminNavigation(adminNavigation, new Set([ - 'platform:lastleg:order:list', - 'platform:lastleg:inbound-plan:list', - 'platform:lastleg:relabel:list', - 'platform:pallet:list', - 'platform:lastleg:inventory:list', + 'platform:overseas-warehouse:last-mile-order:list', + 'platform:overseas-warehouse:inbound-order:list', + 'platform:overseas-warehouse:dispatch:list', + 'platform:overseas-warehouse:relabel-order:list', + 'platform:overseas-warehouse:pallet:list', + 'platform:overseas-warehouse:inventory:list', 'platform:overseas-business:purchase:list', 'platform:overseas-business:truck:list', 'platform:overseas-business:looseboard:list', @@ -154,6 +156,7 @@ describe('adminNavigation', () => { expect(paths).toEqual(expect.arrayContaining([ '/overseas-warehouse/last-mile-orders', '/overseas-warehouse/inbound-orders', + '/overseas-warehouse/dispatches', '/overseas-warehouse/relabel-orders', '/overseas-warehouse/pallets', '/overseas-warehouse/inventory', diff --git a/src/core/navigation/adminNavigation.ts b/src/core/navigation/adminNavigation.ts index fe6cd796..f4ee803c 100644 --- a/src/core/navigation/adminNavigation.ts +++ b/src/core/navigation/adminNavigation.ts @@ -120,11 +120,12 @@ export const adminNavigation: AdminNavigationNode[] = [ title: '海外仓', icon: PrototypeOverseasWarehouseIcon, children: [ - { key: 'last-mile-orders', title: '尾程订单', icon: ClipboardList, path: '/overseas-warehouse/last-mile-orders', permission: 'platform:lastleg:order:list' }, - { key: 'fba-inbound-order', title: '入库单', icon: ClipboardList, path: '/overseas-warehouse/inbound-orders', permission: 'platform:lastleg:inbound-plan:list' }, - { key: 'fba-relabel-order', title: '换标单', icon: ClipboardList, path: '/overseas-warehouse/relabel-orders', permission: 'platform:lastleg:relabel:list' }, - { key: 'fba-pallet', title: '托盘管理', icon: WarehouseIcon, path: '/overseas-warehouse/pallets', permission: 'platform:pallet:list' }, - { key: 'overseas-inventory-list', title: '库存管理', icon: WarehouseIcon, path: '/overseas-warehouse/inventory', permission: 'platform:lastleg:inventory:list' }, + { key: 'last-mile-orders', title: '尾程订单', icon: ClipboardList, path: '/overseas-warehouse/last-mile-orders', permission: 'platform:overseas-warehouse:last-mile-order:list' }, + { key: 'fba-inbound-order', title: '入库单', icon: ClipboardList, path: '/overseas-warehouse/inbound-orders', permission: 'platform:overseas-warehouse:inbound-order:list' }, + { key: 'fba-dispatch', title: '排车列表', icon: Truck, path: '/overseas-warehouse/dispatches', permission: 'platform:overseas-warehouse:dispatch:list' }, + { 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' }, ], }, { diff --git a/src/deployment/nginxConfig.test.ts b/src/deployment/nginxConfig.test.ts index 6a586a5f..3bdbb884 100644 --- a/src/deployment/nginxConfig.test.ts +++ b/src/deployment/nginxConfig.test.ts @@ -3,6 +3,8 @@ import { resolve } from 'node:path' import { describe, expect, it } from 'vitest' const config = readFileSync(resolve(process.cwd(), 'nginx/default.conf.template'), 'utf8') +const testCompose = readFileSync(resolve(process.cwd(), 'docker-compose.test.yml'), 'utf8') +const developmentCompose = readFileSync(resolve(process.cwd(), 'docker-compose.development.yml'), 'utf8') describe('admin nginx deployment config', () => { it('allows qualification uploads with multipart overhead through the API proxy', () => { @@ -24,4 +26,15 @@ describe('admin nginx deployment config', () => { expect(config).toContain('try_files $uri $uri/ /index.html;') expect(config).toContain('Cache-Control "no-cache"') }) + + it('proxies overseas API requests to the gateway API prefix', () => { + expect(config).toContain('location /overseas-api/') + expect(config).toContain('rewrite ^/overseas-api/(.*)$ /api/$1 break;') + expect(config).toContain('proxy_pass ${OVERSEAS_API_UPSTREAM};') + }) + + it('declares an explicit overseas gateway upstream in container deployments', () => { + expect(testCompose).toContain('OVERSEAS_API_UPSTREAM: "http://host.docker.internal:9300"') + expect(developmentCompose).toContain('OVERSEAS_API_UPSTREAM: "http://host.docker.internal:9300"') + }) }) diff --git a/src/modules/overseas-warehouse/API_INTEGRATION_MATRIX.md b/src/modules/overseas-warehouse/API_INTEGRATION_MATRIX.md index 6907d521..5fab7beb 100644 --- a/src/modules/overseas-warehouse/API_INTEGRATION_MATRIX.md +++ b/src/modules/overseas-warehouse/API_INTEGRATION_MATRIX.md @@ -15,6 +15,6 @@ 状态映射仅使用服务端已有事实:`PENDING_ASSIGNMENT`(待分配)、`DESTINATION_ASSIGNED`(已分配目的仓)、`INBOUND_PLANNED`(已生成入库计划)。未定义的原型状态不发送到接口。 -菜单、路由与操作按钮使用真实 Controller 权限:`platform:lastleg:*`、`platform:pallet:*`、`platform:warehouse:*` 和 `platform:location:*`。Admin 未接 PDA、扫码、卸柜照片、内部事件及 `allocate/unallocate/pick/ship` 等离仓命令。 +菜单、路由与操作按钮使用真实 Controller 权限:`platform:overseas-warehouse:*`(尾程、入库、换标、库存、仓库、库位和托盘资源)。Admin 未接 PDA、扫码、卸柜照片、内部事件及 `allocate/unallocate/pick/ship` 等离仓命令。 当前正式路由只保留尾程订单、入库单、换标单、托盘管理和库存管理;箱号和产品库本期暂缓,不恢复正式路由。 diff --git a/src/modules/overseas-warehouse/api/dispatches.test.ts b/src/modules/overseas-warehouse/api/dispatches.test.ts new file mode 100644 index 00000000..261f58e9 --- /dev/null +++ b/src/modules/overseas-warehouse/api/dispatches.test.ts @@ -0,0 +1,39 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { overseasHttp } from '@/shared/api/overseasHttp' +import { listDispatchCandidates, listDispatches, preArrangeDispatch } from './dispatches' + +vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn() } })) + +describe('dispatch overseas API', () => { + beforeEach(() => { + vi.mocked(overseasHttp.get).mockReset() + vi.mocked(overseasHttp.post).mockReset() + }) + + it('uses the platform dispatch page without customer scope', async () => { + vi.mocked(overseasHttp.get).mockResolvedValue({ + code: '200', message: 'success', data: { records: [], page: 2, size: 50, total: 0, pages: 0 }, + }) + + await listDispatches({ + page: 2, size: 50, dispatchNo: 'PC-1', + vehiclePlate: 'CA-100', status: 'PENDING_DEPARTURE', fbaAppointmentNo: 'FBA-1', + }) + + expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches', { + params: { + page: 2, size: 50, dispatchNo: 'PC-1', + vehiclePlate: 'CA-100', status: 'PENDING_DEPARTURE', fbaAppointmentNo: 'FBA-1', + }, + }) + }) + + it('loads candidates and creates a pre-arrange dispatch in an explicit customer scope', async () => { + vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: [] }) + vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', message: 'success', data: { id: '1', dispatchNo: 'PC-1' } }) + await listDispatchCandidates({ fbaWarehouseCode: 'ONT8', sourceStage: 'PLAN' }) + await preArrangeDispatch({ planPalletIds: ['190000000000000099'], route: 'LAX -> ONT8' }) + expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/candidates', { params: { fbaWarehouseCode: 'ONT8', sourceStage: 'PLAN' } }) + expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/last-leg/dispatches/pre-arrange', { planPalletIds: ['190000000000000099'], route: 'LAX -> ONT8' }) + }) +}) diff --git a/src/modules/overseas-warehouse/api/dispatches.ts b/src/modules/overseas-warehouse/api/dispatches.ts new file mode 100644 index 00000000..05b5dfe3 --- /dev/null +++ b/src/modules/overseas-warehouse/api/dispatches.ts @@ -0,0 +1,97 @@ +import { overseasHttp } from '@/shared/api/overseasHttp' +import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' +import type { OverseasPageResult } from './lastMileOrders' + +export interface DispatchQuery { + page: number + size: number + dispatchNo?: string + vehiclePlate?: string + status?: string + fbaAppointmentNo?: string +} + +export interface DispatchPalletCandidateDto { + planId: string + planPalletId: string + warehousePalletId?: string | null + orderId: string + overseasWarehouseId?: string | null + overseasWarehouseName?: string | null + orderNo?: string | null + orderRefs?: string | null + inboundCode?: string | null + palletNo?: string | null + destinationWarehouseId?: string | null + destinationWarehouseCode?: string | null + destinationWarehouseName?: string | null + fbaWarehouseCode?: string | null + packageCount?: number | null + volumeCbm?: number | null + sourceStage?: 'PLAN' | 'RECEIVED' | null +} + +export interface DispatchCandidateQuery { + orderNo?: string + fbaWarehouseCode?: string + sourceStage?: 'PLAN' | 'RECEIVED' +} + +export interface PreArrangeDispatchPayload { + planPalletIds: string[] + route?: string + fbaAppointmentNo?: string + fbaAppointmentStatus?: string + vehicleAppointmentStatus?: string +} + +export interface DispatchSummaryDto { + id: string + dispatchNo: string + vehiclePlate?: string | null + driverName?: string | null + driverPhone?: string | null + carrierName?: string | null + orderNos?: string | null + route?: string | null + overseasWarehouseId?: string | null + overseasWarehouseName?: string | null + departAt?: string | null + fbaAppointmentNo?: string | null + fbaAppointmentStatus?: string | null + vehicleAppointmentStatus?: string | null + estimatedArrivalAt?: string | null + status?: string | null + itemCount?: number | null + palletCount?: number | null + packageCount?: number | null + pickedPackageCount?: number | null + shippedPackageCount?: number | null + remainingPackageCount?: number | null + partialCompletionReason?: string | null + createdAt?: string | null +} + +export async function listDispatches(query: DispatchQuery) { + const response = await overseasHttp.get>, ApiEnvelope>>( + '/platform/v1/last-leg/dispatches', + { params: query }, + ) + return unwrapApiResponse(response) +} + +export async function listDispatchCandidates(query: DispatchCandidateQuery) { + const response = await overseasHttp.get, ApiEnvelope>( + '/platform/v1/last-leg/dispatches/candidates', + { params: query }, + ) + return unwrapApiResponse(response) +} + +export async function preArrangeDispatch(payload: PreArrangeDispatchPayload) { + const response = await overseasHttp.post, ApiEnvelope>( + '/platform/v1/last-leg/dispatches/pre-arrange', + payload, + ) + return unwrapApiResponse(response) +} diff --git a/src/modules/overseas-warehouse/api/inboundOrders.test.ts b/src/modules/overseas-warehouse/api/inboundOrders.test.ts index 90ea1ddc..443982d0 100644 --- a/src/modules/overseas-warehouse/api/inboundOrders.test.ts +++ b/src/modules/overseas-warehouse/api/inboundOrders.test.ts @@ -23,11 +23,11 @@ describe('inbound order overseas API', () => { expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/last-leg/inbound-plans/fba-appointments/APT%2F1', expect.objectContaining({ customerId: '190000000000000001' })) }) - it('loads the paginated list with an explicit customer scope', async () => { + it('loads the paginated list across all customers without a customer scope', async () => { vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: { records: [], page: 1, size: 20, total: 0, pages: 0 } }) - await listInboundOrders({ customerId: '190000000000000001', page: 1, size: 20, appointmentStatus: 'UNAPPOINTED' }) + await listInboundOrders({ page: 1, size: 20, appointmentStatus: 'UNAPPOINTED' }) expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/inbound-plans', { - params: { customerId: '190000000000000001', page: 1, size: 20, appointmentStatus: 'UNAPPOINTED' }, + params: { page: 1, size: 20, appointmentStatus: 'UNAPPOINTED' }, }) }) diff --git a/src/modules/overseas-warehouse/api/inboundOrders.ts b/src/modules/overseas-warehouse/api/inboundOrders.ts index 28c29a97..b30db749 100644 --- a/src/modules/overseas-warehouse/api/inboundOrders.ts +++ b/src/modules/overseas-warehouse/api/inboundOrders.ts @@ -3,7 +3,6 @@ import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' import type { OverseasPageResult } from './lastMileOrders' export interface InboundOrderQuery { - customerId: string page: number size: number planNo?: string @@ -68,9 +67,8 @@ export interface FbaAppointmentDto { export type UpdateFbaAppointmentPayload = Omit export async function listInboundOrders(query: InboundOrderQuery) { - const { customerId, ...params } = query const response = await overseasHttp.get>, ApiEnvelope>>( - '/platform/v1/last-leg/inbound-plans', { params: { customerId, ...params } }, + '/platform/v1/last-leg/inbound-plans', { params: query }, ) return unwrapApiResponse(response) } diff --git a/src/modules/overseas-warehouse/api/inventory.test.ts b/src/modules/overseas-warehouse/api/inventory.test.ts index 55c1bc9f..ffe17c8c 100644 --- a/src/modules/overseas-warehouse/api/inventory.test.ts +++ b/src/modules/overseas-warehouse/api/inventory.test.ts @@ -9,22 +9,22 @@ describe('overseas inventory API', () => { it('keeps skuCode and nullable location filters on real balance queries', async () => { vi.mocked(overseasHttp.get).mockResolvedValueOnce({ code: '200', data: { records: [], page: 1, size: 20, total: 0, pages: 0 } }).mockResolvedValueOnce({ code: '200', data: { totalPackages: 0 } }) - const query = { customerId: '100', page: 1, size: 20, skuCode: 'SKU-1', warehouseLocationCode: 'A-01' } + const query = { page: 1, size: 20, skuCode: 'SKU-1', warehouseLocationCode: 'A-01' } await listInventoryBalances(query) await getInventorySummary(query) expect(overseasHttp.get).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/inventory/balances', { params: query }) - expect(overseasHttp.get).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/inventory/summary', { params: { customerId: '100', skuCode: 'SKU-1', warehouseLocationCode: 'A-01' } }) + expect(overseasHttp.get).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/inventory/summary', { params: { skuCode: 'SKU-1', warehouseLocationCode: 'A-01' } }) }) - it('uses the read-only position and ledger endpoints with explicit customer scope', async () => { + it('uses the read-only position and ledger endpoints without customer scope', async () => { vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: [] }) - await listInventoryPositions('190000000000000099', '100') - await listInventoryLedger({ customerId: '100', page: 1, size: 20, skuCode: 'SKU-1' }) + await listInventoryPositions('190000000000000099') + await listInventoryLedger({ page: 1, size: 20, skuCode: 'SKU-1' }) - expect(overseasHttp.get).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/inventory/balances/190000000000000099/positions', { params: { customerId: '100' } }) - expect(overseasHttp.get).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/inventory/ledger', { params: { customerId: '100', page: 1, size: 20, skuCode: 'SKU-1' } }) + expect(overseasHttp.get).toHaveBeenNthCalledWith(1, '/platform/v1/last-leg/inventory/balances/190000000000000099/positions', { params: {} }) + expect(overseasHttp.get).toHaveBeenNthCalledWith(2, '/platform/v1/last-leg/inventory/ledger', { params: { page: 1, size: 20, skuCode: 'SKU-1' } }) }) }) diff --git a/src/modules/overseas-warehouse/api/inventory.ts b/src/modules/overseas-warehouse/api/inventory.ts index d08de140..52e7f7a7 100644 --- a/src/modules/overseas-warehouse/api/inventory.ts +++ b/src/modules/overseas-warehouse/api/inventory.ts @@ -15,10 +15,9 @@ export interface InventoryBalanceDto { inboundOrderId: string inboundOrderNo?: string | null inboundCode: string - skuCodes: string[] - productNames: string[] - warehouseLocationIds: string[] - warehouseLocationCodes: string[] + skuCode?: string | null + productNameZh?: string | null + productNameEn?: string | null totalPackages: number availablePackages: number holdPackages: number @@ -46,7 +45,6 @@ export interface InventoryDashboardDto { export interface InventoryPositionDto { id: string balanceId: string - customerId: string overseasWarehouseId?: string | null destinationWarehouseCode?: string | null inboundOrderId?: string | null @@ -93,7 +91,6 @@ export interface InventoryLedgerDto { } export interface InventoryBalanceQuery { - customerId: string page: number size: number customerName?: string @@ -129,18 +126,16 @@ export async function getInventorySummary(query: InventoryBalanceQuery) { export async function listInventoryPositions( balanceId: string, - customerId: string, query: { palletNo?: string; batchNo?: string; inventoryStatus?: string } = {}, ) { const response = await overseasHttp.get, ApiEnvelope>( `/platform/v1/last-leg/inventory/balances/${encodeURIComponent(balanceId)}/positions`, - { params: { customerId, ...query } }, + { params: query }, ) return unwrapApiResponse(response) } export interface InventoryLedgerQuery { - customerId: string page: number size: number overseasWarehouseId?: string diff --git a/src/modules/overseas-warehouse/api/lastMileOrders.test.ts b/src/modules/overseas-warehouse/api/lastMileOrders.test.ts index 9fe2579f..bf025d63 100644 --- a/src/modules/overseas-warehouse/api/lastMileOrders.test.ts +++ b/src/modules/overseas-warehouse/api/lastMileOrders.test.ts @@ -1,30 +1,36 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { overseasHttp } from '@/shared/api/overseasHttp' -import { getLastMileOrder, listLastMileOrders } from './lastMileOrders' +import { getLastMileOrder, listLastMileOrders, transferLastMileOrders } from './lastMileOrders' -vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn() } })) +vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), put: vi.fn() } })) describe('last-mile order overseas API', () => { beforeEach(() => vi.mocked(overseasHttp.get).mockReset()) - it('uses the gateway-relative platform list endpoint and explicit customer scope', async () => { + it('loads all platform orders without a customer scope', async () => { vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: { records: [], page: 2, size: 20, total: 0, pages: 0 } }) - await expect(listLastMileOrders({ customerId: '190000000000000001', page: 2, size: 20, status: 'PENDING_ASSIGNMENT' })).resolves.toMatchObject({ page: 2 }) + await expect(listLastMileOrders({ page: 2, size: 20, status: 'PENDING_ASSIGNMENT' })).resolves.toMatchObject({ page: 2 }) expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/orders', { - params: { customerId: '190000000000000001', page: 2, size: 20, status: 'PENDING_ASSIGNMENT' }, + params: { page: 2, size: 20, status: 'PENDING_ASSIGNMENT' }, }) }) it('preserves a long order id as a string in the detail path', async () => { vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: { order: { id: '190000000000000099' }, cargo: [], statusHistory: [] } }) - await getLastMileOrder('190000000000000099', '190000000000000001') - expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/orders/190000000000000099', { - params: { customerId: '190000000000000001' }, - }) + await getLastMileOrder('190000000000000099') + expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/orders/190000000000000099') }) it('does not call outbound or delivery endpoints', () => { const source = `${listLastMileOrders}${getLastMileOrder}` expect(source).not.toMatch(/outbound|delivery|dispatch/) }) + + it('uses the real destination transfer endpoint and edit contract', async () => { + vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', message: 'success', data: 1 }) + await transferLastMileOrders({ + orderIds: ['190000000000000099'], warehouseId: 'WH-1', warehouseCode: 'ONT8', warehouseName: 'Ontario', + }) + expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/last-leg/orders/destination', expect.objectContaining({ orderIds: ['190000000000000099'] })) + }) }) diff --git a/src/modules/overseas-warehouse/api/lastMileOrders.ts b/src/modules/overseas-warehouse/api/lastMileOrders.ts index 9ef78674..9e88d1ec 100644 --- a/src/modules/overseas-warehouse/api/lastMileOrders.ts +++ b/src/modules/overseas-warehouse/api/lastMileOrders.ts @@ -10,24 +10,33 @@ export interface OverseasPageResult { } export interface LastMileOrderQuery { - customerId: string page: number size: number orderNo?: string inboundCode?: string containerNo?: string status?: string + sourceOrderNo?: string + transferWarehouseCode?: string + abnormal?: boolean + detained?: boolean + createdFrom?: string + createdTo?: string } export interface LastMileOrderSummaryDto { id: string + customerId: string orderNo: string orderType?: string | null inboundCode?: string | null sourceOrderNo?: string | null + sourceInboundId?: string | null customerName?: string | null overseasWarehouseName?: string | null transferWarehouseName?: string | null + transferWarehouseId?: string | null + transferWarehouseCode?: string | null fbaWarehouseCode?: string | null containerNo?: string | null containerStatus?: string | null @@ -42,8 +51,9 @@ export interface LastMileOrderSummaryDto { actualArrivalAt?: string | null estimatedWarehouseAt?: string | null inboundOrderNo?: string | null - outboundOrderNo?: string | null + /** Legacy read-only association retained for compatibility; absent from current service DTO. */ deliveryOrderNo?: string | null + outboundOrderNo?: string | null relabelOrderNo?: string | null relabelStatus?: string | null palletMergeFlag?: string | null @@ -81,11 +91,18 @@ export interface LastMileStatusHistoryDto { id: string fromStatus?: string | null toStatus?: string | null - eventId?: string | null remark?: string | null createdAt?: string | null } +export interface TransferDestinationPayload { + orderIds: string[] + warehouseId: string + warehouseCode: string + warehouseName: string + remark?: string +} + export interface LastMileOrderDetailDto { order: LastMileOrderDto cargo: LastMileCargoDto[] @@ -93,18 +110,23 @@ export interface LastMileOrderDetailDto { } export async function listLastMileOrders(query: LastMileOrderQuery) { - const { customerId, ...params } = query const response = await overseasHttp.get>, ApiEnvelope>>( '/platform/v1/last-leg/orders', - { params: { customerId, ...params } }, + { params: query }, ) return unwrapApiResponse(response) } -export async function getLastMileOrder(orderId: string, customerId: string) { +export async function getLastMileOrder(orderId: string) { const response = await overseasHttp.get, ApiEnvelope>( `/platform/v1/last-leg/orders/${encodeURIComponent(orderId)}`, - { params: { customerId } }, + ) + return unwrapApiResponse(response) +} + +export async function transferLastMileOrders(payload: TransferDestinationPayload) { + const response = await overseasHttp.put, ApiEnvelope>( + '/platform/v1/last-leg/orders/destination', payload, ) return unwrapApiResponse(response) } diff --git a/src/modules/overseas-warehouse/api/pallets.test.ts b/src/modules/overseas-warehouse/api/pallets.test.ts index ff875b2c..8bca422c 100644 --- a/src/modules/overseas-warehouse/api/pallets.test.ts +++ b/src/modules/overseas-warehouse/api/pallets.test.ts @@ -7,24 +7,26 @@ vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post describe('overseas pallet API', () => { beforeEach(() => { vi.clearAllMocks() }) - it('uses the customer and warehouse scoped pallet CRUD contract', async () => { + it('uses the public warehouse scoped pallet CRUD contract', async () => { const page = { records: [], page: 1, size: 20, total: 0, pages: 0 } vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', data: page }) vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', data: '9' }) vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', data: null }) vi.mocked(overseasHttp.delete).mockResolvedValue({ code: '200', data: null }) - const payload = { palletCode: 'PLT-1', palletType: 'WOOD', capacityCbm: 2, unitPrice: 10, status: 'IDLE' } + const payload = { palletCode: 'PLT-NEW', palletType: 'WOOD', fbaWarehouseCode: 'ONT8', capacityCbm: 2, unitPrice: 10, status: 'IDLE' } - await listCustomerWarehouses('100') - await listPallets('100', '200', { page: 1, size: 20, keyword: 'PLT' }) - await getPallet('100', '200', '9') - await createPallet('100', '200', payload) - await updatePallet('100', '200', '9', payload) - await deletePallet('100', '200', '9') + await listCustomerWarehouses() + await listPallets('200', { page: 1, size: 20, keyword: 'PLT' }) + await getPallet('200', '9') + await createPallet('200', payload) + await updatePallet('200', '9', payload) + await deletePallet('200', '9') - expect(overseasHttp.get).toHaveBeenNthCalledWith(2, '/platform/v1/warehouse-admin/200/pallets', { params: { customerId: '100', page: 1, size: 20, keyword: 'PLT' } }) - expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/warehouse-admin/200/pallets', payload, { params: { customerId: '100' } }) - expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/warehouse-admin/200/pallets/9', payload, { params: { customerId: '100' } }) - expect(overseasHttp.delete).toHaveBeenCalledWith('/platform/v1/warehouse-admin/200/pallets/9', { params: { customerId: '100' } }) + expect(overseasHttp.get).toHaveBeenNthCalledWith(2, '/platform/v1/warehouse-admin/200/pallets', { params: { page: 1, size: 20, keyword: 'PLT' } }) + expect(overseasHttp.get).toHaveBeenNthCalledWith(3, '/platform/v1/warehouse-admin/200/pallets/9', {}) + expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/warehouse-admin/200/pallets', payload) + expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/warehouse-admin/200/pallets/9', payload) + expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/warehouse-admin/200/pallets/9', expect.objectContaining({ palletCode: 'PLT-NEW', fbaWarehouseCode: 'ONT8' })) + expect(overseasHttp.delete).toHaveBeenCalledWith('/platform/v1/warehouse-admin/200/pallets/9', {}) }) }) diff --git a/src/modules/overseas-warehouse/api/pallets.ts b/src/modules/overseas-warehouse/api/pallets.ts index dac0204d..b8281e4f 100644 --- a/src/modules/overseas-warehouse/api/pallets.ts +++ b/src/modules/overseas-warehouse/api/pallets.ts @@ -18,6 +18,7 @@ export interface PalletDto { locationId?: string | null palletCode: string palletType: string + fbaWarehouseCode: string capacityCbm: number unitPrice: number status: string @@ -29,6 +30,7 @@ export interface PalletDto { export interface SavePalletPayload { palletCode: string palletType: string + fbaWarehouseCode: string capacityCbm: number unitPrice: number status: string @@ -36,49 +38,49 @@ export interface SavePalletPayload { remark?: string | null } -export async function listCustomerWarehouses(customerId: string) { +export async function listCustomerWarehouses() { const response = await overseasHttp.get>, ApiEnvelope>>( '/platform/v1/warehouse-admin', - { params: { customerId, page: 1, size: 200, status: 'ACTIVE' } }, + { params: { page: 1, size: 200, status: 'ACTIVE' } }, ) return unwrapApiResponse(response) } -export async function listPallets(customerId: string, warehouseId: string, query: { page: number; size: number; keyword?: string; palletType?: string; status?: string }) { +export async function listPallets(warehouseId: string, query: { page: number; size: number; keyword?: string; palletType?: string; status?: string }) { const response = await overseasHttp.get>, ApiEnvelope>>( `/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets`, - { params: { customerId, ...query } }, + { params: { ...query } }, ) return unwrapApiResponse(response) } -export async function getPallet(customerId: string, warehouseId: string, palletId: string) { +export async function getPallet(warehouseId: string, palletId: string) { const response = await overseasHttp.get, ApiEnvelope>( `/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets/${encodeURIComponent(palletId)}`, - { params: { customerId } }, + {}, ) return unwrapApiResponse(response) } -export async function createPallet(customerId: string, warehouseId: string, payload: SavePalletPayload) { +export async function createPallet(warehouseId: string, payload: SavePalletPayload) { const response = await overseasHttp.post, ApiEnvelope>( - `/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets`, payload, { params: { customerId } }, + `/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets`, payload, ) return unwrapApiResponse(response) } -export async function updatePallet(customerId: string, warehouseId: string, palletId: string, payload: SavePalletPayload) { +export async function updatePallet(warehouseId: string, palletId: string, payload: SavePalletPayload) { const response = await overseasHttp.put, ApiEnvelope>( `/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets/${encodeURIComponent(palletId)}`, - payload, { params: { customerId } }, + payload, ) return unwrapApiResponse(response) } -export async function deletePallet(customerId: string, warehouseId: string, palletId: string) { +export async function deletePallet(warehouseId: string, palletId: string) { const response = await overseasHttp.delete, ApiEnvelope>( `/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/pallets/${encodeURIComponent(palletId)}`, - { params: { customerId } }, + {}, ) return unwrapApiResponse(response) } diff --git a/src/modules/overseas-warehouse/api/relabelOrders.test.ts b/src/modules/overseas-warehouse/api/relabelOrders.test.ts index 08731ccf..eae046ba 100644 --- a/src/modules/overseas-warehouse/api/relabelOrders.test.ts +++ b/src/modules/overseas-warehouse/api/relabelOrders.test.ts @@ -1,21 +1,31 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { overseasHttp } from '@/shared/api/overseasHttp' -import { getRelabelOrder, listRelabelOrders } from './relabelOrders' +import { changeRelabelOrderStatus, createRelabelOrder, getRelabelOrder, listRelabelOrders } from './relabelOrders' -vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn() } })) +vi.mock('@/shared/api/overseasHttp', () => ({ overseasHttp: { get: vi.fn(), post: vi.fn(), put: vi.fn() } })) describe('relabel overseas API', () => { beforeEach(() => vi.mocked(overseasHttp.get).mockReset()) - it('uses explicit customer scope for the real page endpoint', async () => { + it('uses the platform page endpoint without customer scope', async () => { vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: { records: [], page: 1, size: 20, total: 0, pages: 0 } }) - await listRelabelOrders({ customerId: '190000000000000001', page: 1, size: 20, skuCode: 'SKU-1' }) - expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/relabel-applications', { params: { customerId: '190000000000000001', page: 1, size: 20, skuCode: 'SKU-1' } }) + await listRelabelOrders({ page: 1, size: 20, skuCode: 'SKU-1' }) + expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/relabel-applications', { params: { page: 1, size: 20, skuCode: 'SKU-1' } }) }) it('encodes the relabel number and does not call a mutation endpoint', async () => { vi.mocked(overseasHttp.get).mockResolvedValue({ code: '200', message: 'success', data: { application: { id: '1', relabelNo: 'RL/1' }, items: [], history: [] } }) - await getRelabelOrder('RL/1', '190000000000000001') - expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/relabel-applications/RL%2F1', { params: { customerId: '190000000000000001' } }) + await getRelabelOrder('RL/1') + expect(overseasHttp.get).toHaveBeenCalledWith('/platform/v1/last-leg/relabel-applications/RL%2F1') + }) + + it('uses the real create and status endpoints', async () => { + vi.mocked(overseasHttp.post).mockResolvedValue({ code: '200', data: {} }) + vi.mocked(overseasHttp.put).mockResolvedValue({ code: '200', data: {} }) + const payload = { customerOrderNo: 'CO-1', overseasWarehouseId: '9', labelDeadline: '2026-08-20T10:00:00', contactName: 'A', contactPhone: '1', idempotencyKey: 'k', items: [{ skuCode: 'SKU', productNameZh: 'P', totalPackageCount: 1, notifiedPackageCount: 1, shipmentNo: 'S', newSkuCode: 'SKU-2' }] } + await createRelabelOrder(payload) + await changeRelabelOrderStatus('RL-1', 'PROCESSING', 'ok') + expect(overseasHttp.post).toHaveBeenCalledWith('/platform/v1/last-leg/relabel-applications', payload) + expect(overseasHttp.put).toHaveBeenCalledWith('/platform/v1/last-leg/relabel-applications/RL-1/status', { status: 'PROCESSING', remark: 'ok' }) }) }) diff --git a/src/modules/overseas-warehouse/api/relabelOrders.ts b/src/modules/overseas-warehouse/api/relabelOrders.ts index 34496f82..4c768ad7 100644 --- a/src/modules/overseas-warehouse/api/relabelOrders.ts +++ b/src/modules/overseas-warehouse/api/relabelOrders.ts @@ -3,13 +3,40 @@ import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' import type { OverseasPageResult } from './lastMileOrders' export interface RelabelOrderQuery { - customerId: string page: number size: number skuCode?: string status?: string } +export interface CreateRelabelItemPayload { + skuCode: string + productNameZh: string + totalPackageCount: number + notifiedPackageCount: number + shipmentNo: string + newSkuCode: string + labelRequirement?: string + productImageFileId?: string + productImageFileName?: string + productImageUrl?: string + trackingCode?: string + relabelFileId?: string + relabelFileName?: string + relabelFileUrl?: string +} + +export interface CreateRelabelPayload { + customerOrderNo: string + overseasWarehouseId: string + labelDeadline: string + contactName: string + contactPhone: string + remark?: string + idempotencyKey: string + items: CreateRelabelItemPayload[] +} + export interface RelabelSummaryDto { id: string relabelNo: string @@ -56,16 +83,30 @@ export interface RelabelDetailDto { } export async function listRelabelOrders(query: RelabelOrderQuery) { - const { customerId, ...params } = query const response = await overseasHttp.get>, ApiEnvelope>>( - '/platform/v1/last-leg/relabel-applications', { params: { customerId, ...params } }, + '/platform/v1/last-leg/relabel-applications', { params: query }, ) return unwrapApiResponse(response) } -export async function getRelabelOrder(relabelNo: string, customerId: string) { +export async function getRelabelOrder(relabelNo: string) { const response = await overseasHttp.get, ApiEnvelope>( - `/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}`, { params: { customerId } }, + `/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}`, + ) + return unwrapApiResponse(response) +} + +export async function createRelabelOrder(payload: CreateRelabelPayload) { + const response = await overseasHttp.post, ApiEnvelope>( + '/platform/v1/last-leg/relabel-applications', payload, + ) + return unwrapApiResponse(response) +} + +export async function changeRelabelOrderStatus(relabelNo: string, status: string, remark?: string) { + const response = await overseasHttp.put, ApiEnvelope>( + `/platform/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}/status`, + { status, remark }, ) return unwrapApiResponse(response) } diff --git a/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue b/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue new file mode 100644 index 00000000..81750a1a --- /dev/null +++ b/src/modules/overseas-warehouse/components/SmartDispatchDialog.vue @@ -0,0 +1,129 @@ + + + + + diff --git a/src/modules/overseas-warehouse/inventoryPresentation.test.ts b/src/modules/overseas-warehouse/inventoryPresentation.test.ts new file mode 100644 index 00000000..6de47bde --- /dev/null +++ b/src/modules/overseas-warehouse/inventoryPresentation.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, it } from 'vitest' +import { inventoryProductName, toInventoryListItem } from './inventoryPresentation' + +describe('overseas inventory presentation adapter', () => { + it('maps the latest singular balance DTO fields without inventing position data', () => { + const row = toInventoryListItem({ + id: '2089652067743272963', + customerId: '990018', + customerName: 'Acceptance Customer', + overseasWarehouseId: '990018000000000001', + overseasWarehouseName: 'Acceptance US Warehouse', + inboundOrderId: '2089652066950549505', + inboundOrderNo: 'LIP2089652067021852673', + inboundCode: 'ACCEPT-20260818-R4-IN-03B', + skuCode: 'SKU-ACCEPT-1', + productNameZh: '验收商品', + productNameEn: 'Acceptance Product', + totalPackages: 20, + availablePackages: 18, + holdPackages: 2, + allocatedPackages: 0, + pickedPackages: 0, + inTransitPackages: 0, + receivingPackages: 0, + procurementPackages: 0, + updatedAt: '2026-08-18T17:54:11', + }) + + expect(row).toMatchObject({ + id: '2089652067743272963', + customerId: '990018', + sku: 'SKU-ACCEPT-1', + productName: '验收商品', + location: '—', + onHand: 20, + available: 18, + held: 2, + status: '存在暂扣', + }) + }) + + it('falls back to the English product name only when the Chinese name is absent', () => { + expect(inventoryProductName({ + id: '1', customerId: '2', overseasWarehouseId: '3', inboundOrderId: '4', inboundCode: 'IN-1', + productNameEn: 'English Product', totalPackages: 0, availablePackages: 0, holdPackages: 0, + allocatedPackages: 0, pickedPackages: 0, inTransitPackages: 0, receivingPackages: 0, + procurementPackages: 0, + })).toBe('English Product') + }) +}) diff --git a/src/modules/overseas-warehouse/inventoryPresentation.ts b/src/modules/overseas-warehouse/inventoryPresentation.ts new file mode 100644 index 00000000..bd962467 --- /dev/null +++ b/src/modules/overseas-warehouse/inventoryPresentation.ts @@ -0,0 +1,45 @@ +import type { InventoryBalanceDto } from './api/inventory' +import type { InventoryListItem } from './types' + +function text(value: unknown) { + const normalized = String(value ?? '').trim() + return normalized || '—' +} + +function count(value: unknown) { + const normalized = Number(value) + return Number.isFinite(normalized) ? normalized : 0 +} + +export function inventoryProductName(dto: InventoryBalanceDto) { + return text(dto.productNameZh || dto.productNameEn) +} + +export function inventoryStatusLabel(dto: InventoryBalanceDto) { + if (count(dto.holdPackages) > 0) return '存在暂扣' + if (count(dto.availablePackages) <= 0) return '无可用库存' + return '正常' +} + +export function toInventoryListItem(dto: InventoryBalanceDto): InventoryListItem { + return { + id: String(dto.id), + customerId: String(dto.customerId), + warehouseId: String(dto.overseasWarehouseId || ''), + inboundOrderNo: text(dto.inboundOrderNo) === '—' ? '' : String(dto.inboundOrderNo), + inboundCode: text(dto.inboundCode) === '—' ? '' : String(dto.inboundCode), + owner: text(dto.customerName), + warehouse: text(dto.overseasWarehouseName || dto.overseasWarehouseCode), + sku: text(dto.skuCode), + productName: inventoryProductName(dto), + location: '—', + onHand: count(dto.totalPackages), + available: count(dto.availablePackages), + held: count(dto.holdPackages), + looseboard: '—', + inTransit: count(dto.inTransitPackages), + purchase: count(dto.procurementPackages), + status: inventoryStatusLabel(dto), + updatedAt: text(dto.updatedAt), + } +} diff --git a/src/modules/overseas-warehouse/lastMilePresentation.test.ts b/src/modules/overseas-warehouse/lastMilePresentation.test.ts index 66424633..f25ac8d3 100644 --- a/src/modules/overseas-warehouse/lastMilePresentation.test.ts +++ b/src/modules/overseas-warehouse/lastMilePresentation.test.ts @@ -4,9 +4,9 @@ import { toLastMileOrderListItem } from './lastMilePresentation' describe('last-mile order presentation adapter', () => { it('maps backend codes, associations and long ids without inventing data', () => { const row = toLastMileOrderListItem({ - id: '190000000000000099', orderNo: 'LL-001', orderType: 'ONE_CLICK_DROPSHIP', currentStatus: 'PENDING_ASSIGNMENT', + 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', - }, '190000000000000001') + }) expect(row).toMatchObject({ id: '190000000000000099', customerId: '190000000000000001', orderType: '一件代发', status: '待分配', warning: '异常', abnormal: '是', held: '否', cartons: 12, dispatchOrderNo: 'DEL-READ-ONLY', outboundOrderNo: 'OUT-READ-ONLY', diff --git a/src/modules/overseas-warehouse/lastMilePresentation.ts b/src/modules/overseas-warehouse/lastMilePresentation.ts index 57e25012..c9b82907 100644 --- a/src/modules/overseas-warehouse/lastMilePresentation.ts +++ b/src/modules/overseas-warehouse/lastMilePresentation.ts @@ -48,10 +48,10 @@ export function formatOverseasDateTime(value: unknown) { return Number.isNaN(date.getTime()) ? raw : date.toLocaleString('zh-CN', { hour12: false }) } -export function toLastMileOrderListItem(dto: LastMileOrderSummaryDto, customerId: string): LastMileOrderListItem { +export function toLastMileOrderListItem(dto: LastMileOrderSummaryDto): LastMileOrderListItem { return { id: String(dto.id), - customerId, + customerId: String(dto.customerId), orderNo: text(dto.orderNo), warning: dto.abnormal ? '异常' : '—', orderType: lastMileOrderTypeLabel(dto.orderType), diff --git a/src/modules/overseas-warehouse/pages/DispatchesView.test.ts b/src/modules/overseas-warehouse/pages/DispatchesView.test.ts new file mode 100644 index 00000000..a8b43e3e --- /dev/null +++ b/src/modules/overseas-warehouse/pages/DispatchesView.test.ts @@ -0,0 +1,84 @@ +import ElementPlus from 'element-plus' +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' +import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import QueryToolbar, { type QueryModel } from '@/shared/components/QueryToolbar.vue' +import { listDispatches } from '../api/dispatches' +import DispatchesView from './DispatchesView.vue' + +vi.mock('../api/dispatches', () => ({ listDispatches: vi.fn() })) + +class ResizeObserverStub implements ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +} + +function memoryStorage(): Storage { + const values = new Map() + return { + get length() { return values.size }, + clear: () => values.clear(), + getItem: (key) => values.get(key) ?? null, + key: (index) => [...values.keys()][index] ?? null, + removeItem: (key) => values.delete(key), + setItem: (key, value) => values.set(key, value), + } +} + +enableAutoUnmount(afterEach) +afterAll(() => vi.unstubAllGlobals()) + +describe('DispatchesView', () => { + beforeEach(() => { + vi.stubGlobal('ResizeObserver', ResizeObserverStub) + vi.stubGlobal('localStorage', memoryStorage()) + vi.mocked(listDispatches).mockReset().mockResolvedValue({ + records: [{ + id: '190000000000000099', dispatchNo: 'PC-1', vehiclePlate: 'CA-100', driverName: '张师傅', + driverPhone: '555-0100', carrierName: 'Carrier A', orderNos: 'LM-1,LM-2', route: 'LAX -> ONT8', + overseasWarehouseName: '洛杉矶仓', departAt: '2026-08-20T09:00:00', fbaAppointmentNo: 'FBA-1', + fbaAppointmentStatus: 'CONFIRMED', vehicleAppointmentStatus: 'BOOKED', estimatedArrivalAt: '2026-08-20T18:00:00', + status: 'PENDING_DEPARTURE', palletCount: 12, packageCount: 80, createdAt: '2026-08-19T16:00:00', + }], + page: 1, size: 20, total: 1, pages: 1, + }) + }) + + it('loads immediately and sends only supported page filters', async () => { + const wrapper = mount(DispatchesView, { global: { plugins: [ElementPlus] } }) + await flushPromises() + expect(listDispatches).toHaveBeenCalledWith({ page: 1, size: 20, dispatchNo: undefined, vehiclePlate: undefined, status: undefined, fbaAppointmentNo: undefined }) + + const model: QueryModel = { + dispatchNo: 'PC-1', vehiclePlate: 'CA-100', + status: 'PENDING_DEPARTURE', fbaAppointmentNo: 'FBA-1', + } + wrapper.findComponent(QueryToolbar).vm.$emit('search', model) + await flushPromises() + + expect(listDispatches).toHaveBeenCalledWith({ + page: 1, size: 20, dispatchNo: 'PC-1', + vehiclePlate: 'CA-100', status: 'PENDING_DEPARTURE', fbaAppointmentNo: 'FBA-1', + }) + expect(wrapper.text()).toContain('PC-1') + expect(wrapper.text()).toContain('待发车') + expect(wrapper.text()).toContain('FBA-1 · CONFIRMED') + }) + + it('keeps long dispatch identifiers as strings', async () => { + vi.mocked(listDispatches).mockResolvedValueOnce({ + records: [{ id: '190000000000000099', dispatchNo: '190000000000000099' }], + page: 1, size: 20, total: 1, pages: 1, + }) + const wrapper = mount(DispatchesView, { global: { plugins: [ElementPlus] } }) + await flushPromises() + expect(wrapper.html()).toContain('190000000000000099') + }) + + it('documents the backend feature flag required by a missing endpoint', () => { + const source = readFileSync(resolve(process.cwd(), 'src/modules/overseas-warehouse/pages/DispatchesView.vue'), 'utf8') + expect(source).toContain('请确认海外尾程排车接口已发布') + }) +}) diff --git a/src/modules/overseas-warehouse/pages/DispatchesView.vue b/src/modules/overseas-warehouse/pages/DispatchesView.vue new file mode 100644 index 00000000..9f9ed5b3 --- /dev/null +++ b/src/modules/overseas-warehouse/pages/DispatchesView.vue @@ -0,0 +1,148 @@ + + + diff --git a/src/modules/overseas-warehouse/pages/InboundOrdersView.vue b/src/modules/overseas-warehouse/pages/InboundOrdersView.vue index 7bdd59b8..e30ffea3 100644 --- a/src/modules/overseas-warehouse/pages/InboundOrdersView.vue +++ b/src/modules/overseas-warehouse/pages/InboundOrdersView.vue @@ -1,10 +1,9 @@ diff --git a/src/modules/overseas-warehouse/pages/InventoryDetailView.vue b/src/modules/overseas-warehouse/pages/InventoryDetailView.vue index 2ae70227..3a4ef981 100644 --- a/src/modules/overseas-warehouse/pages/InventoryDetailView.vue +++ b/src/modules/overseas-warehouse/pages/InventoryDetailView.vue @@ -3,12 +3,12 @@ import { computed, onMounted, ref } from 'vue' import { useRoute, useRouter } from 'vue-router' import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'; import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'; import LoadErrorState from '@/shared/components/LoadErrorState.vue' -import { listInventoryBalances, listInventoryLedger, listInventoryPositions, type InventoryBalanceDto, type InventoryLedgerDto, type InventoryPositionDto } from '../api/inventory'; import { formatOverseasDateTime } from '../lastMilePresentation'; import OverseasDetailShell from '../components/OverseasDetailShell.vue' +import { listInventoryBalances, listInventoryLedger, listInventoryPositions, type InventoryBalanceDto, type InventoryLedgerDto, type InventoryPositionDto } from '../api/inventory'; import { inventoryProductName } from '../inventoryPresentation'; import { formatOverseasDateTime } from '../lastMilePresentation'; import OverseasDetailShell from '../components/OverseasDetailShell.vue' -const route = useRoute(); const router = useRouter(); const inventoryId = computed(() => String(route.params.inventoryId || '')); const customerId = computed(() => String(route.query.customerId || '')) +const route = useRoute(); const router = useRouter(); const inventoryId = computed(() => String(route.params.inventoryId || '')) const balance = ref(null); const positions = ref([]); const ledger = ref([]); const loading = ref(false); const loadError = ref(null) function text(value: unknown) { return String(value ?? '').trim() || '—' } -const productValues = computed(() => ({ 客户: text(balance.value?.customerName), 海外仓: text(balance.value?.overseasWarehouseName), SKU编码: balance.value?.skuCodes?.length ? balance.value.skuCodes.join('、') : '—', 产品名称: balance.value?.productNames?.length ? balance.value.productNames.join('、') : '—', 更新时间: formatOverseasDateTime(balance.value?.updatedAt) })) +const productValues = computed(() => ({ 客户: text(balance.value?.customerName), 海外仓: text(balance.value?.overseasWarehouseName), SKU编码: text(balance.value?.skuCode), 产品名称: balance.value ? inventoryProductName(balance.value) : '—', 更新时间: formatOverseasDateTime(balance.value?.updatedAt) })) const stockValues = computed(() => ({ 在库总量: Number(balance.value?.totalPackages || 0), 可用库存: Number(balance.value?.availablePackages || 0), 暂扣库存: Number(balance.value?.holdPackages || 0), 美本土散版: '—', 在途库存: Number(balance.value?.inTransitPackages || 0), 采购库存: Number(balance.value?.procurementPackages || 0), 预计总量: Number(balance.value?.totalPackages || 0) + Number(balance.value?.inTransitPackages || 0) + Number(balance.value?.procurementPackages || 0) })) const productFields = ['客户', '海外仓', 'SKU编码', '产品名称', '更新时间']; const stockFields = ['在库总量', '可用库存', '暂扣库存', '美本土散版', '在途库存', '采购库存', '预计总量'] const locationRows = computed(() => positions.value.map((item) => ({ id: String(item.id), location: text(item.warehouseLocationCode), status: text(item.status), palletNo: text(item.palletNo), cartons: Number(item.totalPackages || 0), quantity: Number(item.totalPackages || 0), available: Number(item.availablePackages || 0), inboundDate: formatOverseasDateTime(item.updatedAt), ageDays: '—' }))) @@ -17,7 +17,7 @@ const ledgerRows = computed(() => ledger.value.map((item) => ({ id: String(item. const locationColumns: DataTableColumn[] = [{ key: 'location', prop: 'location', label: '库位', minWidth: 130 }, { key: 'status', prop: 'status', label: '库存状态', width: 120 }, { key: 'palletNo', prop: 'palletNo', label: '托盘号', minWidth: 150 }, { key: 'cartons', prop: 'cartons', label: '箱数', width: 90, align: 'right' }, { key: 'quantity', prop: 'quantity', label: '库存数量', width: 110, align: 'right' }, { key: 'available', prop: 'available', label: '可用数量', width: 110, align: 'right' }, { key: 'inboundDate', prop: 'inboundDate', label: '最近更新', width: 160 }, { key: 'ageDays', prop: 'ageDays', label: '库龄(天)', width: 100, align: 'right' }] const palletColumns: DataTableColumn[] = [{ key: 'palletNo', prop: 'palletNo', label: '托盘号', minWidth: 150 }, { key: 'cartons', prop: 'cartons', label: '关联箱数', width: 110, align: 'right' }, { key: 'cartonRange', prop: 'cartonRange', label: '箱号范围', minWidth: 200 }, { key: 'location', prop: 'location', label: '库位', minWidth: 130 }, { key: 'quantity', prop: 'quantity', label: '库存数量', width: 110, align: 'right' }] const ledgerColumns: DataTableColumn[] = [{ key: 'occurredAt', prop: 'occurredAt', label: '发生时间', minWidth: 160 }, { key: 'businessType', prop: 'businessType', label: '业务类型', width: 120 }, { key: 'businessOrderNo', prop: 'businessOrderNo', label: '业务订单号', minWidth: 170 }, { key: 'inboundOrderNo', prop: 'inboundOrderNo', label: '入库单号', minWidth: 160 }, { key: 'outboundOrderNo', prop: 'outboundOrderNo', label: '出库单号', minWidth: 160 }, { key: 'location', prop: 'location', label: '库位', minWidth: 120 }, { key: 'inboundQuantity', prop: 'inboundQuantity', label: '入库数量', width: 110, align: 'right' }, { key: 'outboundQuantity', prop: 'outboundQuantity', label: '出库数量', width: 110, align: 'right' }, { key: 'balance', prop: 'balance', label: '结存', width: 100, align: 'right' }, { key: 'operator', prop: 'operator', label: '操作人', minWidth: 120 }, { key: 'remark', prop: 'remark', label: '备注', minWidth: 180 }] -async function load() { if (!inventoryId.value || !customerId.value) { loadError.value = { message: '详情缺少客户范围,请从库存列表重新进入' }; return }; loading.value = true; loadError.value = null; try { const inboundOrderNo = String(route.query.inboundOrderNo || ''); const inboundCode = String(route.query.inboundCode || ''); const skuCode = String(route.query.skuCode || ''); const overseasWarehouseId = String(route.query.overseasWarehouseId || ''); const result = await listInventoryBalances({ customerId: customerId.value, page: 1, size: 200, inboundOrderNo: inboundOrderNo || undefined, inboundCode: inboundCode || undefined, skuCode: skuCode || undefined, overseasWarehouseId: overseasWarehouseId || undefined }); balance.value = result.records.find((item) => String(item.id) === inventoryId.value) || null; if (!balance.value) throw new Error('Inventory balance not found'); const [positionRows, ledgerPage] = await Promise.all([listInventoryPositions(inventoryId.value, customerId.value), listInventoryLedger({ customerId: customerId.value, page: 1, size: 200, inboundOrderNo: balance.value.inboundOrderNo || undefined, inboundCode: balance.value.inboundCode || undefined })]); positions.value = positionRows; ledger.value = ledgerPage.records } catch (error) { loadError.value = getApiErrorDetails(error, '库存详情加载失败,请重试') } finally { loading.value = false } } +async function load() { if (!inventoryId.value) { loadError.value = { message: '详情缺少库存记录编号,请从库存列表重新进入' }; return }; loading.value = true; loadError.value = null; try { const inboundOrderNo = String(route.query.inboundOrderNo || ''); const inboundCode = String(route.query.inboundCode || ''); const skuCode = String(route.query.skuCode || ''); const overseasWarehouseId = String(route.query.overseasWarehouseId || ''); const result = await listInventoryBalances({ page: 1, size: 200, inboundOrderNo: inboundOrderNo || undefined, inboundCode: inboundCode || undefined, skuCode: skuCode || undefined, overseasWarehouseId: overseasWarehouseId || undefined }); balance.value = result.records.find((item) => String(item.id) === inventoryId.value) || null; if (!balance.value) throw new Error('Inventory balance not found'); const [positionRows, ledgerPage] = await Promise.all([listInventoryPositions(inventoryId.value), listInventoryLedger({ page: 1, size: 200, inboundOrderNo: balance.value.inboundOrderNo || undefined, inboundCode: balance.value.inboundCode || undefined })]); positions.value = positionRows; ledger.value = ledgerPage.records } catch (error) { loadError.value = getApiErrorDetails(error, '库存详情加载失败,请重试') } finally { loading.value = false } } function back() { void router.push({ name: 'overseas-inventory' }) }; onMounted(load) diff --git a/src/modules/overseas-warehouse/pages/InventoryTransactionsView.vue b/src/modules/overseas-warehouse/pages/InventoryTransactionsView.vue index b485b512..9f94cb87 100644 --- a/src/modules/overseas-warehouse/pages/InventoryTransactionsView.vue +++ b/src/modules/overseas-warehouse/pages/InventoryTransactionsView.vue @@ -2,15 +2,14 @@ import { computed, onMounted, ref } from 'vue' import { ArrowLeft } from '@lucide/vue' import { useRoute, useRouter } from 'vue-router' -import { listPlatformTenants, type PlatformTenantResource } from '@/modules/iam' import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http' import AdminPage from '@/shared/components/AdminPage.vue'; import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'; import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'; import ListPageLayout from '@/shared/components/ListPageLayout.vue'; import LoadErrorState from '@/shared/components/LoadErrorState.vue'; import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue' import { listInventoryLedger } from '../api/inventory'; import { formatOverseasDateTime } from '../lastMilePresentation'; import type { InventoryTransactionListItem } from '../types' -const route = useRoute(); const router = useRouter(); const rows = ref([]); const customers = ref([]); const page = ref(1); const pageSize = ref(20); const total = ref(0); const loading = ref(false); const customersLoading = ref(false); const loadError = ref(null); const customersError = ref(null) -const filters = ref({ occurredDate: [], customerId: String(route.query.customerId || ''), warehouse: '', sku: '', businessType: '', businessOrderNo: '', inboundOrderNo: '', outboundOrderNo: '' }) +const route = useRoute(); const router = useRouter(); const rows = ref([]); const page = ref(1); const pageSize = ref(20); const total = ref(0); const loading = ref(false); const loadError = ref(null) +const filters = ref({ occurredDate: [], warehouse: '', sku: '', businessType: '', businessOrderNo: '', inboundOrderNo: '', outboundOrderNo: '' }) const fields = computed(() => [ - { key: 'occurredDate', type: 'daterange', label: '发生日期', width: 260 }, { key: 'customerId', type: 'select', label: '客户名称', placeholder: '请选择客户', width: 240, props: { filterable: true, clearable: true, loading: customersLoading.value }, options: customers.value.map((item) => ({ label: item.tenantName, value: String(item.id) })) }, + { key: 'occurredDate', type: 'daterange', label: '发生日期', width: 260 }, { key: 'warehouse', type: 'input', label: '目的仓代码', placeholder: '请输入目的仓代码', width: 190 }, { key: 'sku', type: 'input', label: 'SKU编码', placeholder: '请输入 SKU 编码', width: 170 }, { key: 'businessType', type: 'input', label: '业务类型', placeholder: '请输入业务类型代码', width: 170 }, { key: 'businessOrderNo', type: 'input', label: '业务订单号', placeholder: '请输入业务订单号', width: 180 }, { key: 'inboundOrderNo', type: 'input', label: '入库单号', placeholder: '请输入入库单号', width: 180 }, { key: 'outboundOrderNo', type: 'input', label: '出库单号', placeholder: '只读关联筛选', width: 180 }, @@ -23,15 +22,13 @@ const columns: DataTableColumn[] = [ { key: 'operator', prop: 'operator', label: '操作人', minWidth: 120 }, { key: 'remark', prop: 'remark', label: '备注', minWidth: 180 }, ] function text(value: unknown) { return String(value ?? '').trim() || '—' } -function customerName(id: string) { return customers.value.find((item) => String(item.id) === id)?.tenantName || '—' } -async function loadCustomers() { customersLoading.value = true; customersError.value = null; try { const records: PlatformTenantResource[] = []; let current = 1; let pages = 1; do { const result = await listPlatformTenants({ page: current, size: 200, status: 'ACTIVE' }); records.push(...result.records); pages = Math.max(1, result.pages); current += 1 } while (current <= pages); customers.value = records; if (filters.value.customerId) void load() } catch (error) { customersError.value = getApiErrorDetails(error, '客户列表加载失败,请重试') } finally { customersLoading.value = false } } -async function load(model: QueryModel = filters.value) { Object.assign(filters.value, model); const customerId = String(filters.value.customerId || '').trim(); if (!customerId) { rows.value = []; total.value = 0; loadError.value = null; return }; const range = Array.isArray(filters.value.occurredDate) ? filters.value.occurredDate.map(String) : []; loading.value = true; loadError.value = null; try { const result = await listInventoryLedger({ customerId, page: page.value, size: pageSize.value, destinationWarehouseCode: String(filters.value.warehouse || '').trim() || undefined, skuCode: String(filters.value.sku || '').trim() || undefined, businessType: String(filters.value.businessType || '').trim() || undefined, businessOrderNo: String(filters.value.businessOrderNo || '').trim() || undefined, inboundOrderNo: String(filters.value.inboundOrderNo || '').trim() || undefined, outboundOrderNo: String(filters.value.outboundOrderNo || '').trim() || undefined, occurredFrom: range[0] ? `${range[0]}T00:00:00` : undefined, occurredTo: range[1] ? `${range[1]}T23:59:59` : undefined }); rows.value = result.records.map((item) => ({ id: String(item.id), occurredAt: formatOverseasDateTime(item.occurredAt), owner: customerName(customerId), warehouse: text(item.destinationWarehouseCode), sku: text(item.skuCode), productName: '—', businessType: text(item.businessType), businessOrderNo: text(item.businessOrderNo), inboundOrderNo: text(item.inboundOrderNo), outboundOrderNo: text(item.outboundOrderNo), inboundQuantity: item.deltaPackages > 0 ? item.deltaPackages : 0, outboundQuantity: item.deltaPackages < 0 ? Math.abs(item.deltaPackages) : 0, balance: Number(item.afterPackages || 0), operator: text(item.operatorId), remark: text(item.remark) })); total.value = result.total } catch (error) { rows.value = []; total.value = 0; loadError.value = getApiErrorDetails(error, '进销存记录加载失败,请重试') } finally { loading.value = false } } -function search(model: QueryModel) { page.value = 1; void load(model) }; function reset(model: QueryModel) { page.value = 1; void load(model) }; function back() { void router.push({ name: 'overseas-inventory' }) }; onMounted(loadCustomers) +async function load(model: QueryModel = filters.value) { Object.assign(filters.value, model); const range = Array.isArray(filters.value.occurredDate) ? filters.value.occurredDate.map(String) : []; loading.value = true; loadError.value = null; try { const result = await listInventoryLedger({ page: page.value, size: pageSize.value, destinationWarehouseCode: String(filters.value.warehouse || '').trim() || undefined, skuCode: String(filters.value.sku || '').trim() || undefined, businessType: String(filters.value.businessType || '').trim() || undefined, businessOrderNo: String(filters.value.businessOrderNo || '').trim() || undefined, inboundOrderNo: String(filters.value.inboundOrderNo || '').trim() || undefined, outboundOrderNo: String(filters.value.outboundOrderNo || '').trim() || undefined, occurredFrom: range[0] ? `${range[0]}T00:00:00` : undefined, occurredTo: range[1] ? `${range[1]}T23:59:59` : undefined }); rows.value = result.records.map((item) => ({ id: String(item.id), occurredAt: formatOverseasDateTime(item.occurredAt), owner: text(item.customerId), warehouse: text(item.destinationWarehouseCode), sku: text(item.skuCode), productName: '—', businessType: text(item.businessType), businessOrderNo: text(item.businessOrderNo), inboundOrderNo: text(item.inboundOrderNo), outboundOrderNo: text(item.outboundOrderNo), inboundQuantity: item.deltaPackages > 0 ? item.deltaPackages : 0, outboundQuantity: item.deltaPackages < 0 ? Math.abs(item.deltaPackages) : 0, balance: Number(item.afterPackages || 0), operator: text(item.operatorId), remark: text(item.remark) })); total.value = result.total } catch (error) { rows.value = []; total.value = 0; loadError.value = getApiErrorDetails(error, '进销存记录加载失败,请重试') } finally { loading.value = false } } +function search(model: QueryModel) { page.value = 1; void load(model) }; function reset(model: QueryModel) { page.value = 1; void load(model) }; function back() { void router.push({ name: 'overseas-inventory' }) }; onMounted(load) diff --git a/src/modules/overseas-warehouse/pages/InventoryView.vue b/src/modules/overseas-warehouse/pages/InventoryView.vue index 857d9734..9eed9838 100644 --- a/src/modules/overseas-warehouse/pages/InventoryView.vue +++ b/src/modules/overseas-warehouse/pages/InventoryView.vue @@ -1,8 +1,8 @@