- {{ config.title }}共 {{ total }} 条
{{ props.service === 'relabel' ? '新建退货换标单' : props.service === 'transfer' ? '新增订单' : `新增${config.title}` }}
部分入库完成订单已有 PDA 实测数据,请在对应行确认测量数据。
diff --git a/src/navigation/clientNavigation.test.mjs b/src/navigation/clientNavigation.test.mjs
index 8afb9cf..53160e9 100644
--- a/src/navigation/clientNavigation.test.mjs
+++ b/src/navigation/clientNavigation.test.mjs
@@ -42,7 +42,7 @@ test('keeps account and notification routes as fixed-header utilities', () => {
)
})
-test('organises every implemented business page under five task-oriented sidebar entries', () => {
+test('organises customer-relevant business pages under five task-oriented sidebar entries', () => {
assert.deepEqual(
clientNavigation.filter((node) => !node.hidden).map((node) => ({
title: node.title,
@@ -52,11 +52,11 @@ test('organises every implemented business page under five task-oriented sidebar
{ title: '首页', children: undefined },
{
title: '订单中心',
- children: ['头程订单', '快速建草稿', '海外采购', '美国本土卡', '美本土散板', '入库订单', '派送订单'],
+ children: ['头程订单', '海外采购', '美国本土卡', '美本土散板'],
},
{
title: '海外仓',
- children: ['贴标申请', '仓储转运', '货物废弃', '退货预报与签收', '退货库存', '退货箱号管理'],
+ children: ['贴标申请', '仓储转运', '货物废弃', '退货预报与签收'],
},
{
title: '费用与账单',
@@ -86,18 +86,13 @@ test('flattens visible leaves with unique paths', () => {
assert.deepEqual(paths, [
'/',
'/orders/first-leg',
- '/first-leg/quick-order',
'/cross-border/procurement',
'/cross-border/domestic-trucking',
'/cross-border/pallet-shipping',
- '/orders/inbound',
- '/orders/delivery',
'/overseas-warehouse/labeling',
'/overseas-warehouse/storage-transfer',
'/overseas-warehouse/cargo-disposal',
'/overseas-warehouse/return-relabel',
- '/overseas-warehouse/return-inventory',
- '/overseas-warehouse/return-cartons',
'/finance/account',
'/finance/bills',
'/company/profile',
@@ -340,7 +335,7 @@ test('keeps the dashboard route permission synchronized with navigation', () =>
assert.match(dashboardRoute, /permission: 'dashboard:view'/)
})
-test('keeps only the unified my-orders placeholder cataloged while activating implemented business pages', () => {
+test('keeps internal operational pages routable without exposing them in customer navigation', () => {
const routerSource = [
'first-leg',
'cross-border',
@@ -357,15 +352,24 @@ test('keeps only the unified my-orders placeholder cataloged while activating im
['overseas-warehouse/cargo-disposal', 'WarehouseServiceView.vue'],
['overseas-warehouse/return-relabel', 'ReturnOrdersView.vue'],
['overseas-warehouse/return-inventory', 'ReturnInventoryView.vue'],
+ ['overseas-warehouse/return-cartons', 'ReturnInventoryView.vue'],
['orders/my', 'MyOrdersView.vue'],
['orders/inbound', 'InboundOrdersView.vue'],
['orders/delivery', 'DeliveryOrdersView.vue'],
]
+ const hiddenActivePaths = new Set([
+ 'first-leg/quick-order',
+ 'orders/inbound',
+ 'orders/delivery',
+ 'overseas-warehouse/return-inventory',
+ 'overseas-warehouse/return-cartons',
+ ])
for (const [path, view] of routes) {
- if (path === 'first-leg/quick-order' || path.startsWith('cross-border/') || path.startsWith('overseas-warehouse/') || path === 'orders/inbound' || path === 'orders/delivery') {
+ if (path !== 'orders/my') {
assert.match(routerSource, new RegExp(`path: '${path}'[\\s\\S]*?\\./views/${view.replace('.', '\\.')}[\\s\\S]*?release: 'active'[\\s\\S]*?permission:`))
- assert.equal(getNavigationLeaves(clientNavigation).some((item) => item.path === `/${path}`), true)
+ const visible = !hiddenActivePaths.has(path)
+ assert.equal(getNavigationLeaves(clientNavigation).some((item) => item.path === `/${path}`), visible)
continue
}
assert.match(routerSource, new RegExp(`path: '${path}'[\\s\\S]*?\\./views/${view.replace('.', '\\.')}[\\s\\S]*?release: 'prototype'[\\s\\S]*?permission:`))
diff --git a/src/navigation/clientNavigation.ts b/src/navigation/clientNavigation.ts
index 21caf49..e68501f 100644
--- a/src/navigation/clientNavigation.ts
+++ b/src/navigation/clientNavigation.ts
@@ -52,12 +52,9 @@ export const clientNavigation: ClientNavigationNode[] = [
icon: ClipboardList,
children: [
requireLeaf('first-leg-orders', '头程订单'),
- requireLeaf('first-leg-quick-order', '快速建草稿'),
requireLeaf('cross-border-procurement'),
requireLeaf('cross-border-domestic-trucking'),
requireLeaf('cross-border-pallet-shipping'),
- requireLeaf('orders-inbound', '入库订单'),
- requireLeaf('orders-delivery', '派送订单'),
],
},
{
@@ -69,8 +66,6 @@ export const clientNavigation: ClientNavigationNode[] = [
requireLeaf('overseas-storage-transfer'),
requireLeaf('overseas-cargo-disposal'),
requireLeaf('overseas-return-relabel', '退货预报与签收'),
- requireLeaf('overseas-return-inventory'),
- requireLeaf('overseas-return-cartons'),
],
},
{
--
2.54.0
From 60e875b2c6d8021e3f0352f7ef79e6c9eff1c176 Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Sat, 19 Sep 2026 17:30:27 +0800
Subject: [PATCH 3/3] =?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 +-
public/customer-recharge-qr.png | Bin 0 -> 27120 bytes
src/modules/finance/api/account.ts | 9 ----
.../finance/views/FinanceAccountView.test.mjs | 7 +--
.../finance/views/FinanceAccountView.vue | 48 ++++++++++--------
5 files changed, 31 insertions(+), 35 deletions(-)
create mode 100644 public/customer-recharge-qr.png
diff --git a/AGENTS.md b/AGENTS.md
index da6bea4..71eb0a5 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -53,7 +53,7 @@
| 装柜完成后的退税报关资料 | `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`;退货预报另见本节独立入口;贴标申请先选国外中转仓和有库存历史入仓单,换标类型为只换 SKU、只换箱唛、SKU+箱唛。涉及箱唛换标时,整张贴标申请只选择一个 FBA 目的仓,全部箱级明细统一提交该目的仓,模板和明细不再逐行选择目的仓。箱唛换标明细一行固定代表一箱,箱数不可编辑,提交也固定为 1。货物明细外层始终提供“SKU 关联”“箱号关联”“SKU 混箱换标”三套模板的下载、上传,不随换标类型切换;上传只进入待导入关系区,客户可勾选部分或全选后导入本次明细,只有进入明细的库存才随申请提交和冻结。明细也支持手动新增编辑,组合换标以仓库唛头连接旧/新 SKU 和新 FBA 箱唛关系。涉及箱唛换标时必须选择卡车、自提或快递;快递可选择打印面单并上传 PDF/图片。PDA 逐箱换标、上传照片并由仓库复核后,客户详情展示完工照片和“换标完成确认”;客户确认前不得继续出库,确认后卡车进入预排车,自提/快递进入换标出库待创建。历史库存位置不能解释成箱唛与具体托盘快照,客户页面不暴露托盘、库位和内部库存 ID |
diff --git a/public/customer-recharge-qr.png b/public/customer-recharge-qr.png
new file mode 100644
index 0000000000000000000000000000000000000000..8bcdde67d164502cb639a335301429c1ccf474ec
GIT binary patch
literal 27120
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 method: 'GET', url: '/client/finance/v1/account' })
}
@@ -105,10 +100,6 @@ export async function getRechargeRequests(page: number, size: number) {
return normalizePageResult(result)
}
-export function getRechargeChannel() {
- return apiRequest({ method: 'GET', url: '/client/finance/v1/recharge-channel' })
-}
-
export function createRechargeRequest(amount: string, file: File, idempotencyKey: string) {
const formData = new FormData()
formData.append('amount', amount)
diff --git a/src/modules/finance/views/FinanceAccountView.test.mjs b/src/modules/finance/views/FinanceAccountView.test.mjs
index a165140..c090ad5 100644
--- a/src/modules/finance/views/FinanceAccountView.test.mjs
+++ b/src/modules/finance/views/FinanceAccountView.test.mjs
@@ -8,7 +8,7 @@ const routesSource = readFileSync(new URL('../routes.ts', import.meta.url), 'utf
const permissionsSource = readFileSync(new URL('../../tenant-management/contracts.ts', import.meta.url), 'utf8')
test('uses only authenticated client finance account endpoints without customer scope', () => {
- for (const path of ['/client/finance/v1/account', '/client/finance/v1/account/ledger', '/client/finance/v1/recharge-requests', '/client/finance/v1/recharge-channel']) {
+ for (const path of ['/client/finance/v1/account', '/client/finance/v1/account/ledger', '/client/finance/v1/recharge-requests']) {
assert.match(apiSource, new RegExp(path.replaceAll('/', '\\/')))
}
assert.match(apiSource, /headers: \{ 'Idempotency-Key': idempotencyKey \}/)
@@ -54,8 +54,9 @@ test('renders server balances, ledger and recharge states without internal ident
test('matches the frozen recharge workflow and preserves an idempotency key for failed retries', () => {
assert.match(viewSource, /title="账户充值" width="760px"/)
- for (const amount of ['50.00', '100.00', '200.00', '500.00']) assert.match(viewSource, new RegExp(amount.replace('.', '\\.')))
- assert.match(viewSource, /rechargeChannel\.value\?\.enabled && rechargeChannel\.value\.qrCodeUrl/)
+ for (const amount of ['1000', '5000', '20000', '50000']) assert.match(viewSource, new RegExp(amount))
+ assert.match(viewSource, /自定义/)
+ assert.match(viewSource, /src="\/customer-recharge-qr\.png"/)
assert.match(viewSource, /alt="平台收款二维码"/)
assert.match(viewSource, /请扫描平台收款二维码,付款后上传截图。/)
assert.match(viewSource, /accept="\.jpg,\.jpeg,\.png,image\/jpeg,image\/png"/)
diff --git a/src/modules/finance/views/FinanceAccountView.vue b/src/modules/finance/views/FinanceAccountView.vue
index 0d2640f..d39871c 100644
--- a/src/modules/finance/views/FinanceAccountView.vue
+++ b/src/modules/finance/views/FinanceAccountView.vue
@@ -9,12 +9,10 @@ import {
createRechargeRequest,
getAccountLedger,
getFinanceAccount,
- getRechargeChannel,
getRechargeRequests,
type AccountChangeType,
type AccountLedgerEntry,
type FinanceAccount,
- type RechargeChannel,
type RechargeRequest,
type RechargeStatus,
} from '../api/account'
@@ -23,10 +21,9 @@ const auth = useAuthStore()
const account = ref(null)
const ledger = ref([])
const rechargeRequests = ref([])
-const rechargeChannel = ref(null)
const pageLoading = ref(false)
const pageError = ref('')
-const channelsError = ref('')
+const qrCodeLoadError = ref(false)
const ledgerPage = ref(1)
const ledgerTotal = ref(0)
const ledgerPageSize = ref(10)
@@ -40,10 +37,11 @@ const voucherFile = ref(null)
const voucherUpload = ref()
let pendingIdempotencyKey = ''
-const form = reactive({ amount: '100.00' })
-const quickAmounts = ['50.00', '100.00', '200.00', '500.00']
+const form = reactive({ amount: '1000' })
+const quickAmounts = ['1000', '5000', '20000', '50000']
+const amountMode = ref<'preset' | 'custom'>('preset')
const canRecharge = computed(() => auth.hasPermission('merchant:finance:recharge:create'))
-const channelReady = computed(() => Boolean(rechargeChannel.value?.enabled && rechargeChannel.value.qrCodeUrl))
+const channelReady = computed(() => !qrCodeLoadError.value)
const amountValid = computed(() => {
const value = Number(form.amount)
return Number.isFinite(value) && value > 0 && /^\d+(?:\.\d{1,2})?$/.test(form.amount.trim())
@@ -106,7 +104,7 @@ function amountClass(amount: string) {
async function loadAccountData() {
pageLoading.value = true
pageError.value = ''
- channelsError.value = ''
+ qrCodeLoadError.value = false
const requests = [getFinanceAccount(), getAccountLedger(ledgerPage.value, ledgerPageSize.value), getRechargeRequests(rechargePage.value, rechargePageSize.value)] as const
try {
const [accountResult, ledgerResult, rechargeResult] = await Promise.all(requests)
@@ -125,14 +123,6 @@ async function loadAccountData() {
} finally {
pageLoading.value = false
}
- if (canRecharge.value) {
- try {
- rechargeChannel.value = await getRechargeChannel()
- } catch (error) {
- rechargeChannel.value = null
- channelsError.value = messageOf(error, '收款二维码加载失败,请稍后重试')
- }
- }
}
async function changeLedgerPage(nextPage: number) {
@@ -157,13 +147,26 @@ async function changeRechargePage(nextPage: number) {
function openRecharge() {
submitError.value = ''
+ qrCodeLoadError.value = false
+ amountMode.value = 'preset'
+ form.amount = '1000'
rechargeVisible.value = true
}
+function handleQrCodeError() {
+ qrCodeLoadError.value = true
+}
+
function chooseAmount(amount: string) {
+ amountMode.value = 'preset'
form.amount = amount
}
+function chooseCustomAmount() {
+ amountMode.value = 'custom'
+ if (quickAmounts.includes(form.amount)) form.amount = ''
+}
+
function handleVoucherChange(uploadFile: UploadFile, _uploadFiles: UploadFiles) {
const file = uploadFile.raw
if (!file) return
@@ -276,16 +279,17 @@ onMounted(() => { void loadAccountData() })