Files
admin-web/src/modules/customers/components/CustomerBillingRulesPanel.vue
T
2026-09-03 17:49:01 +08:00

377 lines
27 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, nextTick, onMounted, reactive, ref, watch } from 'vue'
import { Pencil, Ruler, Save } 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 DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'
import { createCustomerChargeRule, listCustomerChargeRules, setCustomerChargeRuleDefault, type CustomerChargeRule, type CreateCustomerChargeRuleRequest } from '@/modules/fba-pricing'
const props = defineProps<{ customerId: string; customerName: string; customerCode?: 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 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>()
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, () => {
editorVisible.value = false
defaultCalculationMode.value = 'ACTUAL'
void loadConfig()
})
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) 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('箱规尺寸尚未保存,确定放弃修改吗?', '放弃编辑', {
confirmButtonText: '放弃', cancelButtonText: '继续编辑', type: 'warning',
})
return true
} catch {
return false
}
}
async function beforeEditorClose(done: () => void) {
if (editorSaving.value) return
if (!editorDirty.value || await confirmAbandon()) done()
}
async function closeEditor() {
if (editorSaving.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) 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(() => { void loadConfig(); void loadChargeRules() })
</script>
<template>
<div class="dimension-rule-panel">
<div v-if="!canView || loadError" class="panel-title rule-heading">
<div><h2>按箱规尺寸计算</h2><p>{{ customerName }}{{ customerCode ? `(${customerCode})` : '' }}的尺寸计算规则</p></div>
</div>
<div v-if="!canView" class="rule-state"><Ruler :size="30" /><strong>暂无尺寸计算规则查看权限</strong><p>请联系管理员申请尺寸计算规则查看权限。</p></div>
<div v-else-if="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="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"><Pencil :size="16" />编辑规则</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 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', 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>
<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 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">计算公式:按长、宽、高匹配箱规;长、宽、高各加 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>
<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>
<div class="rule-save-row"><el-button type="primary" :loading="editorSaving" @click="saveConfig"><Save :size="16" />保存规则</el-button></div>
<section v-if="canChargeView" class="combined-charge-editor"><h3>箱规按权重系数计算</h3><p class="editor-formula">长、宽、高任一项超出当前挡位时进入下一挡;命中规格的一个系数统一应用于长、宽、高。</p><div class="charge-rule-editor-wrap"><table class="charge-rule-editor"><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>{{ BOX_NAMES[row.boxType] }}</th><td><el-input-number v-model="row.length" :min="0.001" :precision="3" :controls="false" /></td><td><el-input-number v-model="row.width" :min="0.001" :precision="3" :controls="false" /></td><td><el-input-number v-model="row.height" :min="0.001" :precision="3" :controls="false" /></td><td><el-input-number v-model="row.coefficient" :min="0.001" :precision="3" :controls="false" /></td><td><el-input-number v-model="row.weight" :min="0.001" :precision="3" :controls="false" /></td><td><el-input v-model="row.remark" maxlength="500" /></td></tr></tbody></table></div><div class="rule-save-row"><el-button type="primary" :loading="chargeSaving" @click="saveChargeConfig"><Save :size="16" />保存系数规则</el-button></div></section>
</el-form>
</div>
<template #footer><div class="dialog-actions"><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); font-size: 18px; }.rule-heading p { margin: 6px 0 0; color: var(--cc-muted); font-size: 13px; }
.heading-actions,.dialog-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 8px; }.heading-actions :deep(.el-button),.dialog-actions :deep(.el-button) { margin-left: 0; }
.rule-card { min-width: 0; }.rule-card-heading { display: flex; min-height: 68px; align-items: center; justify-content: space-between; gap: 16px; padding: 12px 18px; border-bottom: 1px solid var(--cc-border); }.rule-card-heading h2 { margin: 0; color: var(--cc-text); font-size: 18px; }.formula-band { display: flex; min-height: 52px; align-items: center; flex-wrap: wrap; gap: 12px 30px; padding: 12px 18px; border-bottom: 1px solid var(--cc-border); background: #f8fafc; color: var(--cc-muted); font-size: 13px; }.formula-band strong { color: #344054; }.loading-state { min-height: 280px; }
.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 10px; border: 1px solid var(--cc-border); border-radius: 6px; background: #fff; }.rule-title-row :deep(.el-checkbox.is-checked) { border-color: var(--cc-primary); background: var(--cc-primary-soft); }
.rule-state { display: flex; min-height: 220px; align-items: center; justify-content: center; flex-direction: column; gap: 8px; color: #8797b2; text-align: center; }.rule-state strong { color: #475467; font-size: 13px; }.rule-state p { margin: 0; color: var(--cc-muted); font-size: 12px; }
.rule-error { display: flex; min-height: 180px; align-items: center; justify-content: space-between; gap: 18px; padding: 20px; background: #fff4f3; }.rule-error strong { color: #7a271a; }.rule-error p { margin: 5px 0; color: #b42318; font-size: 12px; }
.volume-value { color: #1570ef; }.editor-content { display: grid; gap: 20px; }.editor-formula { margin: 0; padding: 13px 15px; color: #475467; font-size: 13px; line-height: 1.7; }.editor-table-wrap { overflow-x: auto; }.editor-table { width: 100%; min-width: 920px; border-collapse: collapse; }.editor-table th,.editor-table td { padding: 10px 12px; border-bottom: 1px solid var(--cc-border); text-align: left; white-space: nowrap; }.editor-table thead th { background: #eef0fa; color: #475467; font-size: 12px; }.editor-table tbody th { color: var(--cc-text); font-size: 13px; }.editor-table :deep(.el-input-number),.editor-table :deep(.el-input) { width: 140px; }.editor-table :deep(.el-input-number .el-input__wrapper) { width: 100%; }
.charge-rule-card { margin-top: 18px; }.section-hint { margin: 5px 0 0; color: var(--cc-muted); font-size: 12px; font-weight: 400; }.charge-form { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 16px; }.charge-form :deep(.el-form-item:last-child) { grid-column: 1 / -1; }.charge-form :deep(.el-input-number), .charge-form :deep(.el-input) { width: 100%; }
.charge-form--simple { grid-template-columns: 1fr; }.charge-form--simple :deep(.el-form-item) { grid-column: 1; }
.charge-rule-editor-wrap { overflow-x: auto; }.charge-rule-editor { width: 100%; min-width: 920px; border-collapse: collapse; }.charge-rule-editor th,.charge-rule-editor td { padding: 9px 10px; border-bottom: 1px solid var(--cc-border); text-align: left; white-space: nowrap; }.charge-rule-editor thead th { background: #eef0fa; color: #475467; font-size: 12px; }.charge-rule-editor tbody th { color: var(--cc-text); font-size: 13px; }.charge-rule-editor :deep(.el-input-number),.charge-rule-editor :deep(.el-input) { width: 118px; }.charge-rule-editor :deep(.el-input-number .el-input__wrapper) { width: 100%; }
.charge-box-cell { min-height: 34px; display: flex; align-items: center; border-bottom: 1px solid var(--cc-border); }.charge-box-cell:last-child { border-bottom: 0; }
.rule-save-row { display: flex; justify-content: flex-end; padding: 12px 0 4px; }
.dialog-actions { justify-content: center; width: 100%; }
.dialog-actions :deep(.el-button) { min-width: 112px; min-height: 46px; font-size: 15px; }
.charge-rule-card :deep(.el-table) { color: #344054; font-size: 13px; }
.charge-rule-card :deep(.el-table th.el-table__cell) { background: #f8fafc; color: #475467; font-size: 13px; font-weight: 650; }
.charge-rule-card :deep(.el-table td.el-table__cell) { color: #344054; font-size: 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; }.dialog-actions { width: 100%; }.dialog-actions :deep(.el-button) { min-height: 44px; flex: 1; } }
</style>