提交产品列表

This commit is contained in:
wuweihua
2026-09-07 18:09:59 +08:00
parent 3994bd7998
commit 9e98cd791e
14 changed files with 528 additions and 39 deletions
+48
View File
@@ -5,6 +5,7 @@
:columns="columns"
:search-columns="searchColumns"
:query="queryList"
:span-method="objectSpanMethod"
row-key="regionId"
>
<template #toolbar="{ selection, ids }">
@@ -285,6 +286,53 @@ function queryList(params) {
return listRegion(params)
}
// 单元格合并:按「洲/大区」continent 合并相同大区的行(仅作用于 continent 列)
// 关键:el-table 在 :data 引用变化时会重新调用 span-method,
// 但这里 BasicTable 内部的 tableData 是异步请求回来的,父组件拿不到其引用,
// 因此用 watch + nextTick + el-table.doLayout() 强制刷新。
const spanMap = ref([])
function rebuildSpanMap() {
const rows = tableRef.value?.getData?.() || []
const map = new Array(rows.length)
for (let i = 0; i < rows.length; i++) {
const cur = rows[i]?.continent
if (i > 0 && rows[i - 1]?.continent === cur) {
map[i] = { rowspan: 0, colspan: 0 }
} else {
let count = 1
while (i + count < rows.length && rows[i + count]?.continent === cur) count++
map[i] = { rowspan: count, colspan: 1 }
}
}
spanMap.value = map
// 强制 el-table 重新调用 span-method
tableRef.value?.getTableRef?.()?.doLayout?.()
}
function objectSpanMethod({ column, rowIndex }) {
if (column.property !== 'continent') return { rowspan: 1, colspan: 1 }
return spanMap.value[rowIndex] || { rowspan: 1, colspan: 1 }
}
// 监听 BasicTable 数据变化
// 关键:expose.data 是 ref 对象;getData() 是 ()=>tableData.value 函数
// watch 源用 getData() 函数(Vue 会调用 source getter 并比较返回值),
// 每次 tableData.value 被重新赋值(分页/搜索/刷新),返回值(新数组)变化,watch 触发
onMounted(() => {
let lastLen = -1
watch(
() => {
const ref = tableRef.value?.data
return ref?.value || []
},
(rows) => {
if (rows.length !== lastLen) {
lastLen = rows.length
nextTick(rebuildSpanMap)
}
},
{ immediate: true }
)
})
const data = reactive({
form: {},
rules: {
@@ -20,10 +20,10 @@
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['price:product:add']">新建产品</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="success" plain icon="Edit" :disabled="selection.length !== 1" @click="handleUpdate(selection[0])" v-hasPermi="['price:product:edit']">修改</el-button>
<el-button type="success" plain icon="Edit" :disabled="selection.length !== 1 || (selection[0] && isProductActive(selection[0]))" @click="handleUpdate(selection[0])" v-hasPermi="['price:product:edit']">修改</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="danger" plain icon="Delete" :disabled="!selection.length" @click="handleDelete(ids)" v-hasPermi="['price:product:remove']">删除</el-button>
<el-button type="danger" plain icon="Delete" :disabled="!selection.length || selection.some(isProductActive)" @click="handleDelete(ids)" v-hasPermi="['price:product:remove']">删除</el-button>
</el-col>
<el-col :span="1.5">
<el-button type="info" plain icon="Upload" @click="handleImport" v-hasPermi="['price:product:import']">导入 Excel</el-button>
@@ -48,8 +48,8 @@
<template #col-action="{ row }">
<el-button link type="primary" icon="View" @click="openCountries(row)" v-hasPermi="['price:product:query']">产品明细</el-button>
<el-button link type="primary" icon="Edit" @click="handleUpdate(row)" v-hasPermi="['price:product:edit']">编辑</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(row.productId)" v-hasPermi="['price:product:remove']">删除</el-button>
<el-button :disabled="isProductActive(row)" :title="isProductActive(row) ? '生效中不可编辑' : ''" link type="primary" icon="Edit" @click="handleUpdate(row)" v-hasPermi="['price:product:edit']">编辑</el-button>
<el-button :disabled="isProductActive(row)" :title="isProductActive(row) ? '生效中不可删除' : ''" link type="primary" icon="Delete" @click="handleDelete(row.productId)" v-hasPermi="['price:product:remove']">删除</el-button>
</template>
</basic-table>
@@ -123,7 +123,8 @@
</el-dialog>
<el-drawer v-model="countryOpen" size="920px" :title="countryTitle" append-to-body>
<el-table v-loading="countryLoading" :data="countryList" border empty-text="暂无国家明细">
<el-input v-model="countryKeyword" placeholder="搜索国家(中文名 / ISO2)" clearable class="mb8" style="width: 260px" />
<el-table v-loading="countryLoading" :data="filteredCountryList" border :span-method="countrySpanMethod" max-height="calc(100vh - 120px)" empty-text="暂无国家明细">
<el-table-column label="国家" prop="countryName" width="90" show-overflow-tooltip />
<el-table-column label="ISO2" prop="countryCode" width="60" align="center" />
<el-table-column label="重量段(KG)" width="120" align="center">
@@ -173,6 +174,15 @@ const countryOpen = ref(false)
const countryTitle = ref('')
const countryLoading = ref(false)
const countryList = ref([])
const countryKeyword = ref('')
const filteredCountryList = computed(() => {
const kw = countryKeyword.value.trim().toLowerCase()
if (!kw) return countryList.value
return countryList.value.filter(r =>
String(r.countryName || '').toLowerCase().includes(kw) ||
String(r.countryCode || '').toLowerCase().includes(kw)
)
})
const columns = [
{ prop: 'productCode', label: '产品代码', minWidth: 110 },
@@ -225,6 +235,15 @@ function queryList(params) {
return listProduct(params)
}
// 判断产品是否"生效中":status='0'(未停用)且 effectiveDate <= 今天
function isProductActive(row) {
if (!row) return false
if (row.status !== '0') return false
if (!row.effectiveDate) return false
const today = new Date().toISOString().slice(0, 10)
return String(row.effectiveDate).slice(0, 10) <= today
}
function reset() {
form.value = {
productId: undefined,
@@ -249,6 +268,10 @@ function handleAdd() {
}
function handleUpdate(row) {
if (isProductActive(row)) {
proxy.$modal.msgError('生效中的产品不可编辑')
return
}
reset()
getProduct(row.productId).then(res => {
form.value = res.data
@@ -288,12 +311,31 @@ function openCountries(row) {
countryOpen.value = true
countryLoading.value = true
countryList.value = []
countryKeyword.value = ''
listProductCountries(row.productCode).then(res => {
countryList.value = res.data || []
// 按 ISO2 排序,保证相同国家相邻,便于合并单元格
const list = (res.data || []).slice().sort((a, b) => {
return String(a.countryCode || '').localeCompare(String(b.countryCode || ''))
})
countryList.value = list
}).finally(() => {
countryLoading.value = false
})
}
// 弹窗表格合并:相同 countryCode 的连续行合并「国家」「ISO2」列
function countrySpanMethod({ column, rowIndex }) {
if (column.property !== 'countryName' && column.property !== 'countryCode') return
const list = filteredCountryList.value
if (!list.length) return
const cur = list[rowIndex]?.countryCode
if (rowIndex > 0 && list[rowIndex - 1]?.countryCode === cur) {
return { rowspan: 0, colspan: 0 }
}
let count = 1
while (rowIndex + count < list.length && list[rowIndex + count]?.countryCode === cur) count++
return { rowspan: count, colspan: 1 }
}
</script>
<style scoped>
@@ -98,6 +98,7 @@
<el-descriptions-item label="运费单价">{{ formatMoney(result.freightRate) }}</el-descriptions-item>
<el-descriptions-item label="折扣前单价">{{ formatMoney(result.baseFreightRate) }}</el-descriptions-item>
<el-descriptions-item label="折扣%">{{ result.discountPercent != null ? result.discountPercent : '-' }}</el-descriptions-item>
<el-descriptions-item label="取值方式">{{ roundModeLabel(result.roundMode) }}</el-descriptions-item>
<el-descriptions-item label="运费金额">{{ formatMoney(result.freightAmount) }}</el-descriptions-item>
<el-descriptions-item label="挂号费">{{ formatMoney(result.registrationFee) }}</el-descriptions-item>
<el-descriptions-item label="应付总额">
@@ -238,6 +239,15 @@ function sourceLabel(source) {
return map[source] || source || '-'
}
function roundModeLabel(mode) {
const map = {
UP: '向上取值',
DOWN: '向下取整',
HALF_UP: '四舍五入'
}
return map[mode] || mode || '四舍五入'
}
function sourceTagType(source) {
if (source === 'PACKAGE_FIXED' || source === 'FIXED') return 'success'
if (source === 'PACKAGE_DISCOUNT' || source === 'DISCOUNT') return 'warning'
+13 -2
View File
@@ -157,6 +157,13 @@
<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="130">
<template #default="{ row }">
<el-select v-model="row.roundMode" placeholder="取值方式" style="width: 100%">
<el-option v-for="d in bms_round_mode" :key="d.value" :label="d.label" :value="d.value" />
</el-select>
</template>
</el-table-column>
<el-table-column label="操作" width="80" align="center">
<template #default="{ $index }">
<el-button link type="danger" @click="form.details.splice($index, 1)">删除</el-button>
@@ -248,6 +255,7 @@ import { getPriceSet, savePriceSet, validatePriceSet } from '@/api/price/set'
import { listProduct, listProductCountries } from '@/api/price/product'
const { proxy } = getCurrentInstance()
const { bms_round_mode } = useDict('bms_round_mode')
const route = useRoute()
const active = ref(0)
const saving = ref(false)
@@ -366,7 +374,8 @@ function emptyDetail() {
productCountryId: undefined,
fvValue: undefined,
fvRegistrationFee: undefined,
discountValue: undefined
discountValue: undefined,
roundMode: 'HALF_UP'
}
}
@@ -430,6 +439,7 @@ function onWeightChange(row) {
function hydrateDetailRow(d) {
const row = {
...d,
roundMode: d.roundMode || 'HALF_UP',
_countryKey: countryKeyOf(d)
}
// normalize historical weight labels to min≤W≤max when possible
@@ -463,7 +473,8 @@ function buildPayload(submitFlag) {
productCountryId: d.productCountryId || undefined,
fvValue: d.fvValue,
fvRegistrationFee: d.fvRegistrationFee,
discountValue: d.discountValue
discountValue: d.discountValue,
roundMode: d.roundMode || 'HALF_UP'
}))
const customers = (form.customers || [])
.filter(c => c.customerId)
+19 -2
View File
@@ -76,6 +76,9 @@
<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="110" align="center">
<template #default="{ row }">{{ roundModeLabel(row.roundMode) }}</template>
</el-table-column>
<!-- <el-table-column label="关联 pr_id" prop="prId" width="110">
<template #default="{ row }">{{ row.prId || '-' }}</template>
</el-table-column> -->
@@ -175,6 +178,11 @@
<el-form-item v-else label="产品折扣%" prop="discountValue">
<el-input-number v-model="detailForm.discountValue" :min="1" :max="99" controls-position="right" style="width: 100%" />
</el-form-item>
<el-form-item label="取值方式" prop="roundMode">
<el-select v-model="detailForm.roundMode" placeholder="请选择" style="width: 100%">
<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 label="关联 pr_id" prop="prId">
<el-input v-model="detailForm.prId" placeholder="可选" />
</el-form-item> -->
@@ -221,6 +229,7 @@ import { listProduct, listProductCountries } from '@/api/price/product'
import { parseTime } from '@/utils/ruoyi'
const { proxy } = getCurrentInstance()
const { bms_round_mode } = useDict('bms_round_mode')
const route = useRoute()
const loading = ref(false)
const set = ref({})
@@ -232,7 +241,8 @@ const detailTitle = ref('')
const detailFormRef = ref()
const detailForm = ref({})
const detailRules = {
productCode: [{ required: true, message: '请选择产品', trigger: 'change' }]
productCode: [{ required: true, message: '请选择产品', trigger: 'change' }],
roundMode: [{ required: true, message: '请选择取值方式', trigger: 'change' }]
}
const customerOpen = ref(false)
@@ -360,6 +370,11 @@ function onDetailWeightChange() {
const childEditable = computed(() => ['D', 'R', 'E'].includes(set.value.status))
function roundModeLabel(mode) {
const hit = (bms_round_mode.value || []).find(d => d.value === mode)
return hit ? hit.label : (mode || '四舍五入')
}
const statusLabel = computed(() => {
if (set.value.expired) return '已过期'
const map = { D: '草稿', A: '审核中', E: '生效中', R: '驳回', S: '已停用' }
@@ -438,6 +453,7 @@ function onCopy() {
function openDetailDialog(row) {
detailForm.value = row ? {
...row,
roundMode: row.roundMode || 'HALF_UP',
_countryKey: countryKeyOf(row)
} : {
priceSetId: set.value.priceSetId,
@@ -450,7 +466,8 @@ function openDetailDialog(row) {
productCountryId: undefined,
fvValue: undefined,
fvRegistrationFee: undefined,
discountValue: undefined
discountValue: undefined,
roundMode: 'HALF_UP'
}
detailTitle.value = row ? '编辑明细' : '添加明细'
detailOpen.value = true