This commit is contained in:
2026-09-03 17:49:01 +08:00
parent afb534d4ee
commit 4ae5598c2d
10 changed files with 176 additions and 153 deletions
@@ -34,8 +34,8 @@ const config: CustomerDimensionConfig = {
rules: boxes.map(([boxType, boxTypeName, innerLengthCm, innerWidthCm, innerHeightCm, remark], index) => ({
id: String(100 + index),
boxType, boxTypeName, innerLengthCm, innerWidthCm, innerHeightCm, edgeAdditionCm: 2,
outerLengthCm: innerLengthCm + 2, outerWidthCm: innerWidthCm + 2, outerHeightCm: innerHeightCm + 2,
volumeCbm: Number(((innerLengthCm + 2) * (innerWidthCm + 2) * (innerHeightCm + 2) / 1_000_000).toFixed(3)),
outerLengthCm: innerLengthCm + 4, outerWidthCm: innerWidthCm + 4, outerHeightCm: innerHeightCm + 4,
volumeCbm: Number(((innerLengthCm + 4) * (innerWidthCm + 4) * (innerHeightCm + 4) / 1_000_000).toFixed(3)),
remark, version: 3, createdAt: '2026-08-07T10:00:00', updatedAt: '2026-08-07T10:20:00',
})),
}
@@ -71,11 +71,11 @@ describe('CustomerBillingRulesPanel', () => {
await flushPromises()
expect(getCustomerDimensionConfig).toHaveBeenCalledWith('8')
expect(document.body.textContent).toContain('按箱规尺寸计算')
expect(document.body.textContent).toContain('长加 X、宽加 X、高加 X = 外径尺寸')
expect(document.body.textContent).toContain('长、宽、高各加 2X = 外径尺寸')
expect(document.body.textContent).toContain('小箱')
expect(document.body.textContent).toContain('特大箱')
expect(document.body.textContent).toContain('32 × 22 × 22')
expect(document.body.textContent).toContain('0.015')
expect(document.body.textContent).toContain('34 × 24 × 24')
expect(document.body.textContent).toContain('0.020')
expect(document.body.textContent).toContain('标准中箱')
})
@@ -31,7 +31,7 @@ const loadError = ref<ApiErrorDetails | null>(null)
const editorVisible = ref(false)
const editorSaving = ref(false)
const editorDirty = ref(false)
const defaultCalculationMode = ref<'DIMENSION' | 'WEIGHT'>('DIMENSION')
const defaultCalculationMode = ref<'ACTUAL' | 'DIMENSION' | 'WEIGHT'>('ACTUAL')
const defaultModeSaving = ref(false)
const editorForm = ref<FormInstance>()
const form = reactive<CustomerDimensionConfigFormModel>({ rules: [] })
@@ -71,7 +71,7 @@ const BOX_NAMES: Record<CustomerDimensionBoxType, string> = {
const columns: DataTableColumn[] = [
{ key: 'boxTypeName', label: '箱规类型', minWidth: 110, slotName: 'boxTypeName' },
{ key: 'inner', label: '内径尺寸(长×宽×高 cm)', minWidth: 210, slotName: 'inner' },
{ key: 'edgeAdditionCm', label: '尺寸增加值(cm)', width: 150, slotName: 'edgeAdditionCm' },
{ key: 'edgeAdditionCm', label: '单边加宽(cm)', width: 150, slotName: 'edgeAdditionCm' },
{ key: 'outer', label: '外径尺寸(长×宽×高 cm)', minWidth: 210, slotName: 'outer' },
{ key: 'volumeCbm', label: '体积(CBM)', width: 130, slotName: 'volumeCbm' },
{ key: 'remark', label: '备注', minWidth: 140, prop: 'remark' },
@@ -91,7 +91,7 @@ watch(form, () => {
watch(() => props.customerId, () => {
editorVisible.value = false
defaultCalculationMode.value = 'DIMENSION'
defaultCalculationMode.value = 'ACTUAL'
void loadConfig()
})
@@ -107,7 +107,8 @@ async function loadConfig() {
try {
config.value = await getCustomerDimensionConfig(props.customerId)
config.value.rules = sortRules(config.value.rules)
defaultCalculationMode.value = config.value.defaultCalculationMode === 'MULTIPLY' ? 'WEIGHT' : 'DIMENSION'
defaultCalculationMode.value = config.value.defaultCalculationMode === 'MULTIPLY' ? 'WEIGHT'
: config.value.defaultCalculationMode === 'ADD' ? 'DIMENSION' : 'ACTUAL'
} catch (error) {
config.value = null
loadError.value = getApiErrorDetails(error, '箱规尺寸规则加载失败,请稍后重试')
@@ -168,7 +169,7 @@ function validateRules() {
return false
}
if (form.rules.some((rule) => !Number.isFinite(rule.edgeAdditionCm) || rule.edgeAdditionCm < 0.001 || rule.edgeAdditionCm > 100)) {
ElMessage.error('尺寸增加值必须在 0.001 至 100cm 之间')
ElMessage.error('单边加宽必须在 0.001 至 100cm 之间')
return false
}
if (form.rules.some((rule) => (rule.remark || '').length > 500)) {
@@ -207,19 +208,20 @@ function formatDimension(...values: number[]) {
return values.map((value) => Number(value.toFixed(2)).toString()).join(' × ')
}
async function selectDefaultCalculationMode(mode: 'DIMENSION' | 'WEIGHT') {
if (!canEdit.value || defaultModeSaving.value || mode === defaultCalculationMode.value) return
if (mode === 'WEIGHT' && !visibleChargeRules.value.some((rule) => rule.defaultRule)) {
async function selectDefaultCalculationMode(mode: 'DIMENSION' | 'WEIGHT', checked = true) {
const targetMode = checked ? mode : 'ACTUAL'
if (!canEdit.value || defaultModeSaving.value || targetMode === defaultCalculationMode.value) return
if (checked && mode === 'WEIGHT' && !visibleChargeRules.value.some((rule) => rule.defaultRule)) {
ElMessage.warning('请先保存并启用一套权重系数配置,再将其设为默认规则')
return
}
const previous = defaultCalculationMode.value
defaultCalculationMode.value = mode
defaultCalculationMode.value = targetMode
defaultModeSaving.value = true
try {
config.value = await updateCustomerDimensionDefaultCalculationMode(
props.customerId,
mode === 'WEIGHT' ? 'MULTIPLY' : 'ADD',
!checked ? 'ACTUAL' : mode === 'WEIGHT' ? 'MULTIPLY' : 'ADD',
)
config.value.rules = sortRules(config.value.rules)
ElMessage.success('默认计算规则已更新')
@@ -234,10 +236,11 @@ async function selectDefaultCalculationMode(mode: 'DIMENSION' | 'WEIGHT') {
async function saveChargeConfig() {
const coefficient = Number(chargeRows[0].coefficient)
if (!canChargeEdit.value || chargeRows.some((row) => !(Number(row.coefficient) > 0))) { ElMessage.warning('权重系数必须大于 0'); return }
if (chargeRows.some((row) => !(Number(row.weight) > 0))) { ElMessage.warning('每档重量必须大于 0'); return }
chargeSaving.value = true
try {
// A saved version becomes the active coefficient set immediately.
const created = await createCustomerChargeRule(props.customerId, { ...chargeForm, ruleCode: `RULE-${Date.now()}`, lengthFactor: coefficient, widthFactor: coefficient, heightFactor: coefficient, defaultRule: false, remark: chargeRows.map((row) => `${row.boxType}:${row.length},${row.width},${row.height};系数:${row.coefficient};${BOX_NAMES[row.boxType]}:${row.remark || '—'}`).join(';') })
const created = await createCustomerChargeRule(props.customerId, { ...chargeForm, ruleCode: `RULE-${Date.now()}`, lengthFactor: coefficient, widthFactor: coefficient, heightFactor: coefficient, defaultRule: false, remark: chargeRows.map((row) => `${row.boxType}:${row.length},${row.width},${row.height};系数:${row.coefficient};重量:${row.weight};${BOX_NAMES[row.boxType]}:${row.remark || '—'}`).join(';') })
await setCustomerChargeRuleDefault(props.customerId, created.id)
await loadChargeRules()
editorDirty.value = false
@@ -274,6 +277,8 @@ async function loadChargeRules() {
if (match) { row.length = Number(match[1]); row.width = Number(match[2]); row.height = Number(match[3]) }
const coefficient = remark.match(new RegExp(`${row.boxType}(?::[^;]*)?;系数:(\\d+(?:\\.\\d+)?)`))
row.coefficient = coefficient ? Number(coefficient[1]) : fallbackCoefficient
const weight = remark.match(new RegExp(`${row.boxType}(?::[^;]*)?;系数:[^;]+;重量:(\\d+(?:\\.\\d+)?)`))
if (weight) row.weight = Number(weight[1])
const note = remark.match(new RegExp(`${BOX_NAMES[row.boxType]}:([^;]+)`))
if (note) row.remark = note[1]
})
@@ -296,15 +301,15 @@ onMounted(() => { void loadConfig(); void loadChargeRules() })
<header class="rule-card-heading">
<div class="rule-title-row">
<h2>按箱规尺寸计算</h2>
<el-checkbox :model-value="defaultCalculationMode === 'DIMENSION'" :disabled="!canEdit || defaultModeSaving" @change="selectDefaultCalculationMode('DIMENSION')">设为默认规则</el-checkbox>
<el-checkbox :model-value="defaultCalculationMode === 'DIMENSION'" :disabled="!canEdit || defaultModeSaving" @change="selectDefaultCalculationMode('DIMENSION', Boolean($event))">设为默认规则</el-checkbox>
</div>
<el-button v-if="canEdit" type="primary" :disabled="loading" @click="openEditor"><Pencil :size="16" />编辑规则</el-button>
</header>
<div class="formula-band"><span>计算方式:<strong>长加 X、宽加 X、高加 X = 外径尺寸</strong>(X 为尺寸增加值,按实测尺寸匹配最小可容纳箱规)</span><span>体积公式:外径长 × 外径宽 × 外径高 ÷ 1,000,000 = CBM</span><span>超出当前箱规时自动匹配下一档箱规</span></div>
<div class="formula-band"><span>计算方式:<strong>长、宽、高各加 2X = 外径尺寸</strong>(X 为单边加宽,按实测尺寸匹配箱规)</span><span>体积公式:外径长 × 外径宽 × 外径高 ÷ 1,000,000 = CBM</span><span>低于最低档使用最低档,超出当前档自动匹配下一档,超出最大档使用最大档</span></div>
<DataTable :data="config.rules" :columns="columns" :loading="loading" row-key="boxType" empty-text="当前客户暂无箱规尺寸规则">
<template #boxTypeName="{ row }"><strong>{{ row.boxTypeName }}</strong></template>
<template #inner="{ row }">{{ formatDimension(row.innerLengthCm, row.innerWidthCm, row.innerHeightCm) }}</template>
<template #edgeAdditionCm="{ row }"><el-tag effect="light">+{{ row.edgeAdditionCm }}cm</el-tag></template>
<template #edgeAdditionCm="{ row }"><el-tag effect="light">单边 +{{ row.edgeAdditionCm }}cm</el-tag></template>
<template #outer="{ row }">{{ formatDimension(row.outerLengthCm, row.outerWidthCm, row.outerHeightCm) }}</template>
<template #volumeCbm="{ row }"><strong class="volume-value">{{ Number(row.volumeCbm).toFixed(3) }}</strong></template>
<template #empty><div class="rule-state"><Ruler :size="28" /><strong>当前客户暂无箱规尺寸规则</strong></div></template>
@@ -313,7 +318,7 @@ onMounted(() => { void loadConfig(); void loadChargeRules() })
<div v-else v-loading="loading" class="loading-state" />
<section v-if="canChargeView" class="rule-card charge-rule-card">
<header class="rule-card-heading"><div><div class="rule-title-row"><h2>箱规按权重系数计算</h2><el-checkbox :model-value="defaultCalculationMode === 'WEIGHT'" :disabled="!canEdit || defaultModeSaving" @change="selectDefaultCalculationMode('WEIGHT')">设为默认规则</el-checkbox></div><p class="section-hint">勾选的规则将作为该客户订单“外箱规数据”的默认计算方式,同一时间只能选择一个;权重系数用于调整不同箱型的计费体积:加权体积 = 体积 × 权重系数,体积重 = 加权体积 × 167 kg/CBM。</p></div></header>
<header class="rule-card-heading"><div><div class="rule-title-row"><h2>箱规按权重系数计算</h2><el-checkbox :model-value="defaultCalculationMode === 'WEIGHT'" :disabled="!canEdit || defaultModeSaving" @change="selectDefaultCalculationMode('WEIGHT', Boolean($event))">设为默认规则</el-checkbox></div><p class="section-hint">勾选后客户下单数据才会按该规则调整;未勾选时默认使用实测原尺寸(不调整)。</p></div></header>
<el-table v-loading="chargeLoading" :data="chargeDisplayRows" border stripe empty-text="暂无系数配置">
<el-table-column prop="boxTypeName" label="箱规类型" min-width="110" />
<el-table-column label="内径尺寸(长×宽×高 cm)" min-width="240"><template #default="{ row }">{{ row.dimension ? formatDimension(row.dimension.innerLengthCm, row.dimension.innerWidthCm, row.dimension.innerHeightCm) : '—' }}</template></el-table-column>
@@ -328,10 +333,10 @@ onMounted(() => { void loadConfig(); void loadChargeRules() })
<AppDialog v-model="editorVisible" title="编辑箱规尺寸 / 计算规则" width="min(1200px, calc(100vw - 28px))" :confirm-loading="editorSaving" :show-cancel="false" :before-close="beforeEditorClose" @confirm="saveConfig">
<div class="editor-content">
<p class="editor-formula">计算公式:按长、宽、高匹配最小可容纳箱规;长加 X、宽加 X、高加 X(X 为尺寸增加值),体积(CBM) = 外径长 × 外径宽 × 外径高 ÷ 1,000,000</p>
<p class="editor-formula">计算公式:按长、宽、高匹配箱规;长、宽、高各加 2X(X 为单边加宽),体积(CBM) = 外径长 × 外径宽 × 外径高 ÷ 1,000,000</p>
<el-form ref="editorForm" :model="form" label-position="top">
<div class="editor-table-wrap">
<table class="editor-table"><thead><tr><th>箱规类型</th><th>内径长(cm)</th><th>内径宽(cm)</th><th>内径高(cm)</th><th>尺寸增加值(cm)</th><th>备注</th></tr></thead><tbody>
<table class="editor-table"><thead><tr><th>箱规类型</th><th>内径长(cm)</th><th>内径宽(cm)</th><th>内径高(cm)</th><th>单边加宽(cm)</th><th>备注</th></tr></thead><tbody>
<tr v-for="row in form.rules" :key="row.boxType"><th scope="row">{{ row.boxTypeName }}</th><td><el-input-number v-model="row.innerLengthCm" :min="0.001" :precision="3" :controls="false" /></td><td><el-input-number v-model="row.innerWidthCm" :min="0.001" :precision="3" :controls="false" /></td><td><el-input-number v-model="row.innerHeightCm" :min="0.001" :precision="3" :controls="false" /></td><td><el-input-number v-model="row.edgeAdditionCm" :min="0.001" :max="100" :precision="3" :controls="false" /></td><td><el-input v-model="row.remark" maxlength="500" /></td></tr>
</tbody></table>
</div>