套餐明细附加费显示

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
@@ -54,9 +54,9 @@ public class SurchargeController extends BaseController
}
/**
* 查询指定产品已关联的附加费(非分页,供产品列表抽屉使用)
* 查询指定产品已关联的附加费(非分页,供产品列表抽屉、套餐明细展示使用)
*/
@PreAuthorize("@ss.hasPermi('price:surcharge:query')")
@PreAuthorize("@ss.hasPermi('price:surcharge:query') or @ss.hasPermi('price:set:query') or @ss.hasPermi('price:set:add')")
@GetMapping("/byProduct/{productCode}")
public AjaxResult listByProduct(@PathVariable String productCode)
{
+3 -3
View File
@@ -1,5 +1,5 @@
-- ----------------------------
-- BMS price set: 客户中心 · 价格套餐管理
-- BMS price set: 套餐管理 · 价格套餐管理
-- charset: utf8mb4
-- menu_id: 2200-2210
-- 相对 PRD《价格表优化建表.sql》差异:
@@ -77,9 +77,9 @@ CREATE TABLE IF NOT EXISTS prc_customer_price_set (
CONSTRAINT fk_customer_price_set FOREIGN KEY (price_set_id) REFERENCES prc_price_set (price_set_id) ON DELETE RESTRICT
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='客户套餐表-客户某时间段内使用的套餐';
-- 客户中心目录
-- 套餐管理目录
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
SELECT 2200, N'客户中心', 0, 8, 'customer', NULL, '', '', 1, 0, 'M', '0', '0', '', 'peoples', 'admin', sysdate(), N'客户中心目录'
SELECT 2200, N'套餐管理', 0, 8, 'customer', NULL, '', '', 1, 0, 'M', '0', '0', '', 'peoples', 'admin', sysdate(), N'套餐管理目录'
FROM DUAL WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE menu_id = 2200);
INSERT INTO sys_menu (menu_id, menu_name, parent_id, order_num, path, component, query, route_name, is_frame, is_cache, menu_type, visible, status, perms, icon, create_by, create_time, remark)
+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>