Release/test 合并上生产 #2

Merged
admin merged 736 commits from release/test into main 2026-09-17 11:50:56 +08:00
5 changed files with 90 additions and 52 deletions
Showing only changes of commit 28f2cd4cc5 - Show all commits
+3
View File
@@ -96,6 +96,9 @@
## UI 与交互
- Admin 是高信息密度业务系统,默认使用紧凑的表格、键值行、分组标题和连续分区呈现信息;不将普通字段、统计值或流程节点拆成大面积卡片,不为少量信息制造大留白。一屏应优先展示完成当前业务判断所需的多项关键信息。
- 需要突出结果时,仅对总额、异常、待办或当前状态使用克制的强调;其余信息保持同一紧凑信息层级,不重复标题、说明和装饰性容器。
- 修改已有 Admin 原型页面前先读 `../../docs/Admin-Client前端原型还原验收规则.md`,按页面和操作形成原型核对矩阵;本节维护 Admin 的实现约束,不复制跨前端验收文档全文。
- 已向客户讲解或确认过的产品原型是页面视觉与布局实现的直接事实来源。字段顺序、字段分组、字段摆放、列宽、控件宽高、对齐方式、换行方式、区块层级、上下左右边距、内边距、间隙、留白、圆角、边框、阴影、背景色、文字颜色和字号均必须按原型还原,不得只参考大致风格后自行重新排版。
- 原型中的按钮位置、顺序、尺寸、圆角、边框、背景色、文字色、图标、悬停态、激活态、禁用态和危险语义必须直接按原型实现;不得擅自改为文字链接、不同颜色、不同按钮类型、折叠菜单或自创交互效果,也不得增加原型没有的上浮、缩放、渐变线、装饰图标或其他视觉元素。
@@ -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),
+66 -47
View File
@@ -233,7 +233,7 @@ defineExpose({ reload: loadFee })
<span class="fee-heading-icon" aria-hidden="true"><CircleDollarSign :size="20" /></span>
<div>
<h3 id="inbound-fee-title">入库费用快照</h3>
<p>展示审核完成后锁定的计费依据和对客费用,不在此重复触发试算或发布。</p>
<p>测量审核后的头程费用,人工确认后才发布。</p>
</div>
</div>
<div class="fee-heading-actions">
@@ -317,41 +317,44 @@ defineExpose({ reload: loadFee })
</div>
</section>
<div class="fee-detail-grid">
<section aria-labelledby="rule-title">
<div class="section-title"><div><h4 id="rule-title">客户计费规则</h4><p>{{ snapshot.ruleCode }} · V{{ snapshot.chargeRuleVersion }}</p></div></div>
<dl>
<div><dt>客户</dt><dd>{{ snapshot.customerName }}</dd></div>
<div><dt>业务类型</dt><dd>{{ firstLegBusinessTypeLabel(snapshot.businessType) }}</dd></div>
<div><dt>计费口径</dt><dd>{{ chargeModeLabel }}</dd></div>
<div><dt>舍入规则</dt><dd>保留 {{ snapshot.roundingScale }} 位,{{ roundingModeLabel(snapshot.roundingMode) }}</dd></div>
</dl>
</section>
<section aria-labelledby="rate-title">
<div class="section-title"><div><h4 id="rate-title">渠道价格</h4><p>{{ snapshot.rateCode }} · V{{ snapshot.carrierRateVersion }}</p></div></div>
<dl>
<div><dt>起运仓</dt><dd>{{ snapshot.originWarehouseName }}</dd></div>
<div><dt>渠道</dt><dd>{{ snapshot.channelCode }}</dd></div>
<div><dt>FBA 目的仓</dt><dd>{{ snapshot.destinationCode }}</dd></div>
<div><dt>价格有效期</dt><dd>{{ formatDateTime(snapshot.rateEffectiveFrom) }} 至 {{ formatDateTime(snapshot.rateEffectiveTo) }}</dd></div>
</dl>
</section>
</div>
<details class="fee-advanced">
<summary>查看价格来源与时间记录</summary>
<div class="fee-detail-grid">
<section v-if="snapshot.ruleCode" aria-labelledby="rule-title">
<div class="section-title"><div><h4 id="rule-title">客户计费规则</h4><p>{{ snapshot.ruleCode }} · V{{ snapshot.chargeRuleVersion }}</p></div></div>
<dl>
<div><dt>客户</dt><dd>{{ snapshot.customerName }}</dd></div>
<div><dt>业务类型</dt><dd>{{ firstLegBusinessTypeLabel(snapshot.businessType) }}</dd></div>
<div><dt>计费口径</dt><dd>{{ chargeModeLabel }}</dd></div>
<div><dt>舍入规则</dt><dd>{{ snapshot.roundingScale == null ? '—' : `保留 ${snapshot.roundingScale} 位,${roundingModeLabel(snapshot.roundingMode)}` }}</dd></div>
</dl>
</section>
<section aria-labelledby="rate-title">
<div class="section-title"><div><h4 id="rate-title">渠道价格</h4><p>{{ snapshot.rateCode }} · V{{ snapshot.carrierRateVersion }}</p></div></div>
<dl>
<div><dt>起运仓</dt><dd>{{ snapshot.originWarehouseName }}</dd></div>
<div><dt>渠道</dt><dd>{{ snapshot.channelCode }}</dd></div>
<div><dt>FBA 目的仓</dt><dd>{{ snapshot.destinationCode }}</dd></div>
<div><dt>价格有效期</dt><dd>{{ formatDateTime(snapshot.rateEffectiveFrom) }} 至 {{ formatDateTime(snapshot.rateEffectiveTo) }}</dd></div>
</dl>
</section>
</div>
<section class="fee-timeline" aria-labelledby="fee-timeline-title">
<div class="section-title"><div><h4 id="fee-timeline-title">快照时间</h4><p>每个节点均由后端状态机记录。</p></div></div>
<ol>
<li><span>试算</span><strong>{{ formatDateTime(snapshot.calculatedAt) }}</strong></li>
<li :class="{ pending: !snapshot.confirmedAt }"><span>确认</span><strong>{{ formatDateTime(snapshot.confirmedAt) }}</strong></li>
<li :class="{ pending: !snapshot.sentAt }"><span>发布给客户</span><strong>{{ formatDateTime(snapshot.sentAt) }}</strong></li>
</ol>
</section>
<section class="fee-timeline" aria-labelledby="fee-timeline-title">
<div class="section-title"><div><h4 id="fee-timeline-title">时间记录</h4></div></div>
<ol>
<li><span>试算</span><strong>{{ formatDateTime(snapshot.calculatedAt) }}</strong></li>
<li :class="{ pending: !snapshot.confirmedAt }"><span>确认</span><strong>{{ formatDateTime(snapshot.confirmedAt) }}</strong></li>
<li :class="{ pending: !snapshot.sentAt }"><span>发布</span><strong>{{ formatDateTime(snapshot.sentAt) }}</strong></li>
</ol>
</section>
</details>
</div>
<AppDialog
v-model="adjustOpen"
title="调整入库费用"
width="min(560px, calc(100vw - 28px))"
width="min(760px, calc(100vw - 28px))"
confirm-text="确认调整并生成新版本"
confirm-type="warning"
:confirm-loading="adjusting"
@@ -371,15 +374,15 @@ defineExpose({ reload: loadFee })
<div class="adjustment-amounts">
<el-form-item label="调整后计费量" prop="chargeableQuantity" required>
<el-input-number v-model="adjustmentForm.chargeableQuantity" aria-label="调整后计费量" :min="0.0001" :max="999999999999.9999" :precision="4" :step="1" controls-position="right" />
<span>{{ chargeUnitLabel }}</span>
<span class="adjustment-unit">{{ chargeUnitLabel }}</span>
</el-form-item>
<el-form-item label="调整后单价" prop="unitPrice" required>
<el-input-number v-model="adjustmentForm.unitPrice" aria-label="调整后单价" :min="0" :max="999999999999.9999" :precision="4" :step="0.1" controls-position="right" />
<span>{{ snapshot?.currency }} / {{ chargeUnitLabel }}</span>
<span class="adjustment-unit">{{ snapshot?.currency }} / {{ chargeUnitLabel }}</span>
</el-form-item>
<el-form-item label="调整后附加费" prop="additionalAmount" required>
<el-input-number v-model="adjustmentForm.additionalAmount" aria-label="调整后附加费" :min="0" :max="999999999999.99" :precision="2" :step="10" controls-position="right" />
<span>{{ snapshot?.currency }}</span>
<span class="adjustment-unit">{{ snapshot?.currency }}</span>
</el-form-item>
</div>
<div class="adjustment-total"><span>当前服务端计算金额</span><strong>{{ formatMoney(snapshot?.totalAmount, snapshot?.currency) }}</strong><small>提交后以服务端返回的新版本金额为准</small></div>
@@ -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) {
@@ -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)')
@@ -977,13 +977,14 @@ onBeforeUnmount(() => {
</template>
<template v-else-if="activeDetailTab === 'fees'">
<div class="panel-title"><div><h2>费用信息</h2><p>维护运单与入库单应收应付费用</p></div><WalletCards :size="19" /></div>
<div class="panel-title"><div><h2>费用信息</h2><p>头程自动初算与其他人工费用在同一页维护</p></div><WalletCards :size="19" /></div>
<h3 class="fee-section-title">头程自动初算</h3>
<InboundFeePanel
:customer-id="String(detail.summary.tenantId)"
:inbound-id="String(detail.summary.inboundId)"
@changed="loadDetail(true)"
/>
<el-divider content-position="left">费用明细与审核</el-divider>
<h3 class="fee-section-title fee-section-title--secondary">其他费用明细</h3>
<InboundFeeDetailManager
:account-id="String(detail.summary.tenantId)"
:inbound-id="String(detail.summary.inboundId)"
@@ -1293,6 +1294,8 @@ onBeforeUnmount(() => {
.panel-title h2 { margin: 0; color: #344054; font-size: 16px; }
.panel-title p { margin: 5px 0 0; color: var(--cc-muted); font-size: 11px; }
.panel-title > svg { color: var(--cc-primary); }
.fee-section-title { margin: 14px 0 8px; color: #344054; font-size: 13px; line-height: 1.4; }
.fee-section-title--secondary { margin-top: 20px; padding-top: 14px; border-top: 1px solid var(--cc-border); }
.order-info-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0; margin: 0; overflow: hidden; border: 1px solid #edf0f4; background: #fff; }
.order-info-grid > div, .measurement-grid > div { display: flex; min-width: 0; min-height: 38px; align-items: baseline; gap: 8px; padding: 8px 12px; border-right: 1px solid #edf0f4; border-bottom: 1px solid #edf0f4; background: #fff; }
.order-info-grid > div.wide { grid-column: span 2; }