-
下载 APK
-
发布
-
停发
+
+ 下载 APK
+ 发布
+ 停发
{{ row.app === 'domestic' ? '国内' : '海外' }}
@@ -160,7 +154,8 @@ function download(row: PdaRelease) {
{{ (row.sizeBytes / 1024 / 1024).toFixed(1) }} MB
{{ row.createdAt?.replace('T', ' ').slice(0, 19) || '—' }}
{{ row.publishedAt?.replace('T', ' ').slice(0, 19) || '—' }}
-
+
+
@@ -182,11 +177,16 @@ function download(row: PdaRelease) {
From 7586f9d78348782b151c7c82c3e6ae53a743e67e Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Sun, 20 Sep 2026 22:04:59 +0800
Subject: [PATCH 2/6] =?UTF-8?q?=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../pages/LastMileOrdersView.behavior.test.ts | 28 ++++++++++++++-----
.../pages/LastMileOrdersView.vue | 2 +-
2 files changed, 22 insertions(+), 8 deletions(-)
diff --git a/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts b/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts
index 49206ad4..ec295788 100644
--- a/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts
+++ b/src/modules/overseas-warehouse/pages/LastMileOrdersView.behavior.test.ts
@@ -177,8 +177,8 @@ describe('LastMileOrdersView pending order and container conversion', () => {
it.each([
[[], ['查看'], false],
[['inbound-order:create'], ['查看', '生成入库计划'], false],
- [['last-mile-order:edit'], ['查看', '暂扣订单', '人工修改节点'], false],
- [['inbound-order:create', 'last-mile-order:edit'], ['查看', '生成入库计划', '暂扣订单', ''], true],
+ [['last-mile-order:edit'], ['查看', '人工修改节点'], false],
+ [['inbound-order:create', 'last-mile-order:edit'], ['查看', '生成入库计划', '人工修改节点'], false],
])('only collapses row actions above three for %j', async (allowed, labels, overflow) => {
canAccess.mockImplementation(code => allowed.some(action => code === 'platform:overseas-warehouse:' + action))
vi.mocked(listLastMileOrders).mockResolvedValue({ records: [{ id: 'order-1', customerId, orderNo: 'LM-1', currentStatus: 'PENDING_ASSIGNMENT', orderType: 'FIRST_LEG' }], page: 1, size: 10, total: 1, pages: 1 })
@@ -213,9 +213,9 @@ describe('LastMileOrdersView pending order and container conversion', () => {
})
it.each([
- ['PENDING_ASSIGNMENT', true],
- ['INBOUND_COMPLETED', false],
- ])('only enables a new hold after inbound completion for %s', async (currentStatus, disabled) => {
+ ['PENDING_ASSIGNMENT', false],
+ ['INBOUND_COMPLETED', true],
+ ])('only shows a new hold after inbound completion for %s', async (currentStatus, visible) => {
canAccess.mockImplementation(code => code === 'platform:overseas-warehouse:last-mile-order:edit')
vi.mocked(listLastMileOrders).mockResolvedValue({ records: [{
id: 'order-1', customerId, orderNo: 'LM-1', currentStatus, orderType: 'FIRST_LEG', detained: false,
@@ -223,8 +223,22 @@ describe('LastMileOrdersView pending order and container conversion', () => {
const wrapper = await mountView()
const holdButton = wrapper.get('.el-table__body-wrapper .row-actions').findAll('button')
- .find(item => item.text().includes('暂扣订单'))!
- expect((holdButton.element as HTMLButtonElement).disabled).toBe(disabled)
+ .find(item => item.text().includes('暂扣订单'))
+ expect(Boolean(holdButton)).toBe(visible)
+ if (holdButton) expect((holdButton.element as HTMLButtonElement).disabled).toBe(false)
+ })
+
+ it('keeps release hold available for an already detained order', async () => {
+ canAccess.mockImplementation(code => code === 'platform:overseas-warehouse:last-mile-order:edit')
+ vi.mocked(listLastMileOrders).mockResolvedValue({ records: [{
+ id: 'order-held', customerId, orderNo: 'LM-HELD', currentStatus: 'READY_FOR_DISPATCH', orderType: 'FIRST_LEG', detained: true,
+ }], page: 1, size: 10, total: 1, pages: 1 })
+
+ const wrapper = await mountView()
+ const releaseButton = wrapper.get('.el-table__body-wrapper .row-actions').findAll('button')
+ .find(item => item.text().includes('解除暂扣'))
+ expect(releaseButton).toBeTruthy()
+ expect((releaseButton!.element as HTMLButtonElement).disabled).toBe(false)
})
it('removes generation and hold actions after an inbound plan has been submitted', async () => {
diff --git a/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue b/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue
index 8b4a1d16..24ff3dd5 100644
--- a/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue
+++ b/src/modules/overseas-warehouse/pages/LastMileOrdersView.vue
@@ -833,7 +833,7 @@ function lastMileRowActions(row: LastMileOrderListItem): RowActionItem[] {
{ key: 'review-return', label: '审核', priority: 460, tone: 'warning', visible: access.can(permission.returnOrderReview) && row.orderTypeCode === 'RETURN_RELABEL' && row.currentStatusCode === 'PENDING_REVIEW' },
{ key: 'review', label: '审核', priority: 450, tone: 'warning', visible: access.can(permission.warehouseTransferApprove) && row.orderTypeCode === 'WAREHOUSE_TRANSFER' && row.currentStatusCode === 'PENDING_REVIEW' },
{ key: 'inbound-plan', label: '生成入库计划', priority: 400, visible: access.can(permission.inboundOrderCreate) && !inboundPlanCreated },
- { key: 'hold', label: row.detained ? '解除暂扣' : '暂扣订单', priority: 300, tone: 'warning', visible: access.can(permission.lastMileOrderTransfer) && (row.detained || !inboundPlanCreated || row.currentStatusCode === 'INBOUND_COMPLETED'), disabled: !row.detained && row.currentStatusCode !== 'INBOUND_COMPLETED' },
+ { key: 'hold', label: row.detained ? '解除暂扣' : '暂扣订单', priority: 300, tone: 'warning', visible: access.can(permission.lastMileOrderTransfer) && (row.detained || row.currentStatusCode === 'INBOUND_COMPLETED') },
{ key: 'manual-nodes', label: '人工修改节点', priority: 200, visible: access.can(permission.lastMileOrderTransfer) },
{ key: 'confirm-relabel', label: '换标完成确认', priority: 150, visible: access.can(permission.relabelOrderStatus) && Boolean(row.relabelOrderNo) && canConfirmRelabel(row) },
{ key: 'rework-relabel', label: '重新换标', priority: 100, tone: 'danger', divided: true, visible: access.can(permission.relabelOrderStatus) && Boolean(row.relabelOrderNo) && canReworkRelabel(row) },
From 3f78e1d087f4526c22f9f9fdf04b8c3ffdfc4698 Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Sun, 20 Sep 2026 22:13:56 +0800
Subject: [PATCH 3/6] =?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/pda-releases/pages/PdaReleasesView.vue | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/modules/pda-releases/pages/PdaReleasesView.vue b/src/modules/pda-releases/pages/PdaReleasesView.vue
index 692bee1f..4d547cad 100644
--- a/src/modules/pda-releases/pages/PdaReleasesView.vue
+++ b/src/modules/pda-releases/pages/PdaReleasesView.vue
@@ -165,11 +165,11 @@ function download(row: PdaRelease) {
-
-
+
+
-
+
From b9e5bf3fecca1c38b458b71ea89e26ae436e1c98 Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Sun, 20 Sep 2026 23:39:39 +0800
Subject: [PATCH 4/6] fix: preserve stable Admin proxy port
---
scripts/deploy-production.sh | 6 +++---
src/deployment/blueGreenDeployment.test.mjs | 4 ++++
2 files changed, 7 insertions(+), 3 deletions(-)
diff --git a/scripts/deploy-production.sh b/scripts/deploy-production.sh
index e3499ef7..c27fb0b1 100644
--- a/scripts/deploy-production.sh
+++ b/scripts/deploy-production.sh
@@ -62,12 +62,12 @@ compose() {
case "$service" in
admin-web)
base_port=9001
- nginx_config=/www/server/panel/vhost/nginx/admin.buggoglobal.com.conf
+ nginx_config=/www/server/panel/vhost/nginx/00-blue-green-admin-web.conf
legacy_container=firstleg-prod-admin-web
;;
client-web)
base_port=9002
- nginx_config=/www/server/panel/vhost/nginx/app.buggoglobal.com.conf
+ nginx_config=/www/server/panel/vhost/nginx/00-blue-green-client-web.conf
legacy_container=firstleg-prod-client-web
;;
*) echo "不支持蓝绿部署的前端服务: $service" >&2; exit 2 ;;
@@ -102,7 +102,7 @@ fi
if [ "$active_slot" = blue ]; then target_slot=green; else target_slot=blue; fi
[ "$active_slot" = legacy ] && target_slot=green
-if [ "$target_slot" = blue ]; then target_port="$base_port"; else target_port=$((base_port + 10000)); fi
+if [ "$target_slot" = blue ]; then target_port=$((base_port + 20000)); else target_port=$((base_port + 10000)); fi
candidate="${legacy_container}-${target_slot}"
docker rm -f "$candidate" >/dev/null 2>&1 || true
diff --git a/src/deployment/blueGreenDeployment.test.mjs b/src/deployment/blueGreenDeployment.test.mjs
index 9fa90096..ab0275c3 100644
--- a/src/deployment/blueGreenDeployment.test.mjs
+++ b/src/deployment/blueGreenDeployment.test.mjs
@@ -15,7 +15,11 @@ test('production frontend starts and validates the candidate before switching ng
})
test('production frontend supports both slots and restores nginx on failure', () => {
+ assert.match(script, /target_port=\$\(\(base_port \+ 20000\)\)/)
assert.match(script, /target_port=\$\(\(base_port \+ 10000\)\)/)
+ assert.match(script, /00-blue-green-admin-web\.conf/)
+ assert.match(script, /00-blue-green-client-web\.conf/)
+ assert.doesNotMatch(script, /nginx_config=\/www\/server\/panel\/vhost\/nginx\/(?:admin|app)\.buggoglobal\.com\.conf/)
assert.match(script, /cp -f "\$nginx_config" "\$backup"/)
assert.match(script, /cp -f "\$backup" "\$nginx_config"/)
assert.match(script, /docker rm -f "\$candidate"/)
From 3e8fa3c5d7fdbd2dbb15cb72a01ffe5acf679bf1 Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Mon, 21 Sep 2026 10:44:14 +0800
Subject: [PATCH 5/6] fix: allow manual FBA quote when rate is missing
---
src/modules/inbounds/fee/api.test.ts | 12 +++++
src/modules/inbounds/fee/api.ts | 12 ++++-
src/modules/inbounds/fee/types.ts | 6 +--
src/modules/inbounds/index.ts | 1 +
.../pages/UnifiedOrdersView.test.ts | 5 ++
.../pages/UnifiedOrdersView.vue | 48 ++++++++++++++-----
6 files changed, 67 insertions(+), 17 deletions(-)
diff --git a/src/modules/inbounds/fee/api.test.ts b/src/modules/inbounds/fee/api.test.ts
index 95f7aab9..0725148d 100644
--- a/src/modules/inbounds/fee/api.test.ts
+++ b/src/modules/inbounds/fee/api.test.ts
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ApiResponseError, http } from '@/shared/api/http'
import {
adjustPublishedInboundFee,
+ createManualInboundQuote,
confirmInboundFee,
createFeeActionIdempotencyKey,
createFeeAdjustmentIdempotencyKey,
@@ -69,6 +70,17 @@ describe('inbound fee API', () => {
})
})
+ it('creates a manual FBA quote through the dedicated fallback endpoint', async () => {
+ vi.mocked(http.post).mockResolvedValue({ code: '200', message: 'success', data: { ...snapshot, rateCode: 'MANUAL_QUOTE' } })
+ const payload = { selectedWeightKg: 62.5, selectedVolumeCbm: 0.384, unitPrice: 12.2, reason: 'FBA价格未维护' }
+
+ await expect(createManualInboundQuote('10001', '88', payload, 'fee-manual-1'))
+ .resolves.toMatchObject({ rateCode: 'MANUAL_QUOTE' })
+ expect(http.post).toHaveBeenCalledWith('/platform/v1/inbounds/88/fees/manual-quote', payload, {
+ params: { tenantId: '10001' }, headers: { 'Idempotency-Key': 'fee-manual-1' },
+ })
+ })
+
it('preserves backend conflict metadata for stale-version recovery', async () => {
vi.mocked(http.put).mockResolvedValue({
code: 'A0409', message: '费用已被其他操作员修改,请刷新后重试', data: null,
diff --git a/src/modules/inbounds/fee/api.ts b/src/modules/inbounds/fee/api.ts
index 27993cfb..cebe6bf9 100644
--- a/src/modules/inbounds/fee/api.ts
+++ b/src/modules/inbounds/fee/api.ts
@@ -23,6 +23,16 @@ export async function calculateInboundFee(tenantId: string, inboundId: string, p
return unwrapApiResponse(response)
}
+export async function createManualInboundQuote(tenantId: string, inboundId: string, payload: {
+ selectedWeightKg: number; selectedVolumeCbm: number; unitPrice: number; reason: string
+}, idempotencyKey: string) {
+ const response = await http.post
>(
+ `${feeBasePath(inboundId)}/manual-quote`, payload,
+ { params: { tenantId }, headers: { 'Idempotency-Key': idempotencyKey } },
+ )
+ return unwrapApiResponse(response)
+}
+
async function transitionInboundFee(
tenantId: string,
inboundId: string,
@@ -63,7 +73,7 @@ export function createFeeAdjustmentIdempotencyKey() {
return createFeeActionIdempotencyKey('adjust')
}
-export function createFeeActionIdempotencyKey(action: 'adjust' | 'calculate' | 'confirm' | 'send') {
+export function createFeeActionIdempotencyKey(action: 'adjust' | 'calculate' | 'manual' | 'confirm' | 'send') {
const uuid = globalThis.crypto?.randomUUID?.()
if (uuid) return `fee-${action}-${uuid}`
return `fee-${action}-${Date.now()}-${Math.random().toString(36).slice(2, 12)}`
diff --git a/src/modules/inbounds/fee/types.ts b/src/modules/inbounds/fee/types.ts
index 1e3b814b..f658cd07 100644
--- a/src/modules/inbounds/fee/types.ts
+++ b/src/modules/inbounds/fee/types.ts
@@ -13,9 +13,9 @@ export interface InboundFeeSnapshot {
chargeRuleId: string
ruleCode: string
chargeRuleVersion: number
- carrierRateId: string
+ carrierRateId: string | null
rateCode: string
- carrierRateVersion: number
+ carrierRateVersion: number | null
customerId: string
customerName: string
originWarehouseId: string
@@ -45,7 +45,7 @@ export interface InboundFeeSnapshot {
additionalAmount: number
totalAmount: number
currency: string
- rateEffectiveFrom: string
+ rateEffectiveFrom: string | null
rateEffectiveTo: string | null
calculatedAt: string
confirmedAt: string | null
diff --git a/src/modules/inbounds/index.ts b/src/modules/inbounds/index.ts
index 1aadb9cf..8772c58b 100644
--- a/src/modules/inbounds/index.ts
+++ b/src/modules/inbounds/index.ts
@@ -8,6 +8,7 @@ export { default as InboundFeePanel } from './fee/InboundFeePanel.vue'
export {
adjustPublishedInboundFee,
calculateInboundFee,
+ createManualInboundQuote,
confirmInboundFee,
createFeeAdjustmentIdempotencyKey,
createFeeActionIdempotencyKey,
diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts
index 3c8db07c..09f8a540 100644
--- a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts
+++ b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts
@@ -34,6 +34,11 @@ describe('UnifiedOrdersView contract', () => {
expect(sendFeeSource).toContain('adjustPublishedInboundFee')
expect(source).toContain('价格表参考单价')
expect(source).toContain('本次对客单价')
+ expect(openDialogSource).toContain("feeConfirmPriceMissing.value = true")
+ expect(sendFeeSource).toContain('createManualInboundQuote')
+ expect(source).toContain("feeConfirmPriceMissing ? '未命中'")
+ expect(source).toContain("feeConfirmPriceMissing ? '人工定价原因'")
+ expect(source).not.toContain('固定附加费')
expect(sendFeeSource).toContain("ElMessage.error(getApiErrorDetails(error, '确认费用发送失败').message)")
})
diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.vue b/src/modules/unified-orders/pages/UnifiedOrdersView.vue
index e6fffb40..69f9514f 100644
--- a/src/modules/unified-orders/pages/UnifiedOrdersView.vue
+++ b/src/modules/unified-orders/pages/UnifiedOrdersView.vue
@@ -66,6 +66,7 @@ import {
adjustPublishedInboundFee,
downloadWeightImage,
calculateInboundFee,
+ createManualInboundQuote,
confirmInboundFee,
createFeeAdjustmentIdempotencyKey,
createFeeActionIdempotencyKey,
@@ -258,24 +259,27 @@ const feeConfirmWeight = ref(null)
const feeConfirmVolume = ref(null)
const feeConfirmReferenceLoading = ref(false)
const feeConfirmReference = ref(null)
+const feeConfirmPriceMissing = ref(false)
const feeConfirmUnitPrice = ref(null)
const feeConfirmPriceReason = ref('')
type FeeConfirmAttempt = {
fingerprint: string
calculateKey: string
+ manualKey: string
adjustKey: string
confirmKey: string
sendKey: string
}
let feeConfirmAttempt: FeeConfirmAttempt | null = null
const feeConfirmPriceChanged = computed(() => {
+ if (feeConfirmPriceMissing.value) return true
const reference = Number(feeConfirmReference.value?.unitPrice)
const current = Number(feeConfirmUnitPrice.value)
return reference >= 0 && current >= 0 && Math.abs(reference - current) > 0.000001
})
const feeConfirmPriceUnit = computed(() => {
const snapshot = feeConfirmReference.value
- return snapshot ? `${snapshot.currency} / ${snapshot.chargeUnit}` : '—'
+ return snapshot ? `${snapshot.currency} / ${snapshot.chargeUnit}` : 'CNY / 计费单位'
})
const feeConfirmEstimatedTotal = computed(() => {
const snapshot = feeConfirmReference.value
@@ -1669,6 +1673,7 @@ async function openFeeConfirmDialog() {
: measuredGrossWeight > 0 ? measuredGrossWeight : measuredNetWeight > 0 ? measuredNetWeight : null
feeConfirmVolume.value = Number(actualTotals.volume) > 0 ? Number(actualTotals.volume) : null
feeConfirmReference.value = null
+ feeConfirmPriceMissing.value = false
feeConfirmUnitPrice.value = null
feeConfirmPriceReason.value = ''
feeConfirmAttempt = null
@@ -1693,8 +1698,14 @@ async function openFeeConfirmDialog() {
feeConfirmReference.value = snapshot
feeConfirmUnitPrice.value = Number(snapshot.unitPrice)
} catch (error) {
- ElMessage.error(getApiErrorDetails(error, '价格表参考单价加载失败,暂不能推送费用').message)
- feeConfirmOpen.value = false
+ const details = getApiErrorDetails(error, '价格表参考单价加载失败')
+ if (details.code === 'A0404') {
+ feeConfirmPriceMissing.value = true
+ ElMessage.warning('当前FBA价格未命中,请填写本次对客单价和人工定价原因')
+ } else {
+ ElMessage.error(details.message)
+ feeConfirmOpen.value = false
+ }
} finally {
feeConfirmReferenceLoading.value = false
}
@@ -1715,6 +1726,7 @@ function currentFeeConfirmAttempt(
feeConfirmAttempt = {
fingerprint,
calculateKey: createFeeActionIdempotencyKey('calculate'),
+ manualKey: createFeeActionIdempotencyKey('manual'),
adjustKey: createFeeAdjustmentIdempotencyKey(),
confirmKey: createFeeActionIdempotencyKey('confirm'),
sendKey: createFeeActionIdempotencyKey('send'),
@@ -1747,14 +1759,25 @@ async function executeConfirmAndSendFee() {
try {
const tenantId = String(summary.tenantId)
const inboundId = String(summary.inboundId)
- let snapshot = await calculateInboundFee(tenantId, inboundId, {
- selectedWeightKg,
- selectedVolumeCbm,
- weightSource: 'ACTUAL',
- volumeSource: 'ACTUAL',
- }, attempt.calculateKey)
+ let snapshot: InboundFeeSnapshot
+ try {
+ snapshot = await calculateInboundFee(tenantId, inboundId, {
+ selectedWeightKg, selectedVolumeCbm, weightSource: 'ACTUAL', volumeSource: 'ACTUAL',
+ }, attempt.calculateKey)
+ } catch (error) {
+ if (getApiErrorDetails(error, '费用试算失败').code !== 'A0404') throw error
+ feeConfirmPriceMissing.value = true
+ if (feeConfirmPriceReason.value.trim().length < 2) {
+ ElMessage.warning('价格未命中,请填写至少 2 个字符的人工定价原因')
+ return
+ }
+ snapshot = await createManualInboundQuote(tenantId, inboundId, {
+ selectedWeightKg, selectedVolumeCbm, unitPrice: selectedUnitPrice,
+ reason: feeConfirmPriceReason.value.trim(),
+ }, attempt.manualKey)
+ }
const calculatedUnitPrice = Number(snapshot.unitPrice)
- if (Math.abs(selectedUnitPrice - calculatedUnitPrice) > 0.000001) {
+ if (snapshot.rateCode !== 'MANUAL_QUOTE' && Math.abs(selectedUnitPrice - calculatedUnitPrice) > 0.000001) {
snapshot = await adjustPublishedInboundFee(tenantId, inboundId, {
expectedFeeVersion: snapshot.feeVersion,
chargeableQuantity: Number(snapshot.chargeableQuantity),
@@ -2311,13 +2334,12 @@ onBeforeUnmount(() => {
kg
m³
- {{ feeConfirmPriceUnit }}
+ {{ feeConfirmPriceUnit }}
{{ feeConfirmPriceUnit }}
-
+
- 参考计费量
- {{ formatNumber(Number(feeConfirmReference.chargeableQuantity), feeConfirmReference.roundingScale) }} {{ feeConfirmReference.chargeUnit }}
- - 固定附加费
- {{ feeConfirmMoney(Number(feeConfirmReference.additionalAmount || 0), feeConfirmReference.currency) }}
- 按当前单价预计合计
- {{ feeConfirmEstimatedTotal == null ? '—' : feeConfirmMoney(feeConfirmEstimatedTotal, feeConfirmReference.currency) }}
参考数据
From e6771e8e86b619d0dc6036197bfb902764c727fc Mon Sep 17 00:00:00 2001
From: ouyangxq <775153912@qq.com>
Date: Mon, 21 Sep 2026 11:17:11 +0800
Subject: [PATCH 6/6] =?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 | 1 +
.../inbounds/fee/InboundFeePanel.test.ts | 1 +
src/modules/inbounds/fee/InboundFeePanel.vue | 35 +++++++++++++++----
3 files changed, 31 insertions(+), 6 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index a679be38..385d7d2c 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -41,6 +41,7 @@
| 登录、会话与双区域 Token | `modules/auth/`、`core/auth/` | 国内 `auth/`、`iam/`;海外请求客户端再读 `shared/api/AGENTS.md` |
| 启动与懒加载资源恢复 | `main.ts`、`core/routing/navigationRecovery.ts` | 仅识别动态模块/Chunk 资源过期;30 秒内同目标最多自动恢复一次,存在未保存编辑时必须先确认 |
| 工作台与指标 | `modules/dashboard/` | 指标所属模块与服务端,不复制领域契约 |
+| FBA 无匹配价格、人工定价 | `modules/inbounds/fee/api.ts` 的 `createManualInboundQuote`、`types.ts`;页面消费沿 `InboundFeePanel.vue` 核对 | `POST /manual-quote` 提交计费重量、体积、单价、原因和 `Idempotency-Key`;Backend `PlatformFbaPricingController.manualQuote` → `FbaPricingService.manualQuote`,权限 `firstleg:inbound:confirm-send-fee`。仅 FBA、已有实际测量且当前价格未命中时允许,客户已确认费用禁止改价;`MANUAL_QUOTE` 快照允许价格记录与版本为空,不伪造价格簿关联。API 回归 `modules/inbounds/fee/api.test.ts`,不能以 API 存在代替页面接入完成 |
| PDA版本、APK下载、WGT热更新发布与停发 | `modules/pda-releases/pages/PdaReleasesView.vue`、`modules/pda-releases/api/pdaReleases.ts`、`modules/pda-releases/routes.ts` | 系统菜单 `/system/pda-releases`;列表权限 `platform:pda-release:list`,上传/发布/停发权限 `platform:pda-release:manage`。国内/海外按 `domestic/overseas` 分开查询,上传只创建草稿,发布与停发需确认;管理请求使用国内 HTTP 的 `/platform/v1/files/pda-releases`,后端见 `file/AGENTS.md`,设备安装与重启查对应 PDA 的更新工具 |
| 客户、企业审核、客户统计 | `modules/customers/` | `iam/`、`auth/`、`file/`、`firstleg/customerstatistics/`;公司联系电话只作企业联系信息,不修改管理员/成员登录手机号且不要求跨企业唯一。审核必填资质仅营业执照与统一社会信用代码;法人身份证选填,可随资质包提交,也可在已有营业执照后单独补传或替换 |
| 供应商、国内整柜 | `modules/suppliers/`、`modules/domestic-containers/` | Backend `warehouse/` 及对应 OpenAPI |
diff --git a/src/modules/inbounds/fee/InboundFeePanel.test.ts b/src/modules/inbounds/fee/InboundFeePanel.test.ts
index 3930dfb7..c53e01cc 100644
--- a/src/modules/inbounds/fee/InboundFeePanel.test.ts
+++ b/src/modules/inbounds/fee/InboundFeePanel.test.ts
@@ -381,4 +381,5 @@ describe('InboundFeePanel', () => {
expect(document.body.textContent).toContain('系统已刷新最新快照')
expect(document.body.textContent).toContain('当前输入仍保留')
})
+
})
diff --git a/src/modules/inbounds/fee/InboundFeePanel.vue b/src/modules/inbounds/fee/InboundFeePanel.vue
index ed9609f0..50f7ce59 100644
--- a/src/modules/inbounds/fee/InboundFeePanel.vue
+++ b/src/modules/inbounds/fee/InboundFeePanel.vue
@@ -47,6 +47,7 @@ const loadError = ref(null)
const adjustmentError = ref(null)
const conflictRefreshed = ref(false)
const snapshot = ref(null)
+const hasLoadedOnce = ref(false)
const adjustOpen = ref(false)
const trialOpen = ref(false)
const trialLoading = ref(false)
@@ -183,12 +184,25 @@ async function loadFee(options: { preserveAdjustment?: boolean } = {}) {
if (!options.preserveAdjustment && adjustOpen.value) fillAdjustmentForm(result)
} catch (error) {
if (sequence !== loadSequence) return
- snapshot.value = null
- emit('loaded', null)
- if (error instanceof ApiResponseError && error.code === 'A0404') notFound.value = true
- else loadError.value = getApiErrorDetails(error, '费用快照加载失败,请稍后重试')
+ const hadSnapshot = Boolean(snapshot.value)
+ if (error instanceof ApiResponseError && error.code === 'A0404') {
+ notFound.value = !hadSnapshot
+ if (!hadSnapshot) {
+ snapshot.value = null
+ emit('loaded', null)
+ }
+ } else {
+ loadError.value = getApiErrorDetails(error, '费用快照加载失败,请稍后重试')
+ if (!hadSnapshot) {
+ snapshot.value = null
+ emit('loaded', null)
+ }
+ }
} finally {
- if (sequence === loadSequence) loading.value = false
+ if (sequence === loadSequence) {
+ loading.value = false
+ hasLoadedOnce.value = true
+ }
}
}
@@ -353,6 +367,7 @@ watch(
() => {
adjustOpen.value = false
snapshot.value = null
+ hasLoadedOnce.value = false
void loadFee()
},
{ immediate: true },
@@ -435,11 +450,18 @@ defineExpose({ reload: loadFee })
需要权限:firstleg:fba-pricing:view
-
+
正在加载费用快照
+
+ 最新费用快照暂时无法刷新,当前仍显示上一版快照
+
+ {{ loadError.message }}
+
+
+
费用快照加载失败
{{ loadError.message }}
@@ -597,6 +619,7 @@ defineExpose({ reload: loadFee })
.fee-heading-actions .el-button { margin-left: 0; }
.fee-refresh-button .is-spinning { animation: fee-refresh-spin 0.8s linear infinite; }
.fee-loading { padding: 20px 0; }
+.fee-refresh-error { margin-bottom: 10px; }
.inbound-fee-panel :deep(.el-alert p) { margin: 5px 0 0; line-height: 1.55; }
.inbound-fee-panel :deep(.el-alert .el-button) { margin-top: 8px; }
.empty-note { margin: 0; color: var(--cc-muted); font-size: 12px; }