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() {
{{ formatInboundMeasure(row.unitVolumeCbm, 'm³') }}
{{ formatInboundMeasure(row.totalVolumeCbm, 'm³') }}
{{ formatInboundMeasure(row.volumeWeightKg, 'kg') }}
-
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()]) })
@@ -89,16 +103,16 @@ onMounted(load)
查询
openDetail(row.returnNo)">
+ 详情
-
+ {{ operationWarehouseLabel(row.overseasWarehouseId) }}
需要换标否
- {{ row.status }}
-
- 详情
+ {{ statusLabel(row.status) }}
+ {{ formatDateTime(row.createdAt) }}
@@ -114,14 +128,14 @@ onMounted(load)
换标需要换标
客户只需填写预计退回的 SKU 和数量;快递单号由仓库签收时按实际包裹扫描绑定。
+ 删除
- 删除
取消提交预报
- {{ selected.order.returnNo }}{{ selected.order.status }}{{ selected.order.overseasWarehouseId }}{{ selected.order.receivedQuantity }} / {{ selected.order.expectedQuantity }}{{ selected.order.warehouseContactName || '—' }}{{ selected.order.warehouseContactPhone || '—' }}预报与实收
签收批次
{{ row.trackingNo || '无快递单号' }}
+ {{ selected.order.returnNo }}{{ statusLabel(selected.order.status) }}{{ operationWarehouseLabel(selected.order.overseasWarehouseId) }}{{ selected.order.receivedQuantity }} / {{ selected.order.expectedQuantity }}{{ selected.order.warehouseContactName || '—' }}{{ selected.order.warehouseContactPhone || '—' }}预报与实收
签收批次
{{ row.trackingNo || '无快递单号' }}{{ startedByLabel(row.startedBy) }}{{ formatDateTime(row.signedAt) }}
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()]) })
{{ statusLabel(row.status) }}
{{ formatDateTime(row.createdAt) }}
-
+
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, /\*<\/span>总毛重/)
+ assert.match(source, /\*<\/span>总体积/)
assert.match(source, /:prop="`items\.\$\{index\}\.upc`">\s*]*placeholder="UPC NO\.(选填)"/)
assert.doesNotMatch(source, /:prop="`items\.\$\{index\}\.upc`"[^>]*(?:required|:rules)/)
assert.match(source, /UPC NO\. 选填/)
diff --git a/src/modules/first-leg/views/FirstLegOrdersView.vue b/src/modules/first-leg/views/FirstLegOrdersView.vue
index f9f67a0..bbabecd 100644
--- a/src/modules/first-leg/views/FirstLegOrdersView.vue
+++ b/src/modules/first-leg/views/FirstLegOrdersView.vue
@@ -1973,13 +1973,14 @@ async function searchOrders(model: QueryModel) {
| # |
- SKU NO.SKU必填 | 中文品名 | 图片 | 英文品名 | 报关 HS CODE | 清关 HS CODE | 包装类型 | 材质 | 危险品 | 木制品勾选后须商检 |
- 箱规长 cm | 箱规宽 cm | 箱规高 cm | 每箱数量 | 箱数 | 总毛重 | 总体积 | 总数量 | 总净重 |
+ SKU NO.SKU必填 | 混箱组合同号为同一批箱 | 中文品名 | 图片 | 英文品名 | 报关 HS CODE | 清关 HS CODE | 包装类型 | 材质 | 危险品 | 木制品勾选后须商检 |
+ 箱规长 cm | 箱规宽 cm | 箱规高 cm | 每箱数量 | 箱数 | *总毛重 | *总体积 | 总数量 | 总净重 |
清关申报单价 USD | 清关申报金额 USD | 用途 | 型号 | 品牌 | 产品规格 长×宽×高 | 货件编号货件编号必填 | 跟踪编号跟踪编号必填 | UPC NO. | 备注 | 操作 |
| {{ index + 1 }} |
- |
+ |
+ |
|
|
| | |
@@ -1993,7 +1994,7 @@ async function searchOrders(model: QueryModel) {
- SKU NO.、货件编号、跟踪编号为 FBA 货物必填项,UPC NO. 选填
+ 相同混箱组合号表示相同一批物理箱;组内箱数、包装类型和箱规必须一致,物理箱数只计算一次。SKU NO.、货件编号、跟踪编号为 FBA 货物必填项,UPC NO. 选填
diff --git a/src/modules/auth/views/RegisterView.vue b/src/modules/auth/views/RegisterView.vue
index 4068b5e..af7eeae 100644
--- a/src/modules/auth/views/RegisterView.vue
+++ b/src/modules/auth/views/RegisterView.vue
@@ -40,7 +40,6 @@ const rules: FormRules = {
{ max: 64, message: '联系人姓名最多 64 个字符', trigger: 'blur' },
],
legalPerson: [
- { required: true, message: '请输入法定代表人', trigger: 'blur' },
{ max: 64, message: '法定代表人最多 64 个字符', trigger: 'blur' },
],
contactEmail: [
@@ -68,12 +67,13 @@ async function submit() {
submitting.value = true
try {
+ const legalPerson = form.legalPerson.trim()
await registerMerchant({
username: form.username.trim(),
password: form.password,
displayName: form.displayName.trim(),
companyName: form.companyName.trim(),
- legalPerson: form.legalPerson.trim(),
+ ...(legalPerson ? { legalPerson } : {}),
phone: form.phone.trim(),
contactEmail: form.contactEmail.trim() || undefined,
countryCode: form.countryCode,
@@ -122,7 +122,7 @@ async function submit() {
企业信息
-
+
diff --git a/src/modules/dashboard/views/DashboardView.test.mjs b/src/modules/dashboard/views/DashboardView.test.mjs
index 67c6f0c..c19a145 100644
--- a/src/modules/dashboard/views/DashboardView.test.mjs
+++ b/src/modules/dashboard/views/DashboardView.test.mjs
@@ -8,7 +8,16 @@ test('loads real dashboard data allowed by the current permissions', () => {
assert.match(source, /auth\.hasPermission\('company:profile:view'\).*tenant\.loadCompanyContext\(\)/)
assert.match(source, /auth\.hasPermission\('company:member:list'\).*tenant\.loadMembers\(\)/)
assert.match(source, /auth\.hasPermission\('order:first-leg:list'\).*orders\.load\(\)/)
- assert.match(source, /onMounted\(\(\) => void loadDashboardData\(\)\)/)
+ assert.match(source, /onMounted\(async \(\) => \{\s*await loadDashboardData\(\)\s*showQualificationReminderOnce\(\)\s*\}\)/)
+})
+
+test('prompts once per session for incomplete company qualification and opens the upload dialog', () => {
+ assert.match(source, /\['PROFILE_REQUIRED', 'REJECTED'\]\.includes\(tenant\.company\.auditStatus\)/)
+ assert.match(source, /sessionStorage\.getItem\(reminderKey\)/)
+ assert.match(source, /sessionStorage\.setItem\(reminderKey, 'shown'\)/)
+ assert.match(source, /title="完善企业认证资料"/)
+ assert.match(source, /立即上传认证资料/)
+ assert.match(source, /router\.push\(\{ path: '\/company\/profile', query: \{ qualification: 'upload' \} \}\)/)
})
test('isolates recoverable dashboard failures without hiding successful sections', () => {
diff --git a/src/modules/dashboard/views/DashboardView.vue b/src/modules/dashboard/views/DashboardView.vue
index 3803c16..0e2dd1a 100644
--- a/src/modules/dashboard/views/DashboardView.vue
+++ b/src/modules/dashboard/views/DashboardView.vue
@@ -22,6 +22,7 @@ const orders = useFirstLegOrdersStore()
const tenant = useTenantAdministrationStore()
const auth = useAuthStore()
const loading = ref(false)
+const qualificationReminderOpen = ref(false)
type DashboardDataSource = 'company' | 'members' | 'orders'
const loadErrors = ref>>({})
const dataSourceLabels: Record = {
@@ -116,6 +117,20 @@ async function loadDashboardData() {
}
}
+function showQualificationReminderOnce() {
+ if (companyUnavailable.value || !auth.hasPermission('company:qualification:upload')) return
+ if (!['PROFILE_REQUIRED', 'REJECTED'].includes(tenant.company.auditStatus)) return
+ const reminderKey = `client-qualification-reminder:${auth.user?.id ?? 'current'}`
+ if (sessionStorage.getItem(reminderKey)) return
+ sessionStorage.setItem(reminderKey, 'shown')
+ qualificationReminderOpen.value = true
+}
+
+async function openQualificationUpload() {
+ qualificationReminderOpen.value = false
+ await router.push({ path: '/company/profile', query: { qualification: 'upload' } })
+}
+
function openBusinessOrder(business: typeof businessCards[number]) {
if (auth.hasPermission(business.permission)) {
void router.push(business.route)
@@ -133,11 +148,18 @@ function showSupportMessage(type: 'service' | 'faq' | 'quote') {
ElMessage.info(messages[type])
}
-onMounted(() => void loadDashboardData())
+onMounted(async () => {
+ await loadDashboardData()
+ showQualificationReminderOnce()
+})
+
+ 请先上传营业执照和统一社会信用代码并提交审核;法人身份证及其他资质可选填。审核通过后即可提交头程订单。
+ 稍后处理立即上传认证资料
+
@@ -237,6 +259,8 @@ onMounted(() => void loadDashboardData())
margin-bottom: 20px;
}
+.qualification-reminder-copy { margin: 0; color: #475467; font-size: 14px; line-height: 1.75; }
+
.dashboard-load-error ul {
display: grid;
gap: 3px;
diff --git a/src/modules/tenant-management/api/merchantIam.ts b/src/modules/tenant-management/api/merchantIam.ts
index 6a1b2cc..64e40eb 100644
--- a/src/modules/tenant-management/api/merchantIam.ts
+++ b/src/modules/tenant-management/api/merchantIam.ts
@@ -37,6 +37,8 @@ export interface MerchantQualification {
qualificationType: MerchantQualificationType
certificateNo: string | null
documentUrl?: string | null
+ originalFileName?: string | null
+ updatedAt?: string | null
validTo: string | null
status: QualificationStatus
reviewComment: string | null
@@ -119,6 +121,11 @@ export interface MerchantPersonalProfile {
email: string | null
}
+export interface QualificationDeletionResult {
+ qualificationId: number | string
+ deletedDocumentUrl: string | null
+}
+
export interface ChangeMerchantLoginIdentifiersRequest {
username: string
phone: string
@@ -157,6 +164,13 @@ export function getTenantQualifications() {
return apiRequest
({ method: 'GET', url: '/merchant/v1/iam/tenant/qualifications' })
}
+export function deleteTenantQualification(qualificationId: number | string) {
+ return apiRequest({
+ method: 'DELETE',
+ url: `/merchant/v1/iam/tenant/qualifications/${qualificationId}`,
+ })
+}
+
export function getMerchantPermissionResources() {
return apiRequest({ method: 'GET', url: '/merchant/v1/iam/permissions' })
}
diff --git a/src/modules/tenant-management/store/tenantAdministration.ts b/src/modules/tenant-management/store/tenantAdministration.ts
index 5331a45..1be478b 100644
--- a/src/modules/tenant-management/store/tenantAdministration.ts
+++ b/src/modules/tenant-management/store/tenantAdministration.ts
@@ -5,6 +5,7 @@ import {
changeMerchantRoleStatus,
createMerchantMember,
createMerchantRole,
+ deleteTenantQualification,
deleteMerchantRole,
getMerchantMembers,
getMerchantPermissionResources,
@@ -91,6 +92,13 @@ function formatLastLoginAt(value: string | null) {
return date.toLocaleString('zh-CN', { hour12: false })
}
+function formatQualificationUpdatedAt(value: string | null | undefined) {
+ if (!value) return '—'
+ const date = new Date(value)
+ if (Number.isNaN(date.getTime())) return value
+ return date.toLocaleString('zh-CN', { hour12: false })
+}
+
const emptyCompany: TenantCompanyProfile = {
companyName: '',
creditCode: '',
@@ -145,10 +153,10 @@ export const useTenantAdministrationStore = defineStore('tenant-administration',
FREIGHT_FORWARDER_CERTIFICATE: '货代资质证书',
BANK_ACCOUNT_LICENSE: '银行开户许可证',
} as const)[item.qualificationType] || '企业资质',
- fileName: documentUrl.split('/').pop() || '资质文件',
+ fileName: item.originalFileName?.trim() || '—',
status: normalizeQualificationStatus(item.status),
expiresAt: item.validTo || '长期有效',
- updatedAt: '-',
+ updatedAt: formatQualificationUpdatedAt(item.updatedAt),
documentUrl,
}
})
@@ -198,6 +206,11 @@ export const useTenantAdministrationStore = defineStore('tenant-administration',
roles.value = roleRecords.map(mapMerchantRole)
}
+ async function deleteQualification(id: string) {
+ await deleteTenantQualification(id)
+ qualifications.value = qualifications.value.filter((item) => item.id !== id)
+ }
+
async function loadMembers() {
members.value = []
members.value = (await getMerchantMembers()).map(mapMerchantMember)
@@ -335,6 +348,7 @@ export const useTenantAdministrationStore = defineStore('tenant-administration',
roles,
activeMemberCount,
loadCompanyContext,
+ deleteQualification,
loadRoles,
loadMembers,
updateCompany,
diff --git a/src/modules/tenant-management/utils/qualificationFile.test.mjs b/src/modules/tenant-management/utils/qualificationFile.test.mjs
index b145475..1c769df 100644
--- a/src/modules/tenant-management/utils/qualificationFile.test.mjs
+++ b/src/modules/tenant-management/utils/qualificationFile.test.mjs
@@ -2,10 +2,12 @@ import assert from 'node:assert/strict'
import test from 'node:test'
import { formatQualificationFileSize, qualificationUploadErrorMessage, QUALIFICATION_MAX_FILE_SIZE, validateQualificationFile } from './qualificationFile.ts'
-test('accepts the backend-confirmed PDF, JPG and PNG formats up to 5MB', () => {
+test('accepts PDF, JPG, PNG and BMP formats up to 5MB', () => {
assert.equal(validateQualificationFile({ name: '营业执照.jpeg', size: QUALIFICATION_MAX_FILE_SIZE, type: 'image/jpeg' }), '')
assert.equal(validateQualificationFile({ name: '营业执照.PNG', size: 2048, type: 'image/png' }), '')
assert.equal(validateQualificationFile({ name: '货代资质.pdf', size: 2048, type: 'application/pdf' }), '')
+ assert.equal(validateQualificationFile({ name: '法人身份证.bmp', size: 2048, type: 'image/bmp' }), '')
+ assert.equal(validateQualificationFile({ name: '营业执照.bmp', size: 2048, type: 'image/x-ms-bmp' }), '')
})
test('rejects unsupported, empty, mismatched, and oversized files', () => {
diff --git a/src/modules/tenant-management/utils/qualificationFile.ts b/src/modules/tenant-management/utils/qualificationFile.ts
index a7a3916..b3c5827 100644
--- a/src/modules/tenant-management/utils/qualificationFile.ts
+++ b/src/modules/tenant-management/utils/qualificationFile.ts
@@ -1,7 +1,7 @@
export const QUALIFICATION_MAX_FILE_SIZE = 5 * 1024 * 1024
-const allowedExtensions = new Set(['jpg', 'jpeg', 'png', 'pdf'])
-const allowedMimeTypes = new Set(['image/jpeg', 'image/png', 'application/pdf'])
+const allowedExtensions = new Set(['jpg', 'jpeg', 'png', 'bmp', 'pdf'])
+const allowedMimeTypes = new Set(['image/jpeg', 'image/png', 'image/bmp', 'image/x-ms-bmp', 'application/pdf'])
export interface QualificationFileLike {
name: string
@@ -11,7 +11,7 @@ export interface QualificationFileLike {
export function validateQualificationFile(file: QualificationFileLike) {
const extension = file.name.includes('.') ? file.name.split('.').pop()?.toLowerCase() ?? '' : ''
- if (!allowedExtensions.has(extension)) return '文件格式不支持,请选择 PDF、JPG 或 PNG 文件'
+ if (!allowedExtensions.has(extension)) return '文件格式不支持,请选择 PDF、JPG、PNG 或 BMP 文件'
if (file.type && !allowedMimeTypes.has(file.type)) return '文件内容类型与支持的资质格式不一致'
if (file.size <= 0) return '文件内容为空,请重新选择'
if (file.size > QUALIFICATION_MAX_FILE_SIZE) return '文件超过 5MB,请压缩后重新选择'
diff --git a/src/modules/tenant-management/views/CompanyProfileView.vue b/src/modules/tenant-management/views/CompanyProfileView.vue
index ea52697..42e1c1a 100644
--- a/src/modules/tenant-management/views/CompanyProfileView.vue
+++ b/src/modules/tenant-management/views/CompanyProfileView.vue
@@ -1,6 +1,7 @@
- {{ props.cartonOnly ? '退货箱号管理' : '退货库存管理' }}
{{ props.cartonOnly ? '查询合箱完成后的退货箱号、箱规和箱内 SKU 明细。' : '库存数量以仓库 PDA 实收并上架的数量为准。' }}
+ {{ props.cartonOnly ? '退货箱号管理' : '退货库存管理' }}
{{ props.cartonOnly ? '查询签收及合箱生成的退货箱号、箱规和箱内 SKU 明细。' : '库存数量以仓库 PDA 确认实收后的入账数量为准。' }}
-
+
查询
-
-
+
+ 重试加载海外仓
+
+ 查看箱号
+ {{ warehouseLabel(row.overseasWarehouseId) }}
-
- 查看箱号
+
-
+
+ 详情箱唛
+
+ {{ warehouseLabel(row.overseasWarehouseId) }}
+
+
+
+
+ {{ row.length && row.width && row.height ? `${row.length} × ${row.width} × ${row.height} cm` : '—' }}
+ {{ row.weight ? `${row.weight} kg` : '—' }}
+
+
+
+ 详情箱唛
{{ row.cartonMark }}
-
+ {{ warehouseLabel(row.overseasWarehouseId) }}
- 详情箱唛
- {{ 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` : '—' }}
+ {{ cartonDetail.cartonMark }}{{ warehouseLabel(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
index daca1db..6bf2046 100644
--- a/src/modules/overseas-warehouse/views/ReturnOrdersView.vue
+++ b/src/modules/overseas-warehouse/views/ReturnOrdersView.vue
@@ -2,7 +2,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 { getMerchantWarehouses, type MerchantWarehouse } from '@/modules/first-leg/public'
import { createReturnOrder, getReturnOrder, listReturnOrders, type ReturnOrderDetail, type ReturnOrderSummary, type ReturnReceipt } from '../api/returnOrders'
const loading = ref(false)
@@ -63,7 +63,7 @@ function addItem() { form.items.push(emptySku()) }
function removeItem(index: number) { if (form.items.length > 1) form.items.splice(index, 1) }
async function load() {
loading.value = true
- 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 }
+ 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 = Number(response.total) }
finally { loading.value = false }
}
function changePage(page: number) {
From 12edb30cf19ee1a0e1e60f538313fc32f3eca970 Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Sat, 12 Sep 2026 16:28:02 +0800
Subject: [PATCH 13/14] fix(client): align return inventory desktop filters and
display labels
---
src/modules/overseas-warehouse/routes.ts | 2 +-
.../views/ReturnInventoryView.test.mjs | 8 ++++++++
.../views/ReturnInventoryView.vue | 16 +++++++++-------
3 files changed, 18 insertions(+), 8 deletions(-)
diff --git a/src/modules/overseas-warehouse/routes.ts b/src/modules/overseas-warehouse/routes.ts
index 12294d0..1ccd99c 100644
--- a/src/modules/overseas-warehouse/routes.ts
+++ b/src/modules/overseas-warehouse/routes.ts
@@ -38,7 +38,7 @@ export const overseasWarehouseModule = {
{
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' },
+ meta: { title: '退货箱号管理', description: '查询签收及合箱生成的退货箱号和箱内库存', permission: 'merchant:overseas-warehouse:return-inventory:list' },
navigation: { key: 'overseas-return-cartons', icon: Boxes },
},
],
diff --git a/src/modules/overseas-warehouse/views/ReturnInventoryView.test.mjs b/src/modules/overseas-warehouse/views/ReturnInventoryView.test.mjs
index 184c0c0..695a27d 100644
--- a/src/modules/overseas-warehouse/views/ReturnInventoryView.test.mjs
+++ b/src/modules/overseas-warehouse/views/ReturnInventoryView.test.mjs
@@ -5,6 +5,14 @@ import test from 'node:test'
const inventory = readFileSync(new URL('./ReturnInventoryView.vue', import.meta.url), 'utf8')
const orders = readFileSync(new URL('./ReturnOrdersView.vue', import.meta.url), 'utf8')
+test('desktop filters reserve six slots and inventory time and statuses have readable presentation', () => {
+ assert.match(inventory, /grid-template-columns: repeat\(4, minmax\(150px, 1fr\)\) 130px auto/)
+ assert.match(inventory, /white-space: nowrap/)
+ assert.match(inventory, /cartonStatusLabel\(cartonDetail.status\)/)
+ assert.match(inventory, /formatInventoryTime\(row.createdAt\)/)
+ assert.match(inventory, /formatInventoryTime\(row.updatedAt\)/)
+})
+
test('inventory failures are visible and are not presented as empty inventory', () => {
assert.match(inventory, /catch \(error\)/)
assert.match(inventory, /([])
const warehouseError = ref('')
const warehousesLoading = ref(false)
+function formatInventoryTime(value?: string) { return value ? value.replace('T', ' ').slice(0, 19) : '—' }
+function cartonStatusLabel(value: string) { return ({ AVAILABLE: '可用', MERGED: '已合箱', OUTBOUNDED: '已出库' } as Record)[value] || '—' }
function warehouseLabel(warehouseId: string) {
const warehouse = warehouses.value.find(item => String(item.id) === String(warehouseId))
return warehouse ? `${warehouse.warehouseName} · ${warehouse.warehouseCode}` : '—'
@@ -141,7 +143,7 @@ onMounted(() => { void load(); void loadWarehouses() })
{{ props.cartonOnly ? '退货箱号管理' : '退货库存管理' }}
{{ props.cartonOnly ? '查询签收及合箱生成的退货箱号、箱规和箱内 SKU 明细。' : '库存数量以仓库 PDA 确认实收后的入账数量为准。' }}
-
+
@@ -155,7 +157,7 @@ onMounted(() => { void load(); void loadWarehouses() })
查看箱号
{{ warehouseLabel(row.overseasWarehouseId) }}
-
+
{{ formatInventoryTime(row.updatedAt) }}
详情箱唛
@@ -167,7 +169,7 @@ onMounted(() => { void load(); void loadWarehouses() })
{{ row.length && row.width && row.height ? `${row.length} × ${row.width} × ${row.height} cm` : '—' }}
{{ row.weight ? `${row.weight} kg` : '—' }}
-
+ {{ formatInventoryTime(row.createdAt) }}
详情箱唛
@@ -175,11 +177,11 @@ onMounted(() => { void load(); void loadWarehouses() })
{{ warehouseLabel(row.overseasWarehouseId) }}
-
+ {{ cartonStatusLabel(row.status) }}{{ formatInventoryTime(row.receivedAt) }}
- {{ cartonDetail.cartonMark }}{{ warehouseLabel(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` : '—' }}
+ {{ cartonDetail.cartonMark }}{{ warehouseLabel(cartonDetail.overseasWarehouseId) }}{{ cartonDetail.sourceReturnNo || '—' }}{{ cartonDetail.sourceBatchNo || '—' }}{{ cartonStatusLabel(cartonDetail.status) }}{{ cartonDetail.skuTypes }}{{ cartonDetail.totalQuantity }}{{ cartonDetail.length && cartonDetail.width && cartonDetail.height ? `${cartonDetail.length} × ${cartonDetail.width} × ${cartonDetail.height} cm` : '—' }}{{ cartonDetail.weight ? `${cartonDetail.weight} kg` : '—' }}
@@ -187,6 +189,6 @@ onMounted(() => { void load(); void loadWarehouses() })
From 71ae7e8f18089203a374851c197e3d611a3fcfad Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Mon, 14 Sep 2026 10:02:37 +0800
Subject: [PATCH 14/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
---
.../cross-border/businessOrders.test.mjs | 13 ++++++
.../components/BusinessOrdersView.vue | 42 +++++++++++++++++--
2 files changed, 52 insertions(+), 3 deletions(-)
diff --git a/src/modules/cross-border/businessOrders.test.mjs b/src/modules/cross-border/businessOrders.test.mjs
index 0c0a0ed..7a9b649 100644
--- a/src/modules/cross-border/businessOrders.test.mjs
+++ b/src/modules/cross-border/businessOrders.test.mjs
@@ -36,4 +36,17 @@ test('covers list detail create loading error empty pagination and actions-first
assert.match(view, /empty-text="暂无订单"/)
assert.match(view, /
]*fixed="left"/)
+ assert.match(view, /PENDING_OUTBOUND: '待出库'/)
+ assert.match(view, /PARTIALLY_OUTBOUND: '部分出库'/)
+ assert.match(view, /OUTBOUNDED: '全部出库'/)
+ assert.match(view, /\['入库量', read\(summary, 'inboundQuantity'\)\]/)
+ assert.match(view, /\['审核备注', current\.reviewRemark\]/)
+})
+
+test('keeps procurement filters, details and forms readable at PDA width', () => {
+ assert.match(view, /@media \(max-width: 767px\)/)
+ assert.match(view, /\.filter-card :deep\(\.el-form\) \{ display: grid; grid-template-columns: 1fr; \}/)
+ assert.match(view, /\.detail-grid \{ grid-template-columns: 1fr; \}/)
+ assert.match(view, /\.form-card :deep\(\.el-form-item\) \{ display: block; \}/)
+ assert.match(view, /\.budget-form-item :deep\(\.el-form-item__content\) \{ display: flex; flex-wrap: nowrap; \}/)
})
diff --git a/src/modules/cross-border/components/BusinessOrdersView.vue b/src/modules/cross-border/components/BusinessOrdersView.vue
index a2c6e77..e7e23ce 100644
--- a/src/modules/cross-border/components/BusinessOrdersView.vue
+++ b/src/modules/cross-border/components/BusinessOrdersView.vue
@@ -61,6 +61,9 @@ const configuration = computed(() => ({
{ key: 'productSpecification', label: '商品规格', width: 160 },
{ key: 'purchaseQuantity', label: '采购数量', width: 100 },
{ key: 'budgetUnitPrice', label: '预算单价', width: 110 },
+ { key: 'inboundQuantity', label: '入库量', width: 90 },
+ { key: 'outboundQuantity', label: '出库量', width: 90 },
+ { key: 'inventoryQuantity', label: '库存量', width: 90 },
{ key: 'status', label: '状态', width: 120 },
{ key: 'createdAt', label: '创建时间', width: 170 },
] satisfies ColumnDefinition[],
@@ -125,11 +128,13 @@ const detailFields = computed(() => {
['订单号', summary.orderNo], ['采购品类', read(summary, 'purchaseCategory')],
['商品名称', read(summary, 'productName')], ['商品规格', read(summary, 'productSpecification')],
['采购数量', read(summary, 'purchaseQuantity')], ['预算单价', money(read(summary, 'budgetUnitPrice'), read(summary, 'currency'))],
+ ['入库量', read(summary, 'inboundQuantity')], ['出库量', read(summary, 'outboundQuantity')],
+ ['库存量', read(summary, 'inventoryQuantity')], ['预计到达时间', dateTime(current.estimatedArrivalAt)],
['联系人', current.contactName], ['联系电话', current.contactPhone],
['收货地址', current.receivingAddress], ['商品链接', current.productUrl],
['国内运单', joinText(current.domesticCarrier, current.domesticTrackingNo)],
['海外运单', joinText(current.overseasCarrier, current.overseasTrackingNo)],
- ['状态', statusLabel(summary.status)], ['备注', current.remark],
+ ['状态', statusLabel(summary.status)], ['审核备注', current.reviewRemark], ['备注', current.remark],
]
}
const current = detail.value as TransportDetail
@@ -166,7 +171,7 @@ function yesNo(value: unknown) { return value === 'YES' ? '是' : value === 'NO'
function transportTypeLabel(value: unknown) { return ({ PORT_PICKUP: '港口提柜', WAREHOUSE_TO_WAREHOUSE: '仓到仓', WAREHOUSE_TO_DOOR: '仓到门' } as Record)[String(value)] || text(value) }
function transportModeLabel(value: unknown) { return ({ SHARED_TRUCK: '拼车', PRIORITY_CONSOLIDATION: '优先拼载' } as Record)[String(value)] || text(value) }
function statusLabel(value: unknown) {
- return ({ PENDING_REVIEW: '待审核', APPROVED: '已通过', REJECTED: '已拒绝', PENDING_ACCEPTANCE: '待接单', PROCESSING: '处理中', IN_TRANSIT: '运输中', COMPLETED: '已完成', CANCELLED: '已取消' } as Record)[String(value)] || text(value)
+ return ({ PENDING_REVIEW: '待审核', APPROVED: '已通过', REJECTED: '已拒绝', PENDING_OUTBOUND: '待出库', PARTIALLY_OUTBOUND: '部分出库', OUTBOUNDED: '全部出库', PENDING_ACCEPTANCE: '待接单', PROCESSING: '处理中', IN_TRANSIT: '运输中', COMPLETED: '已完成', CANCELLED: '已取消' } as Record)[String(value)] || text(value)
}
function displayCell(candidate: unknown, key: string) {
const row = candidate as BusinessRow
@@ -330,7 +335,7 @@ onMounted(() => void load())
-
+
@@ -355,4 +360,35 @@ onMounted(() => void load())
.form-card :deep(.el-form) { max-width: 860px; }
.form-card :deep(.el-input), .form-card :deep(.el-textarea), .form-card :deep(.el-select) { width: 100%; }
.form-card .currency { width: 110px; margin-left: 12px; }
+
+@media (max-width: 767px) {
+ .business-orders { gap: 14px; }
+ .page-head { align-items: flex-start; gap: 12px; }
+ .page-head h2 { font-size: 24px; }
+ .page-head > div { min-width: 0; }
+
+ .filter-card :deep(.el-form) { display: grid; grid-template-columns: 1fr; }
+ .filter-card :deep(.el-form-item) { width: 100%; margin-right: 0; margin-bottom: 12px; }
+ .filter-card :deep(.el-form-item:last-child) { margin-bottom: 0; }
+ .filter-card :deep(.el-form-item__content),
+ .filter-card :deep(.el-input) { width: 100%; }
+
+ .detail-grid { grid-template-columns: 1fr; }
+ .detail-grid div { padding: 14px 16px; border-right: 0; }
+ .detail-grid dd { overflow-wrap: break-word; word-break: normal; }
+
+ .form-card :deep(.el-form-item) { display: block; }
+ .form-card :deep(.el-form-item__label) {
+ width: auto !important;
+ height: auto;
+ margin-bottom: 7px;
+ line-height: 1.4;
+ justify-content: flex-start;
+ }
+ .form-card :deep(.el-form-item__content) { width: 100%; margin-left: 0 !important; }
+ .form-card :deep(.el-input-number) { width: 100%; }
+ .form-card .budget-form-item :deep(.el-form-item__content) { display: flex; flex-wrap: nowrap; }
+ .form-card .budget-form-item :deep(.el-input-number) { width: auto; min-width: 0; flex: 1 1 auto; }
+ .form-card .currency { width: 96px; flex: 0 0 96px; margin-left: 8px; }
+}