feat: 优惠管理合并重构与商品续费价格参数
Build and Deploy Vue3 / build (push) Successful in 1m31s
Build and Deploy Vue3 / deploy (push) Successful in 39s

- 合并优惠码/代金券为商品管理下优惠管理页面,卡片化展示与过期遮罩

- 用户组新增优惠绑定,商品关联改用懒加载树选择器

- 商品/套餐表单新增 renew_price、renew_recommend_rebate、renew_fixed_price

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
shiran
2026-06-18 17:06:23 +08:00
parent 5e81d33285
commit bdf6dd9382
11 changed files with 1685 additions and 1461 deletions
+235 -159
View File
@@ -161,47 +161,39 @@
</el-select>
</el-form-item>
<el-form-item label="选择类型" prop="select_type" v-if="dialogType === 'add'">
<el-radio-group v-model="form.select_type" @change="handleSelectTypeChange">
<el-radio value="product">商品</el-radio>
<el-radio value="product_group">商品组</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="选择商品" prop="selected_product" v-if="dialogType === 'add' && form.select_type === 'product'">
<el-select
v-model="form.selected_product"
placeholder="请选择商品"
filterable
clearable
style="width: 100%"
@change="handleProductChange"
>
<el-option
v-for="item in productOptions"
:key="item.id"
:label="`${item.name} (ID: ${item.id})`"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="选择商品组" prop="selected_group" v-if="dialogType === 'add' && form.select_type === 'product_group'">
<el-select
v-model="form.selected_group"
placeholder="请选择商品组"
filterable
clearable
style="width: 100%"
@change="handleProductGroupChange"
>
<el-option
v-for="item in productGroupOptions"
:key="item.id"
:label="`${item.name} (ID: ${item.id})`"
:value="item.id"
/>
</el-select>
<el-form-item label="选择关联对象" v-if="dialogType === 'add'">
<div class="goods-tree-wrapper">
<div class="goods-tree-toolbar">
<span class="tree-tip">可自由勾选商品组与商品展开层级查看下属内容</span>
<div class="tree-summary">
已选 <b>{{ checkedSummary.groupCount }}</b> 个商品组 / <b>{{ checkedSummary.productCount }}</b> 个商品
</div>
</div>
<el-tree
ref="goodsTreeRef"
:props="treeProps"
:load="loadTreeNode"
lazy
show-checkbox
check-strictly
node-key="key"
class="goods-tree"
@check="handleTreeCheck"
>
<template #default="{ data }">
<span class="tree-node">
<el-tag size="small" :type="data.nodeType === 'group' ? 'warning' : 'primary'" effect="plain">
{{ data.nodeType === 'group' ? '组' : '品' }}
</el-tag>
<span class="tree-node-label">{{ data.label }}</span>
<span class="tree-node-id">ID: {{ data.rawId }}</span>
<span v-if="data.nodeType === 'product' && data.price != null" class="tree-node-price">
¥{{ (data.price / 100).toFixed(2) }}
</span>
</span>
</template>
</el-tree>
</div>
</el-form-item>
<!-- 编辑模式显示字段 -->
@@ -234,7 +226,7 @@
</template>
<script setup>
import { ref, reactive, onMounted, watch } from 'vue'
import { ref, reactive, onMounted, watch, nextTick } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Delete, Search, Plus, Refresh } from '@element-plus/icons-vue'
import {
@@ -276,36 +268,27 @@ const form = reactive({
code_id: undefined,
goods_id: undefined,
goods_name: '',
goods_type: '',
select_type: 'product', // 选择类型:product 或 product_group
selected_product: undefined, // 选中的商品ID
selected_group: undefined // 选中的商品组ID
goods_type: ''
})
const formRules = {
code_id: [
{ required: true, message: '请选择代金券', trigger: 'change' }
],
select_type: [
{ required: true, message: '请选择类型', trigger: 'change' }
],
selected_product: [
{ required: true, message: '请选择商品', trigger: 'change' }
],
selected_group: [
{ required: true, message: '请选择商品组', trigger: 'change' }
],
goods_id: [
{ required: true, message: '请输入商品ID', trigger: 'blur' }
],
goods_name: [
{ required: true, message: '请输入商品名称', trigger: 'blur' }
],
goods_type: [
{ required: true, message: '请选择商品类型', trigger: 'change' }
]
}
// 折叠层级选择器相关
const goodsTreeRef = ref(null)
const treeProps = {
label: 'label',
children: 'children',
isLeaf: 'isLeaf'
}
const checkedSummary = reactive({ groupCount: 0, productCount: 0 })
// 状态数据
const loading = ref(false)
const goodsList = ref([])
@@ -436,33 +419,79 @@ const fetchProductGroupList = async () => {
}
}
// 选择类型变化
const handleSelectTypeChange = (type) => {
form.selected_product = undefined
form.selected_group = undefined
form.goods_id = undefined
form.goods_name = ''
form.goods_type = ''
}
// 折叠层级选择器:懒加载节点
// node.level === 0 时加载顶级商品组;展开商品组时加载其子分组与下属商品
const loadTreeNode = async (node, resolve) => {
try {
// 根节点:仅加载 level=1 的顶级商品组
if (node.level === 0) {
const res = await getProductGroupList({ level: 1 })
if (res.data.code === 200) {
const groups = res.data.data?.data || []
return resolve(groups.map(buildGroupNode))
}
return resolve([])
}
// 选择商品变化
const handleProductChange = (productId) => {
const product = productOptions.value.find(item => item.id === productId)
if (product) {
form.goods_id = product.id
form.goods_name = product.goodsName || product.name || ''
form.goods_type = 'product'
// 商品组节点:逐级加载子分组 + 下属商品
if (node.data?.nodeType === 'group') {
const groupId = node.data.rawId
const childLevel = (node.data.level || 1) + 1
const tasks = [
getProductList({ good_group_id: groupId, delete: false })
]
// 仅当存在子分组时才请求下一级分组
if (node.data.existSub) {
tasks.push(getProductGroupList({ parent_id: groupId, level: childLevel }))
}
const results = await Promise.all(tasks)
const productRes = results[0]
const productNodes = (productRes.data.code === 200 ? (productRes.data.data?.data || []) : [])
.map(buildProductNode)
let groupNodes = []
if (node.data.existSub && results[1]?.data.code === 200) {
groupNodes = (results[1].data.data?.data || []).map(buildGroupNode)
}
return resolve([...groupNodes, ...productNodes])
}
return resolve([])
} catch (error) {
console.error('加载层级数据失败:', error)
ElMessage.error('加载层级数据失败')
return resolve([])
}
}
// 选择商品组变化
const handleProductGroupChange = (groupId) => {
const group = productGroupOptions.value.find(item => item.id === groupId)
if (group) {
form.goods_id = group.id
form.goods_name = group.name || ''
form.goods_type = 'product_group'
}
// 构建商品组节点
const buildGroupNode = (group) => ({
key: `group_${group.id}`,
rawId: group.id,
nodeType: 'group',
label: group.name,
level: group.level || 1,
existSub: group.existSub || false,
isLeaf: false
})
// 构建商品节点
const buildProductNode = (product) => ({
key: `product_${product.id}`,
rawId: product.id,
nodeType: 'product',
label: product.name,
price: product.price,
isLeaf: true
})
// 勾选变化时更新汇总
const handleTreeCheck = () => {
const nodes = goodsTreeRef.value?.getCheckedNodes() || []
checkedSummary.groupCount = nodes.filter(n => n.nodeType === 'group').length
checkedSummary.productCount = nodes.filter(n => n.nodeType === 'product').length
}
// 获取商品关联列表
@@ -527,16 +556,15 @@ const handleAdd = () => {
code_id: queryParams.code_id ? Number(queryParams.code_id) : undefined,
goods_id: undefined,
goods_name: '',
goods_type: '',
select_type: 'product',
selected_product: undefined,
selected_group: undefined
goods_type: ''
})
checkedSummary.groupCount = 0
checkedSummary.productCount = 0
formRef.value?.resetFields()
// 加载商品和商品组列表
fetchProductList()
fetchProductGroupList()
// 等待对话框渲染后清空树的勾选状态
nextTick(() => {
goodsTreeRef.value?.setCheckedKeys([])
})
}
// 编辑商品关联
@@ -641,80 +669,77 @@ const handleBatchDelete = () => {
// 提交表单
const submitForm = () => {
// 新增模式下的额外验证
if (dialogType.value === 'add') {
if (!form.code_id) {
ElMessage.warning('请选择代金券')
return
}
if (form.select_type === 'product' && !form.selected_product) {
ElMessage.warning('请选择商品')
return
}
if (form.select_type === 'product_group' && !form.selected_group) {
ElMessage.warning('请选择商品组')
return
}
if (!form.goods_id || !form.goods_name || !form.goods_type) {
ElMessage.warning('请先选择商品或商品组')
return
}
if (!form.code_id) {
ElMessage.warning('请选择代金券')
return
}
if (dialogType.value === 'add') {
// 收集树中勾选的商品组与商品
const checkedNodes = goodsTreeRef.value?.getCheckedNodes() || []
const goodIds = checkedNodes.filter(n => n.nodeType === 'product').map(n => n.rawId)
const goodGroupIds = checkedNodes.filter(n => n.nodeType === 'group').map(n => n.rawId)
if (goodIds.length === 0 && goodGroupIds.length === 0) {
ElMessage.warning('请至少勾选一个商品或商品组')
return
}
submitAdd(goodIds, goodGroupIds)
return
}
// 编辑模式
formRef.value?.validate(async (valid) => {
if (valid) {
try {
const submitData = {
code_id: String(form.code_id)
}
// 根据选择类型决定传 good_id 还是 good_group_id
if (dialogType.value === 'add') {
if (form.select_type === 'product') {
// 选择的是商品,传 good_id
submitData.good_ids = String(form.goods_id)
} else if (form.select_type === 'product_group') {
// 选择的是商品组,传 good_group_id
submitData.good_group_ids = String(form.goods_id)
}
} else {
// 编辑模式:传 discount_good_id
submitData.discount_good_id = String(form.id)
// 根据 goods_type 判断传 good_id 还是 good_group_id
if (form.goods_type === 'product') {
submitData.good_id = String(form.goods_id)
} else if (form.goods_type === 'product_group') {
submitData.good_group_id = String(form.goods_id)
} else {
// 其他类型默认使用 good_id
submitData.good_id = String(form.goods_id)
}
}
console.log('提交商品关联数据:', submitData)
let res
if (dialogType.value === 'add') {
res = await addDiscountGoods(submitData)
} else {
res = await updateDiscountGoods(submitData)
}
console.log('提交响应:', res.data)
if (res.data.code === 200) {
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
dialogVisible.value = false
fetchGoodsList()
}
} catch (error) {
console.error('操作失败:', error)
ElMessage.error(error.response?.data?.message || '操作失败')
if (!valid) return
try {
const submitData = {
code_id: String(form.code_id),
discount_good_id: String(form.id)
}
if (form.goods_type === 'product_group') {
submitData.good_group_id = String(form.goods_id)
} else {
submitData.good_id = String(form.goods_id)
}
const res = await updateDiscountGoods(submitData)
if (res.data.code === 200) {
ElMessage.success('修改成功')
dialogVisible.value = false
fetchGoodsList()
}
} catch (error) {
console.error('操作失败:', error)
ElMessage.error(error.response?.data?.message || '操作失败')
}
})
}
// 新增提交:根据勾选构建 good_ids / good_group_ids(逗号分隔)
const submitAdd = async (goodIds, goodGroupIds) => {
try {
const submitData = { code_id: String(form.code_id) }
if (goodIds.length > 0) {
submitData.good_ids = goodIds.join(',')
}
if (goodGroupIds.length > 0) {
submitData.good_group_ids = goodGroupIds.join(',')
}
console.log('提交商品关联数据:', submitData)
const res = await addDiscountGoods(submitData)
if (res.data.code === 200) {
ElMessage.success('新增成功')
dialogVisible.value = false
fetchGoodsList()
}
} catch (error) {
console.error('操作失败:', error)
ElMessage.error(error.response?.data?.message || '操作失败')
}
}
// 初始化
onMounted(() => {
fetchVoucherListOptions()
@@ -798,6 +823,57 @@ onMounted(() => {
padding: 0;
}
/* 折叠层级选择器样式 */
.goods-tree-wrapper {
width: 100%;
border: 1px solid #e1e8ed;
border-radius: 6px;
overflow: hidden;
}
.goods-tree-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: #fafbfc;
border-bottom: 1px solid #e1e8ed;
font-size: 12px;
color: #909399;
}
.tree-summary b {
color: #409eff;
}
.goods-tree {
max-height: 320px;
overflow-y: auto;
padding: 8px;
}
.tree-node {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.tree-node-label {
color: #2c3e50;
}
.tree-node-id {
color: #909399;
font-size: 12px;
}
.tree-node-price {
color: #f56c6c;
font-size: 12px;
font-weight: bold;
}
:deep(.el-card__body) {
padding: 0;
}