套餐明细附加费显示
This commit is contained in:
+2
-2
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user