修复客户订单列设置持久化并统一最新计费量展示

This commit is contained in:
2026-10-28 11:56:49 +08:00
parent 3ce0f2e234
commit c187544a94
6 changed files with 260 additions and 24 deletions
@@ -443,18 +443,16 @@ function clearanceResultStatusLabel(value: MerchantClearanceResult) {
</div>
</section>
</template>
<template #footer><el-button @click="close">关闭</el-button></template>
</el-dialog>
</template>
<style scoped>
:global(.customs-documents-dialog.el-dialog) { display: flex; max-height: 70vh; flex-direction: column; overflow: hidden; padding: 0; border: 1px solid var(--client-border); border-radius: var(--client-radius); }
:global(.customs-documents-dialog .el-dialog__header), :global(.customs-documents-dialog .el-dialog__footer) { flex: 0 0 auto; }
:global(.customs-documents-dialog .el-dialog__header) { flex: 0 0 auto; }
:global(.customs-documents-dialog .el-dialog__header) { margin: 0; padding: 18px 48px 18px 20px; border-bottom: 1px solid var(--client-border); }
:global(.customs-documents-dialog .el-dialog__title) { color: var(--client-text); font-size: 16px; font-weight: 600; line-height: 24px; }
:global(.customs-documents-dialog .el-dialog__headerbtn) { top: 8px; right: 8px; }
:global(.customs-documents-dialog .el-dialog__body) { min-height: 0; flex: 1 1 auto; overflow-y: auto; padding: 20px; color: var(--client-text); line-height: 1.6; overscroll-behavior: contain; }
:global(.customs-documents-dialog .el-dialog__footer) { padding: 14px 20px; border-top: 1px solid var(--client-border); background: var(--client-header-bg); }
.customs-error { display: grid; gap: 10px; justify-items: start; padding: 22px; border: 1px solid #f1b8b8; background: #fff7f7; color: #9f2d2d; }
.customs-error span { overflow-wrap: anywhere; font-size: 12px; }
.customs-summary { display: grid; grid-template-columns: minmax(0, 1.6fr) repeat(2, minmax(0, 1fr)) auto; align-items: center; gap: 18px; padding: 16px; border: 1px solid var(--client-border); border-radius: var(--client-radius); background: var(--client-header-bg); }
@@ -1,8 +1,39 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { compile, computed, createSSRApp, h, inject, provide } from 'vue'
import { renderToString } from 'vue/server-renderer'
import ts from 'typescript'
const source = readFileSync(new URL('./InboundBillingPanel.vue', import.meta.url), 'utf8')
const confirmationBody = source.slice(source.indexOf('<dl class="fee-dialog-summary"'), source.indexOf('<div class="fee-confirm-notice"'))
const formatterSource = readFileSync(new URL('../utils/inboundBilling.ts', import.meta.url), 'utf8')
const formatters = await import(`data:text/javascript;base64,${Buffer.from(ts.transpileModule(formatterSource, {
compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
}).outputText).toString('base64')}`)
async function renderConfirmation(data) {
const app = createSSRApp({
render: compile(`<div>${confirmationBody}</div>`),
setup: () => ({ props: { data }, ...formatters }),
})
app.component('ElTable', {
props: ['data'],
setup(tableProps, { slots }) {
provide('billingRows', computed(() => tableProps.data))
return () => h('table', slots.default?.())
},
})
app.component('ElTableColumn', {
props: ['label', 'prop'],
setup(columnProps, { slots }) {
const rows = inject('billingRows')
return () => h('tr', [h('th', columnProps.label), ...rows.value.map(row =>
h('td', slots.default?.({ row }) ?? row[columnProps.prop] ?? ''))])
},
})
return renderToString(app)
}
test('renders loading, pending, isolated error, and published states', () => {
assert.match(source, /v-if="props\.loading"/)
@@ -45,17 +76,55 @@ test('shows the published merchant inbound, measurement details and customer con
assert.match(source, /getCalculationRuleLabel\(row\.calculationRuleVersion\)/)
})
test('matches the confirmed billing prototype summary, table, notice and footer', () => {
test('keeps the billing summary, confirmation table, notice and footer', () => {
assert.match(source, /width="min\(1180px, calc\(100vw - 40px\)\)"/)
assert.match(source, /class="fee-dialog-summary" aria-label="计费汇总"/)
for (const label of ['计算重', '单价', '总金额', '箱规长(cm)', '箱规宽(cm)', '箱规高(cm)', '每箱体积', '总体积', '总材积重']) assert.ok(source.includes(label), `missing prototype label: ${label}`)
for (const label of ['计算重', '单价', '总金额', '计费毛重', '计费体积', '计费方式', '最终计费量']) assert.ok(confirmationBody.includes(label), `missing billing label: ${label}`)
assert.match(source, /class="fee-confirm-notice"/)
assert.match(source, /请核对计算重、单价和总金额。确认后当前费用版本将锁定。/)
assert.match(source, /请核对计费毛重、计费体积、计费量、单价和总金额。确认后当前费用版本将锁定。/)
assert.match(source, /确认计费量和费用/)
assert.match(source, /\.fee-dialog-summary \{ display:grid; grid-template-columns:repeat\(3,minmax\(0,1fr\)\)/)
assert.match(source, /\.fee-dialog-table :deep\(\.el-table__body td\.el-table__cell\)/)
})
test('confirms the selected billing weight and volume even when outer carton totals differ', async () => {
const html = await renderConfirmation({
latestFee: {
actualWeightKg: 570.5, actualVolumeCbm: 3.6894,
chargeableQuantity: 616.1298, chargeMode: 'KG', chargeUnit: 'KG',
unitPrice: 13.6, totalAmount: 8379.37, currency: 'CNY',
},
billingBasis: { chargeableWeightKg: 650.298 },
measurementItems: [{ lengthCm: 63, widthCm: 53, heightCm: 53, packageCount: 22,
unitVolumeCbm: 0.177, totalVolumeCbm: 3.894, volumeWeightKg: 650.298 }],
})
assert.match(html, /570\.5 kg/)
assert.match(html, /3\.6894 m³/)
assert.match(html, /616\.13 kg/)
assert.match(html, /8,379\.37/)
assert.doesNotMatch(html, /3\.894|650\.298|0\.177/)
assert.doesNotMatch(confirmationBody, /measurementItems|billingBasis|箱规长|每箱体积/)
})
test('uses the published volume charge quantity and retains six decimal places', async () => {
const html = await renderConfirmation({
latestFee: {
actualWeightKg: 600, actualVolumeCbm: 1.234567,
chargeableQuantity: 1.652893, chargeMode: 'CBM', chargeUnit: 'CBM',
unitPrice: 100, totalAmount: 165.29, currency: 'CNY',
},
billingBasis: { chargeableWeightKg: 600 }, measurementItems: [],
})
assert.match(html, /计费量/)
assert.match(html, /按体积计费/)
assert.match(html, /600 kg/)
assert.match(html, /1\.234567 m³/)
assert.match(html, /1\.652893 m³/)
assert.doesNotMatch(html, /1\.652893 kg/)
})
test('stacks core information for 720 and 360 pixel layouts', () => {
assert.match(source, /@media \(max-width: 720px\)/)
assert.match(source, /\.inbound-overview, \.inbound-measurements, \.fee-calculation-grid \{ grid-template-columns: 1fr;/)
@@ -172,8 +172,8 @@ async function confirmFee() {
props.data.latestFee.roundingMode,
) }}</dd>
</div>
<div><dt>实际重量</dt><dd>{{ formatInboundMeasure(props.data.latestFee.actualWeightKg, 'kg') }}</dd></div>
<div><dt>实际体积</dt><dd>{{ formatInboundMeasure(props.data.latestFee.actualVolumeCbm, 'm³') }}</dd></div>
<div><dt>计费毛重</dt><dd>{{ formatInboundMeasure(props.data.latestFee.actualWeightKg, 'kg') }}</dd></div>
<div><dt>计费体积</dt><dd>{{ formatInboundMeasure(props.data.latestFee.actualVolumeCbm, 'm³', 6) }}</dd></div>
<div><dt>体积重量</dt><dd>{{ formatInboundMeasure(props.data.latestFee.convertedQuantity, props.data.latestFee.chargeUnit) }}</dd></div>
<div><dt>最终计费量</dt><dd>{{ formatInboundMeasure(props.data.latestFee.chargeableQuantity, props.data.latestFee.chargeUnit) }}</dd></div>
<div><dt>单价</dt><dd>{{ formatInboundMoney(props.data.latestFee.unitPrice, props.data.latestFee.currency) }} / {{ props.data.latestFee.chargeUnit }}</dd></div>
@@ -214,26 +214,20 @@ async function confirmFee() {
</div>
<template v-else>
<dl class="fee-dialog-summary" aria-label="计费汇总">
<div><dt>计算重</dt><dd>{{ formatInboundMeasure(props.data.billingBasis.chargeableWeightKg, 'kg') }}</dd></div>
<div><dt>{{ props.data.latestFee.chargeUnit === 'KG' ? '计算重' : '计费量' }}</dt><dd>{{ formatInboundMeasure(props.data.latestFee.chargeableQuantity, props.data.latestFee.chargeUnit === 'KG' ? 'kg' : 'm³', props.data.latestFee.chargeUnit === 'KG' ? 3 : 6) }}</dd></div>
<div><dt>单价</dt><dd>{{ formatInboundMoney(props.data.latestFee.unitPrice, props.data.latestFee.currency) }} / {{ props.data.latestFee.chargeUnit.toUpperCase() }}</dd></div>
<div><dt>总金额</dt><dd>{{ formatInboundMoney(props.data.latestFee.totalAmount, props.data.latestFee.currency) }}</dd></div>
</dl>
<el-table class="measurement-detail-table fee-dialog-table" :data="props.data.measurementItems" border empty-text="暂无箱规测量明细">
<el-table-column type="index" label="#" width="54" />
<el-table-column prop="lengthCm" label="箱规长(cm)" min-width="105" />
<el-table-column prop="widthCm" label="箱规宽(cm)" min-width="105" />
<el-table-column prop="heightCm" label="箱规高(cm)" min-width="105" />
<el-table-column prop="packageCount" label="箱数" min-width="76" />
<el-table-column label="每箱体积" min-width="105"><template #default="{ row }">{{ formatInboundMeasure(row.unitVolumeCbm, 'm³') }}</template></el-table-column>
<el-table-column label="总体积" min-width="105"><template #default="{ row }">{{ formatInboundMeasure(row.totalVolumeCbm, 'm³') }}</template></el-table-column>
<el-table-column label="总材积重" min-width="105"><template #default="{ row }">{{ formatInboundMeasure(row.volumeWeightKg, 'kg') }}</template></el-table-column>
<el-table-column label="箱规规则" min-width="170"><template #default="{ row }">{{ getCalculationRuleLabel(row.calculationRuleVersion) }}</template></el-table-column>
<el-table-column prop="boxSpecification" label="备注" min-width="130" />
<el-table class="measurement-detail-table fee-dialog-table" :data="[props.data.latestFee]" border>
<el-table-column label="计费毛重" min-width="180"><template #default="{ row }">{{ formatInboundMeasure(row.actualWeightKg, 'kg') }}</template></el-table-column>
<el-table-column label="计费体积" min-width="180"><template #default="{ row }">{{ formatInboundMeasure(row.actualVolumeCbm, 'm³', 6) }}</template></el-table-column>
<el-table-column label="计费方式" min-width="150"><template #default="{ row }">{{ getChargeModeLabel(row.chargeMode) }}</template></el-table-column>
<el-table-column label="最终计费量" min-width="180"><template #default="{ row }">{{ formatInboundMeasure(row.chargeableQuantity, row.chargeUnit === 'KG' ? 'kg' : 'm³', row.chargeUnit === 'KG' ? 3 : 6) }}</template></el-table-column>
</el-table>
<div class="fee-confirm-notice" :class="{ 'is-confirmed': props.data.latestFee.customerConfirmationStatus === 'CONFIRMED' }" role="status">
<CircleAlert :size="21" aria-hidden="true" />
<span v-if="props.data.latestFee.customerConfirmationStatus === 'CONFIRMED'">客户已于 {{ formatInboundDateTime(props.data.latestFee.customerConfirmedAt) }} 确认,费用已锁定,不允许修改。</span>
<span v-else>请核对计算重、单价和总金额。确认后当前费用版本将锁定。</span>
<span v-else>请核对计费毛重、计费体积、计费量、单价和总金额。确认后当前费用版本将锁定。</span>
</div>
</template>
<template #footer>
@@ -283,7 +277,7 @@ async function confirmFee() {
.fee-dialog-table { margin-top:16px; overflow:hidden; border-radius:10px; box-shadow:0 5px 18px rgba(51,65,85,.04); }
.fee-dialog-table :deep(.el-table__header th.el-table__cell) { height:42px; padding:9px 0; background:linear-gradient(180deg,#f8f9fc,#f3f5f9); color:#697386; font-size:13px; font-weight:650; }
.fee-dialog-table :deep(.el-table__body td.el-table__cell) { height:82px; padding:9px 0; color:#515a6c; font-size:14px; font-variant-numeric:tabular-nums; }
.fee-dialog-table :deep(.el-table__body td:nth-child(7)), .fee-dialog-table :deep(.el-table__body td:nth-child(8)) { color:#34466b; font-weight:650; }
.fee-dialog-table :deep(.el-table__body td:nth-child(2)), .fee-dialog-table :deep(.el-table__body td:nth-child(4)) { color:#34466b; font-weight:650; }
.fee-dialog-table :deep(.cell) { line-height:1.6; }
.fee-confirm-notice { display:flex; align-items:center; gap:10px; margin-top:14px; padding:10px 14px; border:1px solid #f5dfba; border-radius:9px; background:linear-gradient(90deg,#fff9ef,#fffaf2); color:#c6790c; font-size:14px; font-weight:500; }
.fee-confirm-notice > svg { flex:0 0 auto; fill:#eda332; color:#fff; filter:drop-shadow(0 2px 3px rgba(233,154,37,.25)); }