Release/test 合并上生产 #1

Merged
admin merged 236 commits from release/test into main 2026-09-17 11:51:50 +08:00
4 changed files with 113 additions and 2 deletions
Showing only changes of commit ffba7cde03 - Show all commits
@@ -0,0 +1,34 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { createPriceEstimateBreakdown } from './priceEstimate.ts'
test('explains KG estimates and selects volume weight when it is larger', () => {
assert.deepEqual(createPriceEstimateBreakdown(16, 0.6, 'KG'), {
actualLabel: '实际毛重',
actualQuantity: 16,
convertedLabel: '体积重量',
convertedQuantity: 100.2,
convertedFormula: '0.6 m³ × 167',
chargeableLabel: '计费重量',
basisLabel: '采用体积重量',
})
})
test('explains CBM estimates and selects the larger converted quantity', () => {
const result = createPriceEstimateBreakdown(400, 1, 'CBM')
assert.equal(result?.actualLabel, '实际体积')
assert.equal(result?.convertedLabel, '重量折算体积')
assert.equal(result?.convertedQuantity, 400 / 363)
assert.equal(result?.convertedFormula, '400 KG ÷ 363')
assert.equal(result?.chargeableLabel, '计费体积')
assert.equal(result?.basisLabel, '采用重量折算体积')
})
test('does not invent an explanation for an unsupported charge unit', () => {
assert.equal(createPriceEstimateBreakdown(16, 0.6, 'PALLET'), null)
})
test('keeps floating point artifacts out of the displayed formula', () => {
assert.equal(createPriceEstimateBreakdown(16, 0.1 + 0.2, 'KG')?.convertedFormula, '0.3 m³ × 167')
})
@@ -0,0 +1,54 @@
const KG_PER_CBM = 167
const KG_PER_BILLABLE_CBM = 363
function formatFormulaNumber(value: number) {
return value.toLocaleString('zh-CN', { maximumFractionDigits: 6, useGrouping: false })
}
export interface PriceEstimateBreakdown {
actualLabel: string
actualQuantity: number
convertedLabel: string
convertedQuantity: number
convertedFormula: string
chargeableLabel: string
basisLabel: string
}
/**
* Explains the server's current first-leg chargeable-quantity rules. The server
* response remains authoritative for the final chargeable quantity and amount.
*/
export function createPriceEstimateBreakdown(
actualWeightKg: number,
actualVolumeCbm: number,
chargeUnit: string,
): PriceEstimateBreakdown | null {
if (chargeUnit === 'KG') {
const volumeWeightKg = actualVolumeCbm * KG_PER_CBM
return {
actualLabel: '实际毛重',
actualQuantity: actualWeightKg,
convertedLabel: '体积重量',
convertedQuantity: volumeWeightKg,
convertedFormula: `${formatFormulaNumber(actualVolumeCbm)} m³ × ${KG_PER_CBM}`,
chargeableLabel: '计费重量',
basisLabel: actualWeightKg >= volumeWeightKg ? '采用实际毛重' : '采用体积重量',
}
}
if (chargeUnit === 'CBM') {
const convertedVolumeCbm = actualWeightKg / KG_PER_BILLABLE_CBM
return {
actualLabel: '实际体积',
actualQuantity: actualVolumeCbm,
convertedLabel: '重量折算体积',
convertedQuantity: convertedVolumeCbm,
convertedFormula: `${formatFormulaNumber(actualWeightKg)} KG ÷ ${KG_PER_BILLABLE_CBM}`,
chargeableLabel: '计费体积',
basisLabel: actualVolumeCbm >= convertedVolumeCbm ? '采用实际体积' : '采用重量折算体积',
}
}
return null
}
@@ -362,6 +362,10 @@ test('estimates by warehouse, destination, tax mode, weight and volume without b
assert.match(source, /v-for="mode in availablePricingTaxModes"/)
assert.match(source, /pricingTaxModeLabel\(mode\)/)
assert.match(source, /priceEstimate\.chargeableQuantity/)
assert.match(source, /priceEstimateBreakdown\.actualLabel/)
assert.match(source, /priceEstimateBreakdown\.convertedLabel/)
assert.match(source, /priceEstimateBreakdown\.basisLabel/)
assert.match(source, /两者取大/)
assert.match(source, /priceEstimate\.unitPrice/)
assert.match(source, /priceEstimate\.estimatedAmount/)
assert.match(source, /priceEstimateDisclaimer/)
@@ -41,6 +41,7 @@ import {
validateFirstLegOrderDraft,
} from '@/modules/first-leg/utils/firstLegOrder'
import { validateProductImageFile } from '@/modules/first-leg/utils/productImage'
import { createPriceEstimateBreakdown } from '@/modules/first-leg/utils/priceEstimate'
const store = useFirstLegOrdersStore()
const tenantStore = useTenantAdministrationStore()
@@ -754,6 +755,11 @@ function formatEstimateQuantity(value: number, unit: string) {
}
const priceEstimateDisclaimer = computed(() => priceEstimate.value?.disclaimer || '仅供参考,最终以实际结算为准')
const priceEstimateBreakdown = computed(() => {
if (!priceEstimate.value) return null
const summary = summarizeFirstLegCargo(form.items)
return createPriceEstimateBreakdown(summary.grossWeightKg, summary.volumeCbm, priceEstimate.value.chargeUnit)
})
function resetPriceEstimate() {
estimateRequestSequence += 1
@@ -1774,7 +1780,17 @@ async function searchOrders(model: QueryModel) {
<strong>{{ selectedPricingChannel?.channelName || priceEstimate.channelCode }}</strong>
<small>{{ pricingTaxModeLabel(priceEstimate.pricingTaxMode) }}<template v-if="selectedPricingChannel?.referenceTransit"> · 参考时效 {{ selectedPricingChannel.referenceTransit }}</template></small>
</div>
<div><span>计费量</span><strong>{{ formatEstimateQuantity(priceEstimate.chargeableQuantity, priceEstimate.chargeUnit) }}</strong></div>
<div v-if="priceEstimateBreakdown" class="price-estimate__comparison">
<span>计费对比</span>
<strong>{{ priceEstimateBreakdown.actualLabel }} {{ formatEstimateQuantity(priceEstimateBreakdown.actualQuantity, priceEstimate.chargeUnit) }}</strong>
<strong>{{ priceEstimateBreakdown.convertedLabel }} {{ formatEstimateQuantity(priceEstimateBreakdown.convertedQuantity, priceEstimate.chargeUnit) }}</strong>
<small>{{ priceEstimateBreakdown.convertedFormula }}</small>
</div>
<div class="price-estimate__chargeable">
<span>{{ priceEstimateBreakdown?.chargeableLabel || '计费量' }}</span>
<strong>{{ formatEstimateQuantity(priceEstimate.chargeableQuantity, priceEstimate.chargeUnit) }}</strong>
<small v-if="priceEstimateBreakdown">{{ priceEstimateBreakdown.basisLabel }}(两者取大)</small>
</div>
<div><span>基础单价</span><strong>{{ formatEstimateMoney(priceEstimate.unitPrice, priceEstimate.currency) }} / {{ priceEstimate.chargeUnit }}</strong></div>
<div class="price-estimate__amount"><span>参考运费</span><strong>{{ formatEstimateMoney(priceEstimate.estimatedAmount, priceEstimate.currency) }}</strong></div>
<small>{{ priceEstimateDisclaimer }}</small>
@@ -2051,10 +2067,13 @@ async function searchOrders(model: QueryModel) {
.price-estimate__state.is-error > div { display: flex; min-width: 0; flex: 1; flex-direction: column; gap: 4px; }
.price-estimate__state.is-error strong { color: #7a2e0e; font-size: 13px; }
.price-estimate__state.is-error span { overflow-wrap: anywhere; }
.price-estimate__result { display: grid; grid-template-columns: minmax(190px, 1.3fr) repeat(2, minmax(140px, .8fr)) minmax(190px, 1fr); align-items: stretch; }
.price-estimate__result { display: grid; grid-template-columns: minmax(180px, 1.15fr) minmax(220px, 1.25fr) minmax(170px, .9fr) minmax(140px, .8fr) minmax(190px, 1fr); align-items: stretch; }
.price-estimate__result > div { display: flex; min-width: 0; min-height: 96px; justify-content: center; flex-direction: column; gap: 7px; padding: 18px 20px; border-right: 1px solid #edf0f5; }
.price-estimate__result > div > span { color: #8b96a6; font-size: 11px; }
.price-estimate__result > div > strong { overflow-wrap: anywhere; color: #303b50; font-size: 15px; }
.price-estimate__comparison strong { font-size: 13px !important; font-weight: 600; }
.price-estimate__comparison small, .price-estimate__chargeable small { color: #7b8798; font-size: 10px; line-height: 1.4; }
.price-estimate__chargeable strong { color: #526fd8 !important; font-size: 18px !important; }
.price-estimate__channel small { color: #7b8798; font-size: 10px; line-height: 1.5; }
.price-estimate__result .price-estimate__amount { border-right: 0; background: #f8f8ff; }
.price-estimate__result .price-estimate__amount strong { color: #637fec; font-size: 25px; }