提交
This commit is contained in:
@@ -2,12 +2,24 @@ import ElementPlus from 'element-plus'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getCustomerDimensionConfig, updateCustomerDimensionConfig, type CustomerDimensionConfig } from '@/modules/dimension-config'
|
||||
import { getCustomerDimensionConfig, updateCustomerDimensionConfig, updateCustomerDimensionDefaultCalculationMode, type CustomerDimensionConfig } from '@/modules/dimension-config'
|
||||
import {
|
||||
createCustomerChargeRule,
|
||||
listCustomerChargeRules,
|
||||
setCustomerChargeRuleDefault,
|
||||
type CustomerChargeRule,
|
||||
} from '@/modules/fba-pricing'
|
||||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||||
import { useAuthStore } from '@/core/auth/auth'
|
||||
import CustomerBillingRulesPanel from './CustomerBillingRulesPanel.vue'
|
||||
|
||||
vi.mock('@/modules/dimension-config', () => ({ getCustomerDimensionConfig: vi.fn(), updateCustomerDimensionConfig: vi.fn() }))
|
||||
vi.mock('@/modules/dimension-config', () => ({ getCustomerDimensionConfig: vi.fn(), updateCustomerDimensionConfig: vi.fn(), updateCustomerDimensionDefaultCalculationMode: vi.fn() }))
|
||||
vi.mock('@/modules/fba-pricing', () => ({
|
||||
createCustomerChargeRule: vi.fn(),
|
||||
listCustomerChargeRules: vi.fn(),
|
||||
retireCustomerChargeRule: vi.fn(),
|
||||
setCustomerChargeRuleDefault: vi.fn(),
|
||||
}))
|
||||
|
||||
const boxes = [
|
||||
['SMALL', '小箱', 30, 20, 20, '标准小箱'],
|
||||
@@ -17,6 +29,7 @@ const boxes = [
|
||||
] as const
|
||||
const config: CustomerDimensionConfig = {
|
||||
tenantId: '8',
|
||||
defaultCalculationMode: 'ADD',
|
||||
rules: boxes.map(([boxType, boxTypeName, innerLengthCm, innerWidthCm, innerHeightCm, remark], index) => ({
|
||||
id: String(100 + index),
|
||||
boxType, boxTypeName, innerLengthCm, innerWidthCm, innerHeightCm, edgeAdditionCm: 2,
|
||||
@@ -25,6 +38,13 @@ const config: CustomerDimensionConfig = {
|
||||
remark, version: 3, createdAt: '2026-08-07T10:00:00', updatedAt: '2026-08-07T10:20:00',
|
||||
})),
|
||||
}
|
||||
const chargeRule: CustomerChargeRule = {
|
||||
id: '201', tenantId: '8', ruleCode: 'RULE-201', ruleVersion: 1, customerId: '8', 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: '2026-09-02T10:00:00', effectiveTo: null,
|
||||
status: 'ACTIVE', remark: null,
|
||||
}
|
||||
|
||||
enableAutoUnmount(afterEach)
|
||||
|
||||
@@ -33,10 +53,16 @@ describe('CustomerBillingRulesPanel', () => {
|
||||
vi.clearAllMocks()
|
||||
document.body.innerHTML = ''
|
||||
const pinia = createPinia(); setActivePinia(pinia)
|
||||
const auth = useAuthStore(); auth.token = 'token'; auth.permissions = ['firstleg:dimension-config:view', 'firstleg:dimension-config:edit']
|
||||
const auth = useAuthStore(); auth.token = 'token'; auth.permissions = [
|
||||
'firstleg:dimension-config:view', 'firstleg:dimension-config:edit',
|
||||
'firstleg:fba-pricing:rule:view', 'firstleg:fba-pricing:rule:edit',
|
||||
]
|
||||
useAccessPreviewStore().initializeAfterLogin()
|
||||
vi.mocked(getCustomerDimensionConfig).mockResolvedValue(config)
|
||||
vi.mocked(updateCustomerDimensionConfig).mockResolvedValue(config)
|
||||
vi.mocked(updateCustomerDimensionDefaultCalculationMode).mockResolvedValue(config)
|
||||
vi.mocked(listCustomerChargeRules).mockResolvedValue([chargeRule])
|
||||
vi.mocked(createCustomerChargeRule).mockResolvedValue(chargeRule)
|
||||
})
|
||||
|
||||
it('renders the four fixed carton dimension rows and calculated values', async () => {
|
||||
@@ -90,4 +116,33 @@ describe('CustomerBillingRulesPanel', () => {
|
||||
await flushPromises()
|
||||
expect(document.body.textContent).toContain('编辑箱规尺寸')
|
||||
})
|
||||
|
||||
it('saves the coefficient set and can use it as the customer default calculation mode', async () => {
|
||||
vi.mocked(listCustomerChargeRules)
|
||||
.mockResolvedValueOnce([chargeRule])
|
||||
.mockResolvedValueOnce([{ ...chargeRule, defaultRule: true }])
|
||||
vi.mocked(updateCustomerDimensionDefaultCalculationMode).mockResolvedValue({
|
||||
...config,
|
||||
defaultCalculationMode: 'MULTIPLY',
|
||||
})
|
||||
mount(CustomerBillingRulesPanel, { attachTo: document.body, props: { customerId: '8', customerName: '柏悦物流' }, global: { plugins: [ElementPlus] } })
|
||||
await flushPromises()
|
||||
|
||||
;[...document.querySelectorAll<HTMLButtonElement>('button')].find((button) => button.textContent?.trim() === '编辑规则')?.click()
|
||||
await flushPromises()
|
||||
expect(document.body.textContent).toContain('箱规按权重系数计算')
|
||||
|
||||
;[...document.querySelectorAll<HTMLButtonElement>('button')].find((button) => button.textContent?.trim() === '保存系数规则')?.click()
|
||||
await flushPromises()
|
||||
expect(createCustomerChargeRule).toHaveBeenCalledWith('8', expect.objectContaining({
|
||||
defaultRule: false,
|
||||
remark: expect.stringMatching(/SMALL:30,20,20;系数:\d+(?:\.\d+)?/),
|
||||
}))
|
||||
expect(setCustomerChargeRuleDefault).toHaveBeenCalledWith('8', '201')
|
||||
|
||||
const defaultRuleCheckboxes = [...document.querySelectorAll<HTMLInputElement>('input[type="checkbox"]')]
|
||||
defaultRuleCheckboxes[1]?.click()
|
||||
await flushPromises()
|
||||
expect(updateCustomerDimensionDefaultCalculationMode).toHaveBeenCalledWith('8', 'MULTIPLY')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRe
|
||||
import {
|
||||
getCustomerDimensionConfig,
|
||||
updateCustomerDimensionConfig,
|
||||
updateCustomerDimensionDefaultCalculationMode,
|
||||
type CustomerDimensionBoxType,
|
||||
type CustomerDimensionConfig,
|
||||
type CustomerDimensionConfigFormModel,
|
||||
@@ -16,19 +17,51 @@ 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<'DIMENSION' | 'WEIGHT'>('DIMENSION')
|
||||
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> = {
|
||||
@@ -58,6 +91,7 @@ watch(form, () => {
|
||||
|
||||
watch(() => props.customerId, () => {
|
||||
editorVisible.value = false
|
||||
defaultCalculationMode.value = 'DIMENSION'
|
||||
void loadConfig()
|
||||
})
|
||||
|
||||
@@ -73,6 +107,7 @@ 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'
|
||||
} catch (error) {
|
||||
config.value = null
|
||||
loadError.value = getApiErrorDetails(error, '箱规尺寸规则加载失败,请稍后重试')
|
||||
@@ -87,6 +122,7 @@ function cloneRules(rules: CustomerDimensionRule[]) {
|
||||
|
||||
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
|
||||
@@ -157,8 +193,8 @@ async function saveConfig() {
|
||||
}))
|
||||
config.value = await updateCustomerDimensionConfig(props.customerId, { rules })
|
||||
config.value.rules = sortRules(config.value.rules)
|
||||
form.rules = cloneRules(config.value.rules)
|
||||
editorDirty.value = false
|
||||
editorVisible.value = false
|
||||
ElMessage.success('箱规尺寸规则已保存')
|
||||
} catch (error) {
|
||||
ElMessage.error(getApiErrorDetails(error, '箱规尺寸规则保存失败,请刷新后重试').message)
|
||||
@@ -171,11 +207,80 @@ 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)) {
|
||||
ElMessage.warning('请先保存并启用一套权重系数配置,再将其设为默认规则')
|
||||
return
|
||||
}
|
||||
const previous = defaultCalculationMode.value
|
||||
defaultCalculationMode.value = mode
|
||||
defaultModeSaving.value = true
|
||||
try {
|
||||
config.value = await updateCustomerDimensionDefaultCalculationMode(
|
||||
props.customerId,
|
||||
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 }
|
||||
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(';') })
|
||||
await setCustomerChargeRuleDefault(props.customerId, created.id)
|
||||
await loadChargeRules()
|
||||
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) : '暂无记录'
|
||||
}
|
||||
|
||||
onMounted(loadConfig)
|
||||
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 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>
|
||||
@@ -186,8 +291,14 @@ onMounted(loadConfig)
|
||||
<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"><h2>按箱规尺寸计算</h2><el-button v-if="canEdit" type="primary" :disabled="loading" @click="openEditor"><Pencil :size="16" />编辑规则</el-button></header>
|
||||
<div class="formula-band"><span>计算方式:<strong>内径尺寸 + 外边加宽 × 2 = 外径尺寸</strong></span><span>体积公式:外径长 × 外径宽 × 外径高 ÷ 1,000,000 = CBM</span><span>默认加边:2cm(每侧)</span></div>
|
||||
<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>
|
||||
</div>
|
||||
<el-button v-if="canEdit" type="primary" :disabled="loading" @click="openEditor"><Pencil :size="16" />编辑规则</el-button>
|
||||
</header>
|
||||
<div class="formula-band"><span>计算方式:<strong>内径尺寸 + 外边加宽 × 2 = 外径尺寸</strong>(按实测尺寸匹配最小可容纳箱规)</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>
|
||||
@@ -199,18 +310,34 @@ onMounted(loadConfig)
|
||||
</section>
|
||||
<div v-else v-loading="loading" class="loading-state" />
|
||||
|
||||
<AppDialog v-model="editorVisible" title="编辑箱规尺寸" width="min(1100px, calc(100vw - 28px))" :confirm-loading="editorSaving" :show-cancel="false" :before-close="beforeEditorClose" @confirm="saveConfig">
|
||||
<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>
|
||||
<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">计算公式:外径 = 内径 + 加宽 × 2(每侧加宽),体积(CBM) = 外径长 × 外径宽 × 外径高 ÷ 1,000,000</p>
|
||||
<p class="editor-formula">计算公式:按长、宽、高匹配最小可容纳箱规;外径 = 该箱规内径 + 加宽 × 2(每侧加宽),体积(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" @click="closeEditor">取消</el-button><el-button type="primary" :loading="editorSaving" @click="saveConfig"><Save :size="16" />保存规则</el-button></div></template>
|
||||
<template #footer><div class="dialog-actions"><el-button :disabled="editorSaving || chargeSaving" @click="closeEditor">取消</el-button></div></template>
|
||||
</AppDialog>
|
||||
</div>
|
||||
</template>
|
||||
@@ -221,8 +348,22 @@ onMounted(loadConfig)
|
||||
.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>
|
||||
|
||||
Reference in New Issue
Block a user