应付配置的附加费配置完善

This commit is contained in:
liuminghui
2026-11-13 11:57:19 +08:00
parent 6675808dfc
commit 3a104d2d8c
20 changed files with 1369 additions and 310 deletions
+7 -7
View File
@@ -172,21 +172,21 @@ export function delPay(productIds) {
})
}
/** 当前版本的附加费计费条件 */
export function listPaySurchargeRule(productId, termId) {
/** 产品上的附加费计费条件,不按价格版本拆开 */
export function listPaySurchargeRule(productId) {
return request({
url: '/price/pay/surchargeRule',
method: 'get',
params: { productId, termId }
params: { productId }
})
}
/** 上一版计费条件,复制到当前页编辑 */
export function previousPaySurchargeRule(productId) {
/** 这一版产品上的附加费配置操作记录 */
export function listPaySurchargeRuleLog(productId, termId) {
return request({
url: '/price/pay/surchargeRule/previous',
url: '/price/pay/surchargeRule/log',
method: 'get',
params: { productId }
params: { productId, termId }
})
}
@@ -74,6 +74,7 @@
:size="size"
:height="height"
:max-height="maxHeight"
:scrollbar-always-on="scrollbarAlwaysOn"
:default-sort="defaultSort"
:span-method="spanMethod"
@selection-change="handleSelectionChange"
@@ -291,6 +292,8 @@ const props = defineProps({
// 表格高度
height: { type: [String, Number], default: undefined },
maxHeight: { type: [String, Number], default: undefined },
// 横向、纵向滚动条一直显示
scrollbarAlwaysOn: { type: Boolean, default: false },
// 默认排序(透传 el-table default-sort,如 { prop: 'createTime', order: 'descending' })
defaultSort: {
type: Object,
@@ -78,6 +78,14 @@
<el-tab-pane :label="feeTabLabel(pane)" name="fee">
<div class="pay-edit-bar">
<el-input v-model="pane.feeKeyword" clearable placeholder="筛选附加费用、地区、价格、货币或费用解释" class="pay-filter" @input="pane.feePageNo = 1" />
<el-select v-model="pane.feeBill" clearable placeholder="计费条件" class="pay-bill-filter" @change="pane.feePageNo = 1">
<el-option label="需要计费" value="BILL">
<span class="bill-need-text">需要计费</span>
</el-option>
<el-option label="暂不计费" value="MANUAL">
<span class="bill-hold-text">暂不计费</span>
</el-option>
</el-select>
<el-button v-if="editable" size="small" type="primary" plain @click="openFeePicker(pane)">添加附加费</el-button>
<el-button
v-if="editable"
@@ -101,7 +109,7 @@
<el-checkbox :model-value="row._checked === true" @update:model-value="v => { row._checked = !!v }" />
</template>
</el-table-column>
<el-table-column prop="feeName" label="附加费用" min-width="180" show-overflow-tooltip>
<el-table-column prop="feeName" label="附加费用" min-width="180" show-overflow-tooltip fixed="left">
<template #default="{ row }">
<el-select
v-if="isRowEditing(row)"
@@ -189,13 +197,25 @@
<el-table-column prop="feeDesc" label="费用解释" min-width="240" show-overflow-tooltip>
<template #default="{ row }"><cell-text v-model="row.feeDesc" :edit="isRowEditing(row)" /></template>
</el-table-column>
<el-table-column prop="calcRule" label="计费条件" width="140">
<el-table-column prop="calcRule" label="计费条件" width="140" align="center">
<template #default="{ row }">
<el-select v-if="isRowEditing(row)" :model-value="feeBillFlag(row)" size="small" @change="value => setFeeBill(row, value)">
<el-option label="需要计费" value="BILL" />
<el-option label="暂不计费" value="MANUAL" />
<el-select
v-if="isRowEditing(row)"
:model-value="feeBillFlag(row)"
size="small"
:class="feeBillFlag(row) === 'BILL' ? 'bill-need' : 'bill-hold'"
@change="value => setFeeBill(row, value)"
>
<el-option label="需要计费" value="BILL">
<span class="bill-need-text">需要计费</span>
</el-option>
<el-option label="暂不计费" value="MANUAL">
<span class="bill-hold-text">暂不计费</span>
</el-option>
</el-select>
<span v-else>{{ feeBillFlag(row) === 'BILL' ? '需要计费' : '暂不计费' }}</span>
<el-tag v-else :type="feeBillFlag(row) === 'BILL' ? 'success' : 'danger'" size="small" effect="light">
{{ feeBillFlag(row) === 'BILL' ? '需要计费' : '暂不计费' }}
</el-tag>
</template>
</el-table-column>
<el-table-column v-if="editable" label="操作" width="120" align="center" fixed="right">
@@ -483,6 +503,7 @@ watch(source, (list) => {
tab: 'band',
bandKeyword: '',
feeKeyword: '',
feeBill: '',
postKeyword: '',
bandPageNo: 1,
feePageNo: 1,
@@ -495,7 +516,9 @@ function filteredBands(pane) {
}
function filteredFees(pane) {
return filterRows(pane.item.surcharges || [], pane.feeKeyword, ['feeName', 'zoneName', 'chargeText', 'amount', 'currency', 'priceUnit', 'feeDesc', 'calcRule', 'calcParam', 'postcodeParam'])
const rows = filterRows(pane.item.surcharges || [], pane.feeKeyword, ['feeName', 'zoneName', 'chargeText', 'amount', 'currency', 'priceUnit', 'feeDesc', 'calcRule', 'calcParam', 'postcodeParam'])
if (!pane.feeBill) return rows
return rows.filter(row => feeBillFlag(row) === pane.feeBill)
}
function feeTabLabel(pane) {
@@ -721,6 +744,9 @@ function statusText(status) {
.pay-filter {
width: 320px;
}
.pay-bill-filter {
width: 140px;
}
.pay-edit-bar {
display: flex;
align-items: center;
@@ -745,4 +771,16 @@ function statusText(status) {
.limit-tag {
margin-right: 6px;
}
.bill-need :deep(.el-select__selected-item) {
color: var(--el-color-success);
}
.bill-hold :deep(.el-select__selected-item) {
color: var(--el-color-danger);
}
.bill-need-text {
color: var(--el-color-success);
}
.bill-hold-text {
color: var(--el-color-danger);
}
</style>
@@ -5,7 +5,7 @@
<div class="rule-title">绑定产品</div>
<div class="rule-products">
<el-tag v-for="item in products" :key="item.key" closable class="rule-tag" @close="removeProduct(item)">
{{ item.productName }} · 第 {{ item.versionNo || 1 }} 版
{{ item.productName }}
</el-tag>
<span v-if="!products.length" class="rule-empty">还没有产品。勾选列表后打开,或在下面查询后加入。</span>
</div>
@@ -25,7 +25,7 @@
</template>
</el-table-column>
</el-table>
<p class="rule-hint">配置写到每个产品当前生效的版本。同一版本再次保存会覆盖原来的一份。</p>
<p class="rule-hint">配置跟产品走,不跟价格版本。计算时用这个产品当前生效版本上的附加费。</p>
</div>
<div class="rule-block">
@@ -36,8 +36,6 @@
<el-switch v-model="enabled[rule.value]" @change="value => toggleRule(rule.value, value)" />
</div>
</div>
<p v-if="enabled.POSTCODE" class="rule-hint">打开后,用订单目的国去应付邮编配置里对国家和邮编。邮编前缀和完整邮编都会判断,命中偏远或岛屿才加这笔附加费。</p>
<p v-if="enabled.ZONE" class="rule-hint">打开后,用订单目的国和邮编去应付邮编配置里对上分区,再加这一档附加费。</p>
</div>
<div v-for="rule in openedRules" :key="rule.value" class="rule-block">
@@ -55,209 +53,310 @@
<div v-if="rule.value === 'SIZE'" class="rule-grid">
<div class="field">
<span class="label">命中方式</span>
<el-select v-model="row.matchMode" size="small">
<el-option label="任一命中" value="ANY" />
<el-option label="全部命中" value="ALL" />
</el-select>
<div class="control">
<el-select v-model="row.matchMode" size="small">
<el-option label="任一命中" value="ANY" />
<el-option label="全部命中" value="ALL" />
</el-select>
</div>
</div>
<div class="field">
<span class="label">重量前提 kg</span>
<el-input v-model="row.weightMin" size="small" placeholder="下限" />
<span class="sep">至</span>
<el-input v-model="row.weightMax" size="small" placeholder="上限" />
<div class="control">
<el-input v-model="row.weightMin" size="small" placeholder="下限" />
<span class="sep">至</span>
<el-input v-model="row.weightMax" size="small" placeholder="上限" />
</div>
</div>
<div class="field">
<span class="label">最长边 cm</span>
<el-input v-model="row.longestMin" size="small" placeholder="下限" />
<span class="sep">至</span>
<el-input v-model="row.longestMax" size="small" placeholder="上限" />
<div class="control">
<el-input v-model="row.longestMin" size="small" placeholder="下限" />
<span class="sep">至</span>
<el-input v-model="row.longestMax" size="small" placeholder="上限" />
</div>
</div>
<div class="field">
<span class="label">次长边 cm</span>
<el-input v-model="row.secondMin" size="small" placeholder="下限" />
<span class="sep">至</span>
<el-input v-model="row.secondMax" size="small" placeholder="上限" />
<div class="control">
<el-input v-model="row.secondMin" size="small" placeholder="下限" />
<span class="sep">至</span>
<el-input v-model="row.secondMax" size="small" placeholder="上限" />
</div>
</div>
<div class="field">
<span class="label">三边之和 cm</span>
<el-input v-model="row.sumMin" size="small" placeholder="下限" />
<span class="sep">至</span>
<el-input v-model="row.sumMax" size="small" placeholder="上限" />
<div class="control">
<el-input v-model="row.sumMin" size="small" placeholder="下限" />
<span class="sep">至</span>
<el-input v-model="row.sumMax" size="small" placeholder="上限" />
</div>
</div>
<div class="field">
<span class="label">围长 cm</span>
<el-input v-model="row.girthMin" size="small" placeholder="下限" />
<span class="sep">至</span>
<el-input v-model="row.girthMax" size="small" placeholder="上限" />
<div class="control">
<el-input v-model="row.girthMin" size="small" placeholder="下限" />
<span class="sep">至</span>
<el-input v-model="row.girthMax" size="small" placeholder="上限" />
</div>
</div>
<div class="field">
<span class="label">箱规 cm</span>
<el-select v-model="row.boxMode" size="small" class="mode">
<el-option label="任一边超过" value="OVER" />
<el-option label="小于最小尺寸" value="UNDER" />
</el-select>
<el-input v-model="row.boxLength" size="small" placeholder="长" />
<el-input v-model="row.boxWidth" size="small" placeholder="宽" />
<el-input v-model="row.boxHeight" size="small" placeholder="高" />
<div class="control">
<el-select v-model="row.boxMode" size="small" class="mode">
<el-option label="任一边超过" value="OVER" />
<el-option label="小于最小尺寸" value="UNDER" />
</el-select>
<el-input v-model="row.boxLength" class="num" size="small" placeholder="长" />
<el-input v-model="row.boxWidth" class="num" size="small" placeholder="宽" />
<el-input v-model="row.boxHeight" class="num" size="small" placeholder="高" />
</div>
</div>
<div class="field">
<span class="label">体积 m³</span>
<el-input v-model="row.volumeMin" size="small" placeholder="下限" />
<span class="sep">至</span>
<el-input v-model="row.volumeMax" size="small" placeholder="上限" />
<div class="control">
<el-input v-model="row.volumeMin" size="small" placeholder="下限" />
<span class="sep">至</span>
<el-input v-model="row.volumeMax" size="small" placeholder="上限" />
</div>
</div>
<div class="field">
<span class="label">长×宽×高上限 cm³</span>
<el-input v-model="row.cubicMax" size="small" placeholder="例如 432000" />
<div class="control">
<el-input v-model="row.cubicMax" size="small" placeholder="例如 432000" />
</div>
</div>
<div class="field">
<span class="label">每超出 cm</span>
<el-input v-model="row.exceedStepCm" size="small" placeholder="例如 10" />
<div class="control">
<el-input v-model="row.exceedStepCm" size="small" placeholder="例如 10" />
</div>
</div>
<div class="field">
<span class="label">运费加收比例 %</span>
<el-input v-model="row.exceedRatio" size="small" placeholder="例如 35" />
<div class="control">
<el-input v-model="row.exceedRatio" size="small" placeholder="例如 35" />
</div>
</div>
<div class="field span-2">
<div class="field">
<span class="label">说明</span>
<el-input v-model="row.remark" size="small" placeholder="这一档的补充说明" />
<div class="control">
<el-input v-model="row.remark" size="small" placeholder="这一档的补充说明" />
</div>
</div>
</div>
<div v-else-if="rule.value === 'WEIGHT'" class="rule-grid">
<div class="field">
<span class="label">重量 kg</span>
<el-input v-model="row.weightMin" size="small" placeholder="下限" />
<span class="sep">至</span>
<el-input v-model="row.weightMax" size="small" placeholder="上限" />
<div class="control">
<el-input v-model="row.weightMin" size="small" placeholder="下限" />
<span class="sep">至</span>
<el-input v-model="row.weightMax" size="small" placeholder="上限" />
</div>
</div>
<div class="field">
<span class="label">重量口径</span>
<el-select v-model="row.weightBasis" size="small">
<el-option label="实重" value="ACTUAL" />
<el-option label="计费重" value="CHARGE" />
</el-select>
<div class="control">
<el-select v-model="row.weightBasis" size="small">
<el-option label="实重" value="ACTUAL" />
<el-option label="计费重" value="CHARGE" />
</el-select>
</div>
</div>
<div class="field">
<span class="label">每超出 kg</span>
<el-input v-model="row.stepKg" size="small" placeholder="超过上限后按每公斤" />
<div class="control">
<el-input v-model="row.stepKg" size="small" placeholder="超过上限后按每公斤" />
</div>
</div>
<div class="field">
<span class="label">最低消费</span>
<el-input v-model="row.minCharge" size="small" placeholder="没有就空着" />
<div class="control">
<el-input v-model="row.minCharge" size="small" placeholder="没有就空着" />
</div>
</div>
<div class="field">
<span class="label">运费加收比例 %</span>
<el-input v-model="row.exceedRatio" size="small" placeholder="按运费比例加收时填写" />
<div class="control">
<el-input v-model="row.exceedRatio" size="small" placeholder="按运费比例加收时填写" />
</div>
</div>
<div class="field">
<span class="label">说明</span>
<el-input v-model="row.remark" size="small" />
<div class="control">
<el-input v-model="row.remark" size="small" />
</div>
</div>
</div>
<div v-else-if="rule.value === 'EXTRA_KG'" class="rule-grid">
<div class="field">
<span class="label">超过 kg</span>
<el-input v-model="row.thresholdKg" size="small" placeholder="超过这个重量后开始续收" />
<div class="control">
<el-input v-model="row.thresholdKg" size="small" placeholder="超过这个重量后开始续收" />
</div>
</div>
<div class="field">
<span class="label">每超出 kg</span>
<el-input v-model="row.stepKg" size="small" placeholder="例如 1" />
<div class="control">
<el-input v-model="row.stepKg" size="small" placeholder="例如 1" />
</div>
</div>
<div class="field">
<span class="label">重量口径</span>
<el-select v-model="row.weightBasis" size="small">
<el-option label="实重" value="ACTUAL" />
<el-option label="计费重" value="CHARGE" />
</el-select>
<div class="control">
<el-select v-model="row.weightBasis" size="small">
<el-option label="实重" value="ACTUAL" />
<el-option label="计费重" value="CHARGE" />
</el-select>
</div>
</div>
<div class="field">
<span class="label">最低消费</span>
<el-input v-model="row.minCharge" size="small" placeholder="例如 17.5" />
<div class="control">
<el-input v-model="row.minCharge" size="small" placeholder="例如 17.5" />
</div>
</div>
<div class="field span-2">
<div class="field">
<span class="label">邮编</span>
<el-input v-model="row.postcodes" size="small" type="textarea" :rows="2" placeholder="续收还要限定邮编时填写,支持 KA27-28 这种区间" />
<div class="control">
<el-input v-model="row.postcodes" size="small" type="textarea" :rows="2" placeholder="续收还要限定邮编时填写,支持 KA27-28 这种区间" />
</div>
</div>
</div>
<div v-else-if="rule.value === 'POSTCODE' || rule.value === 'ZONE'" class="rule-grid">
<div class="field">
<span class="label">分区</span>
<div class="control">
<el-select v-model="row.zoneName" filterable size="small" placeholder="选择附加费里的高、中、低区">
<el-option v-for="zone in zoneOptions" :key="zone" :label="zone" :value="zone" />
</el-select>
</div>
</div>
<div class="field">
<span class="label">邮编前缀</span>
<div class="control">
<el-input v-model="row.postcodes" size="small" type="textarea" :rows="2" placeholder="多个用逗号或换行。前缀、完整邮编、1000-1099、KA27-28 都可以" />
</div>
</div>
<div class="field">
<span class="label">国家</span>
<div class="control">
<el-input v-model="row.country" size="small" placeholder="不填则不限,例如 DE 或 德国" />
</div>
</div>
</div>
<div v-else-if="rule.value === 'ALWAYS'" class="rule-grid">
<div class="field">
<span class="label">费用项</span>
<div class="control">
<el-select v-model="row.feeName" filterable size="small" placeholder="选择这一条要每票收取的费用" @change="(name) => pickFee(row, name)">
<el-option v-for="item in feeOptions" :key="item.feeName" :label="item.feeName" :value="item.feeName" />
</el-select>
</div>
</div>
<div class="field">
<span class="label">计费方式</span>
<el-select v-model="row.rateMode" size="small">
<el-option label="固定金额" value="FIXED" />
<el-option label="按比例" value="RATIO" />
</el-select>
<div class="control">
<el-select v-model="row.rateMode" size="small">
<el-option label="固定金额" value="FIXED" />
<el-option label="按比例" value="RATIO" />
</el-select>
</div>
</div>
<div class="field">
<span class="label">比例 %</span>
<el-input v-model="row.ratio" size="small" placeholder="燃油、战争附加费这类" />
<div class="control">
<el-input v-model="row.ratio" size="small" placeholder="燃油、战争附加费这类" />
</div>
</div>
<div class="field">
<span class="label">计费基数</span>
<el-select v-model="row.chargeBase" size="small" clearable placeholder="按比例时选择">
<el-option label="处理费" value="HANDLE" />
<el-option label="派送费" value="DELIVERY" />
<el-option label="运费加附加费" value="FREIGHT_FEE" />
</el-select>
<div class="control">
<el-select v-model="row.chargeBase" size="small" clearable placeholder="按比例时选择">
<el-option label="处理费" value="HANDLE" />
<el-option label="派送费" value="DELIVERY" />
<el-option label="运费加附加费" value="FREIGHT_FEE" />
</el-select>
</div>
</div>
<div class="field">
<span class="label">生效日期</span>
<el-input v-model="row.periodStart" size="small" placeholder="开始,如 11-01" />
<span class="sep">至</span>
<el-input v-model="row.periodEnd" size="small" placeholder="结束,如 12-23" />
<div class="control">
<el-date-picker v-model="row.periodStart" type="date" value-format="YYYY-MM-DD" placeholder="开始日期" size="small" clearable />
<span class="sep">至</span>
<el-date-picker v-model="row.periodEnd" type="date" value-format="YYYY-MM-DD" placeholder="结束日期" size="small" clearable />
</div>
</div>
<div class="field span-2">
<div class="field">
<span class="label">说明</span>
<el-input v-model="row.remark" size="small" placeholder="例如每年 11、12 月固定收取" />
<div class="control">
<el-input v-model="row.remark" size="small" placeholder="例如每年 11、12 月固定收取" />
</div>
</div>
</div>
<div v-else class="rule-grid">
<div class="field">
<span class="label">退回方式</span>
<el-select v-model="row.chargeMode" size="small">
<el-option label="同正向运费" value="SAME" />
<el-option label="空运退回" value="AIR" />
<el-option label="陆运退回" value="ROAD" />
<el-option label="固定金额" value="FIXED" />
</el-select>
<div class="control">
<el-select v-model="row.chargeMode" size="small">
<el-option label="同正向运费" value="SAME" />
<el-option label="空运退回" value="AIR" />
<el-option label="陆运退回" value="ROAD" />
<el-option label="固定金额" value="FIXED" />
</el-select>
</div>
</div>
<div class="field">
<span class="label">重量 kg</span>
<el-input v-model="row.weightMin" size="small" placeholder="下限" />
<span class="sep">至</span>
<el-input v-model="row.weightMax" size="small" placeholder="上限" />
<div class="control">
<el-input v-model="row.weightMin" size="small" placeholder="下限" />
<span class="sep">至</span>
<el-input v-model="row.weightMax" size="small" placeholder="上限" />
</div>
</div>
<div class="field">
<span class="label">尺寸命中</span>
<el-select v-model="row.matchMode" size="small" clearable placeholder="退件还看尺寸时选择">
<el-option label="任一命中" value="ANY" />
<el-option label="全部命中" value="ALL" />
</el-select>
<div class="control">
<el-select v-model="row.matchMode" size="small" clearable placeholder="退件还看尺寸时选择">
<el-option label="任一命中" value="ANY" />
<el-option label="全部命中" value="ALL" />
</el-select>
</div>
</div>
<div class="field">
<span class="label">最长边 cm</span>
<el-input v-model="row.longestMin" size="small" placeholder="下限" />
<span class="sep">至</span>
<el-input v-model="row.longestMax" size="small" placeholder="上限" />
<div class="control">
<el-input v-model="row.longestMin" size="small" placeholder="下限" />
<span class="sep">至</span>
<el-input v-model="row.longestMax" size="small" placeholder="上限" />
</div>
</div>
<div class="field">
<span class="label">围长 cm</span>
<el-input v-model="row.girthMin" size="small" placeholder="下限" />
<span class="sep">至</span>
<el-input v-model="row.girthMax" size="small" placeholder="上限" />
<div class="control">
<el-input v-model="row.girthMin" size="small" placeholder="下限" />
<span class="sep">至</span>
<el-input v-model="row.girthMax" size="small" placeholder="上限" />
</div>
</div>
<div class="field">
<span class="label">三边之和 cm</span>
<el-input v-model="row.sumMin" size="small" placeholder="下限" />
<span class="sep">至</span>
<el-input v-model="row.sumMax" size="small" placeholder="上限" />
<div class="control">
<el-input v-model="row.sumMin" size="small" placeholder="下限" />
<span class="sep">至</span>
<el-input v-model="row.sumMax" size="small" placeholder="上限" />
</div>
</div>
<div class="field span-2">
<div class="field">
<span class="label">说明</span>
<el-input v-model="row.remark" size="small" placeholder="例如拒收、地址错误退回" />
<div class="control">
<el-input v-model="row.remark" size="small" placeholder="例如拒收、地址错误退回" />
</div>
</div>
</div>
</div>
@@ -265,7 +364,6 @@
</div>
<template #footer>
<el-button :disabled="!products.length" :loading="copying" @click="copyPrevious">复制上一版本</el-button>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="save">保存</el-button>
</template>
@@ -273,7 +371,7 @@
</template>
<script setup>
import { listPay, listPaySurchargeRule, previousPaySurchargeRule, savePaySurchargeRule } from '@/api/price/pay'
import { getPay, listPay, listPaySurchargeRule, savePaySurchargeRule } from '@/api/price/pay'
const { proxy } = getCurrentInstance()
@@ -293,12 +391,20 @@ const ruleDefs = [
label: '超重续收',
hint: '超过某个重量后只收超出部分。偏远按公斤且有最低消费的,把邮编和最低消费写在这一条。'
},
{ value: 'POSTCODE', label: '命中邮编', switchOnly: true },
{ value: 'ZONE', label: '命中分区', switchOnly: true },
{
value: 'POSTCODE',
label: '命中邮编',
hint: '按附加费里的高、中、低区填写邮编前缀。订单邮编命中这一区,才加这一区的附加费。前缀、完整邮编和区间都认。国家不填则不限目的国。'
},
{
value: 'ZONE',
label: '命中分区',
hint: '写法和命中邮编一样,表示这些邮编属于哪个地区。命中后按该地区的附加费收取。运费用的地区仍按应付邮编配置。'
},
{
value: 'ALWAYS',
label: '每票都收',
hint: '燃油、去碳、道路通行、旺季这类。按比例的填写比例和基数,旺季填写生效日期。'
hint: '每一条都要选费用项,没被选中的费用不会按每票收取。燃油、去碳按比例填写比例和基数,旺季再填生效日期。'
},
{
value: 'RETURN',
@@ -309,11 +415,12 @@ const ruleDefs = [
const visible = ref(false)
const saving = ref(false)
const copying = ref(false)
const searching = ref(false)
const keyword = ref('')
const found = ref([])
const products = ref([])
const feeOptions = ref([])
const zoneOptions = ref([])
const enabled = reactive({})
const groups = reactive({})
@@ -327,8 +434,11 @@ function blank(rule) {
if (rule === 'EXTRA_KG') {
return { thresholdKg: '', stepKg: '1', weightBasis: 'ACTUAL', minCharge: '', postcodes: '' }
}
if (rule === 'POSTCODE' || rule === 'ZONE') {
return { zoneName: '', postcodes: '', country: '' }
}
if (rule === 'ALWAYS') {
return { rateMode: 'FIXED', ratio: '', chargeBase: '', periodStart: '', periodEnd: '', remark: '' }
return { feeName: '', scId: '', rateMode: 'FIXED', ratio: '', chargeBase: '', periodStart: '', periodEnd: '', remark: '' }
}
return { chargeMode: 'SAME', weightMin: '', weightMax: '', matchMode: '', longestMin: '', longestMax: '', girthMin: '', girthMax: '', sumMin: '', sumMax: '', remark: '' }
}
@@ -344,9 +454,10 @@ function reset() {
keyword.value = ''
found.value = []
products.value = []
feeOptions.value = []
zoneOptions.value = []
blankGroups()
saving.value = false
copying.value = false
searching.value = false
}
@@ -357,32 +468,82 @@ function open(rows) {
for (const row of rows || []) addProduct(row, skipped)
if (skipped.length) proxy.$modal.msgWarning('有产品还没有生效版本,未带入:' + skipped.join('、'))
const first = products.value[0]
if (first) loadRules(first.productId, first.termId)
if (first) loadRules(first.productId)
}
function versionOf(row) {
return {
key: String(row.versionProductId || row.productId) + ':' + String(row.termId || ''),
productId: row.versionProductId || row.productId,
termId: row.termId,
versionNo: row.versionNo,
key: String(row.productId),
productId: row.productId,
feeProductId: row.versionProductId || row.productId,
productName: row.productName || '未命名产品'
}
}
function addProduct(row, skipped) {
const item = versionOf(row)
if (!item.productId || !item.termId) {
if (!item.productId) {
if (skipped) skipped.push(item.productName)
else proxy.$modal.msgWarning(item.productName + '还没有生效版本,不能绑定')
else proxy.$modal.msgWarning(item.productName + '不能绑定')
return
}
if (products.value.some(product => product.key === item.key)) return
products.value.push(item)
loadFees()
}
function removeProduct(item) {
products.value = products.value.filter(product => product.key !== item.key)
loadFees()
}
let feeToken = 0
function loadFees() {
const token = ++feeToken
const bound = products.value.slice()
if (!bound.length) {
feeOptions.value = []
zoneOptions.value = keepZones([])
return
}
Promise.all(bound.map(item => getPay(item.feeProductId || item.productId).catch(() => null))).then(results => {
if (token !== feeToken) return
const map = new Map()
const zones = new Set()
results.forEach(res => {
const rows = res?.data?.surcharges || []
rows.forEach(fee => {
const name = String(fee.feeName || '').trim()
const zone = String(fee.zoneName || '').trim()
if (zone) zones.add(zone)
if (!name || map.has(name)) return
map.set(name, { feeName: name, scId: fee.scId || '' })
})
})
zoneOptions.value = keepZones([...zones])
for (const row of groups.ALWAYS || []) {
const name = String(row.feeName || '').trim()
if (name && !map.has(name)) map.set(name, { feeName: name, scId: row.scId || '' })
}
feeOptions.value = [...map.values()]
})
}
function keepZones(zones) {
const set = new Set(zones)
for (const rule of ['POSTCODE', 'ZONE']) {
for (const row of groups[rule] || []) {
const zone = String(row.zoneName || '').trim()
if (zone) set.add(zone)
}
}
return [...set]
}
function pickFee(row, name) {
const found = feeOptions.value.find(item => item.feeName === name)
row.scId = found?.scId || ''
}
function termText(row) {
@@ -422,7 +583,8 @@ function applyRules(list) {
const row = blank(rule)
const detail = item.detail || {}
Object.keys(row).forEach(key => {
if (detail[key] !== undefined && detail[key] !== null) row[key] = String(detail[key])
if (detail[key] === undefined || detail[key] === null) return
row[key] = key === 'periodStart' || key === 'periodEnd' ? asDate(detail[key]) : String(detail[key])
})
groups[rule].push(row)
}
@@ -430,10 +592,26 @@ function applyRules(list) {
if (rule.switchOnly) continue
if (enabled[rule.value] && !groups[rule.value].length) groups[rule.value].push(blank(rule.value))
}
const map = new Map(feeOptions.value.map(item => [item.feeName, item]))
for (const row of groups.ALWAYS || []) {
const name = String(row.feeName || '').trim()
if (name && !map.has(name)) map.set(name, { feeName: name, scId: row.scId || '' })
}
feeOptions.value = [...map.values()]
zoneOptions.value = keepZones(zoneOptions.value)
}
function loadRules(productId, termId) {
listPaySurchargeRule(productId, termId).then(res => applyRules(res.data || []))
function asDate(value) {
const text = String(value).trim()
if (/^\d{4}-\d{2}-\d{2}/.test(text)) return text.slice(0, 10)
const monthDay = text.match(/^(\d{1,2})-(\d{1,2})$/)
if (!monthDay) return ''
const year = new Date().getFullYear()
return `${year}-${monthDay[1].padStart(2, '0')}-${monthDay[2].padStart(2, '0')}`
}
function loadRules(productId) {
listPaySurchargeRule(productId).then(res => applyRules(res.data || []))
}
const openedRules = computed(() => ruleDefs.filter(rule => enabled[rule.value] && !rule.switchOnly))
@@ -465,39 +643,45 @@ function payloadRules() {
return rules
}
function copyPrevious() {
const first = products.value[0]
if (!first) return
copying.value = true
previousPaySurchargeRule(first.productId).then(res => {
const list = res.data || []
if (!list.length) {
proxy.$modal.msgWarning('上一版本没有计费条件')
return
}
applyRules(list)
proxy.$modal.msgSuccess('已带入上一版本,保存后写到当前版本')
}).finally(() => {
copying.value = false
})
}
function save() {
if (!products.value.length) {
proxy.$modal.msgError('请选择要绑定的产品')
return
}
for (const rule of ['POSTCODE', 'ZONE']) {
if (!enabled[rule]) continue
const label = rule === 'POSTCODE' ? '命中邮编' : '命中分区'
for (const row of groups[rule]) {
if (!String(row.zoneName || '').trim() || !String(row.postcodes || '').trim()) {
proxy.$modal.msgError(label + '要选择分区,并填写邮编前缀')
return
}
}
}
if (enabled.ALWAYS) {
const names = new Set()
for (const row of groups.ALWAYS) {
const name = String(row.feeName || '').trim()
if (!name) {
proxy.$modal.msgError('每票都收要选择费用项')
return
}
if (names.has(name)) {
proxy.$modal.msgError('每票都收的费用项不能重复:' + name)
return
}
names.add(name)
}
}
saving.value = true
savePaySurchargeRule({
products: products.value.map(item => ({
productId: item.productId,
termId: item.termId,
versionNo: item.versionNo,
productName: item.productName
})),
rules: payloadRules()
}).then(() => {
proxy.$modal.msgSuccess('已保存到当前版本')
proxy.$modal.msgSuccess('已保存,计算时按当前生效版本上的附加费执行')
visible.value = false
}).finally(() => {
saving.value = false
@@ -519,12 +703,16 @@ defineExpose({ open })
.rule-switches { display: flex; flex-wrap: wrap; gap: 12px 20px; margin-top: 8px; }
.rule-switch { display: flex; align-items: center; gap: 8px; }
.rule-bar, .rule-card-bar { display: flex; align-items: center; justify-content: space-between; }
.rule-card { border: 1px solid var(--el-border-color); border-radius: 6px; padding: 8px 12px 12px; margin-bottom: 10px; }
.rule-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px 16px; }
.field { display: flex; align-items: center; gap: 6px; min-width: 0; }
.field .label { width: 118px; flex: none; font-size: 13px; color: var(--el-text-color-regular); }
.field .el-input, .field .el-select { flex: 1; min-width: 0; }
.field .sep { flex: none; color: var(--el-text-color-secondary); }
.field .mode { flex: 0 0 118px; }
.span-2 { grid-column: 1 / -1; }
.rule-card { border: 1px solid var(--el-border-color); border-radius: 6px; padding: 12px 20px 12px 28px; margin-bottom: 10px; overflow: visible; }
.rule-grid { display: grid; grid-template-columns: 1fr 1fr; column-gap: 48px; row-gap: 12px; align-items: center; }
.field { display: grid; grid-template-columns: 104px minmax(0, 1fr); column-gap: 8px; align-items: center; min-width: 0; }
.field .label { width: 104px; min-width: 0; justify-self: end; text-align: right; white-space: nowrap; overflow: visible; font-size: 13px; line-height: 24px; color: var(--el-text-color-regular); }
.control { display: flex; align-items: center; gap: 6px; min-width: 0; }
.control :deep(.el-input),
.control :deep(.el-select),
.control :deep(.el-textarea),
.control :deep(.el-date-editor) { flex: 1 1 0; width: 0 !important; min-width: 0; max-width: none; }
.control :deep(.mode) { flex: 1.7 1 0; }
.control :deep(.num) { flex: 0.75 1 0; }
.field .sep { flex: none; color: var(--el-text-color-secondary); line-height: 24px; }
</style>
+62 -21
View File
@@ -8,6 +8,7 @@
:search-per-row="4"
search-action-align="right"
row-key="productId"
scrollbar-always-on
>
<template #toolbar="{ selection, ids }">
<el-col :span="1.5">
@@ -87,31 +88,34 @@
<el-table-column label="生效结束" width="110" align="center">
<template #default="{ row }">{{ dayText(row.effectiveEnd) || '不限' }}</template>
</el-table-column>
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-switch
v-if="row.termId"
:model-value="row.status === '1' ? '1' : '2'"
active-value="1"
inactive-value="2"
inline-prompt
active-text="生效"
inactive-text="失效"
@change="(val) => changeStatus(row, 'status', val)"
v-hasPermi="['price:pay:edit']"
/>
</template>
</el-table-column>
<el-table-column label="操作" width="210" align="center">
<el-table-column label="操作" width="220" align="center">
<template #default="{ row }">
<el-button link type="primary" @click="downloadSource(row)" v-hasPermi="['price:pay:query']">下载</el-button>
<el-button link type="primary" @click="openTermDialog(row)" v-hasPermi="['price:pay:edit']">修改有效期</el-button>
<el-button link type="primary" @click="openRuleLog(row)" v-hasPermi="['price:pay:query']">附加费记录</el-button>
<el-button link type="primary" @click="openDetail(row)" v-hasPermi="['price:pay:query']">查看</el-button>
</template>
</el-table-column>
</el-table>
</el-drawer>
<el-dialog v-model="ruleLogVisible" :title="ruleLogTitle" width="860px" append-to-body>
<el-table v-loading="ruleLogLoading" :data="ruleLogRows" border size="small" empty-text="这一版还没有附加费配置记录">
<el-table-column label="操作" width="70" align="center">
<template #default="{ row }">{{ ruleLogAction(row.operationType) }}</template>
</el-table-column>
<el-table-column label="更改前" min-width="220" show-overflow-tooltip>
<template #default="{ row }">{{ ruleLogSide(row, 'before') }}</template>
</el-table-column>
<el-table-column label="更改后" min-width="220" show-overflow-tooltip>
<template #default="{ row }">{{ ruleLogSide(row, 'after') }}</template>
</el-table-column>
<el-table-column prop="createBy" label="操作人" width="100" align="center" />
<el-table-column label="操作时间" width="160" align="center">
<template #default="{ row }">{{ row.createTime || '-' }}</template>
</el-table-column>
</el-table>
</el-dialog>
<el-dialog v-model="termVisible" title="修改有效期" width="460px" append-to-body>
<el-form label-width="90px">
<el-form-item label="生效开始" required>
@@ -188,7 +192,7 @@
</template>
<script setup name="PricePay">
import { listPay, getPay, delPay, updatePay, listPayTerm, updatePayTerm, changePayTermStatus, comparePay } from '@/api/price/pay'
import { listPay, getPay, delPay, updatePay, listPayTerm, updatePayTerm, changePayTermStatus, comparePay, listPaySurchargeRuleLog } from '@/api/price/pay'
import PayImportDialog from './components/PayImportDialog.vue'
import PayConfigDetail from './components/PayConfigDetail.vue'
import PaySurchargeRuleDialog from './components/PaySurchargeRuleDialog.vue'
@@ -207,6 +211,10 @@ const historyRows = ref([])
const historyTitle = ref('价格版本')
const historyProductId = ref(null)
const historyPicked = ref([])
const ruleLogVisible = ref(false)
const ruleLogLoading = ref(false)
const ruleLogRows = ref([])
const ruleLogTitle = ref('附加费配置记录')
const compareVisible = ref(false)
const compareLoading = ref(false)
const compareTitle = ref('版本比对')
@@ -233,8 +241,7 @@ const statusOptions = [
]
const columns = [
{ label: '产品编号', prop: 'productId', align: 'center', width: 90 },
{ label: '产品名称', prop: 'productName', align: 'center', minWidth: 150, showOverflowTooltip: true },
{ label: '产品名称', prop: 'productName', align: 'center', minWidth: 180, showOverflowTooltip: true, fixed: 'left' },
{ label: '价卡归属', prop: 'supplierName', align: 'center', width: 100 },
{ label: '国家/口岸', prop: 'countryScope', align: 'center', minWidth: 120, showOverflowTooltip: true },
{ label: '费用类型', prop: 'chargeType', align: 'center', width: 110 },
@@ -384,9 +391,43 @@ function termText(row) {
return `${start || '不限'} 至 ${end || '不限'}`
}
function openRuleLog(row) {
ruleLogTitle.value = `${row.productName || '产品'} 第 ${row.versionNo || '-'} 版的附加费配置记录`
ruleLogRows.value = []
ruleLogVisible.value = true
ruleLogLoading.value = true
listPaySurchargeRuleLog(row.productId, row.termId).then(res => {
ruleLogRows.value = res.data || []
}).finally(() => {
ruleLogLoading.value = false
})
}
function ruleLogAction(type) {
if (type === '1') return '新增'
if (type === '2') return '修改'
if (type === '3') return '删除'
return '-'
}
function ruleLogSide(row, side) {
let content = row.changeContent
if (typeof content === 'string') {
try {
content = JSON.parse(content)
} catch (error) {
return side === 'after' ? content : '-'
}
}
const values = content?.[side]
if (!values || typeof values !== 'object') return '-'
const text = Object.entries(values).map(([label, value]) => `${label}:${value}`).join(';')
return text || '-'
}
function openHistory(row) {
historyProductId.value = row.productId
historyTitle.value = `${row.productName || '产品'}(编号 ${row.productId})的价格版本`
historyTitle.value = `${row.productName || '产品'}的价格版本`
historyPicked.value = []
historyVisible.value = true
loadHistory()