diff --git a/src/core/navigation/adminNavigation.ts b/src/core/navigation/adminNavigation.ts index 63f677a5..63c0bb50 100644 --- a/src/core/navigation/adminNavigation.ts +++ b/src/core/navigation/adminNavigation.ts @@ -112,6 +112,7 @@ export const adminNavigation: AdminNavigationNode[] = [ children: [ { 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: 'overseas-locations', title: '库位管理', icon: WarehouseIcon, path: '/overseas-warehouse/locations', permission: 'platform:overseas-warehouse:location:list' }, { key: 'fba-dispatch', title: '排车列表', icon: Truck, path: '/overseas-warehouse/dispatches', permission: 'platform:overseas-warehouse:dispatch:list' }, { key: 'fba-outbound', title: '出库单', icon: ClipboardList, path: '/overseas-warehouse/outbounds', permission: 'platform:overseas-warehouse:outbound:list' }, { key: 'fba-delivery', title: '派送单列表', icon: Truck, path: '/overseas-warehouse/delivery-orders', permission: 'platform:overseas-warehouse:delivery:list' }, diff --git a/src/modules/overseas-warehouse/api/inboundPlans.ts b/src/modules/overseas-warehouse/api/inboundPlans.ts index 4593bfce..dc9c5d3d 100644 --- a/src/modules/overseas-warehouse/api/inboundPlans.ts +++ b/src/modules/overseas-warehouse/api/inboundPlans.ts @@ -53,6 +53,7 @@ export interface InboundPlanDetailDto { customerName?: string | null orderType?: string | null containerNo?: string | null + overseasWarehouseId?: string | null overseasWarehouseCode?: string | null overseasWarehouseName?: string | null status?: string | null @@ -67,7 +68,7 @@ export interface InboundPlanDetailDto { updatedAt?: string | null remark?: string | null lines: Array<{ id: string; orderId: string; inboundCode?: string | null; fbaWarehouseCode?: string | null; destinationWarehouseId?: string | null; destinationWarehouseCode?: string | null; destinationWarehouseName?: string | null; grossWeightKg?: number | null; netWeightKg?: number | null; volumeCbm?: number | null; requiredPackageCount: number; plannedPalletCount: number }> - pallets: Array<{ id: string; palletNo?: string | null; destinationWarehouseId?: string | null; destinationWarehouseCode?: string | null; destinationWarehouseName?: string | null; capacityCbm?: number | null; allocatedVolumeCbm?: number | null; sequenceNo: number; warehousePalletId?: string | null; warehouseLocationId?: string | null; warehouseResourceLockId?: string | null; assignmentStatus?: string | null; lockedAt?: string | null; actualVolumeCbm?: number | null; actualPackageCount: number }> + pallets: Array<{ id: string; palletNo?: string | null; destinationWarehouseId?: string | null; destinationWarehouseCode?: string | null; destinationWarehouseName?: string | null; capacityCbm?: number | null; allocatedVolumeCbm?: number | null; sequenceNo: number; warehousePalletId?: string | null; warehouseLocationId?: string | null; warehouseLocationCode?: string | null; warehouseLocationType?: string | null; warehouseLocationStatus?: string | null; sharedOverflow?: boolean | null; assignmentStatus?: string | null; lockedAt?: string | null; actualVolumeCbm?: number | null; actualPackageCount: number; version?: number }> allocations: Array<{ lineId: string; palletId: string; palletNo: string; palletOwnerPlanNo: string; palletOwnedByCurrentPlan: boolean; allocatedVolumeCbm?: number | null; plannedPackageCount: number; countedPackageCount: number; pickedPackageCount: number; actualVolumeCbm?: number | null; status?: string | null; version: number }> } @@ -79,3 +80,27 @@ export async function listInboundPlans(query: InboundPlanQuery) { export async function getInboundPlan(planNo: string, customerId: string) { return unwrapApiResponse(await overseasHttp.get, ApiEnvelope>(`/platform/v1/last-leg/inbound-plans/${encodeURIComponent(planNo)}`, { params: { customerId } })) } + +export interface PlatformInventoryMoveRequest { + overseasWarehouseId: string + sourcePalletCode: string + targetLocationCode: string + expectedPalletVersion: number + reason: string + idempotencyKey: string +} + +export interface PlatformInventoryMoveResult { + commandId: string + status: string + canMove?: boolean + blockReason?: string | null + sourceLocationCode?: string | null + targetLocationCode?: string | null + sharedOverflow?: boolean | null +} + +export async function moveInboundPallet(payload: PlatformInventoryMoveRequest) { + const response = await overseasHttp.post, ApiEnvelope>('/platform/v1/last-leg/inventory-moves', payload, { headers: { 'Idempotency-Key': payload.idempotencyKey } }) + return unwrapApiResponse(response) +} diff --git a/src/modules/overseas-warehouse/api/locations.ts b/src/modules/overseas-warehouse/api/locations.ts new file mode 100644 index 00000000..c575f255 --- /dev/null +++ b/src/modules/overseas-warehouse/api/locations.ts @@ -0,0 +1,64 @@ +import { overseasHttp } from '@/shared/api/overseasHttp' +import { unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http' +import { normalizeOverseasPage, type OverseasPagePayload } from './lastMileOrders' + +export type OverseasLocationType = 'VIRTUAL' | 'NORMAL' | 'SHARED_OVERFLOW' +export type OverseasLocationStatus = 'ACTIVE' | 'IDLE' | 'OCCUPIED' | 'INACTIVE' | 'DISABLED' + +export interface OverseasLocationDto { + id: string + warehouseId: string + warehouseCode?: string | null + warehouseName?: string | null + zoneCode: string + zoneName?: string | null + locationCode: string + locationName?: string | null + locationType: OverseasLocationType + sharedOverflow: boolean + status: OverseasLocationStatus + maxLoadKg?: number | string | null + occupiedPalletCount?: number | null + version: number + remark?: string | null + createdAt?: string | null + updatedAt?: string | null +} + +export interface SaveOverseasLocationPayload { + zoneCode: string + locationCode: string + locationType: 'NORMAL' + maxLoadKg: number + status: 'ACTIVE' | 'INACTIVE' + remark?: string | null +} + +export interface OverseasLocationQuery { page?: number; size?: number; zoneCode?: string; keyword?: string; locationType?: OverseasLocationType | ''; status?: OverseasLocationStatus | '' } + +function base(warehouseId: string) { return `/platform/v1/warehouse-admin/${encodeURIComponent(warehouseId)}/locations` } + +export async function listOverseasLocations(warehouseId: string, query: OverseasLocationQuery = {}) { + const response = await overseasHttp.get>, ApiEnvelope>>(base(warehouseId), { params: { page: query.page ?? 1, size: query.size ?? 20, ...query } }) + return normalizeOverseasPage(unwrapApiResponse(response)) +} + +export async function createOverseasLocation(warehouseId: string, payload: SaveOverseasLocationPayload) { + const response = await overseasHttp.post, ApiEnvelope>(base(warehouseId), payload) + return unwrapApiResponse(response) +} + +export async function updateOverseasLocation(warehouseId: string, locationId: string, payload: SaveOverseasLocationPayload & { version: number }) { + const response = await overseasHttp.put, ApiEnvelope>(`${base(warehouseId)}/${encodeURIComponent(locationId)}`, payload) + return unwrapApiResponse(response) +} + +export async function changeOverseasLocationStatus(warehouseId: string, locationId: string, payload: SaveOverseasLocationPayload & { version: number }) { + const response = await overseasHttp.put, ApiEnvelope>(`${base(warehouseId)}/${encodeURIComponent(locationId)}`, payload) + return unwrapApiResponse(response) +} + +export async function deleteOverseasLocation(warehouseId: string, locationId: string, version: number) { + const response = await overseasHttp.delete, ApiEnvelope>(`${base(warehouseId)}/${encodeURIComponent(locationId)}`, { params: { version } }) + return unwrapApiResponse(response) +} diff --git a/src/modules/overseas-warehouse/pages/InboundOrderDetailView.vue b/src/modules/overseas-warehouse/pages/InboundOrderDetailView.vue index 84257703..5a95845c 100644 --- a/src/modules/overseas-warehouse/pages/InboundOrderDetailView.vue +++ b/src/modules/overseas-warehouse/pages/InboundOrderDetailView.vue @@ -1,12 +1,15 @@ + + + + diff --git a/src/modules/overseas-warehouse/pages/prototypeContract.test.ts b/src/modules/overseas-warehouse/pages/prototypeContract.test.ts index 6057670b..66f1bfc5 100644 --- a/src/modules/overseas-warehouse/pages/prototypeContract.test.ts +++ b/src/modules/overseas-warehouse/pages/prototypeContract.test.ts @@ -37,6 +37,7 @@ describe('overseas warehouse first-batch prototype contract', () => { ['PalletsView.vue', 'overseas-pallets-query'], ['CartonsView.vue', 'overseas-cartons-query'], ['ProductsView.vue', 'overseas-products-query'], + ['LocationsView.vue', 'overseas-locations-query'], ]) const keys = [...expected.entries()].map(([fileName, preferenceKey]) => { const value = source(fileName) @@ -127,6 +128,7 @@ describe('overseas warehouse first-batch prototype contract', () => { expect(value).toContain('@back="back"') expect(value).not.toContain('AppDrawer') if (fileName === 'LastMileOrderDetailView.vue') expect(value).toContain('title="新增费用"') + else if (fileName === 'InboundOrderDetailView.vue') expect(value).toContain('移库') else expect(value).not.toContain('AppDialog') } }) @@ -342,10 +344,17 @@ describe('overseas warehouse first-batch prototype contract', () => { expect(value).toContain('(?:)?查看<\/el-button>/) } } + const locations = source('LocationsView.vue') + expect(locations).toContain('海外仓库位管理') + expect(locations).toContain('库位编码') }) it('connects the dispatch prototype detail to the real overseas API', () => { @@ -387,12 +396,12 @@ describe('overseas warehouse first-batch prototype contract', () => { expect(columnKeys('LastMileOrdersView.vue')).toEqual([ 'actions', 'selection', 'orderNo', 'consolidated', 'palletMergeRelation', 'warning', 'orderType', 'rejectedAt', 'inboundCode', 'lxCode', 'owner', 'warehouse', 'transferWarehouse', 'fbaWarehouse', 'containerNo', 'abnormal', 'held', 'pallets', - 'cartons', 'volume', 'quantity', 'volumeWeight', 'portEta', 'portAta', 'warehouseEta', 'inboundOrderNo', + 'cartons', 'cartonDifference', 'volume', 'quantity', 'volumeWeight', 'portEta', 'portAta', 'warehouseEta', 'inboundOrderNo', 'dispatchStatus', 'outboundOrderNo', 'dispatchOrderNo', 'relabelOrderNo', 'relabelStatus', 'status', 'createdAt', 'internalRemark', 'customerRemark', 'warehouseRemark', ]) expect(columnKeys('InboundOrdersView.vue')).toEqual([ - 'actions', 'selection', 'orderNo', 'inboundCode', 'lxCode', 'type', 'warehouse', 'fbaWarehouse', 'owner', 'cartons', 'pickedCartons', 'volume', + 'actions', 'selection', 'orderNo', 'inboundCode', 'lxCode', 'type', 'warehouse', 'fbaWarehouse', 'owner', 'cartons', 'pickedCartons', 'grossWeight', 'netWeight', 'volume', 'pallets', 'assigned', 'mergeAllowed', 'urgent', 'status', 'appointmentStatus', 'dispatchStatus', 'createdAt', ]) expect(columnKeys('InventoryView.vue')).toEqual([ diff --git a/src/modules/overseas-warehouse/routes.ts b/src/modules/overseas-warehouse/routes.ts index 7155dc5a..9357f8d2 100644 --- a/src/modules/overseas-warehouse/routes.ts +++ b/src/modules/overseas-warehouse/routes.ts @@ -130,6 +130,12 @@ export const overseasWarehouseRoutes = [ component: () => import('./pages/PalletDetailView.vue'), meta: { title: '托盘详情', description: '查看托盘基础信息与使用信息', permission: permission.palletList, showPageHeading: false }, }, + { + path: 'overseas-warehouse/locations', + name: 'overseas-locations', + component: () => import('./pages/LocationsView.vue'), + meta: { title: '库位管理', description: '维护海外仓普通库位与 A0 共享溢出位', permission: permission.locationList }, + }, { path: 'general/mojia-sku-sync', name: 'mojia-sku-sync',