feate:新增套餐管理
Build and Deploy Vue3 / build (push) Successful in 1m11s
Build and Deploy Vue3 / deploy (push) Successful in 1m26s

This commit is contained in:
2026-01-29 15:18:08 +08:00
parent ead7c5bba5
commit 127d54eaa6
6 changed files with 3151 additions and 166 deletions
+82
View File
@@ -144,4 +144,86 @@ export const updateProductParameterValue = (data) => {
'Content-Type':'multipart/form-data'
}
})
}
/**---------------------------------- */
/**商品套餐管理 */
/**获取商品套餐列表 */
export const getProductPlanList = (params) => {
return http2.get('/api/v1/admin/good/plan/list', {params: params})
}
/**获取商品套餐详情 */
export const getProductPlanDetail = (params) => {
return http2.get('/api/v1/admin/good/plan/detail', {params: params})
}
/**创建商品套餐 */
export const createProductPlan = (data) => {
return http2.post('/api/v1/admin/good/plan/create', data,{
headers:{
'Content-Type':'multipart/form-data'
}
})
}
/**更新商品套餐 */
export const updateProductPlan = (data) => {
return http2.post('/api/v1/admin/good/plan/update', data,{
headers:{
'Content-Type':'multipart/form-data'
}
})
}
/**删除商品套餐 */
export const deleteProductPlan = (params) => {
return http2.delete('/api/v1/admin/good/plan/delete', {params: params})
}
/**禁用商品套餐 */
export const disableProductPlan = (data) => {
return http2.post('/api/v1/admin/good/plan/disable', data,{
headers:{
'Content-Type':'multipart/form-data'
}
})
}
/**启用商品套餐 */
export const enableProductPlan = (data) => {
return http2.post('/api/v1/admin/good/plan/enable', data,{
headers:{
'Content-Type':'multipart/form-data'
}
})
}
/**---------------------------------- */
/**商品分组标签管理 */
/**获取商品分组标签列表 */
export const getProductGroupTagList = (params) => {
return http2.get('/api/v1/admin/good/group_tag/list', {params: params})
}
/**获取商品分组标签详情 */
export const getProductGroupTagDetail = (params) => {
return http2.get('/api/v1/admin/good/group_tag/detail', {params: params})
}
/**创建商品分组标签 */
export const createProductGroupTag = (data) => {
return http2.post('/api/v1/admin/good/group_tag/create', data,{
headers:{
'Content-Type':'multipart/form-data'
}
})
}
/**更新商品分组标签 */
export const updateProductGroupTag = (data) => {
return http2.post('/api/v1/admin/good/group_tag/update', data,{
headers:{
'Content-Type':'multipart/form-data'
}
})
}
/**删除商品分组标签 */
export const deleteProductGroupTag = (params) => {
return http2.delete('/api/v1/admin/good/group_tag/delete', {params: params})
}
+1 -1
View File
@@ -424,7 +424,7 @@ const fetchProductGroupList = async () => {
try {
const res = await getProductGroupList({
page: 1,
count: 1000
count: 10
})
console.log('获取商品组列表:', res.data)
if (res.data.code === 200) {
File diff suppressed because it is too large Load Diff
+403 -18
View File
@@ -96,9 +96,10 @@
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<el-table-column label="操作" width="280" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
<el-button type="success" link @click="handlePlan(row)">套餐</el-button>
<el-button type="warning" link @click="handleParameter(row)">参数</el-button>
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
</template>
@@ -162,19 +163,32 @@
<el-form-item label="内容" prop="content">
<el-input v-model="productForm.content" type="textarea" :rows="4" placeholder="请输入内容" />
</el-form-item>
<el-form-item label="封面ID" prop="cover_id">
<div class="cover-selector">
<div class="cover-preview" v-if="productForm.cover_id && coverPreviewUrl">
<el-image :src="coverPreviewUrl" fit="cover" style="width: 80px; height: 80px; border-radius: 4px;" />
</div>
<div class="cover-actions">
<el-button type="primary" @click="openCoverSelector">
<el-icon><Picture /></el-icon>
{{ productForm.cover_id ? '更换封面' : '选择封面' }}
</el-button>
<el-button v-if="productForm.cover_id" @click="clearCover">清除</el-button>
<span v-if="productForm.cover_id" class="cover-id-text">ID: {{ productForm.cover_id }}</span>
<el-form-item label="商品封面" prop="cover_id">
<div class="recommend-user-selector">
<div class="cover-preview-inline" v-if="productForm.cover_id && coverPreviewUrl">
<el-image :src="coverPreviewUrl" fit="cover" style="width: 40px; height: 40px; border-radius: 4px;" />
</div>
<el-input
:model-value="productForm.cover_id ? `文件ID: ${productForm.cover_id}` : ''"
placeholder="点击选择封面图片"
readonly
@click="openCoverSelector"
>
<template #append>
<el-button @click="openCoverSelector">
<el-icon><Search /></el-icon>
</el-button>
</template>
</el-input>
<el-button
v-if="productForm.cover_id"
type="danger"
link
@click="clearCover"
class="clear-btn"
>
清除
</el-button>
</div>
</el-form-item>
@@ -206,6 +220,14 @@
<el-form-item label="推荐返还(%)" prop="recommend_rebate">
<el-input-number v-model="productForm.recommend_rebate" :min="0" :max="100" placeholder="请输入返还百分比" style="width: 100%" />
</el-form-item>
<el-form-item label="参数类型" prop="arg_type">
<el-select v-model="productForm.arg_type" placeholder="请选择参数类型" style="width: 100%">
<el-option label="所有参数" value="all" />
<el-option label="套餐" value="plan" />
<el-option label="自定义" value="customize" />
</el-select>
<div class="form-tip">all: 所有参数 / plan: 套餐 / customize: 自定义</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
@@ -408,6 +430,122 @@
</div>
</template>
</el-dialog>
<!-- 商品套餐管理对话框 -->
<el-dialog
v-model="planDialogVisible"
:title="`商品套餐管理 - ${currentPlanProductName}`"
width="900px"
append-to-body
>
<div class="plan-management">
<div class="plan-header">
<el-button type="primary" @click="handleAddPlan">
<el-icon><Plus /></el-icon>新增套餐
</el-button>
<el-button type="success" @click="fetchPlanList">
<el-icon><Refresh /></el-icon>刷新
</el-button>
</div>
<el-table
v-loading="planLoading"
:data="planList"
style="width: 100%"
:header-cell-style="{ background: '#f8f9fa', color: '#2c3e50', fontWeight: 600 }"
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="套餐名称" min-width="120" />
<el-table-column label="参数配置" min-width="200">
<template #default="{ row }">
<div v-if="row.argsParsed && row.argsParsed.length > 0" class="args-list">
<el-tag
v-for="(arg, index) in row.argsParsed"
:key="index"
size="small"
type="info"
style="margin-right: 4px; margin-bottom: 4px;"
>
{{ arg.name || arg.value || `参数${arg.arg_id}` }}
</el-tag>
</div>
<span v-else class="text-muted">-</span>
</template>
</el-table-column>
<el-table-column prop="note" label="说明" min-width="150" show-overflow-tooltip />
<el-table-column prop="index" label="排序" width="70" />
<el-table-column label="状态" width="80">
<template #default="{ row }">
<el-tag :type="row.disable ? 'danger' : 'success'" size="small">
{{ row.disable ? '禁用' : '启用' }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="{ row }">
<el-button type="primary" link @click="handleEditPlan(row)">编辑</el-button>
<el-button
:type="row.disable ? 'success' : 'warning'"
link
@click="handleTogglePlanStatus(row)"
>
{{ row.disable ? '启用' : '禁用' }}
</el-button>
<el-button type="danger" link @click="handleDeletePlan(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
</div>
</el-dialog>
<!-- 套餐表单对话框 -->
<el-dialog
v-model="planFormDialogVisible"
:title="planFormType === 'add' ? '新增套餐' : '编辑套餐'"
width="600px"
append-to-body
>
<el-form
ref="planFormRef"
:model="planForm"
:rules="planFormRules"
label-width="100px"
>
<el-form-item label="套餐名称" prop="name">
<el-input v-model="planForm.name" placeholder="请输入套餐名称" />
</el-form-item>
<el-form-item label="说明" prop="note">
<el-input v-model="planForm.note" type="textarea" :rows="3" placeholder="请输入套餐说明" />
</el-form-item>
<el-form-item label="参数配置" prop="args">
<el-input
v-model="planForm.args"
type="textarea"
:rows="4"
placeholder='JSON格式,如:[{"arg_id":1,"name":"参数名","attr_id":1,"value":"值"}]'
/>
<div class="form-tip">参数配置为JSON数组格式</div>
</el-form-item>
<el-form-item label="额外参数" prop="extra_arg_ids">
<el-input v-model="planForm.extra_arg_ids" placeholder="额外参数ID,如:1,2,3" />
<div class="form-tip">多个参数ID用英文逗号分隔</div>
</el-form-item>
<el-form-item label="排序索引" prop="index">
<el-input-number v-model="planForm.index" :min="0" style="width: 100%" />
</el-form-item>
<el-form-item label="状态" prop="disable" v-if="planFormType === 'edit'">
<el-radio-group v-model="planForm.disable">
<el-radio :value="false">启用</el-radio>
<el-radio :value="true">禁用</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="planFormDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitPlanForm">确定</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
@@ -426,7 +564,14 @@ import { getProductList, createProduct, updateProduct, deleteProduct, getProduct
deleteProductParameter,
addProductParameterValue,
updateProductParameterValue,
deleteProductParameterValue
deleteProductParameterValue,
getProductPlanList,
getProductPlanDetail,
createProductPlan,
updateProductPlan,
deleteProductPlan,
disableProductPlan,
enableProductPlan
} from '@/api/admin/product'
// 查询参数
@@ -451,7 +596,8 @@ const productForm = reactive({
pay_num: 1,
expire_time: 0,
recommend: false,
recommend_rebate: 0
recommend_rebate: 0,
arg_type: 'all' // 商品参数类型 all/plan/customize
})
const productRules = {
@@ -613,7 +759,8 @@ const handleAdd = () => {
pay_num: 1,
expire_time: 0,
recommend: false,
recommend_rebate: 0
recommend_rebate: 0,
arg_type: 'all'
})
productFormRef.value?.resetFields()
}
@@ -637,7 +784,8 @@ const handleEdit = (row) => {
pay_num: row.payNum,
expire_time: row.expireTime,
recommend: row.recommend,
recommend_rebate: row.recommendRebate
recommend_rebate: row.recommendRebate,
arg_type: row.argType || 'all'
})
// 加载封面预览
loadCoverPreview(row.coverId)
@@ -732,7 +880,8 @@ const submitForm = () => {
price: Math.round(productForm.price * 100) || 0, // 元转分
pay_num: productForm.pay_num || 1,
expire_time: productForm.expire_time || 0,
recommend_rebate: productForm.recommend_rebate || 0
recommend_rebate: productForm.recommend_rebate || 0,
arg_type: productForm.arg_type || 'all' // 商品参数类型
}
console.log('提交的数据:', submitData) // 调试日志
@@ -1091,6 +1240,219 @@ const submitParamValueForm = () => {
})
}
// ==================== 商品套餐管理 ====================
const planDialogVisible = ref(false)
const planLoading = ref(false)
const planList = ref([])
const currentPlanProductId = ref(null)
const currentPlanProductName = ref('')
// 套餐表单
const planFormDialogVisible = ref(false)
const planFormType = ref('add')
const planFormRef = ref(null)
const planForm = reactive({
plan_id: undefined,
name: '',
note: '',
args: '',
extra_arg_ids: '',
index: 0,
disable: false
})
const planFormRules = {
name: [{ required: true, message: '请输入套餐名称', trigger: 'blur' }]
}
// 打开套餐管理
const handlePlan = (row) => {
currentPlanProductId.value = row.id
currentPlanProductName.value = row.name
planDialogVisible.value = true
fetchPlanList()
}
// 解析args JSON字符串
const parseArgs = (argsStr) => {
if (!argsStr) return []
try {
const parsed = JSON.parse(argsStr)
return Array.isArray(parsed) ? parsed : []
} catch (e) {
console.error('解析args失败:', e)
return []
}
}
// 格式化显示args
const formatArgs = (argsParsed) => {
if (!argsParsed || argsParsed.length === 0) return '-'
return argsParsed.map(arg => arg.name || arg.value).filter(Boolean).join(', ')
}
// 获取套餐列表
const fetchPlanList = async () => {
if (!currentPlanProductId.value) return
planLoading.value = true
try {
const res = await getProductPlanList({ good_id: String(currentPlanProductId.value) })
if (res.data.code === 200) {
// 兼容两种数据结构
let data = res.data.data
if (Array.isArray(data)) {
// 处理args字段,将JSON字符串解析为对象
planList.value = data.map(item => ({
...item,
argsParsed: parseArgs(item.args)
}))
} else if (data && data.list) {
planList.value = data.list.map(item => ({
...item,
argsParsed: parseArgs(item.args)
}))
} else if (data && data.data) {
planList.value = data.data.map(item => ({
...item,
argsParsed: parseArgs(item.args)
}))
} else {
planList.value = []
}
}
} catch (error) {
console.error('获取套餐列表失败:', error)
ElMessage.error('获取套餐列表失败')
} finally {
planLoading.value = false
}
}
// 新增套餐
const handleAddPlan = () => {
planFormType.value = 'add'
planFormDialogVisible.value = true
Object.assign(planForm, {
plan_id: undefined,
name: '',
note: '',
args: '',
extra_arg_ids: '',
index: 0,
disable: false
})
nextTick(() => {
planFormRef.value?.resetFields()
})
}
// 编辑套餐
const handleEditPlan = async (row) => {
planFormType.value = 'edit'
try {
const res = await getProductPlanDetail({ good_id: String(currentPlanProductId.value), plan_id: String(row.id) })
if (res.data.code === 200) {
const data = res.data.data
// 处理args字段:如果是字符串先解析,再格式化为漂亮的JSON
let argsValue = ''
if (data.args) {
try {
// 如果args是字符串,先解析为对象
const argsObj = typeof data.args === 'string' ? JSON.parse(data.args) : data.args
// 格式化为漂亮的JSON(带缩进)
argsValue = JSON.stringify(argsObj, null, 2)
} catch (e) {
// 解析失败则保持原值
argsValue = data.args
}
}
Object.assign(planForm, {
plan_id: data.id,
name: data.name || '',
note: data.note || '',
args: argsValue,
extra_arg_ids: data.extraArgIds ? data.extraArgIds.join(',') : '',
index: data.index || 0,
disable: data.disable || false
})
planFormDialogVisible.value = true
}
} catch (error) {
ElMessage.error('获取套餐详情失败')
}
}
// 删除套餐
const handleDeletePlan = (row) => {
ElMessageBox.confirm(`确认删除套餐 ${row.name} 吗?`, '警告', {
confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning'
}).then(async () => {
try {
const res = await deleteProductPlan({ good_id: String(currentPlanProductId.value), plan_id: String(row.id) })
if (res.data.code === 200) {
ElMessage.success('删除成功')
fetchPlanList()
}
} catch (error) {
ElMessage.error('删除失败')
}
}).catch(() => {})
}
// 切换套餐状态
const handleTogglePlanStatus = async (row) => {
try {
const res = row.disable
? await enableProductPlan({ good_id: String(currentPlanProductId.value), plan_id: String(row.id) })
: await disableProductPlan({ good_id: String(currentPlanProductId.value), plan_id: String(row.id) })
if (res.data.code === 200) {
ElMessage.success(row.disable ? '启用成功' : '禁用成功')
fetchPlanList()
}
} catch (error) {
ElMessage.error('操作失败')
}
}
// 提交套餐表单
const submitPlanForm = () => {
planFormRef.value?.validate(async (valid) => {
if (valid) {
try {
const submitData = {
good_id: String(currentPlanProductId.value),
name: planForm.name,
note: planForm.note || '',
args: planForm.args || '',
extra_arg_ids: planForm.extra_arg_ids || '',
index: Number(planForm.index) || 0
}
let res
if (planFormType.value === 'add') {
res = await createProductPlan(submitData)
} else {
submitData.plan_id = String(planForm.plan_id)
submitData.disable = planForm.disable
res = await updateProductPlan(submitData)
}
if (res.data.code === 200) {
ElMessage.success(planFormType.value === 'add' ? '新增成功' : '修改成功')
planFormDialogVisible.value = false
fetchPlanList()
} else {
ElMessage.error(res.data.message || '操作失败')
}
} catch (error) {
console.error('套餐操作失败:', error)
ElMessage.error(error.response?.data?.message || '操作失败')
}
}
})
}
</script>
<style scoped>
@@ -1269,5 +1631,28 @@ const submitParamValueForm = () => {
color: #909399;
font-size: 13px;
}
/* 套餐管理样式 */
.plan-management {
padding: 0;
}
.plan-header {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.form-tip {
font-size: 12px;
color: #909399;
margin-top: 4px;
}
.args-list {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
</style>
+1750 -1
View File
File diff suppressed because it is too large Load Diff
-104
View File
@@ -1,104 +0,0 @@
{
"openapi": "3.0.1",
"info": {
"title": "默认模块",
"description": "",
"version": "1.0.0"
},
"tags": [],
"paths": {
"/api/v1/admin/user/user/update": {
"post": {
"summary": "更新用户信息",
"deprecated": false,
"description": "",
"tags": [],
"parameters": [
{
"name": "Authorization",
"in": "header",
"description": "",
"example": "Bearer {{token}}",
"schema": {
"type": "string",
"default": "Bearer {{token}}"
}
}
],
"requestBody": {
"content": {
"multipart/form-data": {
"schema": {
"type": "object",
"properties": {
"user_id": {
"description": "用户ID",
"example": "",
"type": "string"
},
"sex": {
"description": "性别 true 男 false 女",
"example": "",
"type": "string"
},
"age": {
"description": "年龄",
"example": "",
"type": "string"
},
"cover_id": {
"description": "头像id",
"example": "",
"type": "string"
},
"user_name": {
"description": "用户名",
"example": "",
"type": "string"
},
"recommend_id": {
"description": "推介用户id",
"example": "",
"type": "string"
},
"phone": {
"description": "手机号码",
"example": "",
"type": "string"
},
"email": {
"description": "邮箱",
"example": "",
"type": "string"
}
}
}
}
}
},
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {}
}
}
},
"headers": {}
}
},
"security": []
}
}
},
"components": {
"schemas": {},
"responses": {},
"securitySchemes": {}
},
"servers": [],
"security": []
}