From 6e4483135bdbe868adfc29719a11e49084cdc23b Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 9 Sep 2026 17:58:26 +0800 Subject: [PATCH 01/14] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../first-leg/components/InboundBillingPanel.test.mjs | 2 +- src/modules/first-leg/components/InboundBillingPanel.vue | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/modules/first-leg/components/InboundBillingPanel.test.mjs b/src/modules/first-leg/components/InboundBillingPanel.test.mjs index 77c3e26..ce13418 100644 --- a/src/modules/first-leg/components/InboundBillingPanel.test.mjs +++ b/src/modules/first-leg/components/InboundBillingPanel.test.mjs @@ -25,7 +25,7 @@ test('supports a standalone dialog and keeps the unpushed billing-weight state v }) test('shows the published merchant inbound, measurement details and customer confirmation entry', () => { - for (const label of ['入仓码', '平台收货仓', '实际毛重', '实测体积', '托盘与库位', '换算与精度', '最终计费量', '基础费用', '附加费用', '费用合计', '平台备注']) { + for (const label of ['入仓码', '平台收货仓', '实际毛重', '实测体积', '托盘与库位', '换算与精度', '最终计费量', '对客单价', '基础费用', '附加费用', '费用合计', '平台备注']) { assert.match(source, new RegExp(label)) } assert.match(source, /formatInboundMoney\(props\.data\.latestFee\.totalAmount/) diff --git a/src/modules/first-leg/components/InboundBillingPanel.vue b/src/modules/first-leg/components/InboundBillingPanel.vue index 1cb573d..495111f 100644 --- a/src/modules/first-leg/components/InboundBillingPanel.vue +++ b/src/modules/first-leg/components/InboundBillingPanel.vue @@ -227,6 +227,10 @@ async function confirmFee() {
外箱规总材积重
{{ formatInboundMeasure(props.data.measurementSummary.volumeWeightKg, 'kg') }}
计费体积
{{ formatInboundMeasure(props.data.billingBasis.chargeableVolumeCbm, 'm³') }}
计费重量
{{ formatInboundMeasure(props.data.billingBasis.chargeableWeightKg, 'kg') }}
+
对客单价
{{ formatInboundMoney(props.data.latestFee.unitPrice, props.data.latestFee.currency) }} / {{ props.data.latestFee.chargeUnit }}
+
基础费用
{{ formatInboundMoney(props.data.latestFee.subtotalAmount, props.data.latestFee.currency) }}
+
附加费用
{{ formatInboundMoney(props.data.latestFee.additionalAmount, props.data.latestFee.currency) }}
+
费用合计
{{ formatInboundMoney(props.data.latestFee.totalAmount, props.data.latestFee.currency) }}
@@ -248,11 +252,11 @@ async function confirmFee() { :closable="false" show-icon /> - + From a9de9f762e767bd34ca9c40758676cdcb930e901 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 10 Sep 2026 11:52:33 +0800 Subject: [PATCH 02/14] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/modules/first-leg/components/InboundBillingPanel.test.mjs | 1 + src/modules/first-leg/components/InboundBillingPanel.vue | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/first-leg/components/InboundBillingPanel.test.mjs b/src/modules/first-leg/components/InboundBillingPanel.test.mjs index ce13418..d67fe10 100644 --- a/src/modules/first-leg/components/InboundBillingPanel.test.mjs +++ b/src/modules/first-leg/components/InboundBillingPanel.test.mjs @@ -42,6 +42,7 @@ test('shows the published merchant inbound, measurement details and customer con assert.match(source, /latestFee\.rateCode/) assert.match(source, /latestFee\.chargeRuleVersion/) assert.match(source, /latestFee\.carrierRateVersion/) + assert.doesNotMatch(source, /箱规规则|calculationRuleVersion/) }) test('stacks core information for 720 and 360 pixel layouts', () => { diff --git a/src/modules/first-leg/components/InboundBillingPanel.vue b/src/modules/first-leg/components/InboundBillingPanel.vue index 495111f..5094542 100644 --- a/src/modules/first-leg/components/InboundBillingPanel.vue +++ b/src/modules/first-leg/components/InboundBillingPanel.vue @@ -241,7 +241,6 @@ async function confirmFee() { - Date: Thu, 10 Sep 2026 19:27:22 +0800 Subject: [PATCH 03/14] =?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 ++ 1 file changed, 2 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index ac782aa..4a7a97b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,6 +58,8 @@ ## 租户、认证与数据安全 +- 换标申请必须提交正数 CNY `chargeAmount`;入口为 `src/modules/overseas-warehouse/views/WarehouseServiceView.vue` 与 `api/warehouseServices.ts`,金额由服务端再次校验,资金状态以服务端返回为准,不在客户端模拟冻结成功。 + - 任何查询和变更都必须以服务端确认的当前租户为边界;绝不从可篡改的前端参数推断授权租户。 - 企业成员、角色和权限只能管理当前企业范围。前端隐藏不是安全控制,服务端必须重复校验租户与权限。 - 注册时登录手机号同时初始化公司联系电话;注册完成后两者独立维护。企业资料只修改 `contactPhone`,个人资料或成员管理中的登录手机号由 IAM/Auth 同步并撤销该账号现有会话,不得在前端互相回填或联动提交。 From b2c92d566676704d4478ee1862ba3dc49b0b37e6 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 10 Sep 2026 22:43:11 +0800 Subject: [PATCH 04/14] feat: align customer finance account --- AGENTS.md | 6 ++++-- src/modules/finance/api/account.ts | 3 +++ src/modules/finance/views/FinanceAccountView.vue | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 019eff2..ce0f91d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,6 +39,8 @@ ## 模块快速查询 +退货预报与签收结果:`src/modules/overseas-warehouse/views/ReturnOrdersView.vue`、`api/returnOrders.ts`;菜单仍名为“退货换标”,正式路由 `return-relabel` 已使用独立页面,不再由 `WarehouseServiceView.vue` 承载。客户退货库存/箱号共用 `views/ReturnInventoryView.vue`、`api/returnInventory.ts`,`return-cartons` 传 `cartonOnly=true`。Backend 为 Lastleg `returnreceipt/`,退货单签收完成不等于换标或退货出库完成;后续换标、出库须按各自业务状态核对。 + 先从 `src/modules/registry.ts` 确认模块,再按目标模块的 `routes.ts → views/ → api/、store/、contracts.ts 或 public.ts` 查询。页面是否正式开放以 `routes.ts` 的 `release` 为准,不从任务或进度文档推断。 | 业务 | 模块入口 | 联查模块或服务端 | @@ -51,10 +53,10 @@ | 装柜完成后的退税报关资料 | `src/modules/first-leg/views/FirstLegOrdersView.vue` 的“报关资料”、`components/CustomsDocumentsDialog.vue`、`api/customs.ts` | 只显示登录客户在同柜的退税入仓码;报关开始前可上传、替换、调整同一资料覆盖的一个或多个入仓码及删除误传,全部写入携带幂等键和版本;开始后只读冻结资料,整柜完成后才能下载平台回传的一对一完成文件。Backend `firstleg/customs/container/`、`customs-v1.yaml`,二进制归 `file/` | | 统一订单展示 | `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/finance/views/FinanceAccountView.vue`、`api/account.ts` | 显示当前企业 CNY 实际余额、冻结金额、授信额度、授信有效期、已用授信、冻结授信、总可用余额和不可变流水;数据加载前显示“—”,不得先伪装为零。授信到期日当天仍有效,次日已用授信并入实际余额,负数实际余额会阻止提交新订单。充值只展示服务端启用的微信/支付宝静态收款码,客户上传 JPG/PNG 付款截图后进入待审核。客户范围只取登录态,无收款码时禁止提交,不模拟支付成功 | | 海外采购、美国本土卡与美本土散板 | `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`;贴标申请先选国外中转仓和有库存历史入仓单,换标申请必须提交正数 CNY `chargeAmount`,换标类型为只换 SKU、只换箱唛、SKU+箱唛。箱唛换标明细一行固定代表一箱,箱数不可编辑,提交也固定为 1。货物明细外层始终提供“SKU 关联”“箱号关联”“SKU 混箱换标”三套模板的下载、上传,不随换标类型切换;上传只进入待导入关系区,客户可勾选部分或全选后导入本次明细,只有进入明细的库存才随申请提交和冻结。明细也支持手动新增编辑,组合换标以旧箱唛连接旧/新 SKU 和旧箱唛/新 FBA 箱唛关系。涉及箱唛换标时必须选择卡车、自提或快递;快递可选择打印面单并上传 PDF/图片。PDA 逐箱换标、上传照片并由仓库复核后,客户详情展示完工照片和“换标完成确认”;客户确认前不得继续出库,确认后卡车进入预排车,自提/快递进入换标出库待创建。历史库存位置不能解释成箱唛与具体托盘快照,客户页面不暴露托盘、库位和内部库存 ID | +| 海外仓贴标、仓储转运、货物废弃 | `src/modules/overseas-warehouse/routes.ts`、`views/WarehouseServiceView.vue`、`api/warehouseServices.ts` | 三个路由均为 `active`;退货预报另见本节独立入口;贴标申请先选国外中转仓和有库存历史入仓单,换标申请必须提交正数 CNY `chargeAmount`,换标类型为只换 SKU、只换箱唛、SKU+箱唛。箱唛换标明细一行固定代表一箱,箱数不可编辑,提交也固定为 1。货物明细外层始终提供“SKU 关联”“箱号关联”“SKU 混箱换标”三套模板的下载、上传,不随换标类型切换;上传只进入待导入关系区,客户可勾选部分或全选后导入本次明细,只有进入明细的库存才随申请提交和冻结。明细也支持手动新增编辑,组合换标以旧箱唛连接旧/新 SKU 和旧箱唛/新 FBA 箱唛关系。涉及箱唛换标时必须选择卡车、自提或快递;快递可选择打印面单并上传 PDF/图片。PDA 逐箱换标、上传照片并由仓库复核后,客户详情展示完工照片和“换标完成确认”;客户确认前不得继续出库,确认后卡车进入预排车,自提/快递进入换标出库待创建。历史库存位置不能解释成箱唛与具体托盘快照,客户页面不暴露托盘、库位和内部库存 ID | ## 租户、认证与数据安全 diff --git a/src/modules/finance/api/account.ts b/src/modules/finance/api/account.ts index 703036f..5d51a2d 100644 --- a/src/modules/finance/api/account.ts +++ b/src/modules/finance/api/account.ts @@ -34,6 +34,8 @@ export interface FinanceAccount { creditUsed: string creditFrozen: string availableCredit: string + creditExpiresOn: string | null + creditActive: boolean version: number updatedAt: string } @@ -51,6 +53,7 @@ export interface AccountLedgerEntry { creditLimitAfter: string creditUsedAfter: string availableCreditAfter: string + creditExpiresOnAfter: string | null sourceType: 'CLIENT_RECHARGE' | 'MANUAL_RECHARGE' | 'MANUAL_DEDUCTION' | 'CREDIT_LIMIT_ADJUSTMENT' | string sourceNo: string operatorName: string | null diff --git a/src/modules/finance/views/FinanceAccountView.vue b/src/modules/finance/views/FinanceAccountView.vue index 539a46d..d1da129 100644 --- a/src/modules/finance/views/FinanceAccountView.vue +++ b/src/modules/finance/views/FinanceAccountView.vue @@ -225,7 +225,7 @@ onMounted(() => { void loadAccountData() })
实际余额¥ {{ formatAccountAmount(account?.actualBalance) }}当前账户实际资金
冻结金额¥ {{ formatAccountAmount(account?.frozenBalance) }}暂不可使用的资金
总可用余额¥ {{ formatAccountAmount(account?.availableBalance) }}实际可用资金与可用授信合计
-
授信额度¥ {{ formatAccountAmount(account?.creditLimit) }}平台配置的授信上限
+
授信额度¥ {{ formatAccountAmount(account?.creditLimit) }}{{ account?.creditExpiresOn ? `${account.creditExpiresOn} ${account.creditActive ? '到期' : '已到期'}` : '未设置有效期' }}
已用授信¥ {{ formatAccountAmount(account?.creditUsed) }}已结算使用的授信
可用授信¥ {{ formatAccountAmount(account?.availableCredit) }}扣除已用和冻结后的授信
From 55914382ded266e4ccee78714fa860ecb7d37c6b Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 11 Sep 2026 15:28:19 +0800 Subject: [PATCH 05/14] fix: improve return receipt client display --- .../views/ReturnOrdersView.test.mjs | 21 ++++++++++++ .../views/ReturnOrdersView.vue | 32 +++++++++++++------ 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs b/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs index 4b77e69..db01a44 100644 --- a/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs +++ b/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs @@ -21,3 +21,24 @@ test('新建退货预报使用加宽的双列表单弹窗', () => { assert.match(view, /title="新建退货预报" width="1120px"/) assert.match(view, /return-create-form \{ display:grid; grid-template-columns:repeat\(2,minmax\(0,1fr\)\)/) }) + +test('退货列表和详情使用客户可读仓库、状态与时间', () => { + assert.match(view, /function operationWarehouseLabel\(warehouseId: string\)/) + assert.match(view, /return warehouse \? warehouseLabel\(warehouse\) : '未关联'/) + assert.match(view, /PENDING_RECEIPT: '待签收'/) + assert.match(view, /PARTIALLY_RECEIVED: '部分签收'/) + assert.match(view, /COMPLETED: '已完成'/) + assert.match(view, /function formatDateTime\(value\?: string \| null\)/) + assert.match(view, /\{\{ operationWarehouseLabel\(row\.overseasWarehouseId\) \}\}/) + assert.match(view, /\{\{ statusLabel\(row\.status\) \}\}/) + assert.match(view, /\{\{ formatDateTime\(row\.createdAt\) \}\}/) + assert.doesNotMatch(view, / { + const mainTable = view.slice(view.indexOf('', view.indexOf('/) + assert.match(createTable, /^/) +}) diff --git a/src/modules/overseas-warehouse/views/ReturnOrdersView.vue b/src/modules/overseas-warehouse/views/ReturnOrdersView.vue index e385c89..8eba9ad 100644 --- a/src/modules/overseas-warehouse/views/ReturnOrdersView.vue +++ b/src/modules/overseas-warehouse/views/ReturnOrdersView.vue @@ -3,7 +3,7 @@ import { computed, onMounted, reactive, ref } from 'vue' import { ElMessage } from 'element-plus' import { Plus, Search } from '@lucide/vue' import { getMerchantWarehouses, type MerchantWarehouse } from '@/modules/first-leg/api/firstLeg' -import { createReturnOrder, getReturnOrder, listReturnOrders, type ReturnOrderDetail, type ReturnOrderSummary } from '../api/returnOrders' +import { createReturnOrder, getReturnOrder, listReturnOrders, type ReturnOrderDetail, type ReturnOrderSummary, type ReturnReceipt } from '../api/returnOrders' const loading = ref(false) const creating = ref(false) @@ -32,6 +32,10 @@ const duplicateSkuIds = computed(() => { }) function resetForm() { Object.assign(form, { overseasWarehouseId: '', warehouseContactName: '', warehouseContactPhone: '', customerReferenceNo: '', remark: '', needRelabel: false, items: [emptySku()] }) } function warehouseLabel(warehouse: MerchantWarehouse) { return `${warehouse.warehouseName} · ${warehouse.warehouseCode}` } +function operationWarehouseLabel(warehouseId: string) { + const warehouse = operationWarehouses.value.find(item => String(item.id) === String(warehouseId)) + return warehouse ? warehouseLabel(warehouse) : '未关联' +} function selectOperationWarehouse() { const warehouse = operationWarehouses.value.find(item => String(item.id) === form.overseasWarehouseId) form.warehouseContactName = warehouse?.contactName || '' @@ -53,7 +57,7 @@ async function loadOperationWarehouses() { function openCreate() { resetForm() showCreate.value = true - void loadOperationWarehouses() + if (!operationWarehouses.value.length && !operationWarehousesLoading.value) void loadOperationWarehouses() } function addItem() { form.items.push(emptySku()) } function removeItem(index: number) { if (form.items.length > 1) form.items.splice(index, 1) } @@ -75,7 +79,17 @@ async function submit() { } finally { creating.value = false } } function statusType(status: string) { return status === 'COMPLETED' ? 'success' : status === 'PARTIALLY_RECEIVED' ? 'warning' : 'info' } -onMounted(load) +function statusLabel(status: string) { + return ({ PENDING_RECEIPT: '待签收', PARTIALLY_RECEIVED: '部分签收', COMPLETED: '已完成' } as Record)[status] ?? '未知状态' +} +function startedByLabel(startedBy: ReturnReceipt['startedBy']) { return startedBy === 'SKU' ? 'SKU 查询' : '退货单号' } +function formatDateTime(value?: string | null) { + if (!value) return '—' + const date = new Date(value) + if (Number.isNaN(date.getTime())) return '—' + return new Intl.DateTimeFormat('zh-CN', { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }).format(date) +} +onMounted(() => { void Promise.all([load(), loadOperationWarehouses()]) }) From e1eabf2946d53a3dd91355e00bbbb03820182485 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 11 Sep 2026 15:30:46 +0800 Subject: [PATCH 06/14] fix: avoid duplicate return order loading --- .../overseas-warehouse/views/ReturnOrdersView.test.mjs | 7 +++++++ src/modules/overseas-warehouse/views/ReturnOrdersView.vue | 7 ++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs b/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs index db01a44..6c1d5bc 100644 --- a/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs +++ b/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs @@ -42,3 +42,10 @@ test('可操作退货表格把操作列固定在最左侧', () => { assert.match(mainTable, /^/) assert.match(createTable, /^/) }) + +test('分页初始化不会重复请求退货列表', () => { + assert.match(view, /function changePage\(page: number\) \{\s*if \(page === query\.page\) return/) + assert.match(view, /:current-page="query\.page" :page-size="query\.size"/) + assert.match(view, /@current-change="changePage"/) + assert.doesNotMatch(view, /@current-change="load"/) +}) diff --git a/src/modules/overseas-warehouse/views/ReturnOrdersView.vue b/src/modules/overseas-warehouse/views/ReturnOrdersView.vue index 8eba9ad..2ba6248 100644 --- a/src/modules/overseas-warehouse/views/ReturnOrdersView.vue +++ b/src/modules/overseas-warehouse/views/ReturnOrdersView.vue @@ -66,6 +66,11 @@ async function load() { try { const response = await listReturnOrders({ ...query, returnNo: query.returnNo || undefined, trackingNo: query.trackingNo || undefined, skuCode: query.skuCode || undefined, status: query.status || undefined }); rows.value = response.records; total.value = response.total } finally { loading.value = false } } +function changePage(page: number) { + if (page === query.page) return + query.page = page + void load() +} async function openDetail(returnNo: string) { selected.value = await getReturnOrder(returnNo); showDetail.value = true } async function submit() { const items = form.items.map(({ skuCode, productNameZh, expectedQuantity }) => ({ skuCode: skuCode.trim(), productNameZh: productNameZh.trim(), expectedQuantity })) @@ -114,7 +119,7 @@ onMounted(() => { void Promise.all([load(), loadOperationWarehouses()]) }) - + From 53c48501dcb7c520e364f3398dbb2a2579fc1c54 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 11 Sep 2026 15:33:55 +0800 Subject: [PATCH 07/14] fix: keep return forecast labels on one line --- src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs | 1 + src/modules/overseas-warehouse/views/ReturnOrdersView.vue | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs b/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs index 6c1d5bc..88baa69 100644 --- a/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs +++ b/src/modules/overseas-warehouse/views/ReturnOrdersView.test.mjs @@ -19,6 +19,7 @@ test('新建退货预报选择中转仓后带出联系人和电话', () => { test('新建退货预报使用加宽的双列表单弹窗', () => { assert.match(view, /title="新建退货预报" width="1120px"/) + assert.match(view, //) assert.match(view, /return-create-form \{ display:grid; grid-template-columns:repeat\(2,minmax\(0,1fr\)\)/) }) diff --git a/src/modules/overseas-warehouse/views/ReturnOrdersView.vue b/src/modules/overseas-warehouse/views/ReturnOrdersView.vue index 2ba6248..daca1db 100644 --- a/src/modules/overseas-warehouse/views/ReturnOrdersView.vue +++ b/src/modules/overseas-warehouse/views/ReturnOrdersView.vue @@ -122,7 +122,7 @@ onMounted(() => { void Promise.all([load(), loadOperationWarehouses()]) }) - +
{{ operationWarehousesError }}
From 80e83a66689c2531068f5652dbe3b3eff6191c87 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 11 Sep 2026 15:38:28 +0800 Subject: [PATCH 08/14] feat: model mixed-SKU dropship cartons --- src/modules/first-leg/api/firstLeg.ts | 1 + src/modules/first-leg/store/firstLegOrders.ts | 3 +- src/modules/first-leg/types/firstLegOrder.ts | 1 + .../first-leg/utils/firstLegOrder.test.mjs | 25 +++++++++++++- src/modules/first-leg/utils/firstLegOrder.ts | 33 +++++++++++++++++-- .../views/FirstLegOrdersView.test.mjs | 2 ++ .../first-leg/views/FirstLegOrdersView.vue | 11 ++++--- 7 files changed, 67 insertions(+), 9 deletions(-) diff --git a/src/modules/first-leg/api/firstLeg.ts b/src/modules/first-leg/api/firstLeg.ts index 5273066..964be03 100644 --- a/src/modules/first-leg/api/firstLeg.ts +++ b/src/modules/first-leg/api/firstLeg.ts @@ -113,6 +113,7 @@ export interface FirstLegOrderCargoPayload { shipmentId?: string referenceId?: string productImageFileId?: number | string + packingGroupNo?: string } export interface ProductImageUploadResult { diff --git a/src/modules/first-leg/store/firstLegOrders.ts b/src/modules/first-leg/store/firstLegOrders.ts index 5fc9e54..75703f4 100644 --- a/src/modules/first-leg/store/firstLegOrders.ts +++ b/src/modules/first-leg/store/firstLegOrders.ts @@ -90,6 +90,7 @@ function savePayload(draft: FirstLegOrderDraft, warehouse: ClientWarehouse, cust customsHsCode: item.customsHsCode || undefined, packageType: item.packageType, packageCount: item.packageCount, + packingGroupNo: draft.fulfillmentType === 'ONE_CLICK_DROPSHIP' ? item.packingGroupNo.trim() || undefined : undefined, grossWeightKg: item.grossWeightKg, volumeCbm: item.volumeCbm, lengthCm: item.lengthCm || undefined, @@ -296,7 +297,7 @@ function mapOrder(order: MerchantFirstLegOrder, warehouses: PlatformReceivingWar productImageName: '', productImagePreviewUrl: '', productImageUploading: false, productImageError: '', customsHsCode: item.customsHsCode ?? '', lengthCm: item.lengthCm ?? 0, widthCm: item.widthCm ?? 0, heightCm: item.heightCm ?? 0, unitsPerPackage: item.unitsPerPackage ?? 1, - pieces: item.totalQuantity ?? item.packageCount, packageCount: item.packageCount, grossWeightKg: item.grossWeightKg, + pieces: item.totalQuantity ?? item.packageCount, packageCount: item.packageCount, packingGroupNo: item.packingGroupNo ?? '', grossWeightKg: item.grossWeightKg, volumeCbm: item.volumeCbm, remark: item.cargoRemark ?? '', packageType: item.packageType, hsCode: item.hsCode ?? '', declaredValue: item.declaredValue ?? 0, currency: item.currency ?? 'USD', dangerousGoods: item.dangerousGoods ?? false, diff --git a/src/modules/first-leg/types/firstLegOrder.ts b/src/modules/first-leg/types/firstLegOrder.ts index 18b24e6..d663a29 100644 --- a/src/modules/first-leg/types/firstLegOrder.ts +++ b/src/modules/first-leg/types/firstLegOrder.ts @@ -26,6 +26,7 @@ export interface FirstLegCargoItem { unitsPerPackage: number pieces: number packageCount: number + packingGroupNo: string grossWeightKg: number volumeCbm: number netWeightKg: number diff --git a/src/modules/first-leg/utils/firstLegOrder.test.mjs b/src/modules/first-leg/utils/firstLegOrder.test.mjs index 65a4c79..d7d6a93 100644 --- a/src/modules/first-leg/utils/firstLegOrder.test.mjs +++ b/src/modules/first-leg/utils/firstLegOrder.test.mjs @@ -54,7 +54,7 @@ function validDraft() { destinationType: 'FBA', amazonWarehouseCode: 'ONT8', shipmentId: 'FBA-001', - items: [{ ...createEmptyCargoItem('cargo-1'), sku: 'SKU-001', upc: '012345678905', cargoNameCn: '家居用品', pieces: 100, grossWeightKg: 1200, volumeCbm: 3.5, shipmentId: 'FBA-001', referenceId: 'REF-001' }], + items: [{ ...createEmptyCargoItem('cargo-1'), sku: 'SKU-001', upc: '012345678905', cargoNameCn: '家居用品', unitsPerPackage: 100, pieces: 100, grossWeightKg: 1200, volumeCbm: 3.5, shipmentId: 'FBA-001', referenceId: 'REF-001' }], } } @@ -94,6 +94,17 @@ test('validates the confirmed minimum submission fields', () => { assert.ok(errors.includes('第 1 条货物毛重必须大于 0')) }) +test('rejects a total quantity that does not equal units per package multiplied by package count', () => { + const draft = validDraft() + draft.items[0].unitsPerPackage = 24 + draft.items[0].packageCount = 5 + draft.items[0].pieces = 119 + + assert.ok(validateFirstLegOrderDraft(draft).includes('第 1 条货物总数量应等于每箱数量 × 箱数(120)')) + draft.items[0].pieces = 120 + assert.ok(!validateFirstLegOrderDraft(draft).some((message) => message.includes('总数量应等于'))) +}) + test('only requires sender for pickup and receiver for a private destination', () => { const publicDraft = validDraft() publicDraft.shipperPartySnapshot = null @@ -169,3 +180,15 @@ test('summarizes cargo values from line items', () => { volumeCbm: 4.75, }) }) + +test('counts one physical carton series for SKU lines in the same mixed-carton group', () => { + const first = { ...validDraft().items[0], packingGroupNo: 'A', packageCount: 10, pieces: 20, unitsPerPackage: 2 } + const second = { ...first, id: 'cargo-2', sku: 'SKU-002', pieces: 30, unitsPerPackage: 3 } + + assert.equal(summarizeFirstLegCargo([first, second]).packageCount, 10) + const draft = { ...validDraft(), fulfillmentType: 'ONE_CLICK_DROPSHIP', items: [first, second] } + assert.ok(!validateFirstLegOrderDraft(draft).some((message) => message.includes('混箱组合'))) + + second.packageCount = 9 + assert.ok(validateFirstLegOrderDraft(draft).includes('混箱组合 A 的箱数、包装类型和箱规必须一致')) +}) diff --git a/src/modules/first-leg/utils/firstLegOrder.ts b/src/modules/first-leg/utils/firstLegOrder.ts index 30709d4..708ce27 100644 --- a/src/modules/first-leg/utils/firstLegOrder.ts +++ b/src/modules/first-leg/utils/firstLegOrder.ts @@ -65,6 +65,7 @@ export function createEmptyCargoItem(id = `cargo-${Date.now()}`): FirstLegCargoI unitsPerPackage: 1, pieces: 1, packageCount: 1, + packingGroupNo: '', grossWeightKg: 0, volumeCbm: 0, netWeightKg: 0, @@ -130,22 +131,48 @@ export function validateFirstLegOrderDraft(draft: FirstLegOrderDraft) { if (!Number.isInteger(item.pieces) || item.pieces <= 0) errors.push(`${line}件数必须为正整数`) if (!Number.isInteger(item.packageCount) || item.packageCount <= 0) errors.push(`${line}箱数必须为正整数`) if (!Number.isInteger(item.unitsPerPackage) || item.unitsPerPackage <= 0) errors.push(`${line}每箱数量必须为正整数`) + if (Number.isInteger(item.unitsPerPackage) && item.unitsPerPackage > 0 + && Number.isInteger(item.packageCount) && item.packageCount > 0 + && Number.isInteger(item.pieces) && item.pieces > 0 + && item.pieces !== item.unitsPerPackage * item.packageCount) { + errors.push(`${line}总数量应等于每箱数量 × 箱数(${item.unitsPerPackage * item.packageCount})`) + } if (item.grossWeightKg <= 0) errors.push(`${line}毛重必须大于 0`) if (item.volumeCbm <= 0) errors.push(`${line}体积必须大于 0`) if (item.woodenProduct && draft.customsInspection !== true) errors.push(`${line}为木制品,“是否商检”必须选择“是”`) }) + if (draft.fulfillmentType === 'ONE_CLICK_DROPSHIP') { + const groups = new Map() + draft.items.forEach((item) => { + const group = item.packingGroupNo.trim() + if (!group) return + const first = groups.get(group) + if (!first) groups.set(group, item) + else if (first.packageCount !== item.packageCount || first.packageType !== item.packageType + || first.lengthCm !== item.lengthCm || first.widthCm !== item.widthCm || first.heightCm !== item.heightCm) { + errors.push(`混箱组合 ${group} 的箱数、包装类型和箱规必须一致`) + } + }) + } + return errors } export function summarizeFirstLegCargo(items: FirstLegCargoItem[]) { + const countedGroups = new Set() return items.reduce( - (summary, item) => ({ + (summary, item) => { + const group = item.packingGroupNo.trim() + const packageCount = group && countedGroups.has(group) ? 0 : item.packageCount + if (group) countedGroups.add(group) + return ({ pieces: summary.pieces + item.pieces, - packageCount: summary.packageCount + item.packageCount, + packageCount: summary.packageCount + packageCount, grossWeightKg: summary.grossWeightKg + item.grossWeightKg, volumeCbm: summary.volumeCbm + item.volumeCbm, - }), + }) + }, { pieces: 0, packageCount: 0, grossWeightKg: 0, volumeCbm: 0 }, ) } diff --git a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs index 5653e00..93336b5 100644 --- a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs +++ b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs @@ -430,6 +430,8 @@ test('shows field-level validation and concise guidance for required order input assert.match(source, /prop="consigneePartySnapshot\.address" required/) assert.match(source, /:prop="`items\.\$\{index\}\.cargoNameCn`" required/) assert.match(source, /:prop="`items\.\$\{index\}\.grossWeightKg`" required/) + assert.match(source, /