diff --git a/AGENTS.md b/AGENTS.md index 33cb9591..0fab185f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -96,6 +96,9 @@ ## UI 与交互 +- Admin 是高信息密度业务系统,默认使用紧凑的表格、键值行、分组标题和连续分区呈现信息;不将普通字段、统计值或流程节点拆成大面积卡片,不为少量信息制造大留白。一屏应优先展示完成当前业务判断所需的多项关键信息。 +- 需要突出结果时,仅对总额、异常、待办或当前状态使用克制的强调;其余信息保持同一紧凑信息层级,不重复标题、说明和装饰性容器。 + - 修改已有 Admin 原型页面前先读 `../../docs/Admin-Client前端原型还原验收规则.md`,按页面和操作形成原型核对矩阵;本节维护 Admin 的实现约束,不复制跨前端验收文档全文。 - 已向客户讲解或确认过的产品原型是页面视觉与布局实现的直接事实来源。字段顺序、字段分组、字段摆放、列宽、控件宽高、对齐方式、换行方式、区块层级、上下左右边距、内边距、间隙、留白、圆角、边框、阴影、背景色、文字颜色和字号均必须按原型还原,不得只参考大致风格后自行重新排版。 - 原型中的按钮位置、顺序、尺寸、圆角、边框、背景色、文字色、图标、悬停态、激活态、禁用态和危险语义必须直接按原型实现;不得擅自改为文字链接、不同颜色、不同按钮类型、折叠菜单或自创交互效果,也不得增加原型没有的上浮、缩放、渐变线、装饰图标或其他视觉元素。 diff --git a/src/modules/inbounds/fee/InboundFeePanel.test.ts b/src/modules/inbounds/fee/InboundFeePanel.test.ts index 497d6478..378fe6fe 100644 --- a/src/modules/inbounds/fee/InboundFeePanel.test.ts +++ b/src/modules/inbounds/fee/InboundFeePanel.test.ts @@ -1,4 +1,6 @@ import ElementPlus from 'element-plus' +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' import { createPinia, setActivePinia } from 'pinia' import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' @@ -83,7 +85,8 @@ describe('InboundFeePanel', () => { expect(document.body.textContent).toContain('¥8.50 / KG') expect(document.body.textContent).toContain('RULE-FBA-KG · V3') expect(document.body.textContent).toContain('RATE-LAX · V5') - expect(document.body.textContent).toContain('发布给客户') + expect(document.body.textContent).toContain('时间记录') + expect(document.body.textContent).toContain('发布') expect(document.body.textContent).not.toContain('重新试算') expect(document.body.textContent).not.toContain('发送费用') }) @@ -220,6 +223,15 @@ describe('InboundFeePanel', () => { expect(wrapper.emitted('changed')?.[0]?.[0]).toMatchObject({ feeVersion: 3 }) }) + it('keeps adjustment units on one line in a three-column desktop layout', () => { + const source = readFileSync(resolve('src/modules/inbounds/fee/InboundFeePanel.vue'), 'utf8') + + expect(source).toContain('width="min(760px, calc(100vw - 28px))"') + expect(source).toContain('grid-template-columns: repeat(3, minmax(0, 1fr))') + expect(source).toContain('class="adjustment-unit"') + expect(source).toContain('white-space: nowrap') + }) + it('refreshes the latest snapshot after a conflict while retaining the dialog', async () => { vi.mocked(adjustPublishedInboundFee).mockRejectedValue( new ApiResponseError('费用已被其他操作员修改,请刷新后重试', 'A0409', 409), diff --git a/src/modules/inbounds/fee/InboundFeePanel.vue b/src/modules/inbounds/fee/InboundFeePanel.vue index 5ae73db0..16e913b0 100644 --- a/src/modules/inbounds/fee/InboundFeePanel.vue +++ b/src/modules/inbounds/fee/InboundFeePanel.vue @@ -233,7 +233,7 @@ defineExpose({ reload: loadFee })

入库费用快照

-

展示审核完成后锁定的计费依据和对客费用,不在此重复触发试算或发布。

+

测量审核后的头程费用,人工确认后才发布。

@@ -317,41 +317,44 @@ defineExpose({ reload: loadFee })
-
-
-

客户计费规则

{{ snapshot.ruleCode }} · V{{ snapshot.chargeRuleVersion }}

-
-
客户
{{ snapshot.customerName }}
-
业务类型
{{ firstLegBusinessTypeLabel(snapshot.businessType) }}
-
计费口径
{{ chargeModeLabel }}
-
舍入规则
保留 {{ snapshot.roundingScale }} 位,{{ roundingModeLabel(snapshot.roundingMode) }}
-
-
-
-

渠道价格

{{ snapshot.rateCode }} · V{{ snapshot.carrierRateVersion }}

-
-
起运仓
{{ snapshot.originWarehouseName }}
-
渠道
{{ snapshot.channelCode }}
-
FBA 目的仓
{{ snapshot.destinationCode }}
-
价格有效期
{{ formatDateTime(snapshot.rateEffectiveFrom) }} 至 {{ formatDateTime(snapshot.rateEffectiveTo) }}
-
-
-
+
+ 查看价格来源与时间记录 +
+
+

客户计费规则

{{ snapshot.ruleCode }} · V{{ snapshot.chargeRuleVersion }}

+
+
客户
{{ snapshot.customerName }}
+
业务类型
{{ firstLegBusinessTypeLabel(snapshot.businessType) }}
+
计费口径
{{ chargeModeLabel }}
+
舍入规则
{{ snapshot.roundingScale == null ? '—' : `保留 ${snapshot.roundingScale} 位,${roundingModeLabel(snapshot.roundingMode)}` }}
+
+
+
+

渠道价格

{{ snapshot.rateCode }} · V{{ snapshot.carrierRateVersion }}

+
+
起运仓
{{ snapshot.originWarehouseName }}
+
渠道
{{ snapshot.channelCode }}
+
FBA 目的仓
{{ snapshot.destinationCode }}
+
价格有效期
{{ formatDateTime(snapshot.rateEffectiveFrom) }} 至 {{ formatDateTime(snapshot.rateEffectiveTo) }}
+
+
+
-
-

快照时间

每个节点均由后端状态机记录。

-
    -
  1. 试算{{ formatDateTime(snapshot.calculatedAt) }}
  2. -
  3. 确认{{ formatDateTime(snapshot.confirmedAt) }}
  4. -
  5. 发布给客户{{ formatDateTime(snapshot.sentAt) }}
  6. -
-
+
+

时间记录

+
    +
  1. 试算{{ formatDateTime(snapshot.calculatedAt) }}
  2. +
  3. 确认{{ formatDateTime(snapshot.confirmedAt) }}
  4. +
  5. 发布{{ formatDateTime(snapshot.sentAt) }}
  6. +
+
+
- {{ chargeUnitLabel }} + {{ chargeUnitLabel }} - {{ snapshot?.currency }} / {{ chargeUnitLabel }} + {{ snapshot?.currency }} / {{ chargeUnitLabel }} - {{ snapshot?.currency }} + {{ snapshot?.currency }}
当前服务端计算金额{{ formatMoney(snapshot?.totalAmount, snapshot?.currency) }}提交后以服务端返回的新版本金额为准
@@ -406,24 +409,24 @@ defineExpose({ reload: loadFee }) .inbound-fee-panel :deep(.el-alert .el-button) { margin-top: 8px; } .empty-note { margin: 0; color: var(--cc-muted); font-size: 12px; } .fee-content { border-top: 1px solid var(--cc-border); } -.fee-summary { display: grid; grid-template-columns: minmax(260px, 1.35fr) repeat(2, minmax(180px, 1fr)); border-bottom: 1px solid var(--cc-border); background: #f8fafc; } -.fee-total, .fee-summary-item { display: flex; min-width: 0; flex-direction: column; justify-content: center; gap: 6px; padding: 20px; } +.fee-summary { display: grid; grid-template-columns: minmax(240px, 1.25fr) repeat(2, minmax(160px, 1fr)); border-bottom: 1px solid var(--cc-border); } +.fee-total, .fee-summary-item { display: flex; min-width: 0; flex-direction: column; justify-content: center; gap: 3px; padding: 11px 14px; } .fee-summary-item { border-left: 1px solid var(--cc-border); } .fee-summary span, .fee-summary small { color: var(--cc-muted); font-size: 11px; } .fee-summary strong { font-size: 18px; line-height: 1.25; overflow-wrap: anywhere; } -.fee-total strong { color: #0f5c49; font-size: 27px; letter-spacing: -.02em; } +.fee-total strong { color: #0f5c49; font-size: 21px; letter-spacing: -.01em; } .version-status { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; } -.calculation-section, .fee-timeline { padding: 22px 0; border-bottom: 1px solid var(--cc-border); } +.calculation-section, .fee-timeline { padding: 14px 0; border-bottom: 1px solid var(--cc-border); } .section-title { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; } .section-title h4 { font-size: 14px; } -.calculation-flow { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin-top: 16px; border: 1px solid var(--cc-border); border-radius: 12px; overflow: hidden; } -.calculation-flow > div { display: flex; min-width: 0; min-height: 88px; flex-direction: column; justify-content: center; gap: 6px; padding: 14px 16px; border-right: 1px solid var(--cc-border); } +.calculation-flow { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); margin-top: 10px; border-top: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); } +.calculation-flow > div { display: flex; min-width: 0; min-height: 58px; flex-direction: column; justify-content: center; gap: 3px; padding: 9px 12px; border-right: 1px solid var(--cc-border); } .calculation-flow > div:last-child { border-right: 0; } .calculation-flow span, .calculation-flow small { color: var(--cc-muted); font-size: 11px; } .calculation-flow strong { font-size: 15px; overflow-wrap: anywhere; } -.calculation-flow .is-chargeable { background: #f0fdf9; } +.calculation-flow .is-chargeable { background: #f7fbfa; } .calculation-flow .is-chargeable strong { color: #087d70; } -.amount-equation { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-top: 12px; padding: 12px 14px; border-radius: 8px; background: #f8fafc; color: #475467; font-size: 12px; } +.amount-equation { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-top: 8px; padding: 7px 0; color: #475467; font-size: 12px; } .amount-equation i { color: #98a2b3; font-style: normal; } .amount-equation strong { color: var(--cc-text); font-size: 15px; } .fee-detail-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); border-bottom: 1px solid var(--cc-border); } @@ -442,15 +445,21 @@ defineExpose({ reload: loadFee }) .fee-timeline li.pending::before { background: #98a2b3; } .fee-timeline span { color: var(--cc-muted); font-size: 11px; } .fee-timeline strong { font-size: 12px; overflow-wrap: anywhere; } +.fee-advanced { border-top: 1px solid var(--cc-border); } +.fee-advanced > summary { padding: 10px 2px; color: var(--cc-primary); font-size: 12px; font-weight: 600; cursor: pointer; } +.fee-advanced:not([open]) > summary { border-bottom: 0; } +.fee-advanced .fee-detail-grid > section:only-child { grid-column: 1 / -1; } .adjustment-alert { margin-top: 12px; } .adjustment-form { margin-top: 18px; } -.adjustment-amounts { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 16px; } -.adjustment-amounts :deep(.el-form-item__content) { flex-wrap: nowrap; } +.adjustment-amounts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px; } +.adjustment-amounts :deep(.el-form-item) { min-width: 0; margin-bottom: 18px; } +.adjustment-amounts :deep(.el-form-item__content) { display: grid; grid-template-columns: minmax(0, 1fr) auto; flex-wrap: nowrap; column-gap: 8px; } .adjustment-amounts :deep(.el-input-number) { width: 100%; } -.adjustment-amounts :deep(.el-form-item__content > span) { margin-left: 8px; color: var(--cc-muted); font-size: 12px; } -.adjustment-total { display: flex; align-items: baseline; justify-content: space-between; gap: 16px; margin: 0 0 18px; padding: 13px 14px; border-radius: 8px; background: #f8fafc; } +.adjustment-unit { min-width: max-content; color: var(--cc-muted); font-size: 12px; white-space: nowrap; } +.adjustment-total { display: grid; grid-template-columns: auto minmax(150px, 1fr) auto; align-items: baseline; gap: 18px; margin: 0 0 18px; padding: 13px 16px; border-radius: 8px; background: #f8fafc; } .adjustment-total span { color: var(--cc-muted); font-size: 12px; } -.adjustment-total strong { font-size: 18px; } +.adjustment-total strong { justify-self: center; font-size: 18px; white-space: nowrap; } +.adjustment-total small { color: var(--cc-muted); font-size: 11px; text-align: right; } .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; } @media (max-width: 900px) { @@ -466,7 +475,7 @@ defineExpose({ reload: loadFee }) .fee-heading { align-items: stretch; flex-direction: column; } .fee-heading-actions { justify-content: flex-start; } .fee-heading-actions .el-button:not(.is-circle) { flex: 1; min-height: 44px; } - .fee-summary, .calculation-flow, .fee-detail-grid, .fee-timeline ol, .adjustment-amounts { grid-template-columns: 1fr; } + .fee-summary, .calculation-flow, .fee-detail-grid, .fee-timeline ol { grid-template-columns: 1fr; } .fee-summary-item { border-top: 1px solid var(--cc-border); border-left: 0; } .calculation-flow > div { min-height: 76px; border-right: 0; border-bottom: 1px solid var(--cc-border); } .calculation-flow > div:last-child { border-bottom: 0; } @@ -475,6 +484,16 @@ defineExpose({ reload: loadFee }) .fee-timeline ol { gap: 16px; } .fee-timeline li { padding-left: 20px; } .fee-timeline li::after { display: none; } + .adjustment-amounts { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .adjustment-total { grid-template-columns: auto 1fr; } + .adjustment-total strong { justify-self: end; } + .adjustment-total small { grid-column: 1 / -1; text-align: left; } +} + +@media (max-width: 520px) { + .adjustment-amounts { grid-template-columns: 1fr; gap: 0; } + .adjustment-total { grid-template-columns: 1fr; gap: 6px; } + .adjustment-total strong { justify-self: start; } } @media (max-width: 360px) { diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts index a808f8d7..bcd4522a 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts @@ -285,10 +285,11 @@ describe('UnifiedOrdersView contract', () => { expect(source).toContain("partyValue(detail.order.parties, 'RECEIVER', 'phone')") expect(source).not.toContain("partyValue(detail.order.parties, 'CONSIGNEE'") expect(source).toContain("cargoTotal(detail.order.cargoes, 'netWeightKg')") - expect(source).toContain('InboundFeeDetailManager') expect(source).toContain('InboundFeePanel') + expect(source).toContain('InboundFeeDetailManager') expect(source).toContain(':customer-id="String(detail.summary.tenantId)"') - expect(source).toContain('费用明细与审核') + expect(source).toContain('头程自动初算') + expect(source).toContain('其他费用明细') expect(source).toContain('@changed="loadDetail(true)"') expect(source).toContain('firstLegTransportModeLabel(detail.order.order?.transportMode)') expect(source).toContain('firstLegPickupMethodLabel(detail.order.order?.pickupMethod)') diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.vue b/src/modules/unified-orders/pages/UnifiedOrdersView.vue index 825ad1a4..607a8a42 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.vue +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.vue @@ -977,13 +977,14 @@ onBeforeUnmount(() => {