From 72027e7ff20e287a10a1641e782e25e447410dbd Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Mon, 7 Sep 2026 15:34:51 +0800
Subject: [PATCH 01/12] =?UTF-8?q?=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
AGENTS.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index e454460..87e05ae 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -52,9 +52,9 @@
| 统一订单展示 | `src/modules/order-management/` | `src/modules/first-leg/` 与对应后端订单契约 |
| 客户财务账单、入仓码联查、收款、未收余额与客户确认 | `src/modules/finance/`;确认与明细入口 `views/FinanceBillsView.vue`、`api/finance.ts`,入仓码通过 `src/modules/first-leg/public.ts` 复用 `InboundCodeAssociationDialog` | Backend `finance/`;账单行只显示服务端 `inboundCode` 并按登录客户查询业务关系,缺失显示“—”,不得回退内部 ID。收款方式由服务端枚举映射中文。出账或已出账调整后为 `PENDING_CONFIRMATION`,客户确认提交登录成员、`expectedVersion` 和幂等键 |
| 客户资金账户与充值申请 | `src/modules/finance/views/FinanceAccountView.vue`、`api/account.ts` | 显示当前企业 CNY 实际、冻结、可用余额和不可变流水;充值只展示服务端启用的微信/支付宝静态收款码,客户上传 JPG/PNG 付款截图后进入待审核。客户范围只取登录态,无收款码时禁止提交,不模拟支付成功 |
-| 海外采购、美国本土卡与美本土散板 | `src/modules/cross-border/routes.ts`、`components/BusinessOrdersView.vue`、`api/businessOrders.ts` | 三个路由均为 `active`,客户范围只取 Token;Backend `lastleg-service/` 的 `procurement/`、`trucking/`、`looseboard/` 客户接口 |
+| 海外采购、美国本土卡与美本土散板 | `src/modules/cross-border/routes.ts`、`components/BusinessOrdersView.vue`、`api/businessOrders.ts` | 三个路由均为 `active`,客户范围只取 Token;采购创建的 `customerName` 只取当前认证会话企业名称,不允许自由输入或冒用其他客户。Backend `lastleg-service/` 的 `procurement/`、`trucking/`、`looseboard/` 客户接口 |
| 海外仓入库订单 | `src/modules/order-management/routes.ts`、`views/InboundOrdersView.vue`、`api/inboundPlans.ts` | `orders/inbound` 为只读列表/详情,权限 `merchant:warehouse:inbound:list`;不展示或提交内部 ID |
-| 海外仓贴标、仓储转运、货物废弃、退货换标 | `src/modules/overseas-warehouse/routes.ts`、`views/WarehouseServiceView.vue`、`api/warehouseServices.ts` | 四个路由均为 `active`;Backend `lastleg-service/` 的 `relabel/`、`transfer/`、`discard/`,请求统一经 `src/api/overseasHttp.ts` 与 `gateway-overseas/` |
+| 海外仓贴标、仓储转运、货物废弃、退货换标 | `src/modules/overseas-warehouse/routes.ts`、`views/WarehouseServiceView.vue`、`api/warehouseServices.ts` | 四个路由均为 `active`;换标申请必须提交正数 CNY `chargeAmount`,页面展示服务端钱包冻结/结算/释放状态及错误,不本地推导余额结果。Backend `lastleg-service/` 的 `relabel/`、`transfer/`、`discard/`,请求统一经 `src/api/overseasHttp.ts` 与 `gateway-overseas/` |
## 租户、认证与数据安全
From f76c5cfd0928de6c1f723d965a681c6a47e47f88 Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Tue, 8 Sep 2026 17:24:51 +0800
Subject: [PATCH 02/12] feat: expose dropship palletization progress
---
shared/src/index.ts | 1 +
src/modules/first-leg/api/firstLeg.ts | 1 +
src/modules/first-leg/api/inboundBilling.test.mjs | 4 ++++
src/modules/first-leg/api/unifiedOrders.test.mjs | 5 ++++-
src/modules/first-leg/api/unifiedOrders.ts | 3 +++
.../first-leg/components/OrderProgressPanel.vue | 2 +-
src/modules/first-leg/store/firstLegOrders.test.mjs | 7 +++++++
src/modules/first-leg/store/firstLegOrders.ts | 7 +++++--
src/modules/first-leg/types/inboundBilling.ts | 1 +
src/modules/first-leg/utils/firstLegOrder.test.mjs | 9 ++++++++-
.../first-leg/views/FirstLegOrdersView.test.mjs | 8 +++++++-
src/modules/first-leg/views/FirstLegOrdersView.vue | 2 +-
.../overseas-warehouse/views/WarehouseServiceView.vue | 10 +++++++++-
13 files changed, 52 insertions(+), 8 deletions(-)
diff --git a/shared/src/index.ts b/shared/src/index.ts
index ceb64c2..42c4559 100644
--- a/shared/src/index.ts
+++ b/shared/src/index.ts
@@ -86,6 +86,7 @@ export const FIRST_LEG_ORDER_STATUSES = [
{ code: 'DRAFT', label: '草稿' },
{ code: 'PENDING_REVIEW', label: '待审核' },
{ code: 'WAIT_MEASURE', label: '待测量数据' },
+ { code: 'WAIT_PALLETIZE', label: '待组托' },
{ code: 'WAIT_SPLIT', label: '待分柜' },
{ code: 'LOADING', label: '装柜中' },
{ code: 'LOADED', label: '装柜完成' },
diff --git a/src/modules/first-leg/api/firstLeg.ts b/src/modules/first-leg/api/firstLeg.ts
index 8cbfa7c..5273066 100644
--- a/src/modules/first-leg/api/firstLeg.ts
+++ b/src/modules/first-leg/api/firstLeg.ts
@@ -217,6 +217,7 @@ export interface MerchantFirstLegOrder {
customerId: number | string | null
customerName: string
fulfillmentType?: 'STANDARD' | 'ONE_CLICK_DROPSHIP' | null
+ lxCode?: string | null
status: ApiFirstLegStatus
transportMode?: ApiTransportMode
tradeTerm: string | null
diff --git a/src/modules/first-leg/api/inboundBilling.test.mjs b/src/modules/first-leg/api/inboundBilling.test.mjs
index d0742c1..c238a74 100644
--- a/src/modules/first-leg/api/inboundBilling.test.mjs
+++ b/src/modules/first-leg/api/inboundBilling.test.mjs
@@ -35,3 +35,7 @@ test('keeps all backend Long identifiers as strings', () => {
assert.doesNotMatch(typesSource, /(?:id|Id): number/)
assert.doesNotMatch(typesSource, /internalNotes|statusHistory|files/)
})
+
+test('accepts the one-click dropship palletization process status', () => {
+ assert.match(typesSource, /MerchantInboundProcessStatus[\s\S]*?'WAIT_PALLETIZE'[\s\S]*?'WAIT_SPLIT'/)
+})
diff --git a/src/modules/first-leg/api/unifiedOrders.test.mjs b/src/modules/first-leg/api/unifiedOrders.test.mjs
index ca874ef..9e1ee43 100644
--- a/src/modules/first-leg/api/unifiedOrders.test.mjs
+++ b/src/modules/first-leg/api/unifiedOrders.test.mjs
@@ -4,8 +4,11 @@ import test from 'node:test'
const source = readFileSync(new URL('./unifiedOrders.ts', import.meta.url), 'utf8')
-test('keeps the merchant unified-order boundary and phase-two WAIT_SPLIT status', () => {
+test('keeps the merchant unified-order boundary and one-click palletization status', () => {
assert.match(source, /const unifiedOrderBaseUrl = '\/merchant\/v1\/orders'/)
+ assert.match(source, /ApiUnifiedOrderMainStatus[\s\S]*?'WAIT_PALLETIZE'/)
assert.match(source, /ApiUnifiedOrderMainStatus[\s\S]*?'WAIT_SPLIT'/)
+ assert.match(source, /fulfillmentType\?: 'STANDARD' \| 'ONE_CLICK_DROPSHIP' \| null/)
+ assert.match(source, /lxCode\?: string \| null/)
assert.match(source, /mainStatus\?: ApiUnifiedOrderMainStatus/)
})
diff --git a/src/modules/first-leg/api/unifiedOrders.ts b/src/modules/first-leg/api/unifiedOrders.ts
index fad19f2..e32787f 100644
--- a/src/modules/first-leg/api/unifiedOrders.ts
+++ b/src/modules/first-leg/api/unifiedOrders.ts
@@ -4,6 +4,7 @@ import type { ApiContainerMode, ApiDestinationType, ApiFirstLegStatus, ApiPickup
export type ApiUnifiedOrderMainStatus =
| 'PENDING_REVIEW'
| 'WAIT_MEASURE'
+ | 'WAIT_PALLETIZE'
| 'WAIT_SPLIT'
| 'LOADING'
| 'LOADED'
@@ -54,6 +55,8 @@ export interface MerchantUnifiedOrderRow {
orderId: number | string
tenantId: number | string
orderNo: string | null
+ fulfillmentType?: 'STANDARD' | 'ONE_CLICK_DROPSHIP' | null
+ lxCode?: string | null
mainStatus: ApiUnifiedOrderMainStatus
nextAction: string | null
internalOrderStatus: ApiFirstLegStatus | null
diff --git a/src/modules/first-leg/components/OrderProgressPanel.vue b/src/modules/first-leg/components/OrderProgressPanel.vue
index 32198cc..d56f20c 100644
--- a/src/modules/first-leg/components/OrderProgressPanel.vue
+++ b/src/modules/first-leg/components/OrderProgressPanel.vue
@@ -23,7 +23,7 @@ function stageState(index: number) {
订单进度
- 国内装柜进度及后续履约同步
+ 国内入仓、组托、装柜及后续履约同步
{{ getFirstLegStatusLabel(order.status) }}
diff --git a/src/modules/first-leg/store/firstLegOrders.test.mjs b/src/modules/first-leg/store/firstLegOrders.test.mjs
index 095cb15..9cdeba9 100644
--- a/src/modules/first-leg/store/firstLegOrders.test.mjs
+++ b/src/modules/first-leg/store/firstLegOrders.test.mjs
@@ -80,6 +80,8 @@ test('declares the final C02 row and detail contract in the API adapter', () =>
assert.match(unifiedApiSource, /orderId: number \| string/)
assert.match(unifiedApiSource, /mainStatus: ApiUnifiedOrderMainStatus/)
assert.match(unifiedApiSource, /nextAction: string \| null/)
+ assert.match(unifiedApiSource, /fulfillmentType\?: 'STANDARD' \| 'ONE_CLICK_DROPSHIP' \| null/)
+ assert.match(unifiedApiSource, /lxCode\?: string \| null/)
assert.match(unifiedApiSource, /customerRemark: string \| null/)
for (const field of ['pickupMethod', 'requestedPickupAt', 'containerMode', 'destinationType', 'destinationCode', 'skuNos', 'totalQuantity', 'totalNetWeightKg', 'chargeableVolumeCbm', 'inboundCompletedAt', 'shipmentId', 'referenceId']) {
assert.match(unifiedApiSource, new RegExp(`${field}:`))
@@ -117,6 +119,10 @@ test('maps C02 authoritative status, transport, cargo and pickup snapshots', ()
assert.match(storeSource, /id: String\(summary\.orderId\)/)
assert.match(storeSource, /transportMode: unifiedTransport\(summary\.containerMode\)/)
assert.match(storeSource, /status: summary\.mainStatus/)
+ assert.match(storeSource, /fulfillmentType: summary\.fulfillmentType \?\? 'STANDARD'/)
+ assert.match(storeSource, /lxCode: summary\.lxCode \?\? ''/)
+ assert.match(storeSource, /mapped\.fulfillmentType = detail\.summary\.fulfillmentType \?\? mapped\.fulfillmentType/)
+ assert.match(storeSource, /mapped\.lxCode = detail\.summary\.lxCode \?\? mapped\.lxCode/)
assert.match(storeSource, /requestedPickupAt: summary\.requestedPickupAt \?\? ''/)
assert.match(storeSource, /chargeableVolumeCbm: summary\.chargeableVolumeCbm \?\? null/)
assert.match(storeSource, /inboundedAt: summary\.inboundCompletedAt \?\? ''/)
@@ -127,6 +133,7 @@ test('maps C02 authoritative status, transport, cargo and pickup snapshots', ()
assert.match(storeSource, /summary\?\.loading\?\.pickingVersion/)
assert.match(storeSource, /summary\?\.inboundId == null \? '' : String\(summary\.inboundId\)/)
assert.match(storeSource, /customerRemark: summary\.customerRemark \?\? ''/)
+ assert.match(storeSource, /PALLETIZE_DROPSHIP: '一件代发组托'/)
})
test('sends a manually entered old-system inbound code with the draft payload', () => {
diff --git a/src/modules/first-leg/store/firstLegOrders.ts b/src/modules/first-leg/store/firstLegOrders.ts
index 732728f..5fc9e54 100644
--- a/src/modules/first-leg/store/firstLegOrders.ts
+++ b/src/modules/first-leg/store/firstLegOrders.ts
@@ -231,6 +231,7 @@ const unifiedActionLabels: Readonly> = {
SUBMIT_ORDER: '订单已提交',
APPROVE_ORDER: '订单审核通过',
CONFIRM_MEASUREMENT: '测量数据已确认',
+ PALLETIZE_DROPSHIP: '一件代发组托',
SMART_SPLIT: '分柜方案已更新',
CONFIRM_PRELOAD: '预装清单已确认',
COMPLETE_LOADING: '装柜已完成',
@@ -279,7 +280,7 @@ function mapOrder(order: MerchantFirstLegOrder, warehouses: PlatformReceivingWar
return {
id: String(order.id ?? order.orderId ?? ''), orderNo: order.orderNo, referenceNo: order.referenceId ?? '',
transportMode: order.transportMode ? clientTransport(order) : null,
- businessType: 'EXPORT', fulfillmentType: order.fulfillmentType ?? 'STANDARD', origin: shipper?.city ?? '', destination: order.destinationWarehouseName || order.destinationCountryCode || '',
+ businessType: 'EXPORT', fulfillmentType: order.fulfillmentType ?? 'STANDARD', lxCode: order.lxCode ?? '', origin: shipper?.city ?? '', destination: order.destinationWarehouseName || order.destinationCountryCode || '',
receivingWarehouseId: String(order.receivingWarehouseId), receivingWarehouseName: order.receivingWarehouseName,
receivingWarehouseSnapshot: orderWarehouseSnapshot(order, warehouse), expectedDepartureDate: order.expectedShipDate ?? '', etd: '', eta: '',
shipperPartyId: shipper?.id ?? '', shipperPartySnapshot: shipper, shipper: shipper?.name ?? '',
@@ -328,7 +329,7 @@ function mapUnifiedOrderRow(summary: MerchantUnifiedOrderRow, warehouses: Platfo
const warehouse = warehouses.find((item) => item.id === String(summary.receivingWarehouseId))
return {
id: String(summary.orderId), orderNo: summary.orderNo, referenceNo: summary.referenceId ?? '', transportMode: unifiedTransport(summary.containerMode),
- businessType: 'EXPORT', origin: '', destination: summary.destinationWarehouseName ?? '',
+ businessType: 'EXPORT', fulfillmentType: summary.fulfillmentType ?? 'STANDARD', lxCode: summary.lxCode ?? '', origin: '', destination: summary.destinationWarehouseName ?? '',
receivingWarehouseId: summary.receivingWarehouseId == null ? '' : String(summary.receivingWarehouseId),
receivingWarehouseName: summary.receivingWarehouseName ?? '', receivingWarehouseSnapshot: warehouseSnapshot(warehouse),
expectedDepartureDate: summary.expectedShipDate ?? '', etd: '', eta: '', shipperPartyId: '', shipperPartySnapshot: null,
@@ -381,6 +382,8 @@ function mapUnifiedOrderDetail(detail: MerchantUnifiedOrderDetail, warehouses: P
const mapped = mapOrder(detail.order.order, warehouses, detail.order)
mapped.id = String(detail.summary.orderId)
mapped.status = detail.summary.mainStatus
+ mapped.fulfillmentType = detail.summary.fulfillmentType ?? mapped.fulfillmentType
+ mapped.lxCode = detail.summary.lxCode ?? mapped.lxCode
mapped.businessSummary = businessSummary(detail.summary)
mapped.timeline = unifiedTimeline(detail)
const summary = mapUnifiedOrderRow(detail.summary, warehouses)
diff --git a/src/modules/first-leg/types/inboundBilling.ts b/src/modules/first-leg/types/inboundBilling.ts
index bb34de7..101a2a5 100644
--- a/src/modules/first-leg/types/inboundBilling.ts
+++ b/src/modules/first-leg/types/inboundBilling.ts
@@ -6,6 +6,7 @@ export type MerchantInboundProcessStatus =
| 'WAIT_PRICING'
| 'FEE_CONFIRMING'
| 'INBOUNDING'
+ | 'WAIT_PALLETIZE'
| 'WAIT_SPLIT'
| 'VOIDED'
diff --git a/src/modules/first-leg/utils/firstLegOrder.test.mjs b/src/modules/first-leg/utils/firstLegOrder.test.mjs
index 6ce01cf..65a4c79 100644
--- a/src/modules/first-leg/utils/firstLegOrder.test.mjs
+++ b/src/modules/first-leg/utils/firstLegOrder.test.mjs
@@ -5,6 +5,8 @@ import {
canClientCancelFirstLegOrder,
canClientEditFirstLegOrder,
createEmptyCargoItem,
+ FIRST_LEG_PROGRESS_STAGES,
+ getFirstLegProgressIndex,
getFirstLegStatusLabel,
getFirstLegTransportModeLabel,
summarizeFirstLegCargo,
@@ -58,12 +60,16 @@ function validDraft() {
test('uses the confirmed status and transport labels', () => {
assert.equal(getFirstLegStatusLabel('WAIT_MEASURE'), '待测量数据')
- assert.equal(getFirstLegStatusLabel('WAIT_SPLIT'), '待分柜')
+ assert.equal(getFirstLegStatusLabel('WAIT_PALLETIZE'), '待组托')
assert.equal(getFirstLegStatusLabel('WAIT_SPLIT'), '待分柜')
assert.equal(getFirstLegStatusLabel('LOADING'), '装柜中')
assert.equal(getFirstLegStatusLabel('LOADED'), '装柜完成')
assert.equal(getFirstLegStatusLabel('DELIVERED'), '派送完成')
assert.equal(getFirstLegTransportModeLabel('FCL'), '海运整箱 FCL')
+ assert.deepEqual(FIRST_LEG_PROGRESS_STAGES.map((item) => item.code), [
+ 'PENDING_REVIEW', 'WAIT_MEASURE', 'WAIT_PALLETIZE', 'WAIT_SPLIT', 'LOADING', 'LOADED', 'DELIVERED',
+ ])
+ assert.equal(getFirstLegProgressIndex('WAIT_PALLETIZE'), 2)
})
test('only allows draft orders to be edited in the confirmed phase-one flow', () => {
@@ -72,6 +78,7 @@ test('only allows draft orders to be edited in the confirmed phase-one flow', ()
assert.equal(canClientCancelFirstLegOrder('DRAFT'), true)
assert.equal(canClientCancelFirstLegOrder('PENDING_REVIEW'), true)
assert.equal(canClientCancelFirstLegOrder('WAIT_MEASURE'), false)
+ assert.equal(canClientCancelFirstLegOrder('WAIT_PALLETIZE'), false)
assert.equal(canClientCancelFirstLegOrder('WAIT_SPLIT'), false)
})
diff --git a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs
index 351c9c9..5653e00 100644
--- a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs
+++ b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs
@@ -199,7 +199,7 @@ test('shows one unified progress line and C02 business summaries without detail
assert.match(source, /summary\.expectedPickCount/)
assert.match(source, /summary\.deliveryTrackingNo/)
assert.match(progressSource, /FIRST_LEG_PROGRESS_STAGES/)
- assert.match(progressSource, /国内装柜进度及后续履约同步/)
+ assert.match(progressSource, /国内入仓、组托、装柜及后续履约同步/)
assert.doesNotMatch(progressSource, /进度记录|el-timeline|timeline/)
assert.doesNotMatch(source, /报关回执<\/h3>/)
assert.match(source, /船期摘要/)
@@ -369,6 +369,12 @@ test('distills order creation into the confirmed prototype sections with server
assert.doesNotMatch(source, /v-model="form\.(?:businessType|origin|destination|priority|tradeTerm)"/)
})
+test('shows an optional LX inbound order number for one-click dropship orders', () => {
+ assert.match(source, /v-if="form\.fulfillmentType === 'ONE_CLICK_DROPSHIP'" label="LX入库单号" prop="lxCode"/)
+ assert.match(source, /v-model="form\.lxCode"[^>]*placeholder="请输入LX入库单号"/)
+ assert.doesNotMatch(source, /label="LX入库单号"[^>]*required/)
+})
+
test('loads only active server channels for the selected domestic warehouse', () => {
assert.match(source, /getMerchantPricingChannels\(warehouseId\)/)
assert.match(source, /const channelOptions = ref\(\[\]\)/)
diff --git a/src/modules/first-leg/views/FirstLegOrdersView.vue b/src/modules/first-leg/views/FirstLegOrdersView.vue
index 1b66bbf..f9f67a0 100644
--- a/src/modules/first-leg/views/FirstLegOrdersView.vue
+++ b/src/modules/first-leg/views/FirstLegOrdersView.vue
@@ -1898,7 +1898,7 @@ async function searchOrders(model: QueryModel) {
客户下单
先选择货物如何送到国内收货仓,并填写本次运输安排。
@@ -260,14 +264,18 @@ async function openDetail(value: Record) {
+
+
重置提交{{ config.title }}
From a7f073c7831a9733e80e7ffef9ef23dc0f8cbfd8 Mon Sep 17 00:00:00 2001
From: liuyihe <2513762445@qq.com>
Date: Tue, 8 Sep 2026 20:28:05 +0800
Subject: [PATCH 03/12] =?UTF-8?q?=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
AGENTS.md | 2 +-
src/components.d.ts | 3 +
.../views/businessIdentifiers.test.mjs | 1 -
.../overseas-warehouse/api/returnInventory.ts | 59 ++++
.../overseas-warehouse/api/returnOrders.ts | 29 ++
.../api/warehouseServices.ts | 28 +-
src/modules/overseas-warehouse/routes.ts | 18 +-
.../views/ReturnInventoryView.vue | 124 +++++++
.../views/ReturnOrdersView.vue | 116 +++++++
.../views/WarehouseServiceView.vue | 327 ++++++++++++++----
.../views/relabelAttachments.test.mjs | 108 ++++--
src/modules/tenant-management/contracts.ts | 1 +
src/navigation/clientNavigation.test.mjs | 6 +-
13 files changed, 725 insertions(+), 97 deletions(-)
create mode 100644 src/modules/overseas-warehouse/api/returnInventory.ts
create mode 100644 src/modules/overseas-warehouse/api/returnOrders.ts
create mode 100644 src/modules/overseas-warehouse/views/ReturnInventoryView.vue
create mode 100644 src/modules/overseas-warehouse/views/ReturnOrdersView.vue
diff --git a/AGENTS.md b/AGENTS.md
index e454460..3100764 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -54,7 +54,7 @@
| 客户资金账户与充值申请 | `src/modules/finance/views/FinanceAccountView.vue`、`api/account.ts` | 显示当前企业 CNY 实际、冻结、可用余额和不可变流水;充值只展示服务端启用的微信/支付宝静态收款码,客户上传 JPG/PNG 付款截图后进入待审核。客户范围只取登录态,无收款码时禁止提交,不模拟支付成功 |
| 海外采购、美国本土卡与美本土散板 | `src/modules/cross-border/routes.ts`、`components/BusinessOrdersView.vue`、`api/businessOrders.ts` | 三个路由均为 `active`,客户范围只取 Token;Backend `lastleg-service/` 的 `procurement/`、`trucking/`、`looseboard/` 客户接口 |
| 海外仓入库订单 | `src/modules/order-management/routes.ts`、`views/InboundOrdersView.vue`、`api/inboundPlans.ts` | `orders/inbound` 为只读列表/详情,权限 `merchant:warehouse:inbound:list`;不展示或提交内部 ID |
-| 海外仓贴标、仓储转运、货物废弃、退货换标 | `src/modules/overseas-warehouse/routes.ts`、`views/WarehouseServiceView.vue`、`api/warehouseServices.ts` | 四个路由均为 `active`;Backend `lastleg-service/` 的 `relabel/`、`transfer/`、`discard/`,请求统一经 `src/api/overseasHttp.ts` 与 `gateway-overseas/` |
+| 海外仓贴标、仓储转运、货物废弃、退货换标 | `src/modules/overseas-warehouse/routes.ts`、`views/WarehouseServiceView.vue`、`api/warehouseServices.ts` | 四个路由均为 `active`;贴标申请先从共享仓库主数据选择国外中转仓,并自动带出海外仓签收联系人和联系电话,再从当前客户已经完成入库且仍有可用库存的历史入仓单中单选。货物明细支持箱唛换标、SKU 换标和同箱组合换标:SKU 明细按来源库存箱聚合为一个箱唛任务,整箱只冻结一次,SKU 数量按件保存为子明细;PDA 扫我方箱唛定位任务,返回新 FBA 箱唛和 SKU 子明细,照片完工并复核通过后释放原库存,后续仍进入既有排车出库。客户页面不暴露托盘、库位和内部库存 ID;Backend `lastleg-service/` 的 `relabel/`、`relabel/pda/`、`transfer/`、`discard/`,请求统一经 `src/api/overseasHttp.ts` 与 `gateway-overseas/` |
## 租户、认证与数据安全
diff --git a/src/components.d.ts b/src/components.d.ts
index 723bdea..2240e47 100644
--- a/src/components.d.ts
+++ b/src/components.d.ts
@@ -23,6 +23,8 @@ declare module 'vue' {
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
+ ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
+ ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDrawer: typeof import('element-plus/es')['ElDrawer']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
@@ -34,6 +36,7 @@ declare module 'vue' {
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
ElOption: typeof import('element-plus/es')['ElOption']
+ ElOptionGroup: typeof import('element-plus/es')['ElOptionGroup']
ElPagination: typeof import('element-plus/es')['ElPagination']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElRadio: typeof import('element-plus/es')['ElRadio']
diff --git a/src/modules/first-leg/views/businessIdentifiers.test.mjs b/src/modules/first-leg/views/businessIdentifiers.test.mjs
index 300481a..4d94bf6 100644
--- a/src/modules/first-leg/views/businessIdentifiers.test.mjs
+++ b/src/modules/first-leg/views/businessIdentifiers.test.mjs
@@ -30,7 +30,6 @@ test('keeps overseas warehouse operations first and renders readable list values
assert.match(overseas, /columns: \['操作', '贴标申请单号'/)
assert.match(overseas, /columns: \['操作', '转运单号'/)
assert.match(overseas, /columns: \['操作', '废弃申请单号'/)
- assert.match(overseas, /columns: \['操作', '退货换标单号'/)
assert.match(overseas, /function statusLabel\(value: string\)/)
assert.match(overseas, /function deliveryMethodLabel\(value: string\)/)
assert.match(overseas, /function formatDateTime\(value: string\)/)
diff --git a/src/modules/overseas-warehouse/api/returnInventory.ts b/src/modules/overseas-warehouse/api/returnInventory.ts
new file mode 100644
index 0000000..560a504
--- /dev/null
+++ b/src/modules/overseas-warehouse/api/returnInventory.ts
@@ -0,0 +1,59 @@
+import { overseasApiRequest } from '@/api/overseasHttp'
+import type { PageResult } from './warehouseServices'
+
+export interface ReturnInventoryItem {
+ overseasWarehouseId: string; cartonMark: string; sourceBatchNo?: string; status: string; skuCode: string
+ productNameZh?: string; quantity: number; cartonSkuTypes: number; cartonTotalQuantity: number; receivedAt: string
+}
+
+export interface ReturnInventorySummaryItem {
+ overseasWarehouseId: string; skuCode: string; productNameZh?: string
+ availableQuantity: number; cartonQuantity: number; updatedAt: string
+}
+
+export interface ReturnInventoryCartonItem { skuCode: string; productNameZh?: string; quantity: number }
+
+export interface ReturnInventoryCartonDetail {
+ cartonMark: string; overseasWarehouseId: string; sourceBatchNo?: string; sourceReturnNo?: string; status: string
+ skuTypes: number; totalQuantity: number; length?: number; width?: number; height?: number; weight?: number; receivedAt: string; items: ReturnInventoryCartonItem[]
+}
+
+export interface ReturnInventoryLabel {
+ cartonMark: string; overseasWarehouseId: string; customerId: string; sourceBatchNo?: string; sourceReturnNo?: string
+ totalQuantity: number; length?: number; width?: number; height?: number; weight?: number; items: ReturnInventoryCartonItem[]; generatedAt: string; printableHtml: string
+}
+
+export function listReturnInventory(params: Record) {
+ return overseasApiRequest>({ method: 'GET', url: '/merchant/v1/last-leg/return-inventory', params })
+}
+
+export function listReturnInventorySummary(params: Record) {
+ return overseasApiRequest>({ method: 'GET', url: '/merchant/v1/last-leg/return-inventory/summary', params })
+}
+
+export function getReturnInventoryCarton(cartonMark: string) {
+ return overseasApiRequest({ method: 'GET', url: `/merchant/v1/last-leg/return-inventory/cartons/${encodeURIComponent(cartonMark)}` })
+}
+
+export function getReturnInventoryLabel(cartonMark: string) {
+ return overseasApiRequest({ method: 'GET', url: `/merchant/v1/last-leg/return-inventory/cartons/${encodeURIComponent(cartonMark)}/label` })
+}
+
+export interface ReturnPackingPreview {
+ orders: Array<{ returnNo: string; customerId: string; customerReferenceNo?: string; status: string }>
+ customerId?: string
+ skus: Array<{ skuCode: string; productNameZh?: string; availableQuantity: number }>
+}
+
+export function getReturnPackingPreview(returnNo?: string) {
+ return overseasApiRequest({ method: 'GET', url: '/pda/v1/last-leg/return-inventory/packing/orders', params: { returnNo: returnNo || undefined } })
+}
+
+export function mergeReturnInventory(payload: {
+ returnNos: string[]; items: Array<{ skuCode: string; quantity: number }>
+ length: number; width: number; height: number; weight: number; idempotencyKey: string
+}) {
+ return overseasApiRequest<{ cartonMark: string; skuCode: string; totalQuantity: number; label: ReturnInventoryLabel }>({
+ method: 'POST', url: '/pda/v1/last-leg/return-inventory/cartons/merge', data: payload,
+ })
+}
diff --git a/src/modules/overseas-warehouse/api/returnOrders.ts b/src/modules/overseas-warehouse/api/returnOrders.ts
new file mode 100644
index 0000000..70b354b
--- /dev/null
+++ b/src/modules/overseas-warehouse/api/returnOrders.ts
@@ -0,0 +1,29 @@
+import { overseasApiRequest } from '@/api/overseasHttp'
+import type { PageResult } from './warehouseServices'
+
+export interface ReturnOrderSummary {
+ returnNo: string; overseasWarehouseId: string; customerReferenceNo?: string; status: string
+ skuTypes: number; expectedQuantity: number; receivedQuantity: number; createdAt: string
+}
+export interface ReturnOrderItem {
+ skuCode: string; productNameZh?: string; expectedQuantity: number; receivedQuantity: number
+}
+export interface ReturnReceipt {
+ receiptNo: string; receiptBatchNo: string; cartonMark?: string; trackingNo?: string; startedBy: 'RETURN_NO' | 'SKU'
+ receivedQuantity: number; operatorName?: string; signedAt: string
+}
+export interface ReturnOrderDetail { order: ReturnOrderSummary; remark?: string; items: ReturnOrderItem[]; receipts: ReturnReceipt[] }
+export interface ReturnOrderCreate {
+ overseasWarehouseId: string; customerReferenceNo?: string; remark?: string; idempotencyKey: string
+ items: Array<{ skuCode: string; productNameZh?: string; expectedQuantity: number }>
+}
+
+export function listReturnOrders(params: Record) {
+ return overseasApiRequest>({ method: 'GET', url: '/merchant/v1/last-leg/return-orders', params })
+}
+export function getReturnOrder(returnNo: string) {
+ return overseasApiRequest({ method: 'GET', url: `/merchant/v1/last-leg/return-orders/${encodeURIComponent(returnNo)}` })
+}
+export function createReturnOrder(payload: ReturnOrderCreate) {
+ return overseasApiRequest({ method: 'POST', url: '/merchant/v1/last-leg/return-orders', data: payload })
+}
diff --git a/src/modules/overseas-warehouse/api/warehouseServices.ts b/src/modules/overseas-warehouse/api/warehouseServices.ts
index 6f1dba1..c070e90 100644
--- a/src/modules/overseas-warehouse/api/warehouseServices.ts
+++ b/src/modules/overseas-warehouse/api/warehouseServices.ts
@@ -6,9 +6,13 @@ export interface TransferSummary { id: string; transferNo: string; inboundCode:
export interface RelabelSummary { id: string; relabelNo: string; lastlegOrderId: string; lastlegOrderNo: string; customerOrderNo: string; overseasWarehouseId: string; status: string; itemCount: number; totalPackageCount: number; notifiedPackageCount: number; skuCodes: string; createdAt: string }
export interface RelabelItemDetail {
id: string; skuCode?: string; productNameZh: string; totalPackageCount: number; notifiedPackageCount: number
- shipmentNo: string; trackingCode?: string; newSkuCode: string; labelRequirement?: string; remark?: string
+ shipmentNo: string; trackingCode?: string; newSkuCode?: string; labelRequirement?: string; remark?: string
relabelFileId?: string; relabelFileName?: string; relabelFileUrl?: string
- cartonMark: string; fbaCartonMark: string; destinationWarehouseId: string
+ cartonMark: string; fbaCartonMark?: string; destinationFbaWarehouseCode?: string
+ operationType: 'CARTON_LABEL' | 'SKU_LABEL' | 'CARTON_AND_SKU'
+ skuItems: Array<{ skuCode: string; newSkuCode: string; quantity: number; remark?: string }>
+ sourceInventoryBalanceId: string; sourceInventoryPositionId: string; sourceInboundOrderId: string
+ sourceInboundOrderNo?: string; sourceInboundCode?: string; sourceWarehouseId: string; sourceWarehouseCode?: string; sourceWarehouseName?: string
}
export interface RelabelDetail {
application: RelabelSummary & { labelDeadline?: string; contactName?: string; contactPhone?: string; chargeAmount?: number; chargeCurrency?: string; walletStatus?: string; walletErrorMessage?: string }
@@ -18,6 +22,13 @@ export interface RelabelDetail {
}
export interface DiscardSummary { id: string; discardNo: string; inboundCodes: string; overseasWarehouseId: string; overseasWarehouseName: string; totalPackages: number; totalFee: number; currency: string; status: string; createdAt: string }
export interface DiscardCandidate { orderId: string; inboundCode: string; inboundOrderNo: string; overseasWarehouseId: string; overseasWarehouseName: string; availablePackages: number; skus: Array<{ skuCode: string; productName: string; availablePackages: number }> }
+export interface RelabelInventoryCandidate {
+ inventoryBalanceId: string; inboundOrderId: string; inboundOrderNo?: string; inboundCode?: string
+ overseasWarehouseId: string; overseasWarehouseCode?: string; overseasWarehouseName?: string
+ destinationFbaWarehouseCode?: string; destinationFbaWarehouseName?: string; skuCode?: string; productNameZh?: string
+ availablePackages: number; version: number
+ positions: Array<{ positionId: string; palletNo?: string; batchNo?: string; warehouseLocationCode?: string; availablePackages: number }>
+}
export interface TransferCreate {
inboundCode: string; sourceLastlegOrderIds: string[]; operatingWarehouseId: string; fbaWarehouseCode: string
@@ -26,10 +37,10 @@ export interface TransferCreate {
items: Array<{ skuCode: string; productNameZh: string; packageCount: number; shippingMark?: string }>
}
export interface RelabelCreate {
- customerOrderNo: string; overseasWarehouseId?: string; labelDeadline: string; contactName: string; contactPhone: string; chargeAmount: number
+ customerOrderNo: string; operationWarehouseId: string; labelDeadline: string; contactName: string; contactPhone: string; chargeAmount: number
remark?: string; idempotencyKey: string
- items: Array<{ skuCode?: string; productNameZh: string; totalPackageCount: number; notifiedPackageCount: number; shipmentNo: string; trackingCode?: string; newSkuCode: string; labelRequirement?: string
- cartonMark: string; fbaCartonMark: string; destinationWarehouseId?: string; remark?: string }>
+ items: Array<{ sourceInventoryBalanceId: string; sourceInventoryPositionId: string; sourceInboundOrderId: string; skuCode?: string; productNameZh: string; totalPackageCount: number; notifiedPackageCount: number; shipmentNo: string; trackingCode?: string; newSkuCode?: string; labelRequirement?: string
+ cartonMark: string; fbaCartonMark?: string; destinationFbaWarehouseCode?: string; operationType: 'CARTON_LABEL' | 'SKU_LABEL' | 'CARTON_AND_SKU'; skuItems?: Array<{ skuCode: string; newSkuCode: string; quantity: number; remark?: string }>; remark?: string }>
}
export function listLastlegOrders(inboundCode: string) {
@@ -50,6 +61,13 @@ export function listRelabels(params: Record) {
export function getRelabel(relabelNo: string) {
return overseasApiRequest({ method: 'GET', url: `/merchant/v1/last-leg/relabel-applications/${encodeURIComponent(relabelNo)}` })
}
+
+export function listRelabelInventoryCandidates(operationWarehouseId: string, inboundCode?: string) {
+ return overseasApiRequest({
+ method: 'GET', url: '/merchant/v1/last-leg/relabel-applications/inventory-candidates',
+ params: { operationWarehouseId, inboundCode: inboundCode || undefined },
+ })
+}
export function createRelabel(payload: RelabelCreate) {
return overseasApiRequest>({ method: 'POST', url: '/merchant/v1/last-leg/relabel-applications', data: payload })
}
diff --git a/src/modules/overseas-warehouse/routes.ts b/src/modules/overseas-warehouse/routes.ts
index 9bc50df..01ed1e6 100644
--- a/src/modules/overseas-warehouse/routes.ts
+++ b/src/modules/overseas-warehouse/routes.ts
@@ -1,4 +1,4 @@
-import { PackageX, RefreshCcw, Tags, Truck, Warehouse } from '@lucide/vue'
+import { Boxes, PackageX, RefreshCcw, Tags, Truck, Warehouse } from '@lucide/vue'
import type { ClientModuleManifest } from '../types.ts'
export const overseasWarehouseModule = {
@@ -25,9 +25,21 @@ export const overseasWarehouseModule = {
},
{
path: 'overseas-warehouse/return-relabel', name: 'overseas-return-relabel',
- component: () => import('./views/WarehouseServiceView.vue'), props: { service: 'relabel' }, release: 'active',
- meta: { title: '退货换标', description: '查询并管理退货换标订单', permission: 'dashboard:view' },
+ component: () => import('./views/ReturnOrdersView.vue'), release: 'active',
+ meta: { title: '退货签收', description: '创建退货预报并查看仓库签收结果', permission: 'dashboard:view' },
navigation: { key: 'overseas-return-relabel', icon: RefreshCcw },
},
+ {
+ path: 'overseas-warehouse/return-inventory', name: 'overseas-return-inventory',
+ component: () => import('./views/ReturnInventoryView.vue'), release: 'active',
+ meta: { title: '退货库存', description: '按箱号和 SKU 查询海外仓退货实收库存', permission: 'merchant:overseas-warehouse:return-inventory:list' },
+ navigation: { key: 'overseas-return-inventory', icon: Boxes },
+ },
+ {
+ path: 'overseas-warehouse/return-cartons', name: 'overseas-return-cartons',
+ component: () => import('./views/ReturnInventoryView.vue'), props: { cartonOnly: true }, release: 'active',
+ meta: { title: '退货箱号管理', description: '查询合箱后的退货箱号和箱内库存', permission: 'merchant:overseas-warehouse:return-inventory:list' },
+ navigation: { key: 'overseas-return-cartons', icon: Boxes },
+ },
],
} satisfies ClientModuleManifest
diff --git a/src/modules/overseas-warehouse/views/ReturnInventoryView.vue b/src/modules/overseas-warehouse/views/ReturnInventoryView.vue
new file mode 100644
index 0000000..670b3af
--- /dev/null
+++ b/src/modules/overseas-warehouse/views/ReturnInventoryView.vue
@@ -0,0 +1,124 @@
+
+
+
+
+ {{ props.cartonOnly ? '退货箱号管理' : '退货库存管理' }}
{{ props.cartonOnly ? '查询合箱完成后的退货箱号、箱规和箱内 SKU 明细。' : '库存数量以仓库 PDA 实收并上架的数量为准。' }}
+
+
+
+
+
+
+
+ 查询
+
+
+
+
+
+ 查看箱号
+
+
+ {{ row.cartonMark }}
+
+
+
+
+ 详情箱唛
+
+
+
+ {{ cartonDetail.cartonMark }}{{ cartonDetail.overseasWarehouseId }}{{ cartonDetail.sourceReturnNo || '—' }}{{ cartonDetail.sourceBatchNo || '—' }}{{ cartonDetail.status }}{{ cartonDetail.skuTypes }}{{ cartonDetail.totalQuantity }}{{ cartonDetail.length && cartonDetail.width && cartonDetail.height ? `${cartonDetail.length} × ${cartonDetail.width} × ${cartonDetail.height} cm` : '—' }}{{ cartonDetail.weight ? `${cartonDetail.weight} kg` : '—' }}
+
+
+
+
+
+
+
diff --git a/src/modules/overseas-warehouse/views/ReturnOrdersView.vue b/src/modules/overseas-warehouse/views/ReturnOrdersView.vue
new file mode 100644
index 0000000..443ac05
--- /dev/null
+++ b/src/modules/overseas-warehouse/views/ReturnOrdersView.vue
@@ -0,0 +1,116 @@
+
+
+
+
+ 退货签收
预报退货 SKU 和数量,仓库 PDA 实收后自动更新签收状态。
新建退货预报
+
+
+
+
+
+ 查询
+
+ openDetail(row.returnNo)">
+
+
+
+
+
+
+ {{ row.status }}
+
+ 详情
+
+
+
+
+ {{ operationWarehousesError }}
+
+ 客户只需填写预计退回的 SKU 和数量;快递单号由仓库签收时按实际包裹扫描绑定。
+
+
+
+
+ 删除
+
+ 取消提交预报
+
+ {{ selected.order.returnNo }}{{ selected.order.status }}{{ selected.order.overseasWarehouseId }}{{ selected.order.receivedQuantity }} / {{ selected.order.expectedQuantity }}预报与实收
签收批次
{{ row.trackingNo || '无快递单号' }}
+
+
+
+
diff --git a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue
index 5807c11..8ca2ad9 100644
--- a/src/modules/overseas-warehouse/views/WarehouseServiceView.vue
+++ b/src/modules/overseas-warehouse/views/WarehouseServiceView.vue
@@ -4,21 +4,26 @@ import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft, Download, Eye, Plus, Search, Trash2, Upload } from '@lucide/vue'
import {
createDiscard, createRelabel, createTransfer, getDiscard, getRelabel, getTransfer,
- listDiscardCandidates, listDiscards, listLastlegOrders, listRelabels, listTransfers,
+ listDiscardCandidates, listDiscards, listLastlegOrders, listRelabelInventoryCandidates, listRelabels, listTransfers,
+ type RelabelInventoryCandidate,
+ type RelabelCreate,
type RelabelDetail,
} from '../api/warehouseServices'
+import { getMerchantWarehouses, type MerchantWarehouse } from '@/modules/first-leg/api/firstLeg'
type ServiceType = 'labeling' | 'transfer' | 'disposal' | 'relabel'
type PageMode = 'list' | 'create' | 'detail'
-interface CargoRow { id: number; sku: string; name: string; quantity: number; availableQuantity?: number; sourceOrderId?: string; fnsku: string; shipmentNo: string; newSkuCode: string; shippingMark: string; cartonMark: string; fbaCartonMark: string; destinationWarehouseId: string; remark: string }
+type LabelingMode = 'carton' | 'sku'
+interface CargoRow { id: number; sku: string; name: string; quantity: number; availableQuantity?: number; sourceOrderId?: string; sourceInventoryBalanceId?: string; sourceInventoryPositionId?: string; sourceInboundOrderId?: string; sourceInboundOrderNo?: string; sourceInboundCode?: string; sourceWarehouseName?: string; palletNo?: string; warehouseLocationCode?: string; fnsku: string; shipmentNo: string; newSkuCode: string; shippingMark: string; cartonMark: string; fbaCartonMark: string; destinationWarehouseId: string; remark: string }
+interface SkuRelabelRow { id: number; sourceRowId: number | null; cartonMark: string; originalSku: string; newSku: string; quantity: number; changeCarton: boolean; fbaCartonMark: string; destinationWarehouseId: string; remark: string }
interface ServiceConfig { title: string; createTitle: string; numberLabel: string; emptyText: string; columns: string[] }
const props = defineProps<{ service: ServiceType }>()
const configs: Record = {
- labeling: { title: '贴标申请', createTitle: '新增贴标申请', numberLabel: '贴标申请单号', emptyText: '暂无贴标申请', columns: ['贴标申请单号', '客户单号', '目的海外仓', '贴标要求', 'SKU 数', '总箱数', '状态', '创建时间', '操作'] },
- transfer: { title: '仓储转运单', createTitle: '新增仓储转运单', numberLabel: '转运单号', emptyText: '暂无仓储转运订单', columns: ['转运单号', '进仓编号', '发货箱数', '操作仓', 'FBA 目的仓', '派送方式', '是否换标', '状态', '创建时间', '操作'] },
- disposal: { title: '货物废弃', createTitle: '新增货物废弃申请', numberLabel: '废弃申请单号', emptyText: '暂无货物废弃申请', columns: ['废弃申请单号', '进仓编号', '操作仓', 'SKU 汇总', '废弃数量', '预估费用', '状态', '创建时间', '操作'] },
- relabel: { title: '退货换标', createTitle: '新建退货换标单', numberLabel: '退货换标单号', emptyText: '暂无退货换标订单', columns: ['退货换标单号', '来源 FBA 仓', '目的 FBA 仓', 'SKU 汇总', '退件总数', '是否换标', '换标状态', '尾程状态', '创建时间', '操作'] },
+ labeling: { title: '贴标申请', createTitle: '新增贴标申请', numberLabel: '贴标申请单号', emptyText: '暂无贴标申请', columns: ['操作', '贴标申请单号', '客户单号', '海外操作仓', '贴标要求', 'SKU 数', '总箱数', '状态', '创建时间'] },
+ transfer: { title: '仓储转运单', createTitle: '新增仓储转运单', numberLabel: '转运单号', emptyText: '暂无仓储转运订单', columns: ['操作', '转运单号', '进仓编号', '发货箱数', '操作仓', 'FBA 目的仓', '派送方式', '是否换标', '状态', '创建时间'] },
+ disposal: { title: '货物废弃', createTitle: '新增货物废弃申请', numberLabel: '废弃申请单号', emptyText: '暂无货物废弃申请', columns: ['操作', '废弃申请单号', '进仓编号', '操作仓', 'SKU 汇总', '废弃数量', '预估费用', '状态', '创建时间'] },
+ relabel: { title: '退货换标', createTitle: '新建退货换标单', numberLabel: '退货换标单号', emptyText: '暂无退货换标订单', columns: ['操作', '退货换标单号', '来源 FBA 仓', '目的 FBA 仓', 'SKU 汇总', '退件总数', '是否换标', '换标状态', '尾程状态', '创建时间'] },
}
const config = computed(() => configs[props.service])
const statusOptions = computed(() => props.service === 'labeling'
@@ -30,28 +35,64 @@ const mode = ref('list')
const filters = reactive({ number: '', inboundNo: '', warehouse: '', destination: '', status: '', start: '', end: '' })
const form = reactive({ customerNo: '', inboundNo: '', sourceWarehouse: '', warehouse: '', destination: '', address: '', contact: '', phone: '', deliveryMethod: '', trackingNo: '', reason: '', requirement: '', relabel: false, remark: '', labelDeadline: '', estimatedArrivalAt: '', fbaShipmentNo: '', volumeCbm: 0.01, chargeAmount: 0 })
const cargoRows = ref([])
+const labelingMode = ref('carton')
+const skuRelabelRows = ref([])
+const cartonMarkOptions = computed(() => cargoRows.value.map((item) => ({
+ value: item.id,
+ label: `${item.cartonMark || item.sku || '库存货物'}|可用 ${item.availableQuantity || item.quantity} 箱`,
+})))
const submittedRows = ref[]>([])
const selectedDetail = ref | null>(null)
const selectedRelabelDetail = ref(null)
+const fbaWarehouses = ref([])
+const operationWarehouses = ref([])
+const fbaWarehousesLoading = ref(false)
+const fbaWarehousesError = ref('')
const discardCandidates = ref([])
const selectedDiscardOrderId = ref('')
const discardCandidatesLoading = ref(false)
+const relabelCandidates = ref([])
+const selectedRelabelInboundOrderId = ref('')
+const relabelCandidatesLoading = ref(false)
+const relabelInboundOptions = computed(() => {
+ const grouped = new Map()
+ for (const candidate of relabelCandidates.value) {
+ const value = String(candidate.inboundOrderId)
+ const current = grouped.get(value)
+ if (current) current.availablePackages += candidate.availablePackages
+ else grouped.set(value, {
+ value,
+ inboundOrderNo: candidate.inboundOrderNo || '—',
+ inboundCode: candidate.inboundCode || '—',
+ availablePackages: candidate.availablePackages,
+ })
+ }
+ return [...grouped.values()]
+})
const loading = ref(false)
const saving = ref(false)
const templateInput = ref(null)
let cargoSequence = 0
+let skuRelabelSequence = 0
watch(() => props.service, () => { mode.value = 'list'; resetFilters(); resetForm(); void load() })
+watch(labelingMode, (value) => { if (value === 'sku' && !skuRelabelRows.value.length) addSkuRelabelRow() })
onMounted(() => { void load() })
function resetFilters() { Object.assign(filters, { number: '', inboundNo: '', warehouse: '', destination: '', status: '', start: '', end: '' }) }
function resetForm() {
Object.assign(form, { customerNo: '', inboundNo: '', sourceWarehouse: '', warehouse: '', destination: '', address: '', contact: '', phone: '', deliveryMethod: '', trackingNo: '', reason: '', requirement: '', relabel: false, remark: '', labelDeadline: '', estimatedArrivalAt: '', fbaShipmentNo: '', volumeCbm: 0.01, chargeAmount: 0 })
cargoRows.value = []
+ labelingMode.value = 'carton'
+ skuRelabelRows.value = []
discardCandidates.value = []
selectedDiscardOrderId.value = ''
+ relabelCandidates.value = []
+ selectedRelabelInboundOrderId.value = ''
}
async function openCreate() {
resetForm(); mode.value = 'create'
+ if (props.service === 'labeling' || props.service === 'relabel' || props.service === 'transfer') void loadWarehouses()
+ if (props.service === 'labeling') return
if (props.service !== 'disposal') { addCargoRow(); return }
discardCandidatesLoading.value = true
try { discardCandidates.value = await listDiscardCandidates() }
@@ -59,12 +100,78 @@ async function openCreate() {
finally { discardCandidatesLoading.value = false }
}
function addCargoRow() { cargoRows.value.push({ id: ++cargoSequence, sku: '', name: '', quantity: 1, fnsku: '', shipmentNo: '', newSkuCode: '', shippingMark: '', cartonMark: '', fbaCartonMark: '', destinationWarehouseId: '', remark: '' }) }
-function removeCargoRow(id: number) { cargoRows.value = cargoRows.value.filter((row) => row.id !== id) }
-function csvCell(value: string) { return `"${value.replaceAll('"', '""')}"` }
+function addSkuRelabelRow() { skuRelabelRows.value.push({ id: ++skuRelabelSequence, sourceRowId: null, cartonMark: '', originalSku: '', newSku: '', quantity: 1, changeCarton: false, fbaCartonMark: '', destinationWarehouseId: '', remark: '' }) }
+function removeSkuRelabelRow(id: number) { skuRelabelRows.value = skuRelabelRows.value.filter((row) => row.id !== id) }
+function selectSkuSource(row: SkuRelabelRow) {
+ const source = cargoRows.value.find((item) => item.id === row.sourceRowId)
+ if (!source) return
+ row.cartonMark = source.cartonMark || ''
+ row.originalSku = row.originalSku || source.sku
+ row.destinationWarehouseId = row.destinationWarehouseId || source.destinationWarehouseId
+}
+function buildSkuRelabelItems(): RelabelCreate['items'] {
+ const groups = new Map()
+ for (const row of skuRelabelRows.value) {
+ if (row.sourceRowId == null) throw new Error('请选择每条 SKU 明细对应的来源库存箱')
+ const rows = groups.get(row.sourceRowId) || []
+ rows.push(row)
+ groups.set(row.sourceRowId, rows)
+ }
+ return [...groups.entries()].map(([sourceRowId, rows]) => {
+ const source = cargoRows.value.find((item) => item.id === sourceRowId)
+ if (!source?.sourceInventoryBalanceId || !source.sourceInventoryPositionId || !source.sourceInboundOrderId) {
+ throw new Error('来源库存已变化,请重新选择有库存的历史入仓单')
+ }
+ const cartonMark = rows[0].cartonMark.trim()
+ if (!cartonMark || rows.some((row) => row.cartonMark.trim() !== cartonMark)) {
+ throw new Error('同一来源库存箱的 SKU 明细必须使用同一个我方箱唛')
+ }
+ const changeCarton = rows[0].changeCarton
+ if (rows.some((row) => row.changeCarton !== changeCarton)) {
+ throw new Error('同一我方箱唛的 SKU 明细必须统一是否同时更换箱唛')
+ }
+ const fbaCartonMark = rows[0].fbaCartonMark.trim()
+ const destinationWarehouseId = rows[0].destinationWarehouseId
+ if (changeCarton && (!fbaCartonMark || !destinationWarehouseId
+ || rows.some((row) => row.fbaCartonMark.trim() !== fbaCartonMark || row.destinationWarehouseId !== destinationWarehouseId))) {
+ throw new Error('同一我方箱唛的组合换标必须使用相同的新 FBA 箱唛和目的仓')
+ }
+ return {
+ sourceInventoryBalanceId: source.sourceInventoryBalanceId,
+ sourceInventoryPositionId: source.sourceInventoryPositionId,
+ sourceInboundOrderId: source.sourceInboundOrderId,
+ skuCode: source.sku || rows[0].originalSku,
+ productNameZh: source.name,
+ totalPackageCount: source.quantity,
+ notifiedPackageCount: source.quantity,
+ shipmentNo: source.shipmentNo || cartonMark,
+ trackingCode: form.trackingNo || undefined,
+ newSkuCode: rows[0].newSku,
+ labelRequirement: form.requirement || undefined,
+ cartonMark,
+ fbaCartonMark: changeCarton ? fbaCartonMark : undefined,
+ destinationFbaWarehouseCode: changeCarton ? destinationWarehouseId : undefined,
+ operationType: changeCarton ? 'CARTON_AND_SKU' as const : 'SKU_LABEL' as const,
+ skuItems: rows.map((row) => ({ skuCode: row.originalSku.trim(), newSkuCode: row.newSku.trim(), quantity: row.quantity, remark: row.remark || undefined })),
+ remark: undefined,
+ }
+ })
+}
+function addRelabelCargoRow() {
+ if (!selectedRelabelInboundOrderId.value) { ElMessage.warning('请先选择有库存的历史入仓单'); return }
+ ElMessage.info('货物明细已按所选入仓单的可用库存带出,请直接编辑现有明细')
+}
+function removeCargoRow(id: number) {
+ cargoRows.value = cargoRows.value.filter((row) => row.id !== id)
+}
+function clearRelabelRows() { cargoRows.value = []; selectedRelabelInboundOrderId.value = '' }
+function csvCell(value: string | number | undefined) { return `"${String(value ?? '').replaceAll('"', '""')}"` }
function downloadRelabelTemplate() {
- const header = ['商品SKU(选填)', '中文品名', 'FBA货件编号', '换标新SKU', '我方箱唛', 'FBA箱唛', '目的仓', '备注']
- const example = ['', '示例商品', 'FBA-SHIPMENT-001', 'LX1000-NEW', 'LX1000箱唛', 'FBA-LX1000箱唛', 'ONT8', '']
- const blob = new Blob([`\uFEFF${header.map(csvCell).join(',')}\r\n${example.map(csvCell).join(',')}\r\n`], { type: 'text/csv;charset=utf-8' })
+ if (!cargoRows.value.length) { ElMessage.warning('请先选择有库存的历史入仓单'); return }
+ const header = ['明细行标识', '本次箱数', '商品SKU', '中文品名', 'FBA货件编号', '换标新SKU', '我方箱唛', 'FBA箱唛', 'FBA目的仓', '备注']
+ const rows = cargoRows.value.map((item) => [item.id, item.quantity, item.sku, item.name, item.shipmentNo, item.newSkuCode,
+ item.cartonMark, item.fbaCartonMark, item.destinationWarehouseId, item.remark])
+ const blob = new Blob([`\uFEFF${[header, ...rows].map((row) => row.map(csvCell).join(',')).join('\r\n')}\r\n`], { type: 'text/csv;charset=utf-8' })
const url = URL.createObjectURL(blob)
const anchor = document.createElement('a')
anchor.href = url; anchor.download = '贴标申请-关联面单模板.csv'; anchor.click(); URL.revokeObjectURL(url)
@@ -84,20 +191,37 @@ async function importRelabelTemplate(event: Event) {
if (!file) return
const lines = (await file.text()).replace(/^\uFEFF/, '').split(/\r?\n/).filter((line) => line.trim())
if (lines.length < 2) { ElMessage.error('模板中没有可导入的明细'); return }
- const headers = parseCsvLine(lines[0]); const required = ['中文品名', 'FBA货件编号', '换标新SKU', '我方箱唛', 'FBA箱唛']
- const skuHeader = headers.includes('商品SKU(选填)') ? '商品SKU(选填)' : '商品SKU'
- if (!headers.includes(skuHeader)) { ElMessage.error('模板格式不正确,请重新下载模板填写'); return }
+ const headers = parseCsvLine(lines[0])
+ const required = ['明细行标识', '本次箱数', 'FBA货件编号', '换标新SKU', '我方箱唛', 'FBA箱唛', 'FBA目的仓']
if (required.some((name) => !headers.includes(name))) { ElMessage.error('模板格式不正确,请重新下载模板填写'); return }
- const imported = lines.slice(1).map(parseCsvLine).map((values) => Object.fromEntries(headers.map((key, index) => [key, values[index] || '']))).filter((item) => required.some((key) => item[key]))
- if (!imported.length || imported.some((item) => required.some((key) => !item[key]))) { ElMessage.error('模板中的商品、货件、换标 SKU 和箱唛信息必须填写完整'); return }
- if (new Set(imported.map((item) => item['我方箱唛'])).size !== imported.length) { ElMessage.error('模板中的我方箱唛不能重复'); return }
- cargoRows.value = imported.map((item) => ({
- id: ++cargoSequence, sku: item[skuHeader], name: item['中文品名'], quantity: 1, fnsku: '',
- shipmentNo: item['FBA货件编号'], newSkuCode: item['换标新SKU'], shippingMark: '',
- cartonMark: item['我方箱唛'], fbaCartonMark: item['FBA箱唛'],
- destinationWarehouseId: item['目的仓'] || '', remark: item['备注'] || '',
- }))
- ElMessage.success(`已导入 ${cargoRows.value.length} 箱货物明细`)
+ const imported = lines.slice(1).map(parseCsvLine)
+ .map((values) => Object.fromEntries(headers.map((key, index) => [key, values[index] || ''])))
+ .filter((item) => item['明细行标识'])
+ if (!imported.length || imported.some((item) => required.some((key) => !item[key]))) {
+ ElMessage.error('模板中的明细行、箱数、货件、换标 SKU、箱唛和 FBA 目的仓必须填写完整'); return
+ }
+ if (new Set(imported.map((item) => item['明细行标识'])).size !== imported.length) {
+ ElMessage.error('模板中的明细行标识不能重复'); return
+ }
+ const importedByRow = new Map(imported.map((item) => [item['明细行标识'], item]))
+ if (importedByRow.size !== cargoRows.value.length || cargoRows.value.some((row) => !importedByRow.has(String(row.id)))) {
+ ElMessage.error('模板明细与当前已选入仓单库存不一致,请重新下载模板'); return
+ }
+ for (const row of cargoRows.value) {
+ const item = importedByRow.get(String(row.id))!
+ const quantity = Number(item['本次箱数'])
+ if (!Number.isInteger(quantity) || quantity < 1 || quantity > Number(row.availableQuantity || 0)) {
+ ElMessage.error(`明细行 ${row.id} 的本次箱数不正确或超过可用库存`); return
+ }
+ row.quantity = quantity
+ row.shipmentNo = item['FBA货件编号']
+ row.newSkuCode = item['换标新SKU']
+ row.cartonMark = item['我方箱唛']
+ row.fbaCartonMark = item['FBA箱唛']
+ row.destinationWarehouseId = item['FBA目的仓']
+ row.remark = item['备注'] || ''
+ }
+ ElMessage.success(`已导入 ${imported.length} 条关联面单明细`)
}
function selectDiscardInbound(orderId: string) {
const candidate = discardCandidates.value.find((item) => item.orderId === orderId)
@@ -112,10 +236,21 @@ function selectDiscardInbound(orderId: string) {
}
function dateTime(value: string, end = false) { return value ? `${value}T${end ? '23:59:59' : '00:00:00'}` : undefined }
function row(values: Array) { return Object.fromEntries(values.map((value, index) => [String(index), value == null || value === '' ? '—' : String(value)])) }
+function statusLabel(value: string) {
+ return statusOptions.value.find((option) => option.value === value)?.label || value
+}
+function deliveryMethodLabel(value: string) {
+ return ({ TRUCK: '卡车派送', EXPRESS: '快递派送', '卡车派送': '卡车派送', '快递派送': '快递派送' } as Record)[value] || value
+}
+function formatDateTime(value: string) {
+ return value ? value.replace('T', ' ').slice(0, 19) : '—'
+}
function warehouseDisplay(value?: string) {
if (!value) return '—'
- const labels: Record = { ONT8: '美西海外仓-ONT8', ABE8: '美东海外仓-ABE8', MDW2: '美中海外仓-MDW2' }
- return value.split(',').map((warehouse) => labels[warehouse] || warehouse).join('、')
+ return value.split(',').map((code) => {
+ const warehouse = [...operationWarehouses.value, ...fbaWarehouses.value].find((item) => String(item.id) === code || item.warehouseCode === code)
+ return warehouse ? warehouseLabel(warehouse) : code
+ }).join('、')
}
async function load() {
loading.value = true
@@ -124,16 +259,16 @@ async function load() {
const result = await listRelabels({ page: 1, size: 20, relabelNo: filters.number || undefined, overseasWarehouseId: filters.warehouse || undefined, status: filters.status || undefined, createdFrom: dateTime(filters.start), createdTo: dateTime(filters.end, true) })
submittedRows.value = result.records.map((item) => {
const destinationWarehouses = warehouseDisplay(item.overseasWarehouseId)
- return row([item.relabelNo, item.customerOrderNo, destinationWarehouses, '—', item.itemCount, item.totalPackageCount, item.status, item.createdAt])
+ return row([item.relabelNo, item.customerOrderNo, destinationWarehouses, '—', item.itemCount, item.totalPackageCount, statusLabel(item.status), formatDateTime(item.createdAt)])
})
} else if (props.service === 'disposal') {
const result = await listDiscards({ page: 1, size: 20, discardNo: filters.number || undefined, inboundCode: filters.inboundNo || undefined, overseasWarehouseId: filters.warehouse || undefined, status: filters.status || undefined, createdFrom: dateTime(filters.start), createdTo: dateTime(filters.end, true) })
- submittedRows.value = result.records.map((item) => row([item.discardNo, item.inboundCodes, item.overseasWarehouseName || '—', '—', item.totalPackages, `${item.currency} ${item.totalFee}`, item.status, item.createdAt]))
+ submittedRows.value = result.records.map((item) => row([item.discardNo, item.inboundCodes, item.overseasWarehouseName || '—', '—', item.totalPackages, `${item.currency} ${item.totalFee}`, statusLabel(item.status), formatDateTime(item.createdAt)]))
} else {
const result = await listTransfers({ page: 1, size: 20, transferNo: filters.number || undefined, inboundCode: filters.inboundNo || undefined, operatingWarehouseId: filters.warehouse || undefined, fbaWarehouseCode: filters.destination || undefined, status: filters.status || undefined, needRelabel: props.service === 'relabel', createdFrom: dateTime(filters.start), createdTo: dateTime(filters.end, true) })
submittedRows.value = result.records.map((item) => props.service === 'transfer'
- ? row([item.transferNo, item.inboundCode, item.packageCount, '—', item.fbaWarehouseCode, item.deliveryMethod, item.needRelabel ? '是' : '否', item.status, item.createdAt])
- : row([item.transferNo, '—', item.fbaWarehouseCode, '—', item.packageCount, '是', item.relabelNo ? '已生成' : '待生成', item.status, item.createdAt]))
+ ? row([item.transferNo, item.inboundCode, item.packageCount, '—', item.fbaWarehouseCode, deliveryMethodLabel(item.deliveryMethod), item.needRelabel ? '是' : '否', statusLabel(item.status), formatDateTime(item.createdAt)])
+ : row([item.transferNo, '—', item.fbaWarehouseCode, '—', item.packageCount, '是', item.relabelNo ? '已生成' : '待生成', statusLabel(item.status), formatDateTime(item.createdAt)]))
}
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '列表加载失败') }
finally { loading.value = false }
@@ -141,21 +276,25 @@ async function load() {
function query() { void load() }
async function submit() {
const primaryWarehouse = props.service === 'relabel' ? form.sourceWarehouse : form.warehouse
+ const relabelRows = labelingMode.value === 'sku' ? skuRelabelRows.value : cargoRows.value
const invalidCargo = props.service === 'disposal'
? !selectedDiscardOrderId.value || cargoRows.value.every((item) => item.quantity < 1)
|| cargoRows.value.some((item) => item.quantity < 0 || item.quantity > Number(item.availableQuantity || 0))
- : cargoRows.value.length === 0 || cargoRows.value.some((item) => (props.service !== 'labeling' && !item.sku) || !item.name || item.quantity < 1)
- if ((!form.customerNo && !form.inboundNo) || (props.service !== 'labeling' && !primaryWarehouse) || invalidCargo) {
+ : relabelRows.length === 0 || (props.service === 'labeling' && labelingMode.value === 'sku'
+ ? skuRelabelRows.value.some((item) => !item.sourceRowId || !item.cartonMark || !item.originalSku || !item.newSku || item.quantity < 1
+ || (item.changeCarton && (!item.fbaCartonMark || !item.destinationWarehouseId)))
+ : cargoRows.value.some((item) => (props.service !== 'labeling' && !item.sku) || !item.name || item.quantity < 1))
+ if ((props.service !== 'labeling' && !form.customerNo && !form.inboundNo) || (props.service !== 'disposal' && !primaryWarehouse) || invalidCargo) {
ElMessage.warning('请填写必填信息和完整的货物明细'); return
}
- if (props.service === 'labeling' && (!form.labelDeadline || !form.contact || !form.phone || form.chargeAmount <= 0 || cargoRows.value.some((item) => !item.shipmentNo || !item.newSkuCode))) {
- ElMessage.warning('请填写贴标截止时间、联系人、贴标费用以及货物的货件编号和新 SKU'); return
+ if (props.service === 'labeling' && (!form.labelDeadline || !form.contact || !form.phone || form.chargeAmount <= 0)) {
+ ElMessage.warning('请填写贴标截止时间、联系人和贴标费用'); return
}
- if (props.service === 'labeling' && cargoRows.value.some((item) => !item.cartonMark || !item.fbaCartonMark)) {
+ if (props.service === 'labeling' && labelingMode.value === 'carton' && cargoRows.value.some((item) => !item.shipmentNo || !item.newSkuCode || !item.cartonMark || !item.fbaCartonMark)) {
ElMessage.warning('请上传关联面单,确保每箱的我方箱唛和 FBA 箱唛完整'); return
}
- if (props.service === 'labeling' && cargoRows.value.some((item) => !item.destinationWarehouseId && !form.warehouse)) {
- ElMessage.warning('请填写统一目的海外仓,或为每箱货物填写目的仓'); return
+ if (props.service === 'labeling' && labelingMode.value === 'carton' && cargoRows.value.some((item) => !item.destinationWarehouseId || !item.sourceInventoryPositionId)) {
+ ElMessage.warning('请选择有库存的历史入仓单,并为每项填写 FBA 目的仓'); return
}
if (props.service === 'disposal' && !form.reason) {
ElMessage.warning('请选择废弃原因'); return
@@ -168,7 +307,11 @@ async function submit() {
try {
const idempotencyKey = crypto.randomUUID()
if (props.service === 'labeling') {
- await createRelabel({ customerOrderNo: form.customerNo, overseasWarehouseId: form.warehouse || undefined, labelDeadline: form.labelDeadline, contactName: form.contact, contactPhone: form.phone, chargeAmount: form.chargeAmount, remark: form.remark || undefined, idempotencyKey, items: cargoRows.value.map((item) => ({ skuCode: item.sku, productNameZh: item.name, totalPackageCount: 1, notifiedPackageCount: 1, shipmentNo: item.shipmentNo, trackingCode: form.trackingNo || undefined, newSkuCode: item.newSkuCode, labelRequirement: form.requirement || undefined, cartonMark: item.cartonMark, fbaCartonMark: item.fbaCartonMark, destinationWarehouseId: item.destinationWarehouseId || undefined, remark: item.remark || undefined })) })
+ const sourceInboundOrders = [...new Set(cargoRows.value.map((item) => item.sourceInboundOrderNo || item.sourceInboundCode).filter((value): value is string => Boolean(value)))].join('、').slice(0, 128)
+ const items = labelingMode.value === 'carton'
+ ? cargoRows.value.map((item) => ({ sourceInventoryBalanceId: item.sourceInventoryBalanceId!, sourceInventoryPositionId: item.sourceInventoryPositionId!, sourceInboundOrderId: item.sourceInboundOrderId!, skuCode: item.sku, productNameZh: item.name, totalPackageCount: item.quantity, notifiedPackageCount: item.quantity, shipmentNo: item.shipmentNo, trackingCode: form.trackingNo || undefined, newSkuCode: item.newSkuCode, labelRequirement: form.requirement || undefined, cartonMark: item.cartonMark, fbaCartonMark: item.fbaCartonMark, destinationFbaWarehouseCode: item.destinationWarehouseId, operationType: 'CARTON_LABEL' as const, skuItems: [], remark: item.remark || undefined }))
+ : buildSkuRelabelItems()
+ await createRelabel({ customerOrderNo: sourceInboundOrders || '贴标申请', operationWarehouseId: form.warehouse, labelDeadline: form.labelDeadline, contactName: form.contact, contactPhone: form.phone, chargeAmount: form.chargeAmount, remark: form.remark || undefined, idempotencyKey, items })
} else if (props.service === 'disposal') {
const items = cargoRows.value.filter((item) => item.quantity > 0).map((item) => {
if (!item.sourceOrderId || item.sourceOrderId !== selectedDiscardOrderId.value || item.quantity > Number(item.availableQuantity || 0)) {
@@ -195,6 +338,57 @@ async function openDetail(value: Record) {
mode.value = 'detail'
} catch (error) { ElMessage.error(error instanceof Error ? error.message : '详情加载失败') }
}
+async function loadWarehouses() {
+ fbaWarehousesLoading.value = true
+ fbaWarehousesError.value = ''
+ try {
+ const [fbaResult, operationResult] = await Promise.all([
+ getMerchantWarehouses({ warehouseType: 'FBA' }),
+ getMerchantWarehouses({ warehouseType: 'OVERSEAS' }),
+ ])
+ fbaWarehouses.value = fbaResult.records
+ operationWarehouses.value = operationResult.records
+ } catch (error) {
+ fbaWarehouses.value = []
+ fbaWarehousesError.value = error instanceof Error ? error.message : 'FBA 目的仓加载失败'
+ ElMessage.error(fbaWarehousesError.value)
+ } finally { fbaWarehousesLoading.value = false }
+}
+function warehouseLabel(value: MerchantWarehouse) { return `${value.warehouseName} · ${value.warehouseCode}` }
+async function selectOperationWarehouse() {
+ cargoRows.value = []
+ skuRelabelRows.value = []
+ selectedRelabelInboundOrderId.value = ''
+ relabelCandidates.value = []
+ const warehouse = operationWarehouses.value.find((item) => String(item.id) === form.warehouse)
+ form.contact = warehouse?.contactName || ''
+ form.phone = warehouse?.contactPhone || ''
+ if (!form.warehouse) return
+ relabelCandidatesLoading.value = true
+ try { relabelCandidates.value = await listRelabelInventoryCandidates(form.warehouse) }
+ catch (error) { ElMessage.error(error instanceof Error ? error.message : '当前客户库存加载失败') }
+ finally { relabelCandidatesLoading.value = false }
+}
+function selectRelabelInventory(inboundOrderId: string) {
+ const existing = new Map(cargoRows.value.map((row) => [row.sourceInventoryPositionId, row]))
+ cargoRows.value = relabelCandidates.value
+ .filter((candidate) => String(candidate.inboundOrderId) === inboundOrderId)
+ .flatMap((candidate) => candidate.positions.map((position) => {
+ const previous = existing.get(String(position.positionId))
+ return {
+ id: previous?.id || ++cargoSequence, sku: candidate.skuCode || '', name: candidate.productNameZh || '库存货物',
+ quantity: Math.min(previous?.quantity || 1, position.availablePackages), availableQuantity: position.availablePackages,
+ sourceInventoryBalanceId: String(candidate.inventoryBalanceId), sourceInventoryPositionId: String(position.positionId),
+ sourceInboundOrderId: String(candidate.inboundOrderId), sourceInboundOrderNo: candidate.inboundOrderNo,
+ sourceInboundCode: candidate.inboundCode, sourceWarehouseName: candidate.overseasWarehouseName,
+ palletNo: position.palletNo, warehouseLocationCode: position.warehouseLocationCode,
+ fnsku: '', shipmentNo: previous?.shipmentNo || '', newSkuCode: previous?.newSkuCode || '', shippingMark: '',
+ cartonMark: previous?.cartonMark || '', fbaCartonMark: previous?.fbaCartonMark || '',
+ destinationWarehouseId: previous?.destinationWarehouseId || candidate.destinationFbaWarehouseCode || '', remark: previous?.remark || '',
+ }
+ }))
+ skuRelabelRows.value = []
+}
@@ -215,7 +409,7 @@ async function openDetail(value: Record) {
查询重置
@@ -226,32 +420,47 @@ async function openDetail(value: Record) {