459 lines
30 KiB
Vue
459 lines
30 KiB
Vue
<script setup lang="ts">
|
||
import InlineHelp from '@/shared/components/InlineHelp.vue'
|
||
import '@/styles/adminDensity.css'
|
||
import '@/modules/customers/customerDensity.css'
|
||
import { computed, nextTick, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||
import { Ruler } from '@lucide/vue'
|
||
import { ElMessage, ElMessageBox, type FormInstance } from 'element-plus'
|
||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
||
import {
|
||
getCustomerDimensionConfig,
|
||
updateCustomerDimensionConfig,
|
||
updateCustomerDimensionDefaultCalculationMode,
|
||
type CustomerDimensionBoxType,
|
||
type CustomerDimensionConfig,
|
||
type CustomerDimensionConfigFormModel,
|
||
type CustomerDimensionRule,
|
||
} from '@/modules/dimension-config'
|
||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||
import AppDialog from '@/shared/components/AppDialog.vue'
|
||
import { customerDialogTitle } from '../customerDialogTitle'
|
||
import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
|
||
import { createCustomerChargeRule, listCustomerChargeRules, setCustomerChargeRuleDefault, type CustomerChargeRule, type CreateCustomerChargeRuleRequest } from '@/modules/fba-pricing'
|
||
import { getPlatformTenantDimensionInitializationStatus } from '@/modules/iam'
|
||
|
||
const props = defineProps<{
|
||
customerId: string
|
||
customerName: string
|
||
customerCode?: string
|
||
dimensionInitializationStatus?: string
|
||
}>()
|
||
const access = useAccessPreviewStore()
|
||
const canView = computed(() => access.can('firstleg:dimension-config:view'))
|
||
const canEdit = computed(() => canView.value && access.can('firstleg:dimension-config:edit'))
|
||
const canChargeView = computed(() => access.can('firstleg:fba-pricing:rule:view'))
|
||
const canChargeEdit = computed(() => canChargeView.value && access.can('firstleg:fba-pricing:rule:edit'))
|
||
const observedInitializationStatus = ref(props.dimensionInitializationStatus?.trim().toUpperCase() || 'READY')
|
||
const initializationStatus = computed(() => observedInitializationStatus.value)
|
||
const initializationPending = computed(() => ['PENDING', 'SENDING'].includes(initializationStatus.value))
|
||
const initializationFailed = computed(() => initializationStatus.value === 'FAILED')
|
||
const initializationReady = computed(() => !initializationPending.value && !initializationFailed.value)
|
||
const config = ref<CustomerDimensionConfig | null>(null)
|
||
const loading = ref(false)
|
||
const loadError = ref<ApiErrorDetails | null>(null)
|
||
const editorVisible = ref(false)
|
||
const editorSaving = ref(false)
|
||
const editorDirty = ref(false)
|
||
const defaultCalculationMode = ref<'ACTUAL' | 'DIMENSION' | 'WEIGHT'>('ACTUAL')
|
||
const defaultModeSaving = ref(false)
|
||
const editorForm = ref<FormInstance>()
|
||
let initializationPollTimer: ReturnType<typeof setTimeout> | null = null
|
||
let initializationPollGeneration = 0
|
||
const form = reactive<CustomerDimensionConfigFormModel>({ rules: [] })
|
||
const chargeRules = ref<CustomerChargeRule[]>([])
|
||
const visibleChargeRules = computed(() => chargeRules.value.filter((rule) => rule.status === 'ACTIVE'))
|
||
const chargeDisplayRows = computed(() => {
|
||
const rule = visibleChargeRules.value.find((item) => item.defaultRule) || visibleChargeRules.value[0]
|
||
if (!rule) return []
|
||
return BOX_ORDER.map((boxType) => ({
|
||
...rule,
|
||
boxType,
|
||
boxTypeName: BOX_NAMES[boxType],
|
||
dimension: chargeDimension(boxType),
|
||
coefficient: Number(chargeRows.find((item) => item.boxType === boxType)?.coefficient || rule.lengthFactor || 1),
|
||
weight: chargeRows.find((item) => item.boxType === boxType)?.weight || 0,
|
||
remark: chargeRows.find((item) => item.boxType === boxType)?.remark || '',
|
||
}))
|
||
})
|
||
const chargeLoading = ref(false)
|
||
const chargeSaving = ref(false)
|
||
const chargeForm = reactive<CreateCustomerChargeRuleRequest>({
|
||
ruleCode: '', customerId: props.customerId, customerName: props.customerName, businessType: 'FBA', chargeMode: 'KG', taxMode: 'TAX_INCLUDED', conversionFactor: 167,
|
||
lengthFactor: 1, widthFactor: 1, heightFactor: 1, defaultRule: false, roundingScale: 2, roundingMode: 'HALF_UP', currency: 'CNY', effectiveFrom: new Date().toISOString().slice(0, 19), effectiveTo: null, remark: null,
|
||
})
|
||
const chargeRows = reactive([
|
||
{ boxType: 'SMALL' as CustomerDimensionBoxType, length: 30, width: 20, height: 20, coefficient: 1.1, weight: 5, coefficientType: '体积系数', remark: '标准小箱' },
|
||
{ boxType: 'MEDIUM' as CustomerDimensionBoxType, length: 40, width: 30, height: 30, coefficient: 1.1, weight: 8, coefficientType: '体积系数', remark: '标准中箱' },
|
||
{ boxType: 'LARGE' as CustomerDimensionBoxType, length: 50, width: 40, height: 40, coefficient: 1.1, weight: 12, coefficientType: '体积系数', remark: '标准大箱' },
|
||
{ boxType: 'EXTRA_LARGE' as CustomerDimensionBoxType, length: 60, width: 50, height: 50, coefficient: 1.1, weight: 15, coefficientType: '体积系数', remark: '特大箱' },
|
||
])
|
||
|
||
const BOX_ORDER: readonly CustomerDimensionBoxType[] = ['SMALL', 'MEDIUM', 'LARGE', 'EXTRA_LARGE']
|
||
const BOX_NAMES: Record<CustomerDimensionBoxType, string> = {
|
||
SMALL: '小箱', MEDIUM: '中箱', LARGE: '大箱', EXTRA_LARGE: '特大箱',
|
||
}
|
||
|
||
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: 'outer', label: '外径尺寸(长×宽×高 cm)', minWidth: 210, slotName: 'outer' },
|
||
{ key: 'volumeCbm', label: '体积(CBM)', width: 130, slotName: 'volumeCbm' },
|
||
{ key: 'remark', label: '备注', minWidth: 140, prop: 'remark' },
|
||
]
|
||
|
||
const { captureBaseline } = useUnsavedWorkRegistration({
|
||
key: `customer-dimension-config-${props.customerId}`,
|
||
label: computed(() => `${props.customerName}的箱规尺寸规则`),
|
||
isOpen: editorVisible,
|
||
model: form,
|
||
requiredPermission: computed(() => 'firstleg:dimension-config:edit'),
|
||
})
|
||
|
||
watch(form, () => {
|
||
if (editorVisible.value) editorDirty.value = true
|
||
}, { deep: true })
|
||
|
||
watch(() => props.customerId, () => {
|
||
stopInitializationPolling()
|
||
observedInitializationStatus.value = props.dimensionInitializationStatus?.trim().toUpperCase() || 'READY'
|
||
editorVisible.value = false
|
||
defaultCalculationMode.value = 'ACTUAL'
|
||
if (initializationReady.value) {
|
||
void loadConfig()
|
||
void loadChargeRules()
|
||
} else if (initializationPending.value) {
|
||
startInitializationPolling()
|
||
}
|
||
})
|
||
|
||
watch(() => props.dimensionInitializationStatus, (status) => {
|
||
observedInitializationStatus.value = status?.trim().toUpperCase() || 'READY'
|
||
if (initializationPending.value) startInitializationPolling()
|
||
else stopInitializationPolling()
|
||
})
|
||
|
||
watch(initializationReady, (ready, previous) => {
|
||
if (ready && !previous) {
|
||
void loadConfig()
|
||
void loadChargeRules()
|
||
}
|
||
})
|
||
|
||
function stopInitializationPolling() {
|
||
initializationPollGeneration += 1
|
||
if (initializationPollTimer) clearTimeout(initializationPollTimer)
|
||
initializationPollTimer = null
|
||
}
|
||
|
||
function startInitializationPolling() {
|
||
if (initializationPollTimer) return
|
||
const generation = ++initializationPollGeneration
|
||
const tenantId = props.customerId
|
||
const poll = async () => {
|
||
initializationPollTimer = null
|
||
try {
|
||
const result = await getPlatformTenantDimensionInitializationStatus(tenantId)
|
||
if (generation !== initializationPollGeneration || tenantId !== props.customerId) return
|
||
observedInitializationStatus.value = result.status?.trim().toUpperCase() || 'PENDING'
|
||
} catch {
|
||
// Initialization is retried by IAM. A transient diagnostic failure must not trigger the dimension API early.
|
||
}
|
||
if (generation === initializationPollGeneration && tenantId === props.customerId && initializationPending.value) {
|
||
initializationPollTimer = setTimeout(poll, 2_000)
|
||
}
|
||
}
|
||
void poll()
|
||
}
|
||
|
||
function sortRules(rules: CustomerDimensionRule[]) {
|
||
return [...rules].sort((a, b) => BOX_ORDER.indexOf(a.boxType) - BOX_ORDER.indexOf(b.boxType))
|
||
.map((rule) => ({ ...rule, boxTypeName: BOX_NAMES[rule.boxType] || rule.boxTypeName }))
|
||
}
|
||
|
||
async function loadConfig() {
|
||
if (!canView.value || !initializationReady.value) return
|
||
loading.value = true
|
||
loadError.value = null
|
||
try {
|
||
config.value = await getCustomerDimensionConfig(props.customerId)
|
||
config.value.rules = sortRules(config.value.rules)
|
||
defaultCalculationMode.value = config.value.defaultCalculationMode === 'MULTIPLY' ? 'WEIGHT'
|
||
: config.value.defaultCalculationMode === 'ADD' ? 'DIMENSION' : 'ACTUAL'
|
||
} catch (error) {
|
||
config.value = null
|
||
loadError.value = getApiErrorDetails(error, '箱规尺寸规则加载失败,请稍后重试')
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
function cloneRules(rules: CustomerDimensionRule[]) {
|
||
return sortRules(rules).map((rule) => ({ ...rule }))
|
||
}
|
||
|
||
async function openEditor() {
|
||
if (!config.value) return
|
||
Object.assign(chargeForm, { ruleCode: `RULE-${Date.now()}`, customerId: props.customerId, customerName: props.customerName, businessType: 'FBA', chargeMode: 'CBM', taxMode: 'TAX_INCLUDED', conversionFactor: 167, lengthFactor: 1, widthFactor: 1, heightFactor: 1, defaultRule: false, roundingScale: 2, roundingMode: 'HALF_UP', currency: 'CNY', effectiveFrom: new Date().toISOString().slice(0, 19), effectiveTo: null, remark: null })
|
||
form.rules = cloneRules(config.value.rules)
|
||
await nextTick()
|
||
editorDirty.value = false
|
||
editorVisible.value = true
|
||
await nextTick()
|
||
captureBaseline()
|
||
editorForm.value?.clearValidate()
|
||
}
|
||
|
||
async function confirmAbandon(): Promise<boolean> {
|
||
try {
|
||
await ElMessageBox.confirm('箱规尺寸尚未保存,确定放弃修改吗?', customerDialogTitle('放弃编辑', { tenantCode: props.customerCode, tenantName: props.customerName }), {
|
||
customClass: 'admin-density-confirm', confirmButtonText: '放弃', cancelButtonText: '继续编辑', type: 'warning',
|
||
})
|
||
return true
|
||
} catch {
|
||
return false
|
||
}
|
||
}
|
||
|
||
async function beforeEditorClose(done: () => void) {
|
||
if (editorSaving.value || chargeSaving.value) return
|
||
if (!editorDirty.value || await confirmAbandon()) done()
|
||
}
|
||
|
||
async function closeEditor() {
|
||
if (editorSaving.value || chargeSaving.value) return
|
||
if (editorDirty.value && !await confirmAbandon()) return
|
||
editorVisible.value = false
|
||
}
|
||
|
||
function validateRules() {
|
||
if (form.rules.length !== BOX_ORDER.length || form.rules.some((rule) => !BOX_ORDER.includes(rule.boxType))) {
|
||
ElMessage.error('必须完整保存小箱、中箱、大箱、特大箱四条规则')
|
||
return false
|
||
}
|
||
if (new Set(form.rules.map((rule) => rule.boxType)).size !== BOX_ORDER.length) {
|
||
ElMessage.error('箱规类型不能重复')
|
||
return false
|
||
}
|
||
if (form.rules.some((rule) => ![rule.innerLengthCm, rule.innerWidthCm, rule.innerHeightCm].every((value) => Number.isFinite(value) && value > 0))) {
|
||
ElMessage.error('内径长、宽、高必须大于 0')
|
||
return false
|
||
}
|
||
if (form.rules.some((rule) => !Number.isFinite(rule.edgeAdditionCm) || rule.edgeAdditionCm < 0.001 || rule.edgeAdditionCm > 100)) {
|
||
ElMessage.error('单边加宽必须在 0.001 至 100cm 之间')
|
||
return false
|
||
}
|
||
if (form.rules.some((rule) => (rule.remark || '').length > 500)) {
|
||
ElMessage.error('备注不能超过 500 个字符')
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
|
||
async function saveConfig() {
|
||
if (!config.value || !validateRules()) return
|
||
editorSaving.value = true
|
||
try {
|
||
const rules = form.rules.map((rule) => ({
|
||
boxType: rule.boxType,
|
||
innerLengthCm: rule.innerLengthCm,
|
||
innerWidthCm: rule.innerWidthCm,
|
||
innerHeightCm: rule.innerHeightCm,
|
||
edgeAdditionCm: rule.edgeAdditionCm,
|
||
remark: rule.remark,
|
||
version: rule.version,
|
||
}))
|
||
config.value = await updateCustomerDimensionConfig(props.customerId, { rules })
|
||
config.value.rules = sortRules(config.value.rules)
|
||
editorDirty.value = false
|
||
editorVisible.value = false
|
||
ElMessage.success('箱规尺寸规则已保存')
|
||
} catch (error) {
|
||
ElMessage.error(getApiErrorDetails(error, '箱规尺寸规则保存失败,请刷新后重试').message)
|
||
} finally {
|
||
editorSaving.value = false
|
||
}
|
||
}
|
||
|
||
function formatDimension(...values: number[]) {
|
||
return values.map((value) => Number(value.toFixed(2)).toString()).join(' × ')
|
||
}
|
||
|
||
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 = targetMode
|
||
defaultModeSaving.value = true
|
||
try {
|
||
config.value = await updateCustomerDimensionDefaultCalculationMode(
|
||
props.customerId,
|
||
!checked ? 'ACTUAL' : mode === 'WEIGHT' ? 'MULTIPLY' : 'ADD',
|
||
)
|
||
config.value.rules = sortRules(config.value.rules)
|
||
ElMessage.success('默认计算规则已更新')
|
||
} catch (error) {
|
||
defaultCalculationMode.value = previous
|
||
ElMessage.error(getApiErrorDetails(error, '默认计算规则保存失败').message)
|
||
} finally {
|
||
defaultModeSaving.value = false
|
||
}
|
||
}
|
||
|
||
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};重量:${row.weight};${BOX_NAMES[row.boxType]}:${row.remark || '—'}`).join(';') })
|
||
await setCustomerChargeRuleDefault(props.customerId, created.id)
|
||
await loadChargeRules()
|
||
editorDirty.value = false
|
||
editorVisible.value = false
|
||
ElMessage.success('权重系数规则已保存')
|
||
} catch (error) { ElMessage.error(getApiErrorDetails(error, '权重系数规则保存失败').message) }
|
||
finally { chargeSaving.value = false }
|
||
}
|
||
|
||
function dimensionRule(boxType: CustomerDimensionBoxType) { return config.value?.rules.find((rule) => rule.boxType === boxType) }
|
||
function chargeDimension(boxType: CustomerDimensionBoxType) {
|
||
const row = chargeRows.find((item) => item.boxType === boxType)
|
||
if (!row) return null
|
||
return { innerLengthCm: row.length, innerWidthCm: row.width, innerHeightCm: row.height, volumeCbm: (row.length * row.width * row.height) / 1_000_000 }
|
||
}
|
||
function dimensionText(boxType: CustomerDimensionBoxType) { const rule = dimensionRule(boxType); return rule ? formatDimension(rule.innerLengthCm, rule.innerWidthCm, rule.innerHeightCm) : '—' }
|
||
function volumeText(boxType: CustomerDimensionBoxType) { const rule = dimensionRule(boxType); return rule ? Number(rule.volumeCbm).toFixed(3) : '—' }
|
||
function weightedVolumeText(boxType: CustomerDimensionBoxType, coefficient: number) { const rule = dimensionRule(boxType); return rule ? (Number(rule.volumeCbm) * Number(coefficient || 1)).toFixed(3) : '—' }
|
||
function weightText(boxType: CustomerDimensionBoxType) { return chargeRows.find((row) => row.boxType === boxType)?.weight.toFixed(2) || '—' }
|
||
|
||
function formatDate(value: string | null) {
|
||
return value ? value.replace('T', ' ').slice(0, 16) : '暂无记录'
|
||
}
|
||
|
||
async function loadChargeRules() {
|
||
if (!canChargeView.value || !initializationReady.value) return
|
||
chargeLoading.value = true
|
||
try {
|
||
chargeRules.value = await listCustomerChargeRules(props.customerId)
|
||
const remark = chargeRules.value.find((item) => item.defaultRule)?.remark || ''
|
||
const fallbackCoefficient = Number(chargeRules.value.find((item) => item.defaultRule)?.lengthFactor || 1)
|
||
chargeRows.forEach((row) => {
|
||
const match = remark.match(new RegExp(`${row.boxType}:(\\d+(?:\\.\\d+)?),(\\d+(?:\\.\\d+)?),(\\d+(?:\\.\\d+)?)`))
|
||
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]
|
||
})
|
||
}
|
||
catch (error) { ElMessage.error(getApiErrorDetails(error, '客户计费规则加载失败').message) }
|
||
finally { chargeLoading.value = false }
|
||
}
|
||
|
||
onMounted(() => {
|
||
if (initializationPending.value) {
|
||
startInitializationPolling()
|
||
return
|
||
}
|
||
if (!initializationReady.value) return
|
||
void loadConfig()
|
||
void loadChargeRules()
|
||
})
|
||
|
||
onBeforeUnmount(stopInitializationPolling)
|
||
</script>
|
||
|
||
<template>
|
||
<div class="dimension-rule-panel admin-density customer-density">
|
||
<div v-if="initializationPending" class="rule-state initialization-state" role="status">
|
||
<Ruler :size="30" />
|
||
<strong>客户计费规则正在初始化</strong>
|
||
<p>默认箱规和权重系数正在后台准备,完成后本页会自动加载。</p>
|
||
</div>
|
||
<div v-else-if="initializationFailed" class="rule-error initialization-state" role="alert">
|
||
<div><strong>客户计费规则初始化失败</strong><p>请在客户管理中重试初始化,成功后再查看计费规则。</p></div>
|
||
</div>
|
||
<div v-if="initializationReady && (!canView || loadError)" class="panel-title rule-heading">
|
||
<div><h2>按箱规尺寸计算</h2><p>{{ customerName }}{{ customerCode ? `(${customerCode})` : '' }}的尺寸计算规则</p></div>
|
||
</div>
|
||
<div v-if="initializationReady && !canView" class="rule-state"><Ruler :size="30" /><strong>暂无尺寸计算规则查看权限</strong><p>请联系管理员申请尺寸计算规则查看权限。</p></div>
|
||
<div v-else-if="initializationReady && loadError" class="rule-error" role="alert"><div><strong>箱规尺寸规则加载失败</strong><p>{{ loadError.message }}</p><ApiErrorMeta :details="loadError" /></div><el-button type="primary" plain :loading="loading" @click="loadConfig">重新加载</el-button></div>
|
||
<section v-else-if="initializationReady && config" class="rule-card">
|
||
<header class="rule-card-heading">
|
||
<div class="rule-title-row">
|
||
<h2>按箱规尺寸计算</h2>
|
||
<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">编辑规则</el-button>
|
||
</header>
|
||
<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 #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>
|
||
</DataTable>
|
||
</section>
|
||
<div v-else-if="initializationReady" v-loading="loading" class="loading-state" />
|
||
|
||
<section v-if="initializationReady && canChargeView" class="rule-card charge-rule-card">
|
||
<header class="rule-card-heading"><div><div class="rule-title-row"><h2>箱规按权重系数计算<InlineHelp title="默认规则说明"><p>勾选后客户下单数据才会按该规则调整;未勾选时默认使用实测原尺寸(不调整)。</p></InlineHelp></h2><el-checkbox :model-value="defaultCalculationMode === 'WEIGHT'" :disabled="!canEdit || defaultModeSaving" @change="selectDefaultCalculationMode('WEIGHT', Boolean($event))">设为默认规则</el-checkbox></div></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>
|
||
<el-table-column label="权重系数" min-width="110"><template #default="{ row }"><strong>{{ row.coefficient }}</strong></template></el-table-column>
|
||
<el-table-column label="体积(CBM)" min-width="120"><template #default="{ row }">{{ row.dimension ? Number(row.dimension.volumeCbm).toFixed(3) : '—' }}</template></el-table-column>
|
||
<el-table-column label="加权体积(CBM)" min-width="145"><template #default="{ row }">{{ row.dimension ? (Number(row.dimension.volumeCbm) * row.coefficient).toFixed(3) : '—' }}</template></el-table-column>
|
||
<el-table-column label="重量(kg)" min-width="105"><template #default="{ row }">{{ row.weight ? row.weight.toFixed(2) : '—' }}</template></el-table-column>
|
||
<el-table-column label="体积重(kg)" min-width="115"><template #default="{ row }">{{ row.dimension ? (Number(row.dimension.volumeCbm) * row.coefficient * 167).toFixed(2) : '—' }}</template></el-table-column>
|
||
<el-table-column prop="remark" label="备注" min-width="180" align="left" header-align="left" class-name="charge-remark-column" label-class-name="charge-remark-column"><template #default="{ row }">{{ row.remark || '—' }}</template></el-table-column>
|
||
</el-table>
|
||
</section>
|
||
|
||
<AppDialog class="admin-density-dialog customer-density-dialog" v-model="editorVisible" :title="customerDialogTitle('编辑箱规尺寸 / 计算规则', { tenantCode: customerCode, tenantName: customerName })" fullscreenable width="min(1200px, calc(100vw - 28px))" :confirm-loading="editorSaving || chargeSaving" :show-cancel="false" :before-close="beforeEditorClose" @confirm="saveConfig">
|
||
<el-form ref="editorForm" :model="form" label-position="top" class="density-form density-dialog-sections">
|
||
<section class="density-dialog-section" data-tone="work">
|
||
<h3 class="density-dialog-section__title">按箱规尺寸计算<InlineHelp title="箱规计算公式"><p>计算公式:按长、宽、高匹配箱规;长、宽、高各加 2X(X 为单边加宽),体积(CBM) = 外径长 × 外径宽 × 外径高 ÷ 1,000,000</p></InlineHelp></h3>
|
||
|
||
<div class="editor-table-wrap">
|
||
<table class="density-table density-table--editable editor-table" aria-label="箱规尺寸规则填写"><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" placeholder="请输入内径长" /></td><td><el-input-number v-model="row.innerWidthCm" :min="0.001" :precision="3" :controls="false" placeholder="请输入内径宽" /></td><td><el-input-number v-model="row.innerHeightCm" :min="0.001" :precision="3" :controls="false" placeholder="请输入内径高" /></td><td><el-input-number v-model="row.edgeAdditionCm" :min="0.001" :max="100" :precision="3" :controls="false" placeholder="请输入边长加值" /></td><td><el-input v-model="row.remark" maxlength="500" placeholder="请输入备注(选填)" /></td></tr>
|
||
</tbody></table>
|
||
</div>
|
||
<div class="rule-save-row density-actions density-actions--footer"><el-button type="primary" :loading="editorSaving" @click="saveConfig">保存规则</el-button></div>
|
||
</section>
|
||
<section v-if="canChargeView" class="density-dialog-section" data-tone="work"><h3 class="density-dialog-section__title">箱规按权重系数计算<InlineHelp title="系数匹配规则"><p>长、宽、高任一项超出当前挡位时进入下一挡;命中规格的一个系数统一应用于长、宽、高。</p></InlineHelp></h3><div class="charge-rule-editor-wrap"><table class="density-table density-table--editable charge-rule-editor" aria-label="权重系数规则填写"><thead><tr><th>箱规类型</th><th>内径长(cm)</th><th>内径宽(cm)</th><th>内径高(cm)</th><th>权重系数</th><th>重量(kg)</th><th>备注</th></tr></thead><tbody><tr v-for="row in chargeRows" :key="`combined-${row.boxType}`"><th scope="row">{{ BOX_NAMES[row.boxType] }}</th><td><el-input-number v-model="row.length" :min="0.001" :precision="3" :controls="false" placeholder="请输入内径长" /></td><td><el-input-number v-model="row.width" :min="0.001" :precision="3" :controls="false" placeholder="请输入内径宽" /></td><td><el-input-number v-model="row.height" :min="0.001" :precision="3" :controls="false" placeholder="请输入内径高" /></td><td><el-input-number v-model="row.coefficient" :min="0.001" :precision="3" :controls="false" placeholder="请输入权重系数" /></td><td><el-input-number v-model="row.weight" :min="0.001" :precision="3" :controls="false" placeholder="请输入重量" /></td><td><el-input v-model="row.remark" maxlength="500" placeholder="请输入备注(选填)" /></td></tr></tbody></table></div><div class="rule-save-row density-actions density-actions--footer"><el-button type="primary" :loading="chargeSaving" @click="saveChargeConfig">保存系数规则</el-button></div></section>
|
||
</el-form>
|
||
<template #footer><div class="density-actions density-actions--footer"><el-button :disabled="editorSaving || chargeSaving" @click="closeEditor">取消</el-button></div></template>
|
||
</AppDialog>
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.dimension-rule-panel { min-width: 0; }
|
||
.rule-heading { align-items: center; }
|
||
.rule-heading h2 { margin: 0; color: var(--cc-text); }.rule-heading p { margin: var(--admin-space-2, 6px) 0 0; color: var(--cc-muted); }
|
||
.heading-actions { display: flex; align-items: center; flex-wrap: wrap; gap: var(--admin-space-3, 8px); }
|
||
.heading-actions :deep(.el-button) { margin-left: 0; }
|
||
.rule-card { min-width: 0; }.rule-card-heading { display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--cc-border); }.rule-card-heading h2 { margin: 0; color: var(--cc-text); }.formula-band { display: flex; align-items: center; flex-wrap: wrap; border-bottom: 1px solid var(--cc-border); background: var(--admin-label-bg, #f8fafc); color: var(--cc-muted); font-size: var(--admin-font-body, 13px); }.formula-band strong { color: #344054; }
|
||
.rule-title-row { display: flex; align-items: center; flex-wrap: wrap; gap: 14px; }.rule-title-row :deep(.el-checkbox) { height: 30px; margin-right: 0; padding: 0 var(--admin-space-4, 10px); border: 1px solid var(--cc-border); border-radius: 6px; background: var(--admin-surface, #fff); }.rule-title-row :deep(.el-checkbox.is-checked) { border-color: var(--cc-primary); background: var(--cc-primary-soft); }
|
||
.rule-state { display: flex; align-items: center; justify-content: center; flex-direction: column; gap: var(--admin-space-3, 8px); color: #8797b2; text-align: center; }.rule-state strong { color: #475467; font-size: var(--admin-font-body, 13px); }.rule-state p { margin: 0; color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); }
|
||
.rule-error { display: flex; align-items: center; justify-content: space-between; gap: 18px; background: #fff4f3; }.rule-error strong { color: #7a271a; }.rule-error p { margin: 5px 0; color: #b42318; font-size: var(--admin-font-caption, 12px); }
|
||
.volume-value { color: #1570ef; } .editor-table,.charge-rule-editor { min-width:920px; table-layout:fixed; } .editor-table th:first-child,.charge-rule-editor th:first-child { width:100px; } .editor-table th:last-child,.charge-rule-editor th:last-child { width:20%; }.editor-table-wrap { overflow-x: auto; }.section-hint { margin: 5px 0 0; color: var(--cc-muted); font-size: var(--admin-font-caption, 12px); font-weight: 400; }
|
||
|
||
.charge-rule-editor-wrap { overflow-x: auto; }
|
||
|
||
.rule-save-row { padding: var(--admin-space-5, 12px) 0 var(--admin-space-1, 4px); }
|
||
|
||
|
||
.charge-rule-card :deep(.el-table) { color: #344054; font-size: var(--admin-font-body, 13px); }
|
||
.charge-rule-card :deep(.el-table th.el-table__cell) { background: var(--admin-label-bg, #f8fafc); color: #475467; font-size: var(--admin-font-body, 13px); font-weight: 650; }
|
||
.charge-rule-card :deep(.el-table td.el-table__cell) { color: #344054; font-size: var(--admin-font-body, 13px); }
|
||
.charge-rule-card :deep(.el-table .cell) { line-height: 1.5; text-align: center; }
|
||
.charge-rule-card :deep(.el-table .charge-remark-column .cell) { text-align: left; }
|
||
.charge-rule-card :deep(.el-table strong) { color: #1570ef; font-weight: 650; }
|
||
@media (max-width: 760px) { .rule-heading { align-items: stretch; flex-direction: column; }.rule-card-heading { align-items: stretch; flex-direction: column; }.rule-card-heading :deep(.el-button) { min-height: 44px; }.formula-band { align-items: flex-start; flex-direction: column; gap: 8px; }.rule-error { align-items: stretch; flex-direction: column; } }
|
||
</style>
|