套餐明细附加费显示

This commit is contained in:
wuweihua
2026-09-08 14:28:09 +08:00
parent 520fc32a4e
commit 49b1d2c321
4 changed files with 254 additions and 12 deletions
+117 -4
View File
@@ -77,7 +77,7 @@
:closable="false"
show-icon
class="mb12"
title="FIXED 模式:可不填明细(计费取 bms_product_country 默认运费/挂号费);有明细时按产品×国家×重量段覆盖固定价。"
title="FIXED 模式:可不填明细(计费取 bms_product_country 默认运费/挂号费);有明细时按产品×国家×重量段覆盖固定价。附加费来自产品关联,只读展示。"
/>
<el-alert
v-else
@@ -157,6 +157,41 @@
<el-input-number v-model="row.discountValue" :min="1" :max="99" :precision="2" controls-position="right" style="width: 100%" placeholder="空=默认折扣" />
</template>
</el-table-column>
<el-table-column label="附加费" min-width="220">
<template #default="{ row }">
<div v-if="!row.productCode" class="muted">-</div>
<el-popover
v-else-if="surchargesOf(row).length"
placement="top"
:width="400"
trigger="hover"
>
<template #default>
<div class="sc-pop-list">
<div v-for="s in surchargesOf(row)" :key="s.relId || s.scId" class="sc-pop-item">
<div class="sc-pop-name">{{ s.scName }}</div>
<div class="sc-pop-meta">
{{ chargeTypeLabel(s.chargeType) }} · {{ formatChargeValue(s) }}
· {{ surchargeScopeLabel(s) }}
</div>
</div>
</div>
</template>
<template #reference>
<div class="sc-tags">
<el-tag
v-for="s in surchargesOf(row)"
:key="s.relId || s.scId"
size="small"
type="danger"
effect="plain"
>{{ s.scName }} {{ formatChargeValue(s) }}</el-tag>
</div>
</template>
</el-popover>
<span v-else class="muted">无</span>
</template>
</el-table-column>
<el-table-column label="取值方式" min-width="130">
<template #default="{ row }">
<el-select v-model="row.roundMode" placeholder="取值方式" style="width: 100%">
@@ -253,9 +288,10 @@
<script setup name="PriceSetCreate">
import { getPriceSet, savePriceSet, validatePriceSet } from '@/api/price/set'
import { listProduct, listProductCountries } from '@/api/price/product'
import { listSurchargeByProduct } from '@/api/price/surcharge'
const { proxy } = getCurrentInstance()
const { bms_round_mode } = useDict('bms_round_mode')
const { bms_round_mode, bms_charge_type } = useDict('bms_round_mode', 'bms_charge_type')
const route = useRoute()
const active = ref(0)
const saving = ref(false)
@@ -264,6 +300,9 @@ const checkItems = ref([])
const productOptions = ref([])
/** productCode -> bms_product_country rows */
const countryCache = reactive({})
/** productCode -> 有效附加费关联 */
const surchargeCache = reactive({})
const surchargePending = {}
const form = reactive({
priceSetId: undefined,
@@ -329,6 +368,54 @@ function ensureCountries(productCode) {
})
}
function ensureSurcharges(productCode) {
if (!productCode) return Promise.resolve([])
if (surchargeCache[productCode]) return Promise.resolve(surchargeCache[productCode])
if (surchargePending[productCode]) return surchargePending[productCode]
surchargePending[productCode] = listSurchargeByProduct(productCode).then(res => {
const rows = (res.data || []).filter(item => String(item.ssCode || '').toLowerCase() !== 'of')
surchargeCache[productCode] = rows
return rows
}).catch(() => {
surchargeCache[productCode] = []
return []
}).finally(() => {
delete surchargePending[productCode]
})
return surchargePending[productCode]
}
function surchargesOf(row) {
if (!row || !row.productCode) return []
const all = surchargeCache[row.productCode] || []
return all.filter(s => {
if (s.countryCode && row.countryCode && String(s.countryCode).toUpperCase() !== String(row.countryCode).toUpperCase()) {
return false
}
if (s.detailId == null) return true
if (row.productCountryId != null && Number(s.detailId) === Number(row.productCountryId)) return true
return false
})
}
function chargeTypeLabel(val) {
const found = (bms_charge_type.value || []).find(c => c.value === val)
return found ? found.label : (val || '-')
}
function formatChargeValue(row) {
if (row.chargeValue == null) return '-'
const v = Number(row.chargeValue)
if (row.chargeType === 'RATE') return (v * 100).toFixed(2) + '%'
return v + ' ' + (row.currency || 'CNY')
}
function surchargeScopeLabel(s) {
if (s.detailId == null) return '产品级'
const w = s.weightMin != null && s.weightMax != null ? `${s.weightMin}-${s.weightMax}kg` : '-'
return `${s.countryName || '-'} · ${w}`
}
function countryOptionsOf(row) {
const rows = countryCache[row.productCode] || []
const map = new Map()
@@ -411,8 +498,9 @@ function onProductChange(row) {
const found = productOptions.value.find(p => p.value === row.productCode)
row.productName = found ? found.name : row.productCode
clearCascadeAfterProduct(row)
if (!row.productCode || form.setType !== 'FIXED') return
ensureCountries(row.productCode)
if (!row.productCode) return
ensureSurcharges(row.productCode)
if (form.setType === 'FIXED') ensureCountries(row.productCode)
}
function onCountryChange(row) {
@@ -443,6 +531,7 @@ function hydrateDetailRow(d) {
_countryKey: countryKeyOf(d)
}
// normalize historical weight labels to min≤W≤max when possible
if (row.productCode) ensureSurcharges(row.productCode)
if (row.productCode && form.setType === 'FIXED') {
ensureCountries(row.productCode).then(rows => {
if (!row.weightLabel) return
@@ -660,4 +749,28 @@ onMounted(() => {
text-align: center;
padding: 8px 0 24px;
}
.muted {
color: #c0c4cc;
}
.sc-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
cursor: pointer;
}
.sc-pop-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.sc-pop-item {
line-height: 1.4;
}
.sc-pop-name {
font-weight: 600;
}
.sc-pop-meta {
font-size: 12px;
color: #909399;
}
</style>
+132 -3
View File
@@ -76,6 +76,40 @@
<el-table-column v-if="set.setType === 'DISCOUNT'" label="产品折扣" min-width="110">
<template #default="{ row }">{{ row.discountValue != null ? row.discountValue + '%' : '默认折扣' }}</template>
</el-table-column>
<el-table-column label="附加费" min-width="220">
<template #default="{ row }">
<el-popover
v-if="surchargesOf(row).length"
placement="top"
:width="400"
trigger="hover"
>
<template #default>
<div class="sc-pop-list">
<div v-for="s in surchargesOf(row)" :key="s.relId || s.scId" class="sc-pop-item">
<div class="sc-pop-name">{{ s.scName }}</div>
<div class="sc-pop-meta">
{{ chargeTypeLabel(s.chargeType) }} · {{ formatChargeValue(s) }}
· {{ surchargeScopeLabel(s) }}
</div>
</div>
</div>
</template>
<template #reference>
<div class="sc-tags">
<el-tag
v-for="s in surchargesOf(row)"
:key="s.relId || s.scId"
size="small"
type="danger"
effect="plain"
>{{ s.scName }} {{ formatChargeValue(s) }}</el-tag>
</div>
</template>
</el-popover>
<span v-else class="muted">无</span>
</template>
</el-table-column>
<el-table-column label="取值方式" min-width="110" align="center">
<template #default="{ row }">{{ roundModeLabel(row.roundMode) }}</template>
</el-table-column>
@@ -183,6 +217,18 @@
<el-option v-for="d in bms_round_mode" :key="d.value" :label="d.label" :value="d.value" />
</el-select>
</el-form-item>
<el-form-item v-if="detailForm.productCode" label="附加费">
<div v-if="detailSurcharges.length" class="sc-tags">
<el-tag
v-for="s in detailSurcharges"
:key="s.relId || s.scId"
size="small"
type="danger"
effect="plain"
>{{ s.scName }} {{ formatChargeValue(s) }}</el-tag>
</div>
<span v-else class="muted">该产品暂无匹配附加费</span>
</el-form-item>
<!-- <el-form-item label="关联 pr_id" prop="prId">
<el-input v-model="detailForm.prId" placeholder="可选" />
</el-form-item> -->
@@ -226,15 +272,18 @@ import {
addSetCustomer, updateSetCustomer, delSetCustomer
} from '@/api/price/set'
import { listProduct, listProductCountries } from '@/api/price/product'
import { listSurchargeByProduct } from '@/api/price/surcharge'
import { parseTime } from '@/utils/ruoyi'
const { proxy } = getCurrentInstance()
const { bms_round_mode } = useDict('bms_round_mode')
const { bms_round_mode, bms_charge_type } = useDict('bms_round_mode', 'bms_charge_type')
const route = useRoute()
const loading = ref(false)
const set = ref({})
const productOptions = ref([])
const countryCache = reactive({})
const surchargeCache = reactive({})
const surchargePending = {}
const detailOpen = ref(false)
const detailTitle = ref('')
@@ -287,6 +336,56 @@ function ensureCountries(productCode) {
})
}
function ensureSurcharges(productCode) {
if (!productCode) return Promise.resolve([])
if (surchargeCache[productCode]) return Promise.resolve(surchargeCache[productCode])
if (surchargePending[productCode]) return surchargePending[productCode]
surchargePending[productCode] = listSurchargeByProduct(productCode).then(res => {
const rows = (res.data || []).filter(item => String(item.ssCode || '').toLowerCase() !== 'of')
surchargeCache[productCode] = rows
return rows
}).catch(() => {
surchargeCache[productCode] = []
return []
}).finally(() => {
delete surchargePending[productCode]
})
return surchargePending[productCode]
}
function surchargesOf(row) {
if (!row || !row.productCode) return []
const all = surchargeCache[row.productCode] || []
return all.filter(s => {
if (s.countryCode && row.countryCode && String(s.countryCode).toUpperCase() !== String(row.countryCode).toUpperCase()) {
return false
}
if (s.detailId == null) return true
if (row.productCountryId != null && Number(s.detailId) === Number(row.productCountryId)) return true
return false
})
}
function chargeTypeLabel(val) {
const found = (bms_charge_type.value || []).find(c => c.value === val)
return found ? found.label : (val || '-')
}
function formatChargeValue(row) {
if (row.chargeValue == null) return '-'
const v = Number(row.chargeValue)
if (row.chargeType === 'RATE') return (v * 100).toFixed(2) + '%'
return v + ' ' + (row.currency || 'CNY')
}
function surchargeScopeLabel(s) {
if (s.detailId == null) return '产品级'
const w = s.weightMin != null && s.weightMax != null ? `${s.weightMin}-${s.weightMax}kg` : '-'
return `${s.countryName || '-'} · ${w}`
}
const detailSurcharges = computed(() => surchargesOf(detailForm.value))
const detailCountryOptions = computed(() => {
const rows = countryCache[detailForm.value.productCode] || []
const map = new Map()
@@ -342,7 +441,9 @@ function onDetailProductChange() {
detailForm.value.productCountryId = undefined
detailForm.value.fvValue = undefined
detailForm.value.fvRegistrationFee = undefined
if (detailForm.value.productCode && set.value.setType === 'FIXED') {
if (!detailForm.value.productCode) return
ensureSurcharges(detailForm.value.productCode)
if (set.value.setType === 'FIXED') {
ensureCountries(detailForm.value.productCode)
}
}
@@ -397,6 +498,8 @@ function load() {
loading.value = true
getPriceSet(id).then(res => {
set.value = res.data || {}
const codes = [...new Set((set.value.details || []).map(d => d.productCode).filter(Boolean))]
codes.forEach(code => ensureSurcharges(code))
}).finally(() => {
loading.value = false
})
@@ -471,7 +574,9 @@ function openDetailDialog(row) {
}
detailTitle.value = row ? '编辑明细' : '添加明细'
detailOpen.value = true
if (detailForm.value.productCode && set.value.setType === 'FIXED') {
if (!detailForm.value.productCode) return
ensureSurcharges(detailForm.value.productCode)
if (set.value.setType === 'FIXED') {
ensureCountries(detailForm.value.productCode)
}
}
@@ -583,4 +688,28 @@ onMounted(() => {
color: #d48806;
font-weight: 600;
}
.muted {
color: #c0c4cc;
}
.sc-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
cursor: pointer;
}
.sc-pop-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.sc-pop-item {
line-height: 1.4;
}
.sc-pop-name {
font-weight: 600;
}
.sc-pop-meta {
font-size: 12px;
color: #909399;
}
</style>