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>
+2 -1
View File
@@ -24,7 +24,8 @@ function normalizeRule(rule: RawRule): CustomerDimensionRule {
function normalizeConfig(config: RawConfig): CustomerDimensionConfig {
return {
tenantId: String(config.tenantId),
defaultCalculationMode: config.defaultCalculationMode === 'MULTIPLY' ? 'MULTIPLY' : 'ADD',
defaultCalculationMode: config.defaultCalculationMode === 'MULTIPLY' ? 'MULTIPLY'
: config.defaultCalculationMode === 'ACTUAL' ? 'ACTUAL' : 'ADD',
rules: config.rules.map(normalizeRule),
}
}
+1 -1
View File
@@ -1,5 +1,5 @@
export type CustomerDimensionBoxType = 'SMALL' | 'MEDIUM' | 'LARGE' | 'EXTRA_LARGE'
export type CustomerDimensionCalculationMode = 'ADD' | 'MULTIPLY'
export type CustomerDimensionCalculationMode = 'ACTUAL' | 'ADD' | 'MULTIPLY'
export interface CustomerDimensionRule {
id: string
@@ -43,6 +43,8 @@ const snapshot: InboundFeeSnapshot = {
rateEffectiveFrom: '2026-08-01T00:00:00', rateEffectiveTo: null,
calculatedAt: '2026-08-04T10:00:00', confirmedAt: '2026-08-04T10:01:00', sentAt: '2026-08-04T10:02:00',
nextProcessStatus: 'WAIT_SPLIT',
customerConfirmationStatus: 'PENDING_CONFIRMATION', customerConfirmedBy: null,
customerConfirmedByName: null, customerConfirmedAt: null,
}
enableAutoUnmount(afterEach)
+6
View File
@@ -8,6 +8,8 @@ export interface InboundFeeSnapshot {
feeStatus: InboundFeeStatus
inboundId: string
inboundCode: string | null
measurementId?: string | null
measurementSource?: string | null
chargeRuleId: string
ruleCode: string
chargeRuleVersion: number
@@ -44,6 +46,10 @@ export interface InboundFeeSnapshot {
confirmedAt: string | null
sentAt: string | null
nextProcessStatus: string | null
customerConfirmationStatus: 'PENDING_CONFIRMATION' | 'CONFIRMED'
customerConfirmedBy: string | null
customerConfirmedByName: string | null
customerConfirmedAt: string | null
}
export interface AdjustPublishedInboundFeeRequest {
@@ -10,7 +10,6 @@ import LoadErrorState from '@/shared/components/LoadErrorState.vue'
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
import { inboundProcessStatusLabels, inboundStatusLabel } from '../status'
import { createManualCartonScan, downloadInboundMeasurementImage, getInboundMeasurement, saveInboundMeasurementDraft, submitInboundMeasurement, updateConfirmedPackageCount } from './api'
import { listCustomerChargeRules, type CustomerChargeRule } from '@/modules/fba-pricing'
import type {
InboundMeasurementDetail,
InboundMeasurementItem,
@@ -21,7 +20,6 @@ const props = defineProps<{
customerId?: string
tenantId?: string
inboundId: string
openWarehouseSuggestion?: boolean
}>()
const emit = defineEmits<{
@@ -62,15 +60,10 @@ const loading = ref(false)
const saving = ref(false)
const submitting = ref(false)
const manualEntryOpen = ref(false)
const warehouseSuggestionOpen = ref(false)
const warehouseSuggestionAutoOpened = ref(false)
const warehouseSuggestion = reactive({ itemId: '', packageCount: 1, remark: '', coefficientMode: 'ADD' as 'ADD' | 'MULTIPLY' })
const manualEntrySubmitting = ref(false)
const manualEntryImage = ref<File | null>(null)
const manualEntryErrors = ref<string[]>([])
const detail = ref<InboundMeasurementDetail | null>(null)
const chargeRules = ref<CustomerChargeRule[]>([])
const chargeRulesLoading = ref(false)
const loadError = ref<ApiErrorDetails | null>(null)
const operationError = ref<ApiErrorDetails | null>(null)
const conflictDetected = ref(false)
@@ -105,7 +98,6 @@ const canView = computed(() => access.can('platform:firstleg:inbound:measure:vie
const canEdit = computed(() => access.can('platform:firstleg:inbound:measure:edit'))
const canManualEntry = computed(() => access.can('platform:firstleg:inbound:measure:manual-entry'))
const canSubmit = computed(() => access.can('platform:firstleg:inbound:measure:submit'))
const canWarehouseSuggestion = computed(() => access.can('platform:firstleg:inbound:warehouse-suggestion:edit'))
const customerId = computed(() => props.customerId || props.tenantId || '')
const hasContext = computed(() => Boolean(customerId.value && props.inboundId))
const countEditableProcess = computed(() => ['WAIT_MEASURE', 'MEASURING', 'MEASURE_PENDING', 'MEASURE_REJECTED'].includes(detail.value?.processStatus || ''))
@@ -123,9 +115,6 @@ const isDirty = computed(() => Boolean(detail.value) && snapshot.value !== basel
const registrationOpen = computed(() => hasContext.value && canView.value && Boolean(detail.value))
const requiredPermission = computed(() => canEdit.value ? 'platform:firstleg:inbound:measure:edit' : null)
const sourceLabel = computed(() => ({ THIRD_PARTY: '第三方设备', MANUAL: '平台人工', OPENAPI: '第三方设备' }[detail.value?.sourceType || ''] || (detail.value?.sourceType ? '未知来源' : '尚未测量')))
const suggestionItem = computed(() => form.items.find((item) => item.itemId === warehouseSuggestion.itemId) || null)
const suggestionTotalVolume = computed(() => Number(suggestionItem.value?.serverValues.unitVolumeCbm || 0) * warehouseSuggestion.packageCount)
const suggestionGirth = computed(() => suggestionItem.value ? suggestionItem.value.lengthCm + 2 * (suggestionItem.value.widthCm + suggestionItem.value.heightCm) : 0)
interface MeasurementDeviationWarning {
key: string
@@ -305,36 +294,6 @@ function editableItem(item: InboundMeasurementItem): EditableMeasurementItem {
}
}
function openWarehouseSuggestion() {
if (!canWarehouseSuggestion.value && !canEdit.value) return
const first = form.items[0]
warehouseSuggestion.itemId = first?.itemId || ''
warehouseSuggestion.packageCount = first?.packageCount || 1
warehouseSuggestion.remark = first?.remark || ''
warehouseSuggestion.coefficientMode = first?.coefficientMode === 'MULTIPLY' ? 'MULTIPLY' : 'ADD'
warehouseSuggestionOpen.value = true
}
function applyWarehouseSuggestion() {
const item = suggestionItem.value
if (!item) { ElMessage.warning('请选择当前实测 SKU'); return }
if (!Number.isInteger(warehouseSuggestion.packageCount) || warehouseSuggestion.packageCount < 1) { ElMessage.warning('箱数必须是大于 0 的整数'); return }
item.packageCount = warehouseSuggestion.packageCount
item.remark = warehouseSuggestion.remark.trim() || null
item.coefficientMode = warehouseSuggestion.coefficientMode
warehouseSuggestionOpen.value = false
ElMessage.success('仓库建议数据已加入,请保存测量数据')
}
async function loadChargeRules() {
if (!customerId.value || !access.can('firstleg:fba-pricing:rule:view')) return
chargeRulesLoading.value = true
try {
chargeRules.value = await listCustomerChargeRules(customerId.value)
} catch { chargeRules.value = [] }
finally { chargeRulesLoading.value = false }
}
function clearForm() {
form.measurementId = null
form.version = 0
@@ -366,11 +325,6 @@ async function loadMeasurement() {
const resource = await getInboundMeasurement(customerId.value, props.inboundId)
if (sequence !== requestSequence) return
applyDetail(resource)
void loadChargeRules()
if (props.openWarehouseSuggestion && !warehouseSuggestionAutoOpened.value && form.items.length) {
warehouseSuggestionAutoOpened.value = true
openWarehouseSuggestion()
}
} catch (error) {
if (sequence !== requestSequence) return
detail.value = null
@@ -603,17 +557,6 @@ function measurementRowClassName({ row }: { row: EditableMeasurementItem }) {
return validationErrors.value[row.clientKey]?.length ? 'measurement-table__row--error' : ''
}
function chargeRuleName(ruleId: string | null | undefined) {
if (!ruleId) return '默认规则'
const rule = chargeRules.value.find((item) => item.id === ruleId)
return rule ? chargeRuleLabel(rule) : ruleId
}
function chargeRuleLabel(rule: CustomerChargeRule) {
const remark = rule.remark?.trim()
return `系数 ${rule.lengthFactor}${remark ? ` · ${remark}` : ''}${rule.defaultRule ? '(默认)' : ''}`
}
watch(
() => [customerId.value, props.inboundId, canView.value] as const,
() => {
@@ -623,7 +566,6 @@ watch(
operationError.value = null
conflictDetected.value = false
validationErrors.value = {}
warehouseSuggestionAutoOpened.value = false
clearForm()
if (hasContext.value && canView.value) void loadMeasurement()
},
@@ -733,7 +675,6 @@ defineExpose({ saveDraft, submitForReview, updateActualWeights, mayMaintain, may
<div class="measurement-section__heading">
<div><h3>箱规明细</h3></div>
<div class="measurement-section-actions">
<el-button v-if="canWarehouseSuggestion" type="primary" plain :disabled="!mayMaintain || !form.items.length" @click="openWarehouseSuggestion"><Plus :size="15" />添加仓库建议数据</el-button>
<el-button v-if="canManualEntry" type="primary" plain :disabled="!mayCreateManualEntry" @click="openManualEntry"><Plus :size="15" />手动录入箱规</el-button>
</div>
</div>
@@ -789,15 +730,6 @@ defineExpose({ saveDraft, submitForReview, updateActualWeights, mayMaintain, may
/>
</template>
</el-table-column>
<el-table-column label="计算方式" min-width="150">
<template #default="{ row }">
<el-select v-model="row.coefficientMode" placeholder="选择计算方式">
<el-option label="实测原尺寸(不调整)" value="ACTUAL" />
<el-option label="按箱规尺寸策略" value="ADD" />
<el-option label="按箱规尺寸系数" value="MULTIPLY" :disabled="!chargeRules.some((rule) => rule.defaultRule && rule.status === 'ACTIVE')" />
</el-select>
</template>
</el-table-column>
<el-table-column label="总体积(m³)" width="108" align="right">
<template #default="{ row }">{{ formatNumber(row.serverValues.totalVolumeCbm, 4) }}</template>
</el-table-column>
@@ -812,18 +744,6 @@ defineExpose({ saveDraft, submitForReview, updateActualWeights, mayMaintain, may
</el-table-column>
</el-table>
<el-empty v-else description="暂无扫描箱测量数据" />
<div v-if="form.items.length" class="warehouse-suggestion-table">
<h3>仓库建议数据</h3>
<el-table :data="form.items" border stripe empty-text="暂无仓库建议数据">
<el-table-column prop="itemNo" label="当前实测 SKU" min-width="150" />
<el-table-column label="外箱规 长×宽×高(cm)" min-width="190"><template #default="{ row }">{{ formatNumber(row.outerLengthCm) }} × {{ formatNumber(row.outerWidthCm) }} × {{ formatNumber(row.outerHeightCm) }}</template></el-table-column>
<el-table-column prop="packageCount" label="箱数" width="90" />
<el-table-column label="周长(cm)" width="110"><template #default="{ row }">{{ formatNumber(row.serverValues.girthCm) }}</template></el-table-column>
<el-table-column label="总体积(m³)" width="120"><template #default="{ row }">{{ formatNumber(row.serverValues.unitVolumeCbm == null ? null : row.serverValues.unitVolumeCbm * row.packageCount, 6) }}</template></el-table-column>
<el-table-column label="计算策略" min-width="190"><template #default="{ row }">{{ row.coefficientMode === 'MULTIPLY' ? '按箱规尺寸系数' : '按箱规尺寸策略' }}<span v-if="row.coefficientMode === 'MULTIPLY'">({{ chargeRuleName(row.chargeRuleId) }})</span></template></el-table-column>
<el-table-column prop="remark" label="备注" min-width="160" show-overflow-tooltip />
</el-table>
</div>
</section>
</fieldset>
</form>
@@ -837,21 +757,6 @@ defineExpose({ saveDraft, submitForReview, updateActualWeights, mayMaintain, may
</div>
</footer>
<AppDialog v-model="warehouseSuggestionOpen" title="添加仓库建议数据" width="min(680px, calc(100vw - 28px))" confirm-text="加入建议" @confirm="applyWarehouseSuggestion">
<el-form label-position="top" class="warehouse-suggestion-form">
<el-form-item label="当前实测 SKU" required><el-select v-model="warehouseSuggestion.itemId" filterable><el-option v-for="item in form.items" :key="item.itemId" :label="`${item.itemNo} · ${item.boxSpecification || item.boxCode || '实测SKU'}`" :value="item.itemId" /></el-select></el-form-item>
<el-form-item label="箱数" required><el-input-number v-model="warehouseSuggestion.packageCount" :min="1" :max="1000000" step-strictly /></el-form-item>
<el-form-item label="计算方式"><el-select v-model="warehouseSuggestion.coefficientMode"><el-option label="按箱规尺寸策略" value="ADD" /><el-option label="按箱规尺寸系数" value="MULTIPLY" /></el-select></el-form-item>
<el-form-item label="备注"><el-input v-model="warehouseSuggestion.remark" type="textarea" :rows="3" maxlength="500" show-word-limit /></el-form-item>
</el-form>
<dl v-if="suggestionItem" class="suggestion-preview">
<div><dt>长 × 宽 × 高</dt><dd>{{ formatNumber(suggestionItem.lengthCm) }} × {{ formatNumber(suggestionItem.widthCm) }} × {{ formatNumber(suggestionItem.heightCm) }} cm</dd></div>
<div><dt>箱规周长</dt><dd>{{ formatNumber(suggestionGirth) }} cm</dd></div>
<div><dt>单箱体积</dt><dd>{{ formatNumber(suggestionItem.serverValues.unitVolumeCbm, 6) }} m³</dd></div>
<div><dt>总体积</dt><dd>{{ formatNumber(suggestionTotalVolume, 6) }} m³</dd></div>
</dl>
</AppDialog>
<el-dialog v-model="imagePreviewOpen" title="当前有效测量图片" width="min(720px, 92vw)" destroy-on-close @closed="releaseImagePreview">
<div class="measurement-image-preview">
<el-skeleton v-if="imagePreviewLoading" :rows="5" animated />
+6 -2
View File
@@ -34,8 +34,12 @@ export interface InboundMeasurementItem {
outerLengthCm: number | null
outerWidthCm: number | null
outerHeightCm: number | null
calculatedOuterLengthCm?: number | null
calculatedOuterWidthCm?: number | null
calculatedOuterHeightCm?: number | null
calculationRuleVersion?: string | null
chargeRuleId?: MeasurementIdentifier | null
coefficientMode?: 'ADD' | 'MULTIPLY' | null
coefficientMode?: 'ACTUAL' | 'ADD' | 'MULTIPLY' | null
remark: string | null
}
@@ -133,7 +137,7 @@ export interface SaveInboundMeasurementItemRequest {
outerWidthCm: number | null
outerHeightCm: number | null
chargeRuleId?: MeasurementIdentifier | null
coefficientMode?: 'ADD' | 'MULTIPLY' | null
coefficientMode?: 'ACTUAL' | 'ADD' | 'MULTIPLY' | null
remark: string | null
}
@@ -7,6 +7,12 @@ const source = readFileSync(resolve(process.cwd(), 'src/modules/unified-orders/p
const routeSource = readFileSync(resolve(process.cwd(), 'src/modules/unified-orders/routes.ts'), 'utf8')
describe('UnifiedOrdersView contract', () => {
it('blocks confirm-send when the customer has already confirmed the fee', () => {
expect(source).toContain("snapshot.customerConfirmationStatus === 'CONFIRMED'")
expect(source).toContain('客户已确认该费用,费用已锁定,不允许再修改或重新发送')
expect(source).toContain('async function openFeeConfirmDialog()')
})
it('only exposes the six product main statuses', () => {
expect(unifiedOrderStatusOptions.map((item) => item.label)).toEqual([
'待审核', '待测量数据', '待分柜', '装柜中', '装柜完成', '派送完成',
@@ -337,35 +343,61 @@ describe('UnifiedOrdersView contract', () => {
expect(source).toContain('getCustomerDimensionConfig')
expect(source).toContain('matchingDimensionRule')
expect(source).toContain('|| rules[rules.length - 1]')
expect(source).toContain('`原尺寸 + ${values[0]}cm`')
expect(source).toContain('`原尺寸 + ${Number(values[0]) * 2}cm(单边 +${values[0]}cm)`')
expect(source).toContain('`原尺寸 × ${rule.lengthFactor}`')
expect(source).toContain('label="箱规尺寸" value="ADD"')
expect(source).toContain('label="箱规系数" value="MULTIPLY"')
})
it('keeps the three section calculation modes independent and recalculates their display rows', () => {
it('keeps preview modes independent and persists both outer-carton selectors immediately', () => {
expect(source).toContain("measurementSectionModes = reactive<Record<MeasurementSection, MeasurementCalculationMode>>")
expect(source).toContain(':model-value="measurementSectionModes.customer"')
expect(source).toContain(':model-value="measurementSectionModes.outer"')
expect(source).toContain(':model-value="measurementSectionModes.actual"')
expect(source).toContain("updateMeasurementSectionMode('customer', $event)")
expect(source).toContain("updateMeasurementSectionMode('outer', $event)")
expect(source).toContain("updateMeasurementSectionMode('actual', $event)")
expect(source).not.toContain("updateMeasurementSectionMode('outer', $event)")
expect(source.match(/@change="selectAndSaveOuterMeasurementMode\(\$event\)"/g)).toHaveLength(2)
expect(source.match(/:disabled="Boolean\(measurementStrategySavingId\) \|\| !can\('platform:firstleg:inbound:measure:edit'\)"/g)).toHaveLength(2)
expect(source).toContain('const previousMode = measurementSectionModes.outer')
expect(source).toContain('if (!saved) measurementSectionModes.outer = previousMode')
expect(source).toContain('const saved = await updateMeasurementCalculationMode(null, mode)')
expect(source).toContain('calculatedMeasurementRow(row, measurementSectionModes.customer, true)')
expect(source).toContain('calculatedMeasurementRow(source, measurementSectionModes.outer)')
expect(source).toContain('calculatedMeasurementRow(row as unknown as Record<string, unknown>, measurementSectionModes.actual)')
expect(source).toContain("return Number(hasOuterOverride && override != null && override !== '' ? override : value)")
expect(source).toContain('rawDimensions.map((value) => value + rule.edgeAdditionCm)')
expect(source).toContain('rawDimensions.map((value) => value + rule.edgeAdditionCm * 2)')
expect(source).toContain('matchedSpecificationCoefficient(chargeRule, calculationSource)')
expect(source).toContain('listCustomerChargeRules(customerId, tenantId)')
expect(source).toContain('matchedDimensions.map((value) => roundedUp(value * specificationCoefficient, 3))')
expect(source).toContain("const selectedMode: 'ADD' | 'MULTIPLY' = measurementSectionModes.outer === 'MULTIPLY'")
expect(source).toContain('? selectedMode')
expect(source).toContain('const selectedMode: MeasurementCalculationMode = measurementSectionModes.outer')
expect(source).not.toContain('[chargeRule.lengthFactor, chargeRule.widthFactor, chargeRule.heightFactor]')
expect(source).not.toContain('column.key === \'coefficientMode\'')
expect(source).not.toContain('@change="updateMeasurementCalculationMode(null, $event)"')
})
it('confirms customer billing from the saved outer-carton totals', () => {
expect(source).toContain('const outerTotals = measurementTotals(outerMeasurementRows.value)')
expect(source).toContain("ElMessage.warning('外箱规数据不完整,请先保存外箱规尺寸和计算规则')")
expect(source).toContain("weightSource: 'OUTER'")
expect(source).toContain("volumeSource: 'OUTER'")
expect(source).toContain('selectAndSaveOuterMeasurementMode($event)')
expect(source).toContain('const saved = await updateMeasurementCalculationMode(null, mode)')
expect(source).toContain('coefficientMode: targetIds.has(String(item.id)) ? mode')
expect(source).not.toContain('feeConfirmVolume.value = Number(summary.measurement?.actualVolumeCbm)')
const sendFeeSource = source.slice(source.indexOf('async function executeConfirmAndSendFee()'), source.indexOf('function handleOrderSelectionChange'))
expect(sendFeeSource).not.toContain('saveInboundMeasurementDraft')
expect(sendFeeSource).not.toContain('updateMeasurementCalculationMode')
})
it('restores the saved outer-carton rule and server-calculated dimensions on re-entry', () => {
expect(source).toContain("mode is MeasurementCalculationMode => mode === 'ACTUAL' || mode === 'ADD' || mode === 'MULTIPLY'")
expect(source).toContain('measurementSectionModes.outer = savedModes.length === 1 ? savedModes[0]!')
expect(source).toContain('row.calculatedOuterLengthCm')
expect(source).toContain('measurementCalculationModeForRow(source) === measurementSectionModes.outer')
expect(source).toContain('lengthCm: Number(savedDimensions[0])')
})
it('binds dimension and weight evidence to their formal server records without mixing galleries', () => {
expect(source).toContain('<el-table :data="actualMeasurementRows" row-key="id"')
expect(source).toContain('<el-table-column label="箱号" prop="boxCode"')
@@ -292,7 +292,7 @@ function matchingDimensionRule(row: Record<string, unknown>): CustomerDimensionR
function addCoefficientLabel(row?: Record<string, unknown>) {
const rows = row ? [row] : (measurementDetail.value?.items || []) as unknown as Record<string, unknown>[]
const values = [...new Set(rows.map((item) => matchingDimensionRule(item)?.edgeAdditionCm).filter((value) => value != null))]
if (values.length === 1) return `原尺寸 + ${values[0]}cm`
if (values.length === 1) return `原尺寸 + ${Number(values[0]) * 2}cm(单边 +${values[0]}cm)`
if (measurementDimensionConfigLoading.value) return '原尺寸 + 加载中…'
return values.length > 1 ? '原尺寸 + 按箱规匹配' : '原尺寸 + 未匹配箱规'
}
@@ -305,18 +305,22 @@ function multiplyCoefficientLabel() {
function matchedSpecificationCoefficient(rule: CustomerChargeRule, row: Record<string, unknown>) {
const tiers = [...(rule.remark || '').matchAll(
/(SMALL|MEDIUM|LARGE|EXTRA_LARGE):(\d+(?:\.\d+)?),(\d+(?:\.\d+)?),(\d+(?:\.\d+)?);系数:(\d+(?:\.\d+)?)/g,
/(SMALL|MEDIUM|LARGE|EXTRA_LARGE):(\d+(?:\.\d+)?),(\d+(?:\.\d+)?),(\d+(?:\.\d+)?);系数:(\d+(?:\.\d+)?)(?:;重量:(\d+(?:\.\d+)?))?/g,
)].map((match) => ({
dimensions: [Number(match[2]), Number(match[3]), Number(match[4])].sort((left, right) => right - left),
coefficient: Number(match[5]),
maxWeightKg: match[6] == null ? null : Number(match[6]),
})).filter((tier) => tier.dimensions.every((value) => Number.isFinite(value) && value > 0)
&& Number.isFinite(tier.coefficient) && tier.coefficient > 0)
&& Number.isFinite(tier.coefficient) && tier.coefficient > 0
&& (tier.maxWeightKg == null || (Number.isFinite(tier.maxWeightKg) && tier.maxWeightKg > 0)))
.sort((left, right) => left.dimensions.reduce((total, value) => total * value, 1)
- right.dimensions.reduce((total, value) => total * value, 1))
if (!tiers.length) return Number(rule.lengthFactor || 1)
const measured = [Number(row.lengthCm), Number(row.widthCm), Number(row.heightCm)]
.sort((left, right) => right - left)
const matched = tiers.find((tier) => measured.every((value, index) => value <= tier.dimensions[index]))
const unitWeight = Number(row.unitWeightKg)
const matched = tiers.find((tier) => measured.every((value, index) => value <= tier.dimensions[index])
&& (tier.maxWeightKg == null || !Number.isFinite(unitWeight) || unitWeight <= tier.maxWeightKg))
return (matched || tiers[tiers.length - 1]).coefficient
}
@@ -351,7 +355,7 @@ function calculatedMeasurementRow(
if (!rawDimensions.every((value) => Number.isFinite(value) && value > 0)) return { ...source }
const matchedDimensions = mode === 'ADD' && rule
? rawDimensions.map((value) => value + rule.edgeAdditionCm)
? rawDimensions.map((value) => value + rule.edgeAdditionCm * 2)
: rawDimensions
const chargeRule = activeMeasurementChargeRule.value
const specificationCoefficient = mode === 'MULTIPLY' && chargeRule
@@ -364,9 +368,13 @@ function calculatedMeasurementRow(
? Math.round((lengthCm * widthCm * heightCm / 1_000_000) * 1000) / 1000
: roundedUp(lengthCm * widthCm * heightCm / 1_000_000, 4)
const totalVolumeCbm = Number.isFinite(packageCount) ? roundedUp(unitVolumeCbm * packageCount, 4) : null
const fallbackMeasuredUnitWeight = !customerSource && Number.isFinite(Number(measurementDetail.value?.actualGrossWeightKg))
&& Number(measurementDetail.value?.actualPackageCount) > 0
? Number(measurementDetail.value?.actualGrossWeightKg) / Number(measurementDetail.value?.actualPackageCount)
: null
const sourceUnitWeight = customerSource
? Number(source.grossWeightKg) / packageCount
: Number(source.unitWeightKg)
: Number.isFinite(Number(source.unitWeightKg)) ? Number(source.unitWeightKg) : fallbackMeasuredUnitWeight
const unitWeightKg = Number.isFinite(sourceUnitWeight) ? sourceUnitWeight : null
const totalWeightKg = unitWeightKg != null && Number.isFinite(packageCount) ? unitWeightKg * packageCount : null
return {
@@ -383,6 +391,20 @@ const customerMeasurementRows = computed(() => (detail.value?.order.cargoes || [
)))
const outerMeasurementRows = computed(() => (measurementDetail.value?.items || []).map((row) => {
const source = row as unknown as Record<string, unknown>
const savedDimensions = [row.calculatedOuterLengthCm, row.calculatedOuterWidthCm, row.calculatedOuterHeightCm]
if (savedDimensions.every((value) => Number.isFinite(Number(value)) && Number(value) > 0)
&& measurementCalculationModeForRow(source) === measurementSectionModes.outer) {
const packageCount = Number(row.packageCount)
const unitWeightKg = Number.isFinite(Number(row.unitWeightKg)) ? Number(row.unitWeightKg) : null
return {
...source,
lengthCm: Number(savedDimensions[0]),
widthCm: Number(savedDimensions[1]),
heightCm: Number(savedDimensions[2]),
unitWeightKg,
totalWeightKg: unitWeightKg != null && Number.isFinite(packageCount) ? unitWeightKg * packageCount : null,
}
}
return calculatedMeasurementRow(source, measurementSectionModes.outer)
}))
const actualMeasurementRows = computed(() => (measurementDetail.value?.items || []).map((row) => (
@@ -397,6 +419,18 @@ function updateMeasurementSectionMode(section: MeasurementSection, mode: Measure
measurementSectionModes[section] = mode
}
async function selectAndSaveOuterMeasurementMode(mode: MeasurementCalculationMode) {
if (measurementStrategySavingId.value) return
const previousMode = measurementSectionModes.outer
if (mode === 'MULTIPLY' && !activeMeasurementChargeRule.value) {
ElMessage.warning('该客户尚未启用系数配置,请先到客户计费规则中设置默认规则')
return
}
measurementSectionModes.outer = mode
const saved = await updateMeasurementCalculationMode(null, mode)
if (!saved) measurementSectionModes.outer = previousMode
}
const statusTabs = computed(() => [
{ value: '' as const, label: '全部' },
...unifiedOrderStatusOptions,
@@ -777,6 +811,10 @@ function measurementValue(row: Record<string, unknown>, key: string, customerSou
const count = Number(row.packageCount)
return Number.isFinite(total) && Number.isFinite(count) && count > 0 ? formatNumber(total / count, 4) : '—'
}
if (key === 'unitWeightKg') {
const value = Number(row[key])
return Number.isFinite(value) ? formatNumber(value, 4) : '—'
}
if (customerSource && key === 'unitVolumeCbm') {
const total = Number(row.volumeCbm)
const count = Number(row.packageCount)
@@ -804,10 +842,10 @@ function measurementCalculationModeForRow(row: Record<string, unknown>): Measure
return measuredItem?.coefficientMode === 'MULTIPLY' ? 'MULTIPLY' : measuredItem?.coefficientMode === 'ADD' ? 'ADD' : 'ACTUAL'
}
async function updateMeasurementCalculationMode(row: Record<string, unknown> | null, mode: Exclude<MeasurementCalculationMode, 'ACTUAL'>) {
async function updateMeasurementCalculationMode(row: Record<string, unknown> | null, mode: MeasurementCalculationMode): Promise<boolean> {
const summary = detail.value?.summary
const measurement = measurementDetail.value
if (!summary?.inboundId || !measurement?.measurementId || measurement.version == null || !can('platform:firstleg:inbound:measure:edit')) return
if (!summary?.inboundId || !measurement?.measurementId || measurement.version == null || !can('platform:firstleg:inbound:measure:edit')) return false
const itemId = normalizedIdentifier(row?.id)
const itemNo = normalizedIdentifier(row?.itemNo)
const targetIds = new Set(measurement.items
@@ -815,7 +853,7 @@ async function updateMeasurementCalculationMode(row: Record<string, unknown> | n
.map((item) => String(item.id)))
if (!targetIds.size) {
ElMessage.warning('当前数据尚未关联实测明细,暂不能设置计算方式')
return
return false
}
measurementStrategySavingId.value = row ? (itemId || `item-${itemNo}`) : 'all'
try {
@@ -826,7 +864,7 @@ async function updateMeasurementCalculationMode(row: Record<string, unknown> | n
) === 'MULTIPLY')
if (requiresMultiply && !activeRule) {
ElMessage.warning('该客户尚未启用系数配置,请先到客户计费规则中设置默认规则')
return
return false
}
await saveInboundMeasurementDraft(String(summary.tenantId), String(summary.inboundId), {
measurementId: String(measurement.measurementId),
@@ -852,17 +890,19 @@ async function updateMeasurementCalculationMode(row: Record<string, unknown> | n
chargeRuleId: (targetIds.has(String(item.id)) ? mode : item.coefficientMode) === 'MULTIPLY'
? activeRule!.id
: null,
coefficientMode: targetIds.has(String(item.id)) ? mode : item.coefficientMode || 'ADD',
coefficientMode: targetIds.has(String(item.id)) ? mode : item.coefficientMode || 'ACTUAL',
remark: item.remark,
})),
})
measurementLoadedKey.value = ''
await loadMeasurementDetails()
ElMessage.success(`${row ? '当前数据' : '全部测量数据'}的计算方式已更新,体积已重新计算`)
return true
} catch (error) {
measurementLoadedKey.value = ''
await loadMeasurementDetails()
ElMessage.error(getApiErrorDetails(error, '计算方式更新失败').message)
return false
} finally {
measurementStrategySavingId.value = ''
}
@@ -892,9 +932,7 @@ async function saveWarehouseOuterDimensions(row: Record<string, unknown>) {
ElMessage.warning('仓库修改外箱尺寸必须大于 0')
return
}
const selectedMode: 'ADD' | 'MULTIPLY' = measurementSectionModes.outer === 'MULTIPLY'
? 'MULTIPLY'
: 'ADD'
const selectedMode: MeasurementCalculationMode = measurementSectionModes.outer
const selectedChargeRuleId = selectedMode === 'MULTIPLY'
? activeMeasurementChargeRule.value?.id || null
: null
@@ -924,7 +962,7 @@ async function saveWarehouseOuterDimensions(row: Record<string, unknown>) {
chargeRuleId: String(item.id) === String(row.id) ? selectedChargeRuleId : item.chargeRuleId,
coefficientMode: String(item.id) === String(row.id)
? selectedMode
: item.coefficientMode || 'ADD',
: item.coefficientMode || 'ACTUAL',
remark: item.remark,
})),
})
@@ -1054,11 +1092,16 @@ async function loadMeasurementDetails() {
loadMeasurementDimensionConfig(),
])
if (serial !== measurementRequestSerial) return
const configuredMode = measurementDimensionConfig.value?.defaultCalculationMode
measurementSectionModes.outer = configuredMode === 'MULTIPLY' && activeMeasurementChargeRule.value
? 'MULTIPLY'
: 'ADD'
measurementDetail.value = response
const savedModes = [...new Set(response.items.map((item) => item.coefficientMode)
.filter((mode): mode is MeasurementCalculationMode => mode === 'ACTUAL' || mode === 'ADD' || mode === 'MULTIPLY'))]
const configuredMode = measurementDimensionConfig.value?.defaultCalculationMode
// 已保存的明细规则优先于客户默认配置,确保再次进入时回显上次选择及其计算结果。
measurementSectionModes.outer = savedModes.length === 1 ? savedModes[0]!
: configuredMode === 'MULTIPLY' && activeMeasurementChargeRule.value ? 'MULTIPLY'
: configuredMode === 'ADD' ? 'ADD' : 'ACTUAL'
measurementSectionModes.customer = measurementSectionModes.outer
measurementSectionModes.actual = 'ACTUAL'
measurementLoadedKey.value = key
} catch (error) {
if (serial !== measurementRequestSerial) return
@@ -1286,7 +1329,7 @@ function openMeasurement(row: UnifiedOrderResource) {
measurementDialogOpen.value = true
}
function openFeeConfirmDialog() {
async function openFeeConfirmDialog() {
const summary = detail.value?.summary
const reviewStatus = String(summary?.measurement?.reviewStatus ?? '').trim().toUpperCase()
if (reviewStatus !== 'APPROVED') {
@@ -1300,8 +1343,28 @@ function openFeeConfirmDialog() {
if (confirmFeeSending.value) {
return
}
feeConfirmWeight.value = Number(summary.measurement?.actualGrossWeightKg) || null
feeConfirmVolume.value = Number(summary.measurement?.actualVolumeCbm) || null
try {
const snapshot = await getLatestInboundFee(String(summary.tenantId), String(summary.inboundId))
if (snapshot.customerConfirmationStatus === 'CONFIRMED' || snapshot.customerConfirmedAt) {
ElMessage.warning('客户已确认该费用,费用已锁定,不允许再修改或重新发送')
return
}
} catch (error) {
ElMessage.error(getApiErrorDetails(error, '费用状态查询失败,请刷新后重试').message)
return
}
if (measurementStrategySavingId.value) {
ElMessage.warning('外箱规计算规则正在保存,请稍后再发送费用')
return
}
const outerTotals = measurementTotals(outerMeasurementRows.value)
if (!outerMeasurementRows.value.length || !(Number(outerTotals.gross) > 0)
|| !(Number(outerTotals.volume) > 0)) {
ElMessage.warning('外箱规数据不完整,请先保存外箱规尺寸和计算规则')
return
}
feeConfirmWeight.value = Number(outerTotals.gross)
feeConfirmVolume.value = Number(outerTotals.volume)
feeConfirmOpen.value = true
}
@@ -1319,12 +1382,17 @@ async function executeConfirmAndSendFee() {
const tenantId = String(summary.tenantId)
const inboundId = String(summary.inboundId)
let snapshot = await getLatestInboundFee(tenantId, inboundId)
if (snapshot.customerConfirmationStatus === 'CONFIRMED' || snapshot.customerConfirmedAt) {
ElMessage.warning('客户已确认该费用,费用已锁定,不允许再修改或重新发送')
feeConfirmOpen.value = false
return
}
if (snapshot.feeStatus === 'CALCULATED') {
snapshot = await calculateInboundFee(tenantId, inboundId, {
selectedWeightKg,
selectedVolumeCbm,
weightSource: 'ACTUAL',
volumeSource: 'ACTUAL',
weightSource: 'OUTER',
volumeSource: 'OUTER',
}, createFeeActionIdempotencyKey('calculate'))
snapshot = await confirmInboundFee(tenantId, inboundId, createFeeActionIdempotencyKey('confirm'))
}
@@ -1982,7 +2050,7 @@ onBeforeUnmount(() => {
</div>
</section>
<section class="measurement-section">
<div class="measurement-section-title"><button type="button" class="measurement-section-toggle" :aria-expanded="!measurementCollapsed.outer" @click="toggleMeasurementSection('outer')"><h3>二、外箱规数据</h3><span class="measurement-section-summary"><span>总件数:{{ measurementTotals(outerMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(outerMeasurementRows).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(outerMeasurementRows).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals(outerMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals(outerMeasurementRows).weightVolume, 6) }}</span></span><ChevronUp v-if="!measurementCollapsed.outer" :size="18" /><ChevronDown v-else :size="18" /></button><el-select class="measurement-section-mode" :model-value="measurementSectionModes.outer" aria-label="外箱规数据计算方式" @change="updateMeasurementSectionMode('outer', $event)"><el-option label="实测原尺寸(不调整)" value="ACTUAL" /><el-option label="箱规尺寸" value="ADD" /><el-option label="箱规系数" value="MULTIPLY" :disabled="multiplyModeDisabled" /></el-select></div>
<div class="measurement-section-title"><button type="button" class="measurement-section-toggle" :aria-expanded="!measurementCollapsed.outer" @click="toggleMeasurementSection('outer')"><h3>二、外箱规数据</h3><span class="measurement-section-summary"><span>总件数:{{ measurementTotals(outerMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(outerMeasurementRows).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(outerMeasurementRows).volume, 6) }}</span><span>总材积重:{{ formatNumber(measurementTotals(outerMeasurementRows).volumetric) }}</span><span>重量体积:{{ formatNumber(measurementTotals(outerMeasurementRows).weightVolume, 6) }}</span></span><ChevronUp v-if="!measurementCollapsed.outer" :size="18" /><ChevronDown v-else :size="18" /></button><el-select class="measurement-section-mode" :model-value="measurementSectionModes.outer" aria-label="外箱规数据计算方式" :loading="Boolean(measurementStrategySavingId)" :disabled="Boolean(measurementStrategySavingId) || !can('platform:firstleg:inbound:measure:edit')" @change="selectAndSaveOuterMeasurementMode($event)"><el-option label="实测原尺寸(不调整)" value="ACTUAL" /><el-option label="箱规尺寸" value="ADD" /><el-option label="箱规系数" value="MULTIPLY" :disabled="multiplyModeDisabled" /></el-select></div>
<div v-if="!measurementCollapsed.outer">
<div v-loading="measurementLoading" class="detail-table-wrap"><el-table :data="outerMeasurementRows" border stripe empty-text="暂无外箱规数据"><el-table-column label="#" prop="itemNo" min-width="70" /><el-table-column v-for="column in outerColumns.filter((item) => item.key !== 'itemNo')" :key="column.key" :label="column.label" :prop="column.key" min-width="150"><template #default="{ row }"><LineClampText v-if="column.key === 'remark'" :content="measurementValue(row as Record<string, unknown>, column.key)" /><template v-else>{{ measurementValue(row as Record<string, unknown>, column.key) }}</template></template></el-table-column></el-table></div>
<div class="measurement-total"><strong>合计</strong><span>箱数:{{ measurementTotals(outerMeasurementRows).count ?? '—' }}</span><span>总毛重:{{ formatNumber(measurementTotals(outerMeasurementRows).gross) }}</span><span>总体积:{{ formatNumber(measurementTotals(outerMeasurementRows).volume, 6) }}</span><span>总材积:{{ formatNumber(measurementTotals(outerMeasurementRows).volumetric) }}</span></div>
@@ -1990,7 +2058,7 @@ onBeforeUnmount(() => {
</section>
<div class="measurement-rule-selector">
<span class="measurement-rule-selector__label">箱规计算规则选择</span>
<el-select class="measurement-rule-selector__control" :model-value="measurementSectionModes.outer" aria-label="外箱规数据计算方式" @change="updateMeasurementSectionMode('outer', $event)">
<el-select class="measurement-rule-selector__control" :model-value="measurementSectionModes.outer" aria-label="外箱规数据计算方式" :loading="Boolean(measurementStrategySavingId)" :disabled="Boolean(measurementStrategySavingId) || !can('platform:firstleg:inbound:measure:edit')" @change="selectAndSaveOuterMeasurementMode($event)">
<el-option label="实测原尺寸(不调整)" value="ACTUAL" />
<el-option label="箱规尺寸" value="ADD" />
<el-option label="箱规系数" value="MULTIPLY" :disabled="multiplyModeDisabled" />