Compare commits
5 Commits
2a0196d332
...
deploy
| Author | SHA1 | Date | |
|---|---|---|---|
| 8254fedb5f | |||
| bdf6dd9382 | |||
| 34c47afd10 | |||
| 5e81d33285 | |||
| 38c63cc451 |
@@ -0,0 +1,43 @@
|
|||||||
|
import { http2 } from '@/utils/request.js'
|
||||||
|
|
||||||
|
const formHeaders = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
|
||||||
|
|
||||||
|
// ========== 邮件主控服务 ==========
|
||||||
|
|
||||||
|
export const getMailServiceList = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/server/mail_service/list', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMailServiceDetail = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/server/mail_service/detail', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createMailService = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/mail_service/create', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateMailService = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/mail_service/update', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteMailService = (data) => {
|
||||||
|
return http2.delete('/api/v1/admin/server/mail_service/delete', { data, ...formHeaders })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 邮件额度商品 ==========
|
||||||
|
|
||||||
|
export const getMailGoodsList = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/server/mail_service/goods/list', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createMailGoods = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/mail_service/goods/create', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateMailGoods = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/server/mail_service/goods/update', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteMailGoods = (data) => {
|
||||||
|
return http2.delete('/api/v1/admin/server/mail_service/goods/delete', { data, ...formHeaders })
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { http2 } from '@/utils/request.js'
|
||||||
|
|
||||||
|
const formHeaders = { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } }
|
||||||
|
|
||||||
|
// ========== 用户组优惠(用户组 与 商品/商品组 绑定优惠) ==========
|
||||||
|
|
||||||
|
/** 获取用户组优惠列表(可按 user_group_id / good_id / good_group_id 过滤) */
|
||||||
|
export const getUserGroupDiscountList = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/user_group/discount/list', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 获取用户组优惠详情 */
|
||||||
|
export const getUserGroupDiscountDetail = (params) => {
|
||||||
|
return http2.get('/api/v1/admin/user_group/discount/detail', { params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 添加用户组优惠(将用户组与商品/商品组绑定优惠) */
|
||||||
|
export const addUserGroupDiscount = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/user_group/discount/add', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 修改用户组优惠(可改绑商品/商品组) */
|
||||||
|
export const updateUserGroupDiscount = (data) => {
|
||||||
|
return http2.post('/api/v1/admin/user_group/discount/update', data, formHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除用户组优惠(解绑用户组与商品/商品组的优惠) */
|
||||||
|
export const deleteUserGroupDiscount = (params) => {
|
||||||
|
return http2.delete('/api/v1/admin/user_group/discount/delete', { params })
|
||||||
|
}
|
||||||
+13
-17
@@ -47,7 +47,8 @@ export const menus = [
|
|||||||
title: '商品管理',
|
title: '商品管理',
|
||||||
icon: 'Goods',
|
icon: 'Goods',
|
||||||
children: [
|
children: [
|
||||||
{ path: '/product/manage', title: '商品管理' }
|
{ path: '/product/manage', title: '商品管理' },
|
||||||
|
{ path: '/product/discount', title: '优惠管理' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -70,22 +71,6 @@ export const menus = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/marketing',
|
|
||||||
title: '优惠营销',
|
|
||||||
icon: 'Present',
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: '/marketing/discount',
|
|
||||||
title: '优惠码管理'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/marketing/voucher',
|
|
||||||
title: '代金券管理'
|
|
||||||
},
|
|
||||||
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/activity',
|
path: '/activity',
|
||||||
title: '活动管理',
|
title: '活动管理',
|
||||||
@@ -172,6 +157,17 @@ export const menus = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/mail',
|
||||||
|
title: '邮箱平台管理',
|
||||||
|
icon: 'Message',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '/mail/service',
|
||||||
|
title: '主控服务管理'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '虚拟化平台管理',
|
title: '虚拟化平台管理',
|
||||||
icon: 'Platform',
|
icon: 'Platform',
|
||||||
|
|||||||
+37
-30
@@ -253,6 +253,18 @@ const routes = [
|
|||||||
component: () => import('../views/product/ProductGroup.vue'),
|
component: () => import('../views/product/ProductGroup.vue'),
|
||||||
meta: { title: '商品管理' }
|
meta: { title: '商品管理' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'discount',
|
||||||
|
name: 'DiscountManage',
|
||||||
|
component: () => import('../views/product/DiscountManage.vue'),
|
||||||
|
meta: { title: '优惠管理' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'discount/voucher/:id/manage',
|
||||||
|
name: 'VoucherManagement',
|
||||||
|
component: () => import('../views/marketing/VoucherManagement.vue'),
|
||||||
|
meta: { title: '代金券分发管理', hidden: true, activeMenu: '/product/discount' }
|
||||||
|
},
|
||||||
{ path: 'list', redirect: '/product/manage' },
|
{ path: 'list', redirect: '/product/manage' },
|
||||||
{ path: 'group', redirect: '/product/manage' }
|
{ path: 'group', redirect: '/product/manage' }
|
||||||
]
|
]
|
||||||
@@ -310,43 +322,20 @@ const routes = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
// 优惠营销路由
|
// 优惠营销路由(已合并至 /product/discount,保留重定向兼容旧链接)
|
||||||
{
|
{
|
||||||
path: 'marketing',
|
path: 'marketing',
|
||||||
name: 'Marketing',
|
name: 'Marketing',
|
||||||
meta: {
|
meta: {
|
||||||
title: '优惠营销',
|
title: '优惠营销',
|
||||||
icon: 'Present'
|
icon: 'Present',
|
||||||
|
hidden: true
|
||||||
},
|
},
|
||||||
redirect: '/marketing/discount',
|
redirect: '/product/discount',
|
||||||
children: [
|
children: [
|
||||||
{
|
{ path: 'discount', redirect: '/product/discount' },
|
||||||
path: 'discount',
|
{ path: 'voucher', redirect: '/product/discount' },
|
||||||
name: 'DiscountCode',
|
{ path: 'voucher/:id/manage', redirect: to => `/product/discount/voucher/${to.params.id}/manage` }
|
||||||
component: () => import('../views/marketing/DiscountCode.vue'),
|
|
||||||
meta: {
|
|
||||||
title: '优惠码管理'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'voucher',
|
|
||||||
name: 'Voucher',
|
|
||||||
component: () => import('../views/marketing/Voucher.vue'),
|
|
||||||
meta: {
|
|
||||||
title: '代金券管理'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'voucher/:id/manage',
|
|
||||||
name: 'VoucherManagement',
|
|
||||||
component: () => import('../views/marketing/VoucherManagement.vue'),
|
|
||||||
meta: {
|
|
||||||
title: '代金券详情管理',
|
|
||||||
hidden: true,
|
|
||||||
activeMenu: '/marketing/voucher'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
// 活动管理路由
|
// 活动管理路由
|
||||||
@@ -645,6 +634,24 @@ const routes = [
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
// 邮箱平台管理路由
|
||||||
|
{
|
||||||
|
path: 'mail',
|
||||||
|
name: 'Mail',
|
||||||
|
meta: {
|
||||||
|
title: '邮箱平台管理',
|
||||||
|
icon: 'Message'
|
||||||
|
},
|
||||||
|
redirect: '/mail/service',
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'service',
|
||||||
|
name: 'MailService',
|
||||||
|
component: () => import('../views/mail/MailService.vue'),
|
||||||
|
meta: { title: '主控服务管理' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
// 站点审计路由
|
// 站点审计路由
|
||||||
{
|
{
|
||||||
path: 'audit',
|
path: 'audit',
|
||||||
|
|||||||
@@ -0,0 +1,420 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mail-service-page">
|
||||||
|
<!-- 页面头部 -->
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="header-info">
|
||||||
|
<div class="header-icon">
|
||||||
|
<svg viewBox="0 0 24 24" width="28" height="28" fill="none" stroke="#409eff" stroke-width="1.8">
|
||||||
|
<rect x="2" y="4" width="20" height="16" rx="2"/>
|
||||||
|
<path d="M22 6L12 13 2 6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="header-title">邮件主控服务管理</h2>
|
||||||
|
<p class="header-desc">管理邮件平台的主控服务实例,每个服务对应一个 mail-server 节点</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" @click="handleAdd">
|
||||||
|
<el-icon><Plus /></el-icon> 新增服务
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 搜索栏 -->
|
||||||
|
<div class="filter-bar">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.key"
|
||||||
|
placeholder="搜索名称 / 说明 / 地址"
|
||||||
|
clearable
|
||||||
|
style="width: 300px"
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
@clear="handleSearch"
|
||||||
|
>
|
||||||
|
<template #prefix><el-icon><Search /></el-icon></template>
|
||||||
|
</el-input>
|
||||||
|
<el-button type="primary" @click="handleSearch">搜索</el-button>
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<el-table :data="tableData" v-loading="loading" stripe border style="width: 100%">
|
||||||
|
<el-table-column prop="id" label="ID" width="70" align="center" />
|
||||||
|
<el-table-column prop="name" label="服务名称" min-width="150">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="service-name-cell">
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="#67c23a" stroke-width="2">
|
||||||
|
<rect x="2" y="4" width="20" height="16" rx="2"/>
|
||||||
|
<path d="M22 6L12 13 2 6"/>
|
||||||
|
</svg>
|
||||||
|
<span class="name-text">{{ row.name }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="note" label="说明" min-width="150" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="note-text">{{ row.note || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="host" label="服务地址" min-width="220">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag type="info" effect="plain" class="host-tag">{{ row.host }}</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="SMTP 配置" min-width="200">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div v-if="row.smtpHost" class="smtp-info">
|
||||||
|
<span class="smtp-host">{{ row.smtpHost }}:{{ row.smtpPort || '-' }}</span>
|
||||||
|
<el-tag v-if="row.smtpTlsEnable" type="success" size="small" effect="plain">TLS</el-tag>
|
||||||
|
<el-tag v-else type="info" size="small" effect="plain">无TLS</el-tag>
|
||||||
|
</div>
|
||||||
|
<span v-else class="note-text">-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="serviceToken" label="Service Token" min-width="180">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="token-cell">
|
||||||
|
<span v-if="!row._showToken" class="token-mask">{{ maskToken(row.serviceToken) }}</span>
|
||||||
|
<span v-else class="token-full">{{ row.serviceToken }}</span>
|
||||||
|
<el-button link size="small" @click="row._showToken = !row._showToken">
|
||||||
|
{{ row._showToken ? '隐藏' : '显示' }}
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="createdAt" label="创建时间" width="170" align="center">
|
||||||
|
<template #default="{ row }">{{ formatTime(row.createdAt || row.CreatedAt) }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="200" align="center" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button link type="success" size="small" @click="openConsole(row)">控制台</el-button>
|
||||||
|
<el-button link type="primary" size="small" @click="handleEdit(row)">编辑</el-button>
|
||||||
|
<el-popconfirm title="确认删除该邮件服务?" @confirm="handleDelete(row)">
|
||||||
|
<template #reference>
|
||||||
|
<el-button link type="danger" size="small">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-popconfirm>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 分页 -->
|
||||||
|
<div class="pagination-wrap">
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="queryParams.page"
|
||||||
|
v-model:page-size="queryParams.count"
|
||||||
|
:total="total"
|
||||||
|
:page-sizes="[10, 20, 50]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
@size-change="fetchList"
|
||||||
|
@current-change="fetchList"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 新增/编辑弹窗 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="dialogVisible"
|
||||||
|
:title="isEdit ? '编辑邮件主控服务' : '新增邮件主控服务'"
|
||||||
|
width="600px"
|
||||||
|
destroy-on-close
|
||||||
|
>
|
||||||
|
<el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
|
||||||
|
<el-form-item label="服务名称" prop="name">
|
||||||
|
<el-input v-model="form.name" placeholder="请输入服务名称" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="说明" prop="note">
|
||||||
|
<el-input v-model="form.note" type="textarea" :rows="2" placeholder="服务说明(可选)" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="服务地址" prop="host">
|
||||||
|
<el-input v-model="form.host" placeholder="https://mail.example.com" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="Service Token" prop="service_token">
|
||||||
|
<el-input v-model="form.service_token" placeholder="mail-server 的 SERVICE_TOKEN" show-password />
|
||||||
|
</el-form-item>
|
||||||
|
<el-divider content-position="left">SMTP 配置(可选)</el-divider>
|
||||||
|
<el-form-item label="SMTP 地址" prop="smtp_host">
|
||||||
|
<el-input v-model="form.smtp_host" placeholder="smtp.example.com" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="SMTP 端口" prop="smtp_port">
|
||||||
|
<el-input-number v-model="form.smtp_port" :min="1" :max="65535" placeholder="465" style="width: 100%" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="启用 TLS" prop="smtp_tls_enable">
|
||||||
|
<el-switch v-model="form.smtp_tls_enable" active-text="启用" inactive-text="关闭" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="dialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="handleSubmit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { Plus, Search } from '@element-plus/icons-vue'
|
||||||
|
import {
|
||||||
|
getMailServiceList,
|
||||||
|
createMailService,
|
||||||
|
updateMailService,
|
||||||
|
deleteMailService
|
||||||
|
} from '@/api/admin/mailService.js'
|
||||||
|
|
||||||
|
const loading = ref(false)
|
||||||
|
const submitting = ref(false)
|
||||||
|
const tableData = ref([])
|
||||||
|
const total = ref(0)
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const isEdit = ref(false)
|
||||||
|
const formRef = ref(null)
|
||||||
|
|
||||||
|
const queryParams = reactive({
|
||||||
|
page: 1,
|
||||||
|
count: 10,
|
||||||
|
key: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
id: null,
|
||||||
|
name: '',
|
||||||
|
note: '',
|
||||||
|
host: '',
|
||||||
|
service_token: '',
|
||||||
|
smtp_host: '',
|
||||||
|
smtp_port: null,
|
||||||
|
smtp_tls_enable: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const rules = {
|
||||||
|
name: [{ required: true, message: '请输入服务名称', trigger: 'blur' }],
|
||||||
|
host: [{ required: true, message: '请输入服务地址', trigger: 'blur' }],
|
||||||
|
service_token: [{ required: true, message: '请输入 Service Token', trigger: 'blur' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (t) => {
|
||||||
|
if (!t) return '-'
|
||||||
|
return new Date(t).toLocaleString('zh-CN', { hour12: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
const maskToken = (token) => {
|
||||||
|
if (!token) return '-'
|
||||||
|
if (token.length <= 8) return '****'
|
||||||
|
return token.slice(0, 4) + '****' + token.slice(-4)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getMailServiceList(queryParams)
|
||||||
|
const body = res.data
|
||||||
|
if (body.code === 200) {
|
||||||
|
const d = body.data?.data || body.data || []
|
||||||
|
tableData.value = (Array.isArray(d) ? d : []).map(item => ({ ...item, _showToken: false }))
|
||||||
|
total.value = body.data?.all_count || body.total || 0
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearch = () => {
|
||||||
|
queryParams.page = 1
|
||||||
|
fetchList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleReset = () => {
|
||||||
|
queryParams.key = ''
|
||||||
|
queryParams.page = 1
|
||||||
|
fetchList()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
isEdit.value = false
|
||||||
|
form.value = { id: null, name: '', note: '', host: '', service_token: '', smtp_host: '', smtp_port: null, smtp_tls_enable: false }
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEdit = (row) => {
|
||||||
|
isEdit.value = true
|
||||||
|
form.value = {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
note: row.note || '',
|
||||||
|
host: row.host,
|
||||||
|
service_token: row.serviceToken || row.service_token || '',
|
||||||
|
smtp_host: row.smtpHost || row.smtp_host || '',
|
||||||
|
smtp_port: row.smtpPort || row.smtp_port || null,
|
||||||
|
smtp_tls_enable: !!(row.smtpTlsEnable ?? row.smtp_tls_enable)
|
||||||
|
}
|
||||||
|
dialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
await formRef.value?.validate()
|
||||||
|
submitting.value = true
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (isEdit.value) params.append('id', form.value.id)
|
||||||
|
params.append('name', form.value.name)
|
||||||
|
params.append('note', form.value.note)
|
||||||
|
params.append('host', form.value.host)
|
||||||
|
params.append('service_token', form.value.service_token)
|
||||||
|
if (form.value.smtp_host) params.append('smtp_host', form.value.smtp_host)
|
||||||
|
if (form.value.smtp_port) params.append('smtp_port', form.value.smtp_port)
|
||||||
|
params.append('smtp_tls_enable', form.value.smtp_tls_enable ? 'true' : 'false')
|
||||||
|
|
||||||
|
const fn = isEdit.value ? updateMailService : createMailService
|
||||||
|
const res = await fn(params)
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
|
||||||
|
dialogVisible.value = false
|
||||||
|
fetchList()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.data.message || '操作失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('操作失败')
|
||||||
|
} finally {
|
||||||
|
submitting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (row) => {
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.append('id', row.id)
|
||||||
|
const res = await deleteMailService(params)
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
fetchList()
|
||||||
|
} else {
|
||||||
|
ElMessage.error(res.data.message || '删除失败')
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
ElMessage.error('删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const openConsole = (row) => {
|
||||||
|
const base = (row.host || '').replace(/\/+$/, '')
|
||||||
|
if (!base) return ElMessage.warning('该服务未配置地址')
|
||||||
|
window.open(`${base}/ui/index.html?token=${encodeURIComponent(row.serviceToken || '')}`, '_blank')
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
fetchList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.mail-service-page {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
padding: 20px 24px;
|
||||||
|
background: linear-gradient(135deg, #f0f7ff 0%, #e8f4f8 100%);
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #e0ecf5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-desc {
|
||||||
|
margin: 4px 0 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.service-name-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-text {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note-text {
|
||||||
|
color: #909399;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.host-tag {
|
||||||
|
font-family: 'Monaco', 'Menlo', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smtp-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.smtp-host {
|
||||||
|
font-family: 'Monaco', 'Menlo', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-cell {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-mask {
|
||||||
|
font-family: 'Monaco', 'Menlo', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.token-full {
|
||||||
|
font-family: 'Monaco', 'Menlo', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #303133;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-wrap {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,677 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="discount-code-container">
|
|
||||||
<!-- 主容器 -->
|
|
||||||
<el-card class="main-container" shadow="never">
|
|
||||||
<!-- 搜索和操作栏 -->
|
|
||||||
<div class="filter-section">
|
|
||||||
<div class="filter-content">
|
|
||||||
<div class="action-bar">
|
|
||||||
<el-button type="primary" @click="handleAdd">
|
|
||||||
<el-icon><Plus /></el-icon>新增优惠码
|
|
||||||
</el-button>
|
|
||||||
<el-button type="success" @click="fetchDiscountList">
|
|
||||||
<el-icon><Refresh /></el-icon>刷新
|
|
||||||
</el-button>
|
|
||||||
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
|
||||||
<el-icon><Delete /></el-icon>批量删除
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 优惠码列表 -->
|
|
||||||
<div class="table-section">
|
|
||||||
<!-- 骨架屏 -->
|
|
||||||
<div v-if="loading" class="skeleton-container">
|
|
||||||
<div v-for="i in 5" :key="i" class="skeleton-row">
|
|
||||||
<div class="skeleton-cell skeleton-checkbox"></div>
|
|
||||||
<div class="skeleton-cell skeleton-id"></div>
|
|
||||||
<div class="skeleton-cell skeleton-code"></div>
|
|
||||||
<div class="skeleton-cell skeleton-name"></div>
|
|
||||||
<div class="skeleton-cell skeleton-type"></div>
|
|
||||||
<div class="skeleton-cell skeleton-value"></div>
|
|
||||||
<div class="skeleton-cell skeleton-min"></div>
|
|
||||||
<div class="skeleton-cell skeleton-max"></div>
|
|
||||||
<div class="skeleton-cell skeleton-times"></div>
|
|
||||||
<div class="skeleton-cell skeleton-action"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-table
|
|
||||||
v-else
|
|
||||||
v-loading="loading"
|
|
||||||
:data="discountList"
|
|
||||||
@selection-change="handleSelectionChange"
|
|
||||||
style="width: 100%"
|
|
||||||
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
|
||||||
>
|
|
||||||
<el-table-column type="selection" width="55" />
|
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
|
||||||
<el-table-column prop="code" label="优惠码" min-width="150" />
|
|
||||||
<el-table-column prop="name" label="名称" min-width="180" />
|
|
||||||
<el-table-column label="优惠类型" width="120">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="row.percentage ? 'success' : 'primary'">
|
|
||||||
{{ row.percentage ? '百分比折扣' : '固定金额' }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="优惠值" width="120">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span v-if="row.percentage" class="discount-value">{{ (row.percentage / 100).toFixed(0) }}%</span>
|
|
||||||
<span v-else class="amount">¥{{ (row.amount / 100).toFixed(2) }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="最低消费" width="120">
|
|
||||||
<template #default="{ row }">
|
|
||||||
¥{{ (row.minAmount / 100).toFixed(2) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="最大抵扣" width="120">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span v-if="row.maxAmount">¥{{ (row.maxAmount / 100).toFixed(2) }}</span>
|
|
||||||
<span v-else>-</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="maxTimes" label="最大使用次数" width="120" />
|
|
||||||
<el-table-column prop="userTimes" label="单用户次数" width="120" />
|
|
||||||
<el-table-column label="可叠加" width="100" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-icon v-if="row.canStacking" color="#67c23a" :size="20"><SuccessFilled /></el-icon>
|
|
||||||
<el-icon v-else color="#f56c6c" :size="20"><CircleCloseFilled /></el-icon>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="续费可用" width="100" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-icon v-if="row.renew" color="#67c23a" :size="20"><SuccessFilled /></el-icon>
|
|
||||||
<el-icon v-else color="#f56c6c" :size="20"><CircleCloseFilled /></el-icon>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="200" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<div class="action-buttons">
|
|
||||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
|
||||||
<el-button type="success" link @click="handleView(row)">查看</el-button>
|
|
||||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<!-- 分页 -->
|
|
||||||
<el-pagination
|
|
||||||
v-model:current-page="queryParams.page"
|
|
||||||
v-model:page-size="queryParams.count"
|
|
||||||
:page-sizes="[10, 20, 50, 100]"
|
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
|
||||||
:total="total"
|
|
||||||
@size-change="handleSizeChange"
|
|
||||||
@current-change="handleCurrentChange"
|
|
||||||
background
|
|
||||||
class="pagination"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<!-- 优惠码表单对话框 -->
|
|
||||||
<el-dialog
|
|
||||||
v-model="dialogVisible"
|
|
||||||
:title="dialogType === 'add' ? '新增优惠码' : '编辑优惠码'"
|
|
||||||
width="700px"
|
|
||||||
append-to-body
|
|
||||||
>
|
|
||||||
<el-form
|
|
||||||
ref="discountFormRef"
|
|
||||||
:model="discountForm"
|
|
||||||
:rules="discountRules"
|
|
||||||
label-width="140px"
|
|
||||||
>
|
|
||||||
<el-form-item label="优惠码" prop="code">
|
|
||||||
<el-input v-model="discountForm.code" placeholder="请输入优惠码" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="优惠码名称" prop="name">
|
|
||||||
<el-input v-model="discountForm.name" placeholder="请输入优惠码名称" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="备注" prop="note">
|
|
||||||
<el-input v-model="discountForm.note" type="textarea" :rows="2" placeholder="请输入备注" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="优惠类型" prop="discount_mode">
|
|
||||||
<el-radio-group v-model="discountForm.discount_mode">
|
|
||||||
<el-radio label="amount">固定金额</el-radio>
|
|
||||||
<el-radio label="percentage">百分比折扣</el-radio>
|
|
||||||
</el-radio-group>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="discountForm.discount_mode === 'amount'" label="优惠金额" prop="amount">
|
|
||||||
<div class="unit-input-row">
|
|
||||||
<el-input-number v-model="discountForm.amount" :min="0" :precision="2" :step="0.01" placeholder="请输入优惠金额" style="flex:1" />
|
|
||||||
<span class="unit-text">元</span>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item v-if="discountForm.discount_mode === 'percentage'" label="优惠百分比(%)" prop="percentage">
|
|
||||||
<el-input-number v-model="discountForm.percentage" :min="0" :max="100" :precision="0" placeholder="请输入百分比(1-100)" style="width: 100%" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="最低消费" prop="min_amount">
|
|
||||||
<div class="unit-input-row">
|
|
||||||
<el-input-number v-model="discountForm.min_amount" :min="0" :precision="2" :step="0.01" placeholder="满多少可使用" style="flex:1" />
|
|
||||||
<span class="unit-text">元</span>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="最大抵扣" prop="max_amount">
|
|
||||||
<div class="unit-input-row">
|
|
||||||
<el-input-number v-model="discountForm.max_amount" :min="0" :precision="2" :step="0.01" placeholder="0表示无限制" style="flex:1" />
|
|
||||||
<span class="unit-text">元</span>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="最大使用次数" prop="max_times">
|
|
||||||
<el-input-number v-model="discountForm.max_times" :min="0" placeholder="0表示无限制" style="width: 100%" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="单用户最大次数" prop="user_times">
|
|
||||||
<el-input-number v-model="discountForm.user_times" :min="0" placeholder="0表示无限制" style="width: 100%" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="有效期" prop="timeRange">
|
|
||||||
<el-date-picker
|
|
||||||
v-model="discountForm.timeRange"
|
|
||||||
type="datetimerange"
|
|
||||||
range-separator="至"
|
|
||||||
start-placeholder="开始时间"
|
|
||||||
end-placeholder="结束时间"
|
|
||||||
value-format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
:teleported="true"
|
|
||||||
popper-class="discount-date-picker"
|
|
||||||
placement="top-start"
|
|
||||||
:editable="true"
|
|
||||||
:clearable="true"
|
|
||||||
style="width: 100%"
|
|
||||||
@keyup.enter="handleDatePickerEnter"
|
|
||||||
/>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="续费可用" prop="renew">
|
|
||||||
<el-switch v-model="discountForm.renew" active-text="是" inactive-text="否" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="同类型可叠加" prop="can_stacking">
|
|
||||||
<el-switch v-model="discountForm.can_stacking" active-text="是" inactive-text="否" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="其他类型可叠加" prop="can_combine">
|
|
||||||
<el-switch v-model="discountForm.can_combine" active-text="是" inactive-text="否" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<div class="dialog-footer">
|
|
||||||
<el-button @click="dialogVisible = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<!-- 详情查看对话框 -->
|
|
||||||
<DiscountDetailDialog
|
|
||||||
v-model="detailDialogVisible"
|
|
||||||
type="code"
|
|
||||||
:detail-data="currentDetail"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
||||||
import { Plus, Delete, Search, Refresh, SuccessFilled, CircleCloseFilled } from '@element-plus/icons-vue'
|
|
||||||
import {
|
|
||||||
getDiscountCodeList,
|
|
||||||
getDiscountCodeDetail,
|
|
||||||
createDiscountCode,
|
|
||||||
updateDiscountCode,
|
|
||||||
deleteDiscountCode
|
|
||||||
} from '@/api/admin/discount'
|
|
||||||
import { timeToTimestamp } from '@/utils/tool'
|
|
||||||
import DiscountDetailDialog from '@/components/marketing/DiscountDetailDialog.vue'
|
|
||||||
|
|
||||||
// 查询参数
|
|
||||||
const queryParams = reactive({
|
|
||||||
discount_type: 'code', // 固定为code表示优惠码
|
|
||||||
page: 1,
|
|
||||||
count: 10
|
|
||||||
})
|
|
||||||
|
|
||||||
// 优惠码表单
|
|
||||||
const discountForm = reactive({
|
|
||||||
code_id: undefined,
|
|
||||||
discount_type: 'code', // 固定为code
|
|
||||||
code: '',
|
|
||||||
name: '',
|
|
||||||
note: '',
|
|
||||||
discount_mode: 'amount', // amount 或 percentage
|
|
||||||
amount: 0,
|
|
||||||
percentage: 0,
|
|
||||||
min_amount: 0,
|
|
||||||
max_amount: 0,
|
|
||||||
max_times: 0,
|
|
||||||
user_times: 0,
|
|
||||||
timeRange: [],
|
|
||||||
renew: false,
|
|
||||||
can_stacking: false,
|
|
||||||
can_combine: false
|
|
||||||
})
|
|
||||||
|
|
||||||
const discountRules = {
|
|
||||||
code: [
|
|
||||||
{ required: true, message: '请输入优惠码', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
name: [
|
|
||||||
{ required: true, message: '请输入优惠码名称', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
discount_mode: [
|
|
||||||
{ required: true, message: '请选择优惠类型', trigger: 'change' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 状态数据
|
|
||||||
const loading = ref(false)
|
|
||||||
const discountList = ref([])
|
|
||||||
const total = ref(0)
|
|
||||||
const selectedRows = ref([])
|
|
||||||
const dialogVisible = ref(false)
|
|
||||||
const dialogType = ref('add')
|
|
||||||
const discountFormRef = ref(null)
|
|
||||||
const detailDialogVisible = ref(false)
|
|
||||||
const currentDetail = ref(null)
|
|
||||||
|
|
||||||
// 获取优惠码列表
|
|
||||||
const fetchDiscountList = async () => {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const res = await getDiscountCodeList(queryParams)
|
|
||||||
console.log('优惠码列表数据:', res.data)
|
|
||||||
if (res.data.code === 200) {
|
|
||||||
discountList.value = res.data.data?.data || []
|
|
||||||
total.value = res.data.data?.all_count || 0
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取优惠码列表失败:', error)
|
|
||||||
ElMessage.error('获取优惠码列表失败')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 选择项变化
|
|
||||||
const handleSelectionChange = (selection) => {
|
|
||||||
selectedRows.value = selection
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分页
|
|
||||||
const handleSizeChange = (size) => {
|
|
||||||
queryParams.count = size
|
|
||||||
fetchDiscountList()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCurrentChange = (page) => {
|
|
||||||
queryParams.page = page
|
|
||||||
fetchDiscountList()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 新增优惠码
|
|
||||||
const handleAdd = () => {
|
|
||||||
dialogType.value = 'add'
|
|
||||||
dialogVisible.value = true
|
|
||||||
Object.assign(discountForm, {
|
|
||||||
code_id: undefined,
|
|
||||||
discount_type: 'code',
|
|
||||||
code: '',
|
|
||||||
name: '',
|
|
||||||
note: '',
|
|
||||||
discount_mode: 'amount',
|
|
||||||
amount: 0,
|
|
||||||
percentage: 0,
|
|
||||||
min_amount: 0,
|
|
||||||
max_amount: 0,
|
|
||||||
max_times: 0,
|
|
||||||
user_times: 0,
|
|
||||||
timeRange: [],
|
|
||||||
renew: false,
|
|
||||||
can_stacking: false,
|
|
||||||
can_combine: false
|
|
||||||
})
|
|
||||||
discountFormRef.value?.resetFields()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 编辑优惠码
|
|
||||||
const handleEdit = (row) => {
|
|
||||||
dialogType.value = 'edit'
|
|
||||||
dialogVisible.value = true
|
|
||||||
|
|
||||||
// 转换日期字符串为日期选择器格式
|
|
||||||
const startTime = row.startTime ? new Date(row.startTime).toLocaleString('zh-CN', {year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit'}).replace(/\//g, '-') : ''
|
|
||||||
const endTime = row.endTime ? new Date(row.endTime).toLocaleString('zh-CN', {year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit'}).replace(/\//g, '-') : ''
|
|
||||||
|
|
||||||
Object.assign(discountForm, {
|
|
||||||
code_id: row.id,
|
|
||||||
discount_type: 'code',
|
|
||||||
code: row.code,
|
|
||||||
name: row.name,
|
|
||||||
note: row.note || '',
|
|
||||||
discount_mode: row.percentage ? 'percentage' : 'amount',
|
|
||||||
amount: row.amount ? row.amount / 100 : 0,
|
|
||||||
percentage: row.percentage ? row.percentage / 100 : 0,
|
|
||||||
min_amount: row.minAmount ? row.minAmount / 100 : 0,
|
|
||||||
max_amount: row.maxAmount ? row.maxAmount / 100 : 0,
|
|
||||||
max_times: row.maxTimes || 0,
|
|
||||||
user_times: row.userTimes || 0,
|
|
||||||
timeRange: startTime && endTime ? [startTime, endTime] : [],
|
|
||||||
renew: row.renew || false,
|
|
||||||
can_stacking: row.canStacking || false,
|
|
||||||
can_combine: row.canCombine || false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查看优惠码详情
|
|
||||||
const handleView = async (row) => {
|
|
||||||
try {
|
|
||||||
const res = await getDiscountCodeDetail({ code_id: row.id })
|
|
||||||
console.log('优惠码详情:', res.data)
|
|
||||||
if (res.data.code === 200) {
|
|
||||||
currentDetail.value = res.data.data
|
|
||||||
detailDialogVisible.value = true
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取优惠码详情失败:', error)
|
|
||||||
ElMessage.error('获取优惠码详情失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除优惠码
|
|
||||||
const handleDelete = (row) => {
|
|
||||||
ElMessageBox.confirm(`确认删除优惠码 ${row.code} 吗?`, '警告', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
}).then(async () => {
|
|
||||||
try {
|
|
||||||
const res = await deleteDiscountCode({ code_id: row.id })
|
|
||||||
console.log('删除响应:', res.data)
|
|
||||||
if (res.data.code === 200) {
|
|
||||||
ElMessage.success('删除成功')
|
|
||||||
fetchDiscountList()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('删除失败:', error)
|
|
||||||
ElMessage.error(error.response?.data?.message || '删除失败')
|
|
||||||
}
|
|
||||||
}).catch(() => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量删除
|
|
||||||
const handleBatchDelete = () => {
|
|
||||||
if (selectedRows.value.length === 0) {
|
|
||||||
ElMessage.warning('请至少选择一条记录')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条记录吗?`, '警告', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
}).then(async () => {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const deletePromises = selectedRows.value.map(row =>
|
|
||||||
deleteDiscountCode({ code_id: row.id })
|
|
||||||
)
|
|
||||||
|
|
||||||
const results = await Promise.allSettled(deletePromises)
|
|
||||||
|
|
||||||
const successCount = results.filter(r => r.status === 'fulfilled' && r.value?.data?.code === 200).length
|
|
||||||
const failCount = results.length - successCount
|
|
||||||
|
|
||||||
if (failCount === 0) {
|
|
||||||
ElMessage.success(`批量删除成功,共删除 ${successCount} 条记录`)
|
|
||||||
} else if (successCount === 0) {
|
|
||||||
ElMessage.error(`批量删除失败,所有 ${failCount} 条记录删除失败`)
|
|
||||||
} else {
|
|
||||||
ElMessage.warning(`批量删除完成,成功 ${successCount} 条,失败 ${failCount} 条`)
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchDiscountList()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('批量删除失败:', error)
|
|
||||||
ElMessage.error('批量删除操作异常')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}).catch(() => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理日期选择器回车事件
|
|
||||||
const handleDatePickerEnter = (event) => {
|
|
||||||
// 回车键确认日期选择
|
|
||||||
const datePicker = event.target.closest('.el-date-editor')
|
|
||||||
if (datePicker) {
|
|
||||||
// 触发失焦事件,确认日期选择
|
|
||||||
event.target.blur()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交表单
|
|
||||||
const submitForm = () => {
|
|
||||||
discountFormRef.value?.validate(async (valid) => {
|
|
||||||
if (valid) {
|
|
||||||
try {
|
|
||||||
const submitData = {
|
|
||||||
discount_type: 'code',
|
|
||||||
code: discountForm.code,
|
|
||||||
name: discountForm.name,
|
|
||||||
note: discountForm.note,
|
|
||||||
min_amount: Math.round(discountForm.min_amount * 100),
|
|
||||||
max_amount: Math.round(discountForm.max_amount * 100),
|
|
||||||
max_times: discountForm.max_times || 0,
|
|
||||||
user_times: discountForm.user_times || 0,
|
|
||||||
renew: discountForm.renew,
|
|
||||||
can_stacking: discountForm.can_stacking,
|
|
||||||
can_combine: discountForm.can_combine
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据优惠类型设置amount或percentage
|
|
||||||
if (discountForm.discount_mode === 'percentage') {
|
|
||||||
submitData.percentage = Math.round(discountForm.percentage * 100)
|
|
||||||
submitData.amount = 0
|
|
||||||
} else {
|
|
||||||
submitData.amount = Math.round(discountForm.amount * 100)
|
|
||||||
submitData.percentage = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理时间(转换为秒级时间戳)
|
|
||||||
if (discountForm.timeRange && discountForm.timeRange.length === 2) {
|
|
||||||
submitData.start_time = timeToTimestamp(discountForm.timeRange[0])
|
|
||||||
submitData.end_time = timeToTimestamp(discountForm.timeRange[1])
|
|
||||||
} else {
|
|
||||||
submitData.start_time = ''
|
|
||||||
submitData.end_time = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果是编辑,添加code_id
|
|
||||||
if (dialogType.value === 'edit') {
|
|
||||||
submitData.code_id = discountForm.code_id
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('提交优惠码数据:', submitData)
|
|
||||||
|
|
||||||
let res
|
|
||||||
if (dialogType.value === 'add') {
|
|
||||||
res = await createDiscountCode(submitData)
|
|
||||||
} else {
|
|
||||||
res = await updateDiscountCode(submitData)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('提交响应:', res.data)
|
|
||||||
if (res.data.code === 200) {
|
|
||||||
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
|
|
||||||
dialogVisible.value = false
|
|
||||||
fetchDiscountList()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('操作失败:', error)
|
|
||||||
ElMessage.error(error.response?.data?.message || '操作失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化
|
|
||||||
onMounted(() => {
|
|
||||||
fetchDiscountList()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.discount-code-container {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-container {
|
|
||||||
border: 1px solid #e1e8ed;
|
|
||||||
background: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-section {
|
|
||||||
padding: 0;
|
|
||||||
border-bottom: 1px solid #e1e8ed;
|
|
||||||
background: #fafbfc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-content {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: center;
|
|
||||||
padding: 16px 20px;
|
|
||||||
gap: 20px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-bar {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-section {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount {
|
|
||||||
color: #f56c6c;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discount-value {
|
|
||||||
color: #67c23a;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination {
|
|
||||||
margin-top: 20px;
|
|
||||||
padding: 16px 20px;
|
|
||||||
border-top: 1px solid #e1e8ed;
|
|
||||||
background: #fafbfc;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-footer {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 12px;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 表格样式优化 */
|
|
||||||
:deep(.el-table) {
|
|
||||||
border: none;
|
|
||||||
color: #2c3e50;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table__header) {
|
|
||||||
background: #f8f9fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table th) {
|
|
||||||
background: #f8f9fa !important;
|
|
||||||
border-bottom: 2px solid #e1e8ed;
|
|
||||||
color: #2c3e50;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table td) {
|
|
||||||
border-bottom: 1px solid #f0f2f5;
|
|
||||||
color: #34495e;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-table tr:hover > td) {
|
|
||||||
background-color: #f8f9fa !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
:deep(.el-card__body) {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 骨架屏样式 */
|
|
||||||
.skeleton-container {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding: 16px 0;
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-row:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-cell {
|
|
||||||
height: 20px;
|
|
||||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
|
||||||
background-size: 200% 100%;
|
|
||||||
animation: skeleton-loading 1.5s ease-in-out infinite;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-checkbox { width: 55px; }
|
|
||||||
.skeleton-id { width: 80px; }
|
|
||||||
.skeleton-code { width: 150px; }
|
|
||||||
.skeleton-name { width: 180px; }
|
|
||||||
.skeleton-type { width: 120px; }
|
|
||||||
.skeleton-value { width: 120px; }
|
|
||||||
.skeleton-min { width: 120px; }
|
|
||||||
.skeleton-max { width: 120px; }
|
|
||||||
.skeleton-times { width: 120px; }
|
|
||||||
.skeleton-action { width: 200px; height: 32px; }
|
|
||||||
|
|
||||||
@keyframes skeleton-loading {
|
|
||||||
0% { background-position: 200% 0; }
|
|
||||||
100% { background-position: -200% 0; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.unit-input-row { display: flex; align-items: center; gap: 6px; width: 100%; }
|
|
||||||
.unit-text { font-size: 13px; color: #606266; flex-shrink: 0; white-space: nowrap; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* 时间选择器弹出层样式 - 非 scoped */
|
|
||||||
.discount-date-picker {
|
|
||||||
z-index: 9999 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.discount-date-picker .el-picker-panel {
|
|
||||||
max-width: 90vw;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -161,47 +161,39 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
|
||||||
<el-form-item label="选择类型" prop="select_type" v-if="dialogType === 'add'">
|
<el-form-item label="选择关联对象" v-if="dialogType === 'add'">
|
||||||
<el-radio-group v-model="form.select_type" @change="handleSelectTypeChange">
|
<div class="goods-tree-wrapper">
|
||||||
<el-radio value="product">商品</el-radio>
|
<div class="goods-tree-toolbar">
|
||||||
<el-radio value="product_group">商品组</el-radio>
|
<span class="tree-tip">可自由勾选商品组与商品,展开层级查看下属内容</span>
|
||||||
</el-radio-group>
|
<div class="tree-summary">
|
||||||
</el-form-item>
|
已选 <b>{{ checkedSummary.groupCount }}</b> 个商品组 / <b>{{ checkedSummary.productCount }}</b> 个商品
|
||||||
|
</div>
|
||||||
<el-form-item label="选择商品" prop="selected_product" v-if="dialogType === 'add' && form.select_type === 'product'">
|
</div>
|
||||||
<el-select
|
<el-tree
|
||||||
v-model="form.selected_product"
|
ref="goodsTreeRef"
|
||||||
placeholder="请选择商品"
|
:props="treeProps"
|
||||||
filterable
|
:load="loadTreeNode"
|
||||||
clearable
|
lazy
|
||||||
style="width: 100%"
|
show-checkbox
|
||||||
@change="handleProductChange"
|
check-strictly
|
||||||
|
node-key="key"
|
||||||
|
class="goods-tree"
|
||||||
|
@check="handleTreeCheck"
|
||||||
>
|
>
|
||||||
<el-option
|
<template #default="{ data }">
|
||||||
v-for="item in productOptions"
|
<span class="tree-node">
|
||||||
:key="item.id"
|
<el-tag size="small" :type="data.nodeType === 'group' ? 'warning' : 'primary'" effect="plain">
|
||||||
:label="`${item.name} (ID: ${item.id})`"
|
{{ data.nodeType === 'group' ? '组' : '品' }}
|
||||||
:value="item.id"
|
</el-tag>
|
||||||
/>
|
<span class="tree-node-label">{{ data.label }}</span>
|
||||||
</el-select>
|
<span class="tree-node-id">ID: {{ data.rawId }}</span>
|
||||||
</el-form-item>
|
<span v-if="data.nodeType === 'product' && data.price != null" class="tree-node-price">
|
||||||
|
¥{{ (data.price / 100).toFixed(2) }}
|
||||||
<el-form-item label="选择商品组" prop="selected_group" v-if="dialogType === 'add' && form.select_type === 'product_group'">
|
</span>
|
||||||
<el-select
|
</span>
|
||||||
v-model="form.selected_group"
|
</template>
|
||||||
placeholder="请选择商品组"
|
</el-tree>
|
||||||
filterable
|
</div>
|
||||||
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>
|
</el-form-item>
|
||||||
|
|
||||||
<!-- 编辑模式显示字段 -->
|
<!-- 编辑模式显示字段 -->
|
||||||
@@ -234,7 +226,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted, watch } from 'vue'
|
import { ref, reactive, onMounted, watch, nextTick } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Delete, Search, Plus, Refresh } from '@element-plus/icons-vue'
|
import { Delete, Search, Plus, Refresh } from '@element-plus/icons-vue'
|
||||||
import {
|
import {
|
||||||
@@ -276,36 +268,27 @@ const form = reactive({
|
|||||||
code_id: undefined,
|
code_id: undefined,
|
||||||
goods_id: undefined,
|
goods_id: undefined,
|
||||||
goods_name: '',
|
goods_name: '',
|
||||||
goods_type: '',
|
goods_type: ''
|
||||||
select_type: 'product', // 选择类型:product 或 product_group
|
|
||||||
selected_product: undefined, // 选中的商品ID
|
|
||||||
selected_group: undefined // 选中的商品组ID
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const formRules = {
|
const formRules = {
|
||||||
code_id: [
|
code_id: [
|
||||||
{ required: true, message: '请选择代金券', trigger: 'change' }
|
{ 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: [
|
goods_id: [
|
||||||
{ required: true, message: '请输入商品ID', trigger: 'blur' }
|
{ 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 loading = ref(false)
|
||||||
const goodsList = ref([])
|
const goodsList = ref([])
|
||||||
@@ -436,33 +419,79 @@ const fetchProductGroupList = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 选择类型变化
|
// 折叠层级选择器:懒加载节点
|
||||||
const handleSelectTypeChange = (type) => {
|
// node.level === 0 时加载顶级商品组;展开商品组时加载其子分组与下属商品
|
||||||
form.selected_product = undefined
|
const loadTreeNode = async (node, resolve) => {
|
||||||
form.selected_group = undefined
|
try {
|
||||||
form.goods_id = undefined
|
// 根节点:仅加载 level=1 的顶级商品组
|
||||||
form.goods_name = ''
|
if (node.level === 0) {
|
||||||
form.goods_type = ''
|
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) => {
|
if (node.data?.nodeType === 'group') {
|
||||||
const product = productOptions.value.find(item => item.id === productId)
|
const groupId = node.data.rawId
|
||||||
if (product) {
|
const childLevel = (node.data.level || 1) + 1
|
||||||
form.goods_id = product.id
|
const tasks = [
|
||||||
form.goods_name = product.goodsName || product.name || ''
|
getProductList({ good_group_id: groupId, delete: false })
|
||||||
form.goods_type = 'product'
|
]
|
||||||
|
// 仅当存在子分组时才请求下一级分组
|
||||||
|
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 buildGroupNode = (group) => ({
|
||||||
const group = productGroupOptions.value.find(item => item.id === groupId)
|
key: `group_${group.id}`,
|
||||||
if (group) {
|
rawId: group.id,
|
||||||
form.goods_id = group.id
|
nodeType: 'group',
|
||||||
form.goods_name = group.name || ''
|
label: group.name,
|
||||||
form.goods_type = 'product_group'
|
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,
|
code_id: queryParams.code_id ? Number(queryParams.code_id) : undefined,
|
||||||
goods_id: undefined,
|
goods_id: undefined,
|
||||||
goods_name: '',
|
goods_name: '',
|
||||||
goods_type: '',
|
goods_type: ''
|
||||||
select_type: 'product',
|
|
||||||
selected_product: undefined,
|
|
||||||
selected_group: undefined
|
|
||||||
})
|
})
|
||||||
|
checkedSummary.groupCount = 0
|
||||||
|
checkedSummary.productCount = 0
|
||||||
formRef.value?.resetFields()
|
formRef.value?.resetFields()
|
||||||
|
// 等待对话框渲染后清空树的勾选状态
|
||||||
// 加载商品和商品组列表
|
nextTick(() => {
|
||||||
fetchProductList()
|
goodsTreeRef.value?.setCheckedKeys([])
|
||||||
fetchProductGroupList()
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 编辑商品关联
|
// 编辑商品关联
|
||||||
@@ -641,69 +669,43 @@ const handleBatchDelete = () => {
|
|||||||
|
|
||||||
// 提交表单
|
// 提交表单
|
||||||
const submitForm = () => {
|
const submitForm = () => {
|
||||||
// 新增模式下的额外验证
|
|
||||||
if (dialogType.value === 'add') {
|
|
||||||
if (!form.code_id) {
|
if (!form.code_id) {
|
||||||
ElMessage.warning('请选择代金券')
|
ElMessage.warning('请选择代金券')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (form.select_type === 'product' && !form.selected_product) {
|
|
||||||
ElMessage.warning('请选择商品')
|
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
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
submitAdd(goodIds, goodGroupIds)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑模式
|
||||||
formRef.value?.validate(async (valid) => {
|
formRef.value?.validate(async (valid) => {
|
||||||
if (valid) {
|
if (!valid) return
|
||||||
try {
|
try {
|
||||||
const submitData = {
|
const submitData = {
|
||||||
code_id: String(form.code_id)
|
code_id: String(form.code_id),
|
||||||
|
discount_good_id: String(form.id)
|
||||||
}
|
}
|
||||||
|
if (form.goods_type === 'product_group') {
|
||||||
// 根据选择类型决定传 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)
|
submitData.good_group_id = String(form.goods_id)
|
||||||
} else {
|
} else {
|
||||||
// 其他类型默认使用 good_id
|
|
||||||
submitData.good_id = String(form.goods_id)
|
submitData.good_id = String(form.goods_id)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
console.log('提交商品关联数据:', submitData)
|
const res = await updateDiscountGoods(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) {
|
if (res.data.code === 200) {
|
||||||
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
|
ElMessage.success('修改成功')
|
||||||
dialogVisible.value = false
|
dialogVisible.value = false
|
||||||
fetchGoodsList()
|
fetchGoodsList()
|
||||||
}
|
}
|
||||||
@@ -711,10 +713,33 @@ const submitForm = () => {
|
|||||||
console.error('操作失败:', error)
|
console.error('操作失败:', error)
|
||||||
ElMessage.error(error.response?.data?.message || '操作失败')
|
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(() => {
|
onMounted(() => {
|
||||||
fetchVoucherListOptions()
|
fetchVoucherListOptions()
|
||||||
@@ -798,6 +823,57 @@ onMounted(() => {
|
|||||||
padding: 0;
|
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) {
|
:deep(.el-card__body) {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,569 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="voucher-container">
|
|
||||||
<!-- 搜索和操作栏 -->
|
|
||||||
<el-card class="filter-container" shadow="never">
|
|
||||||
<div class="action-bar">
|
|
||||||
<el-button type="primary" @click="handleAdd">
|
|
||||||
<el-icon><Plus /></el-icon>新增代金券
|
|
||||||
</el-button>
|
|
||||||
<el-button type="success" @click="fetchVoucherList">
|
|
||||||
<el-icon><Refresh /></el-icon>刷新
|
|
||||||
</el-button>
|
|
||||||
<el-button type="danger" :disabled="!selectedRows.length" @click="handleBatchDelete">
|
|
||||||
<el-icon><Delete /></el-icon>批量删除
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<!-- 代金券列表 -->
|
|
||||||
<el-card class="table-container" shadow="never">
|
|
||||||
<el-table
|
|
||||||
v-loading="loading"
|
|
||||||
:data="voucherList"
|
|
||||||
@selection-change="handleSelectionChange"
|
|
||||||
style="width: 100%"
|
|
||||||
>
|
|
||||||
<el-table-column type="selection" width="55" />
|
|
||||||
<el-table-column prop="id" label="ID" width="80" />
|
|
||||||
<el-table-column prop="name" label="代金券名称" min-width="200" />
|
|
||||||
<el-table-column label="面额" width="120">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span class="amount">¥{{ (row.amount / 100).toFixed(2) }}</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="最低消费" width="120">
|
|
||||||
<template #default="{ row }">
|
|
||||||
¥{{ (row.minAmount / 100).toFixed(2) }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="最大抵扣" width="120">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<span v-if="row.maxAmount">¥{{ (row.maxAmount / 100).toFixed(2) }}</span>
|
|
||||||
<span v-else>无限制</span>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="maxTimes" label="最大使用次数" width="130">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ row.maxTimes || '无限制' }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="userTimes" label="单用户次数" width="120">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ row.userTimes || '无限制' }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="有效期(天)" width="100">
|
|
||||||
<template #default="{ row }">
|
|
||||||
{{ row.duration ? (row.duration / 86400).toFixed(0) : '-' }}
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="续费可用" width="100" align="center">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-icon v-if="row.renew" color="#67c23a" :size="20"><SuccessFilled /></el-icon>
|
|
||||||
<el-icon v-else color="#f56c6c" :size="20"><CircleCloseFilled /></el-icon>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="280" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-button type="primary" link @click="handleEdit(row)">编辑</el-button>
|
|
||||||
<el-button type="primary" link @click="handleManage(row)">管理</el-button>
|
|
||||||
<el-button type="success" link @click="handleView(row)">查看</el-button>
|
|
||||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
|
|
||||||
<!-- 分页 -->
|
|
||||||
<el-pagination
|
|
||||||
v-model:current-page="queryParams.page"
|
|
||||||
v-model:page-size="queryParams.count"
|
|
||||||
:page-sizes="[10, 20, 50, 100]"
|
|
||||||
layout="total, sizes, prev, pager, next, jumper"
|
|
||||||
:total="total"
|
|
||||||
@size-change="handleSizeChange"
|
|
||||||
@current-change="handleCurrentChange"
|
|
||||||
background
|
|
||||||
class="pagination"
|
|
||||||
/>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<!-- 代金券表单对话框 -->
|
|
||||||
<el-dialog
|
|
||||||
v-model="dialogVisible"
|
|
||||||
:title="dialogType === 'add' ? '新增代金券' : '编辑代金券'"
|
|
||||||
width="700px"
|
|
||||||
>
|
|
||||||
<el-form
|
|
||||||
ref="voucherFormRef"
|
|
||||||
:model="voucherForm"
|
|
||||||
:rules="voucherRules"
|
|
||||||
label-width="140px"
|
|
||||||
>
|
|
||||||
<el-form-item label="代金券名称" prop="name">
|
|
||||||
<el-input v-model="voucherForm.name" placeholder="请输入代金券名称" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="备注" prop="note">
|
|
||||||
<el-input v-model="voucherForm.note" type="textarea" :rows="2" placeholder="请输入备注" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="面额" prop="amount">
|
|
||||||
<div class="unit-input-row">
|
|
||||||
<el-input-number v-model="voucherForm.amount" :min="0" :precision="2" :step="0.01" placeholder="请输入面额" style="flex:1" />
|
|
||||||
<span class="unit-text">元</span>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="最低消费" prop="min_amount">
|
|
||||||
<div class="unit-input-row">
|
|
||||||
<el-input-number v-model="voucherForm.min_amount" :min="0" :precision="2" :step="0.01" placeholder="满多少可使用" style="flex:1" />
|
|
||||||
<span class="unit-text">元</span>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="最大抵扣" prop="max_amount">
|
|
||||||
<div class="unit-input-row">
|
|
||||||
<el-input-number v-model="voucherForm.max_amount" :min="0" :precision="2" :step="0.01" placeholder="0表示无限制" style="flex:1" />
|
|
||||||
<span class="unit-text">元</span>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="最大使用次数" prop="max_times">
|
|
||||||
<el-input-number v-model="voucherForm.max_times" :min="0" placeholder="0表示无限制" style="width: 100%" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="单用户最大次数" prop="user_times">
|
|
||||||
<el-input-number v-model="voucherForm.user_times" :min="0" placeholder="0表示无限制" style="width: 100%" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="有效期" prop="duration_days">
|
|
||||||
<div class="unit-input-row">
|
|
||||||
<el-input-number v-model="voucherForm.duration_days" :min="1" placeholder="代金券有效天数" style="flex:1" />
|
|
||||||
<span class="unit-text">天</span>
|
|
||||||
</div>
|
|
||||||
<div class="form-tip">代金券领取后的有效持续时间</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="发放时间范围" prop="timeRange">
|
|
||||||
<el-date-picker
|
|
||||||
v-model="voucherForm.timeRange"
|
|
||||||
type="datetimerange"
|
|
||||||
range-separator="至"
|
|
||||||
start-placeholder="开始时间"
|
|
||||||
end-placeholder="结束时间"
|
|
||||||
value-format="YYYY-MM-DD HH:mm:ss"
|
|
||||||
:teleported="true"
|
|
||||||
popper-class="voucher-date-picker"
|
|
||||||
placement="top-start"
|
|
||||||
:editable="true"
|
|
||||||
:clearable="true"
|
|
||||||
style="width: 100%"
|
|
||||||
@keyup.enter="handleDatePickerEnter"
|
|
||||||
/>
|
|
||||||
<div class="form-tip">代金券可以发放给用户的时间范围</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="续费可用" prop="renew">
|
|
||||||
<el-switch v-model="voucherForm.renew" active-text="是" inactive-text="否" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="同类型可叠加" prop="can_stacking">
|
|
||||||
<el-switch v-model="voucherForm.can_stacking" active-text="是" inactive-text="否" />
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="其他类型可叠加" prop="can_combine">
|
|
||||||
<el-switch v-model="voucherForm.can_combine" active-text="是" inactive-text="否" />
|
|
||||||
</el-form-item>
|
|
||||||
</el-form>
|
|
||||||
<template #footer>
|
|
||||||
<el-button @click="dialogVisible = false">取消</el-button>
|
|
||||||
<el-button type="primary" @click="submitForm">确定</el-button>
|
|
||||||
</template>
|
|
||||||
</el-dialog>
|
|
||||||
|
|
||||||
<!-- 详情查看对话框 -->
|
|
||||||
<DiscountDetailDialog
|
|
||||||
v-model="detailDialogVisible"
|
|
||||||
type="coupon"
|
|
||||||
:detail-data="currentDetail"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
||||||
import { Plus, Delete, Refresh, SuccessFilled, CircleCloseFilled } from '@element-plus/icons-vue'
|
|
||||||
import {
|
|
||||||
getDiscountCodeList,
|
|
||||||
getDiscountCodeDetail,
|
|
||||||
createDiscountCode,
|
|
||||||
updateDiscountCode,
|
|
||||||
deleteDiscountCode
|
|
||||||
} from '@/api/admin/discount'
|
|
||||||
import { timeToTimestamp } from '@/utils/tool'
|
|
||||||
import DiscountDetailDialog from '@/components/marketing/DiscountDetailDialog.vue'
|
|
||||||
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
// 查询参数
|
|
||||||
const queryParams = reactive({
|
|
||||||
discount_type: 'coupon', // 固定为coupon表示代金券
|
|
||||||
page: 1,
|
|
||||||
count: 10
|
|
||||||
})
|
|
||||||
|
|
||||||
// 代金券表单
|
|
||||||
const voucherForm = reactive({
|
|
||||||
code_id: undefined,
|
|
||||||
discount_type: 'coupon', // 固定为coupon
|
|
||||||
name: '',
|
|
||||||
note: '',
|
|
||||||
amount: 0,
|
|
||||||
min_amount: 0,
|
|
||||||
max_amount: 0,
|
|
||||||
max_times: 0,
|
|
||||||
user_times: 0,
|
|
||||||
duration_days: 30, // 默认30天
|
|
||||||
timeRange: [],
|
|
||||||
renew: false,
|
|
||||||
can_stacking: false,
|
|
||||||
can_combine: false
|
|
||||||
})
|
|
||||||
|
|
||||||
const voucherRules = {
|
|
||||||
name: [
|
|
||||||
{ required: true, message: '请输入代金券名称', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
amount: [
|
|
||||||
{ required: true, message: '请输入面额', trigger: 'blur' }
|
|
||||||
],
|
|
||||||
duration_days: [
|
|
||||||
{ required: true, message: '请输入有效期天数', trigger: 'blur' }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 状态数据
|
|
||||||
const loading = ref(false)
|
|
||||||
const voucherList = ref([])
|
|
||||||
const total = ref(0)
|
|
||||||
const selectedRows = ref([])
|
|
||||||
const dialogVisible = ref(false)
|
|
||||||
const dialogType = ref('add')
|
|
||||||
const voucherFormRef = ref(null)
|
|
||||||
const detailDialogVisible = ref(false)
|
|
||||||
const currentDetail = ref(null)
|
|
||||||
|
|
||||||
// 获取代金券列表
|
|
||||||
const fetchVoucherList = async () => {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const res = await getDiscountCodeList(queryParams)
|
|
||||||
console.log('代金券列表数据:', res.data)
|
|
||||||
if (res.data.code === 200) {
|
|
||||||
voucherList.value = res.data.data?.data || []
|
|
||||||
total.value = res.data.data?.all_count || 0
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取代金券列表失败:', error)
|
|
||||||
ElMessage.error('获取代金券列表失败')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 选择项变化
|
|
||||||
const handleSelectionChange = (selection) => {
|
|
||||||
selectedRows.value = selection
|
|
||||||
}
|
|
||||||
|
|
||||||
// 分页
|
|
||||||
const handleSizeChange = (size) => {
|
|
||||||
queryParams.count = size
|
|
||||||
fetchVoucherList()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCurrentChange = (page) => {
|
|
||||||
queryParams.page = page
|
|
||||||
fetchVoucherList()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 新增代金券
|
|
||||||
const handleAdd = () => {
|
|
||||||
dialogType.value = 'add'
|
|
||||||
dialogVisible.value = true
|
|
||||||
Object.assign(voucherForm, {
|
|
||||||
code_id: undefined,
|
|
||||||
discount_type: 'coupon',
|
|
||||||
name: '',
|
|
||||||
note: '',
|
|
||||||
amount: 0,
|
|
||||||
min_amount: 0,
|
|
||||||
max_amount: 0,
|
|
||||||
max_times: 0,
|
|
||||||
user_times: 0,
|
|
||||||
duration_days: 30,
|
|
||||||
timeRange: [],
|
|
||||||
renew: false,
|
|
||||||
can_stacking: false,
|
|
||||||
can_combine: false
|
|
||||||
})
|
|
||||||
voucherFormRef.value?.resetFields()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 编辑代金券
|
|
||||||
const handleEdit = (row) => {
|
|
||||||
dialogType.value = 'edit'
|
|
||||||
dialogVisible.value = true
|
|
||||||
|
|
||||||
console.log('编辑代金券原始数据:', row)
|
|
||||||
|
|
||||||
// 转换时间为日期字符串(YYYY-MM-DD HH:mm:ss 格式)
|
|
||||||
let startTime = ''
|
|
||||||
let endTime = ''
|
|
||||||
|
|
||||||
if (row.startTime) {
|
|
||||||
// 处理字符串格式的时间(如 "2026-01-08T00:00:00+08:00")
|
|
||||||
const start = new Date(row.startTime)
|
|
||||||
if (!isNaN(start.getTime())) {
|
|
||||||
startTime = start.getFullYear() + '-' +
|
|
||||||
String(start.getMonth() + 1).padStart(2, '0') + '-' +
|
|
||||||
String(start.getDate()).padStart(2, '0') + ' ' +
|
|
||||||
String(start.getHours()).padStart(2, '0') + ':' +
|
|
||||||
String(start.getMinutes()).padStart(2, '0') + ':' +
|
|
||||||
String(start.getSeconds()).padStart(2, '0')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (row.endTime) {
|
|
||||||
// 处理字符串格式的时间(如 "2026-02-25T00:00:00+08:00")
|
|
||||||
const end = new Date(row.endTime)
|
|
||||||
if (!isNaN(end.getTime())) {
|
|
||||||
endTime = end.getFullYear() + '-' +
|
|
||||||
String(end.getMonth() + 1).padStart(2, '0') + '-' +
|
|
||||||
String(end.getDate()).padStart(2, '0') + ' ' +
|
|
||||||
String(end.getHours()).padStart(2, '0') + ':' +
|
|
||||||
String(end.getMinutes()).padStart(2, '0') + ':' +
|
|
||||||
String(end.getSeconds()).padStart(2, '0')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('转换后的时间:', { startTime, endTime })
|
|
||||||
|
|
||||||
Object.assign(voucherForm, {
|
|
||||||
code_id: row.id,
|
|
||||||
discount_type: 'coupon',
|
|
||||||
name: row.name,
|
|
||||||
note: row.note || '',
|
|
||||||
amount: row.amount ? row.amount / 100 : 0,
|
|
||||||
min_amount: row.minAmount ? row.minAmount / 100 : 0,
|
|
||||||
max_amount: row.maxAmount ? row.maxAmount / 100 : 0,
|
|
||||||
max_times: row.maxTimes || 0,
|
|
||||||
user_times: row.userTimes || 0,
|
|
||||||
duration_days: row.duration ? Math.round(row.duration / 86400) : 30, // 秒转天
|
|
||||||
timeRange: startTime && endTime ? [startTime, endTime] : [],
|
|
||||||
renew: row.renew || false,
|
|
||||||
can_stacking: row.canStacking || false,
|
|
||||||
can_combine: row.canCombine || false
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('表单数据:', voucherForm)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 管理代金券
|
|
||||||
const handleManage = (row) => {
|
|
||||||
router.push(`/marketing/voucher/${row.id}/manage`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查看代金券详情
|
|
||||||
const handleView = async (row) => {
|
|
||||||
try {
|
|
||||||
const res = await getDiscountCodeDetail({ code_id: row.id })
|
|
||||||
console.log('代金券详情:', res.data)
|
|
||||||
if (res.data.code === 200) {
|
|
||||||
currentDetail.value = res.data.data
|
|
||||||
detailDialogVisible.value = true
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取代金券详情失败:', error)
|
|
||||||
ElMessage.error('获取代金券详情失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除代金券
|
|
||||||
const handleDelete = (row) => {
|
|
||||||
ElMessageBox.confirm(`确认删除代金券 ${row.name} 吗?`, '警告', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
}).then(async () => {
|
|
||||||
try {
|
|
||||||
const res = await deleteDiscountCode({ code_id: row.id })
|
|
||||||
console.log('删除响应:', res.data)
|
|
||||||
if (res.data.code === 200) {
|
|
||||||
ElMessage.success('删除成功')
|
|
||||||
fetchVoucherList()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('删除失败:', error)
|
|
||||||
ElMessage.error(error.response?.data?.message || '删除失败')
|
|
||||||
}
|
|
||||||
}).catch(() => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 批量删除
|
|
||||||
const handleBatchDelete = () => {
|
|
||||||
if (selectedRows.value.length === 0) {
|
|
||||||
ElMessage.warning('请至少选择一条记录')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ElMessageBox.confirm(`确认删除选中的 ${selectedRows.value.length} 条记录吗?`, '警告', {
|
|
||||||
confirmButtonText: '确定',
|
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning'
|
|
||||||
}).then(async () => {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const deletePromises = selectedRows.value.map(row =>
|
|
||||||
deleteDiscountCode({ code_id: row.id })
|
|
||||||
)
|
|
||||||
|
|
||||||
const results = await Promise.allSettled(deletePromises)
|
|
||||||
|
|
||||||
const successCount = results.filter(r => r.status === 'fulfilled' && r.value?.data?.code === 200).length
|
|
||||||
const failCount = results.length - successCount
|
|
||||||
|
|
||||||
if (failCount === 0) {
|
|
||||||
ElMessage.success(`批量删除成功,共删除 ${successCount} 条记录`)
|
|
||||||
} else if (successCount === 0) {
|
|
||||||
ElMessage.error(`批量删除失败,所有 ${failCount} 条记录删除失败`)
|
|
||||||
} else {
|
|
||||||
ElMessage.warning(`批量删除完成,成功 ${successCount} 条,失败 ${failCount} 条`)
|
|
||||||
}
|
|
||||||
|
|
||||||
fetchVoucherList()
|
|
||||||
} catch (error) {
|
|
||||||
console.error('批量删除失败:', error)
|
|
||||||
ElMessage.error('批量删除操作异常')
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}).catch(() => {})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理日期选择器回车事件
|
|
||||||
const handleDatePickerEnter = (event) => {
|
|
||||||
// 回车键确认日期选择
|
|
||||||
const datePicker = event.target.closest('.el-date-editor')
|
|
||||||
if (datePicker) {
|
|
||||||
// 触发失焦事件,确认日期选择
|
|
||||||
event.target.blur()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交代金券表单
|
|
||||||
const submitForm = () => {
|
|
||||||
voucherFormRef.value?.validate(async (valid) => {
|
|
||||||
if (valid) {
|
|
||||||
try {
|
|
||||||
const submitData = {
|
|
||||||
discount_type: 'coupon',
|
|
||||||
name: voucherForm.name,
|
|
||||||
note: voucherForm.note,
|
|
||||||
amount: Math.round(voucherForm.amount * 100),
|
|
||||||
percentage: 0, // 代金券固定为0
|
|
||||||
min_amount: Math.round(voucherForm.min_amount * 100),
|
|
||||||
max_amount: Math.round(voucherForm.max_amount * 100),
|
|
||||||
max_times: voucherForm.max_times || 0,
|
|
||||||
user_times: voucherForm.user_times || 0,
|
|
||||||
duration: voucherForm.duration_days * 86400, // 天转秒
|
|
||||||
renew: voucherForm.renew,
|
|
||||||
can_stacking: voucherForm.can_stacking,
|
|
||||||
can_combine: voucherForm.can_combine
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理时间(转换为秒级时间戳)
|
|
||||||
if (voucherForm.timeRange && voucherForm.timeRange.length === 2) {
|
|
||||||
submitData.start_time = timeToTimestamp(voucherForm.timeRange[0])
|
|
||||||
submitData.end_time = timeToTimestamp(voucherForm.timeRange[1])
|
|
||||||
} else {
|
|
||||||
submitData.start_time = ''
|
|
||||||
submitData.end_time = ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果是编辑,添加code_id
|
|
||||||
if (dialogType.value === 'edit') {
|
|
||||||
submitData.code_id = voucherForm.code_id
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('提交代金券数据:', submitData)
|
|
||||||
|
|
||||||
let res
|
|
||||||
if (dialogType.value === 'add') {
|
|
||||||
res = await createDiscountCode(submitData)
|
|
||||||
} else {
|
|
||||||
res = await updateDiscountCode(submitData)
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('提交响应:', res.data)
|
|
||||||
if (res.data.code === 200) {
|
|
||||||
ElMessage.success(dialogType.value === 'add' ? '新增成功' : '修改成功')
|
|
||||||
dialogVisible.value = false
|
|
||||||
fetchVoucherList()
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('操作失败:', error)
|
|
||||||
ElMessage.error(error.response?.data?.message || '操作失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化
|
|
||||||
onMounted(() => {
|
|
||||||
fetchVoucherList()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.voucher-container {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-container {
|
|
||||||
margin-bottom: 20px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.action-bar {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-container {
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.amount {
|
|
||||||
color: #f56c6c;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-tip {
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pagination {
|
|
||||||
margin-top: 24px;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
.unit-input-row { display: flex; align-items: center; gap: 6px; width: 100%; }
|
|
||||||
.unit-text { font-size: 13px; color: #606266; flex-shrink: 0; white-space: nowrap; }
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* 时间选择器弹出层样式 - 非 scoped */
|
|
||||||
.voucher-date-picker {
|
|
||||||
z-index: 9999 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.voucher-date-picker .el-picker-panel {
|
|
||||||
max-width: 90vw;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ const activeTab = ref('user-distribution')
|
|||||||
const voucherId = computed(() => route.params.id)
|
const voucherId = computed(() => route.params.id)
|
||||||
|
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
router.push('/marketing/voucher')
|
router.push('/product/discount')
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -567,6 +567,9 @@
|
|||||||
<el-icon class="group-picker-arrow"><ArrowRight /></el-icon>
|
<el-icon class="group-picker-arrow"><ArrowRight /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="所属表" prop="table">
|
||||||
|
<el-input v-model="productForm.table" placeholder="请输入商品所属表(如 kvm_service)" />
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="商品介绍" prop="content">
|
<el-form-item label="商品介绍" prop="content">
|
||||||
<el-input
|
<el-input
|
||||||
v-model="productForm.content"
|
v-model="productForm.content"
|
||||||
@@ -588,9 +591,9 @@
|
|||||||
@confirm="handleProductCoverSelect"
|
@confirm="handleProductCoverSelect"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- 价格与销售 -->
|
<!-- Tab 栏管理 -->
|
||||||
<div class="form-section">
|
<el-tabs v-model="productFormTab" class="product-form-tabs">
|
||||||
<div class="form-section-title">价格与销售</div>
|
<el-tab-pane label="价格与销售" name="sales">
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<el-form-item prop="price">
|
<el-form-item prop="price">
|
||||||
<template #label>
|
<template #label>
|
||||||
@@ -634,6 +637,20 @@
|
|||||||
inactive-text="禁止"
|
inactive-text="禁止"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item prop="renew_price" v-if="productForm.can_renew">
|
||||||
|
<template #label>
|
||||||
|
<span>续费价格<span class="unit-suffix">(元,0 沿用商品价格)</span></span>
|
||||||
|
</template>
|
||||||
|
<el-input-number
|
||||||
|
v-model="productForm.renew_price"
|
||||||
|
:min="0"
|
||||||
|
:precision="2"
|
||||||
|
:step="0.01"
|
||||||
|
placeholder="续费基础价格"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="购买类型" prop="arg_type">
|
<el-form-item label="购买类型" prop="arg_type">
|
||||||
<el-select
|
<el-select
|
||||||
v-model="productForm.arg_type"
|
v-model="productForm.arg_type"
|
||||||
@@ -654,11 +671,9 @@
|
|||||||
<div style="font-size:12px;color:#909399;margin-top:4px">启用后,用户购买/续费/升级此商品前须完成实名认证</div>
|
<div style="font-size:12px;color:#909399;margin-top:4px">启用后,用户购买/续费/升级此商品前须完成实名认证</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</el-tab-pane>
|
||||||
|
|
||||||
<!-- 库存管理 -->
|
<el-tab-pane label="库存管理" name="inventory">
|
||||||
<div class="form-section">
|
|
||||||
<div class="form-section-title">库存管理</div>
|
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<el-form-item label="库存控制" prop="inventory_control">
|
<el-form-item label="库存控制" prop="inventory_control">
|
||||||
<el-switch
|
<el-switch
|
||||||
@@ -677,12 +692,29 @@
|
|||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="售罄状态" prop="sold_out">
|
||||||
|
<el-switch
|
||||||
|
v-model="productForm.sold_out"
|
||||||
|
active-text="售罄"
|
||||||
|
inactive-text="正常"
|
||||||
|
active-color="#f56c6c"
|
||||||
|
/>
|
||||||
|
<div style="font-size:12px;color:#909399;margin-top:4px">开启后用户端将无法选购该商品</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="购买限制" prop="max_per_user">
|
||||||
|
<el-input-number
|
||||||
|
v-model="productForm.max_per_user"
|
||||||
|
:min="0"
|
||||||
|
placeholder="0 表示不限"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
<div style="font-size:12px;color:#909399;margin-top:4px">限制单用户最大购买数量,0 表示不限制</div>
|
||||||
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</el-tab-pane>
|
||||||
|
|
||||||
<!-- 推荐与参数 -->
|
<el-tab-pane label="推荐与通知" name="promotion">
|
||||||
<div class="form-section">
|
|
||||||
<div class="form-section-title">推荐与参数</div>
|
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<el-form-item label="推荐" prop="recommend">
|
<el-form-item label="推荐" prop="recommend">
|
||||||
<el-switch
|
<el-switch
|
||||||
@@ -705,8 +737,31 @@
|
|||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
/>
|
/>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item prop="renew_recommend_rebate">
|
||||||
|
<template #label>
|
||||||
|
<span>续费推介返还<span class="unit-suffix">(%,0 沿用推荐返还)</span></span>
|
||||||
|
</template>
|
||||||
|
<el-input-number
|
||||||
|
v-model="productForm.renew_recommend_rebate"
|
||||||
|
:min="0"
|
||||||
|
:max="100"
|
||||||
|
:disabled="!productForm.recommend"
|
||||||
|
placeholder="续费推介返还百分比"
|
||||||
|
controls-position="right"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="购买通知" prop="send_notice">
|
||||||
|
<el-switch
|
||||||
|
v-model="productForm.send_notice"
|
||||||
|
active-text="发送"
|
||||||
|
inactive-text="不发送"
|
||||||
|
/>
|
||||||
|
<div style="font-size:12px;color:#909399;margin:4px">用户购买后是否发送通知给管理员</div>
|
||||||
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</el-tab-pane>
|
||||||
|
</el-tabs>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<div class="dialog-footer">
|
<div class="dialog-footer">
|
||||||
@@ -873,7 +928,12 @@ const productForm = reactive({
|
|||||||
recommend_rebate: 0,
|
recommend_rebate: 0,
|
||||||
arg_type: 'all',
|
arg_type: 'all',
|
||||||
can_renew: true,
|
can_renew: true,
|
||||||
require_real_name: false
|
require_real_name: false,
|
||||||
|
sold_out: false,
|
||||||
|
max_per_user: 0,
|
||||||
|
send_notice: false,
|
||||||
|
renew_price: 0,
|
||||||
|
renew_recommend_rebate: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
const productRules = {
|
const productRules = {
|
||||||
@@ -895,6 +955,7 @@ const productRules = {
|
|||||||
// 商品对话框状态
|
// 商品对话框状态
|
||||||
const productDialogVisible = ref(false)
|
const productDialogVisible = ref(false)
|
||||||
const productDialogType = ref('add')
|
const productDialogType = ref('add')
|
||||||
|
const productFormTab = ref('sales')
|
||||||
const productFormRef = ref(null)
|
const productFormRef = ref(null)
|
||||||
const coverSelectorVisible = ref(false)
|
const coverSelectorVisible = ref(false)
|
||||||
|
|
||||||
@@ -1557,7 +1618,12 @@ const handleEditProduct = (product, parentGroupId) => {
|
|||||||
recommend_rebate: product.recommendRebate,
|
recommend_rebate: product.recommendRebate,
|
||||||
arg_type: product.argType || 'all',
|
arg_type: product.argType || 'all',
|
||||||
can_renew: product.canRenew !== undefined ? product.canRenew : (product.can_renew !== undefined ? product.can_renew : true),
|
can_renew: product.canRenew !== undefined ? product.canRenew : (product.can_renew !== undefined ? product.can_renew : true),
|
||||||
require_real_name: product.requireRealName ?? product.require_real_name ?? false
|
require_real_name: product.requireRealName ?? product.require_real_name ?? false,
|
||||||
|
sold_out: !!product.soldOut,
|
||||||
|
max_per_user: product.maxPerUser ?? product.max_per_user ?? 0,
|
||||||
|
send_notice: !!product.sendNotice,
|
||||||
|
renew_price: (product.renewPrice ?? product.renew_price ?? 0) / 100,
|
||||||
|
renew_recommend_rebate: product.renewRecommendRebate ?? product.renew_recommend_rebate ?? 0
|
||||||
})
|
})
|
||||||
|
|
||||||
productDialogVisible.value = true
|
productDialogVisible.value = true
|
||||||
@@ -1583,7 +1649,12 @@ const submitProductForm = () => {
|
|||||||
recommend_rebate: Number(productForm.recommend_rebate) || 0,
|
recommend_rebate: Number(productForm.recommend_rebate) || 0,
|
||||||
arg_type: productForm.arg_type,
|
arg_type: productForm.arg_type,
|
||||||
can_renew: productForm.can_renew,
|
can_renew: productForm.can_renew,
|
||||||
require_real_name: productForm.require_real_name
|
require_real_name: productForm.require_real_name,
|
||||||
|
sold_out: productForm.sold_out === true,
|
||||||
|
max_per_user: Number(productForm.max_per_user) || 0,
|
||||||
|
send_notice: productForm.send_notice === true,
|
||||||
|
renew_price: Number(productForm.renew_price) || 0,
|
||||||
|
renew_recommend_rebate: Number(productForm.renew_recommend_rebate) || 0
|
||||||
}
|
}
|
||||||
|
|
||||||
let res
|
let res
|
||||||
@@ -2185,6 +2256,18 @@ onMounted(() => {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-form-tabs {
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-form-tabs :deep(.el-tabs__header) {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.product-form-tabs :deep(.el-tab-pane) {
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.form-section {
|
.form-section {
|
||||||
margin-bottom: var(--section-gap);
|
margin-bottom: var(--section-gap);
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -113,6 +113,9 @@
|
|||||||
<el-tag :type="row.canUpdate || row.can_update ? 'success' : 'info'" size="small" effect="plain">
|
<el-tag :type="row.canUpdate || row.can_update ? 'success' : 'info'" size="small" effect="plain">
|
||||||
{{ row.canUpdate || row.can_update ? '允许升级' : '禁止升级' }}
|
{{ row.canUpdate || row.can_update ? '允许升级' : '禁止升级' }}
|
||||||
</el-tag>
|
</el-tag>
|
||||||
|
<el-tag v-if="(row.maxPerUser || row.max_per_user) > 0" type="warning" size="small" effect="plain">
|
||||||
|
限购{{ row.maxPerUser || row.max_per_user }}次
|
||||||
|
</el-tag>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 说明 -->
|
<!-- 说明 -->
|
||||||
@@ -271,6 +274,13 @@
|
|||||||
<span class="unit-text">元</span>
|
<span class="unit-text">元</span>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="续费固定价格" prop="renew_fixed_price" v-if="planForm.enable_fixed_price === true">
|
||||||
|
<div class="unit-input-row">
|
||||||
|
<el-input-number v-model="planForm.renew_fixed_price" :min="0" :precision="2" :step="0.01" style="flex:1" placeholder="0 表示沿用固定价格" />
|
||||||
|
<span class="unit-text">元</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-tip">续费时使用的固定价格,为 0 时沿用上方固定价格</div>
|
||||||
|
</el-form-item>
|
||||||
<el-form-item label="排序索引" prop="index">
|
<el-form-item label="排序索引" prop="index">
|
||||||
<el-input-number v-model="planForm.index" :min="0" style="width: 100%" />
|
<el-input-number v-model="planForm.index" :min="0" style="width: 100%" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@@ -288,6 +298,10 @@
|
|||||||
<el-switch v-model="planForm.can_update" active-text="允许" inactive-text="不允许" />
|
<el-switch v-model="planForm.can_update" active-text="允许" inactive-text="不允许" />
|
||||||
<div class="form-tip">控制用户是否可以升级到此套餐</div>
|
<div class="form-tip">控制用户是否可以升级到此套餐</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="购买限制" prop="max_per_user">
|
||||||
|
<el-input-number v-model="planForm.max_per_user" :min="0" placeholder="单用户最大购买数量" style="width: 100%" />
|
||||||
|
<div class="form-tip">限制单个用户对该套餐的最大购买数量,0 表示不限制</div>
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
</div>
|
</div>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
@@ -378,7 +392,9 @@ const planForm = reactive({
|
|||||||
index: 0,
|
index: 0,
|
||||||
disable: false,
|
disable: false,
|
||||||
show_home: false,
|
show_home: false,
|
||||||
can_update: false
|
can_update: false,
|
||||||
|
max_per_user: 0,
|
||||||
|
renew_fixed_price: 0
|
||||||
})
|
})
|
||||||
const planFormRules = {
|
const planFormRules = {
|
||||||
name: [{ required: true, message: '请输入套餐名称', trigger: 'blur' }]
|
name: [{ required: true, message: '请输入套餐名称', trigger: 'blur' }]
|
||||||
@@ -546,11 +562,11 @@ const findMatchingNumberAttr = (spec, numValue) => {
|
|||||||
if (!spec.attrs || spec.attrs.length === 0) return null
|
if (!spec.attrs || spec.attrs.length === 0) return null
|
||||||
const sortedAttrs = [...spec.attrs].sort((a, b) => (a.index || 0) - (b.index || 0))
|
const sortedAttrs = [...spec.attrs].sort((a, b) => (a.index || 0) - (b.index || 0))
|
||||||
for (const attr of sortedAttrs) {
|
for (const attr of sortedAttrs) {
|
||||||
const phase = attr.phase || 0
|
const rangeVal = attr.range ?? attr.phase ?? 0
|
||||||
const rangeType = attr.rangeType || 'before'
|
const rangeType = attr.rangeType || 'before'
|
||||||
if (rangeType === 'before' && numValue <= phase) return attr
|
if (rangeType === 'before' && numValue <= rangeVal) return attr
|
||||||
else if (rangeType === 'after' && numValue >= phase) return attr
|
else if (rangeType === 'after' && numValue >= rangeVal) return attr
|
||||||
else if (rangeType === 'equal' && numValue === phase) return attr
|
else if (rangeType === 'equal' && numValue === rangeVal) return attr
|
||||||
}
|
}
|
||||||
return sortedAttrs[sortedAttrs.length - 1]
|
return sortedAttrs[sortedAttrs.length - 1]
|
||||||
}
|
}
|
||||||
@@ -758,7 +774,7 @@ const handleAddPlan = async () => {
|
|||||||
displayUnits[spec.id] = getParamDefaultUnit(spec)
|
displayUnits[spec.id] = getParamDefaultUnit(spec)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Object.assign(planForm, { plan_id: undefined, name: '', note: '', args: '', extra_arg_ids: '', extra_arg_ids_array: [], inventory: 0, fixed_price: 0, enable_fixed_price: false, index: 0, disable: false, show_home: false, can_update: false })
|
Object.assign(planForm, { plan_id: undefined, name: '', note: '', args: '', extra_arg_ids: '', extra_arg_ids_array: [], inventory: 0, fixed_price: 0, enable_fixed_price: false, index: 0, disable: false, show_home: false, can_update: false, max_per_user: 0 })
|
||||||
planFormDialogVisible.value = true
|
planFormDialogVisible.value = true
|
||||||
nextTick(() => { planFormRef.value?.resetFields() })
|
nextTick(() => { planFormRef.value?.resetFields() })
|
||||||
}
|
}
|
||||||
@@ -785,7 +801,9 @@ const handleEditPlan = async (row) => {
|
|||||||
inventory: data.inventory || 0, fixed_price: ((data.fixedPrice || data.fixed_price || 0) / 100).toFixed(2) * 1,
|
inventory: data.inventory || 0, fixed_price: ((data.fixedPrice || data.fixed_price || 0) / 100).toFixed(2) * 1,
|
||||||
enable_fixed_price: !!(data.enableFixedPrice || data.enable_fixed_price),
|
enable_fixed_price: !!(data.enableFixedPrice || data.enable_fixed_price),
|
||||||
index: data.index || 0, disable: data.disable || false, show_home: !!(data.showHome || data.show_home),
|
index: data.index || 0, disable: data.disable || false, show_home: !!(data.showHome || data.show_home),
|
||||||
can_update: !!(data.canUpdate || data.can_update)
|
can_update: !!(data.canUpdate || data.can_update),
|
||||||
|
max_per_user: data.maxPerUser ?? data.max_per_user ?? 0,
|
||||||
|
renew_fixed_price: ((data.renewFixedPrice ?? data.renew_fixed_price ?? 0) / 100).toFixed(2) * 1
|
||||||
})
|
})
|
||||||
initSelectedArgsFromJson(data.args, extraArgIdsArray)
|
initSelectedArgsFromJson(data.args, extraArgIdsArray)
|
||||||
planFormDialogVisible.value = true
|
planFormDialogVisible.value = true
|
||||||
@@ -842,7 +860,9 @@ const submitPlanForm = () => {
|
|||||||
args: planForm.args || '', extra_arg_ids: extraArgIdsStr || planForm.extra_arg_ids || '',
|
args: planForm.args || '', extra_arg_ids: extraArgIdsStr || planForm.extra_arg_ids || '',
|
||||||
inventory: Number(planForm.inventory) || 0, fixed_price: Math.round(Number(planForm.fixed_price) * 100) || 0,
|
inventory: Number(planForm.inventory) || 0, fixed_price: Math.round(Number(planForm.fixed_price) * 100) || 0,
|
||||||
index: Number(planForm.index) || 0, show_home: planForm.show_home === true,
|
index: Number(planForm.index) || 0, show_home: planForm.show_home === true,
|
||||||
can_update: planForm.can_update === true
|
can_update: planForm.can_update === true,
|
||||||
|
max_per_user: Number(planForm.max_per_user) || 0,
|
||||||
|
renew_fixed_price: Math.round(Number(planForm.renew_fixed_price) * 100) || 0
|
||||||
}
|
}
|
||||||
if (planFormType.value === 'add') submitData.enable_fixed_price = planForm.enable_fixed_price === true
|
if (planFormType.value === 'add') submitData.enable_fixed_price = planForm.enable_fixed_price === true
|
||||||
let res
|
let res
|
||||||
|
|||||||
@@ -85,7 +85,7 @@
|
|||||||
{{ row.create_time || row.CreateTime || row.CreatedAt || '-' }}
|
{{ row.create_time || row.CreateTime || row.CreatedAt || '-' }}
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" width="280" fixed="right">
|
<el-table-column label="操作" width="340" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<el-button type="primary" link @click="handleEdit(row)">
|
<el-button type="primary" link @click="handleEdit(row)">
|
||||||
@@ -94,6 +94,9 @@
|
|||||||
<el-button type="success" link @click="handleViewMembers(row)">
|
<el-button type="success" link @click="handleViewMembers(row)">
|
||||||
<el-icon><User /></el-icon>成员
|
<el-icon><User /></el-icon>成员
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button type="warning" link @click="handleViewDiscount(row)">
|
||||||
|
<el-icon><Present /></el-icon>优惠
|
||||||
|
</el-button>
|
||||||
<el-button type="danger" link @click="handleDelete(row)">
|
<el-button type="danger" link @click="handleDelete(row)">
|
||||||
<el-icon><Delete /></el-icon>删除
|
<el-icon><Delete /></el-icon>删除
|
||||||
</el-button>
|
</el-button>
|
||||||
@@ -288,13 +291,129 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 用户组优惠管理对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="discountDialogVisible"
|
||||||
|
:title="`优惠管理 - ${currentGroupName}`"
|
||||||
|
width="900px"
|
||||||
|
append-to-body
|
||||||
|
class="custom-dialog"
|
||||||
|
>
|
||||||
|
<div class="discount-action-bar">
|
||||||
|
<el-button type="primary" @click="handleAddDiscount">
|
||||||
|
<el-icon><Plus /></el-icon>新增优惠绑定
|
||||||
|
</el-button>
|
||||||
|
<el-button type="success" @click="fetchDiscountList">
|
||||||
|
<el-icon><Refresh /></el-icon>刷新
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
<el-table
|
||||||
|
v-loading="discountLoading"
|
||||||
|
:data="discountList"
|
||||||
|
style="width: 100%"
|
||||||
|
:header-cell-style="{ background: '#fafafa', color: '#333', fontWeight: 600 }"
|
||||||
|
>
|
||||||
|
<el-table-column prop="id" label="ID" width="80" />
|
||||||
|
<el-table-column label="绑定对象" min-width="220">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.good" type="primary" effect="plain">
|
||||||
|
商品:{{ row.good.name }} (ID:{{ row.good.id }})
|
||||||
|
</el-tag>
|
||||||
|
<el-tag v-else-if="row.goodGroup" type="warning" effect="plain">
|
||||||
|
商品组:{{ row.goodGroup.name }} (ID:{{ row.goodGroup.id }})
|
||||||
|
</el-tag>
|
||||||
|
<span v-else style="color:#c0c4cc">-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="固定抵扣额" width="130">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="row.amount">¥{{ (row.amount / 100).toFixed(2) }}</span>
|
||||||
|
<span v-else style="color:#c0c4cc">-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="百分比抵扣" width="120">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="row.percentage">{{ (row.percentage / 100).toFixed(0) }}%</span>
|
||||||
|
<span v-else style="color:#c0c4cc">-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="140" fixed="right">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-button type="primary" link @click="handleEditDiscount(row)">编辑</el-button>
|
||||||
|
<el-button type="danger" link @click="handleDeleteDiscount(row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<template #empty>
|
||||||
|
<el-empty description="暂无优惠绑定" :image-size="80" />
|
||||||
|
</template>
|
||||||
|
</el-table>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 用户组优惠 新增/编辑 对话框 -->
|
||||||
|
<el-dialog
|
||||||
|
v-model="discountFormVisible"
|
||||||
|
:title="discountFormType === 'add' ? '新增优惠绑定' : '编辑优惠绑定'"
|
||||||
|
width="560px"
|
||||||
|
append-to-body
|
||||||
|
>
|
||||||
|
<el-form
|
||||||
|
ref="discountFormRef"
|
||||||
|
:model="discountForm"
|
||||||
|
:rules="discountFormRules"
|
||||||
|
label-width="120px"
|
||||||
|
>
|
||||||
|
<el-form-item label="绑定类型" prop="bind_type">
|
||||||
|
<el-radio-group v-model="discountForm.bind_type" @change="handleBindTypeChange">
|
||||||
|
<el-radio label="good">商品</el-radio>
|
||||||
|
<el-radio label="good_group">商品组</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="discountForm.bind_type === 'good'" label="选择商品" prop="good_id">
|
||||||
|
<el-select v-model="discountForm.good_id" placeholder="请选择商品" filterable style="width: 100%">
|
||||||
|
<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 v-else label="选择商品组" prop="good_group_id">
|
||||||
|
<el-select v-model="discountForm.good_group_id" placeholder="请选择商品组" filterable style="width: 100%">
|
||||||
|
<el-option
|
||||||
|
v-for="item in productGroupOptions"
|
||||||
|
:key="item.id"
|
||||||
|
:label="`${item.name} (ID:${item.id})`"
|
||||||
|
:value="item.id"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="固定抵扣额" prop="amount">
|
||||||
|
<div class="unit-input-row">
|
||||||
|
<el-input-number v-model="discountForm.amount" :min="0" :precision="2" :step="0.01" placeholder="0表示不使用" style="flex:1" />
|
||||||
|
<span class="unit-text">元</span>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="百分比抵扣" prop="percentage">
|
||||||
|
<el-input-number v-model="discountForm.percentage" :min="0" :max="100" :precision="0" placeholder="0表示不使用" style="width: 100%" />
|
||||||
|
<div style="color:#909399;font-size:12px;margin-top:4px">固定抵扣额与百分比抵扣二选一填写</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<div class="dialog-footer">
|
||||||
|
<el-button @click="discountFormVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitDiscountForm">确定</el-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted } from 'vue'
|
import { ref, reactive, onMounted } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Plus, Refresh, Edit, User, Delete, Connection, Close } from '@element-plus/icons-vue'
|
import { Plus, Refresh, Edit, User, Delete, Connection, Close, Present } from '@element-plus/icons-vue'
|
||||||
import {
|
import {
|
||||||
getUserGroupList,
|
getUserGroupList,
|
||||||
getUserGroupMemberList,
|
getUserGroupMemberList,
|
||||||
@@ -303,6 +422,13 @@ import {
|
|||||||
deleteUserGroup,
|
deleteUserGroup,
|
||||||
addUserGroupMember
|
addUserGroupMember
|
||||||
} from '@/api/admin/user'
|
} from '@/api/admin/user'
|
||||||
|
import {
|
||||||
|
getUserGroupDiscountList,
|
||||||
|
addUserGroupDiscount,
|
||||||
|
updateUserGroupDiscount,
|
||||||
|
deleteUserGroupDiscount
|
||||||
|
} from '@/api/admin/userGroupDiscount'
|
||||||
|
import { getProductList, getProductGroupList } from '@/api/admin/product'
|
||||||
import { formatTime } from '@/utils/tool'
|
import { formatTime } from '@/utils/tool'
|
||||||
import UserGroupSelector from '@/components/admin/UserGroupSelector.vue'
|
import UserGroupSelector from '@/components/admin/UserGroupSelector.vue'
|
||||||
|
|
||||||
@@ -624,6 +750,167 @@ const clearHigherGroup = () => {
|
|||||||
selectedHigherGroupInfo.value = null
|
selectedHigherGroupInfo.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** -------------------- 用户组优惠管理 -------------------- */
|
||||||
|
const discountDialogVisible = ref(false)
|
||||||
|
const discountFormVisible = ref(false)
|
||||||
|
const discountLoading = ref(false)
|
||||||
|
const discountList = ref([])
|
||||||
|
const discountFormType = ref('add')
|
||||||
|
const discountFormRef = ref(null)
|
||||||
|
const currentGroupId = ref(undefined)
|
||||||
|
const currentGroupName = ref('')
|
||||||
|
const productOptions = ref([])
|
||||||
|
const productGroupOptions = ref([])
|
||||||
|
|
||||||
|
const discountForm = reactive({
|
||||||
|
id: undefined,
|
||||||
|
bind_type: 'good',
|
||||||
|
good_id: undefined,
|
||||||
|
good_group_id: undefined,
|
||||||
|
amount: 0,
|
||||||
|
percentage: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const discountFormRules = {
|
||||||
|
bind_type: [{ required: true, message: '请选择绑定类型', trigger: 'change' }],
|
||||||
|
good_id: [{ required: true, message: '请选择商品', trigger: 'change' }],
|
||||||
|
good_group_id: [{ required: true, message: '请选择商品组', trigger: 'change' }]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载商品/商品组选项
|
||||||
|
const loadProductOptions = async () => {
|
||||||
|
try {
|
||||||
|
const [pRes, gRes] = await Promise.all([
|
||||||
|
getProductList({ page: 1, count: 1000 }),
|
||||||
|
getProductGroupList({ page: 1, count: 1000 })
|
||||||
|
])
|
||||||
|
if (pRes.data.code === 200) {
|
||||||
|
productOptions.value = pRes.data.data?.data || pRes.data.data || []
|
||||||
|
}
|
||||||
|
if (gRes.data.code === 200) {
|
||||||
|
productGroupOptions.value = gRes.data.data?.data || gRes.data.data || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载商品选项失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户组优惠列表
|
||||||
|
const fetchDiscountList = async () => {
|
||||||
|
if (!currentGroupId.value) return
|
||||||
|
discountLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await getUserGroupDiscountList({ user_group_id: currentGroupId.value, page: 1, count: 1000 })
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
discountList.value = res.data.data || []
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取用户组优惠列表失败:', error)
|
||||||
|
ElMessage.error('获取用户组优惠列表失败')
|
||||||
|
} finally {
|
||||||
|
discountLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 打开优惠管理对话框
|
||||||
|
const handleViewDiscount = (row) => {
|
||||||
|
currentGroupId.value = row.group_id || row.GroupId || row.id || row.Id
|
||||||
|
currentGroupName.value = row.group_name || row.name || row.Name || ''
|
||||||
|
discountDialogVisible.value = true
|
||||||
|
loadProductOptions()
|
||||||
|
fetchDiscountList()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定类型切换时清空对端选择
|
||||||
|
const handleBindTypeChange = () => {
|
||||||
|
discountForm.good_id = undefined
|
||||||
|
discountForm.good_group_id = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// 新增优惠绑定
|
||||||
|
const handleAddDiscount = () => {
|
||||||
|
discountFormType.value = 'add'
|
||||||
|
discountFormVisible.value = true
|
||||||
|
Object.assign(discountForm, {
|
||||||
|
id: undefined,
|
||||||
|
bind_type: 'good',
|
||||||
|
good_id: undefined,
|
||||||
|
good_group_id: undefined,
|
||||||
|
amount: 0,
|
||||||
|
percentage: 0
|
||||||
|
})
|
||||||
|
discountFormRef.value?.resetFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑优惠绑定
|
||||||
|
const handleEditDiscount = (row) => {
|
||||||
|
discountFormType.value = 'edit'
|
||||||
|
discountFormVisible.value = true
|
||||||
|
const isGroup = !!row.goodGroup
|
||||||
|
Object.assign(discountForm, {
|
||||||
|
id: row.id,
|
||||||
|
bind_type: isGroup ? 'good_group' : 'good',
|
||||||
|
good_id: row.good ? row.good.id : undefined,
|
||||||
|
good_group_id: row.goodGroup ? row.goodGroup.id : undefined,
|
||||||
|
amount: row.amount ? row.amount / 100 : 0,
|
||||||
|
percentage: row.percentage ? row.percentage / 100 : 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除优惠绑定
|
||||||
|
const handleDeleteDiscount = (row) => {
|
||||||
|
ElMessageBox.confirm('确认删除该优惠绑定吗?', '警告', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(async () => {
|
||||||
|
try {
|
||||||
|
const res = await deleteUserGroupDiscount({ discount_id: row.id })
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
fetchDiscountList()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('删除失败:', error)
|
||||||
|
ElMessage.error(error.response?.data?.message || '删除失败')
|
||||||
|
}
|
||||||
|
}).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交优惠绑定表单
|
||||||
|
const submitDiscountForm = () => {
|
||||||
|
discountFormRef.value?.validate(async (valid) => {
|
||||||
|
if (!valid) return
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
params.append('user_group_id', currentGroupId.value)
|
||||||
|
if (discountForm.bind_type === 'good') {
|
||||||
|
params.append('good_id', discountForm.good_id)
|
||||||
|
} else {
|
||||||
|
params.append('good_group_id', discountForm.good_group_id)
|
||||||
|
}
|
||||||
|
params.append('amount', Math.round((discountForm.amount || 0) * 100))
|
||||||
|
params.append('percentage', Math.round((discountForm.percentage || 0) * 100))
|
||||||
|
|
||||||
|
let res
|
||||||
|
if (discountFormType.value === 'add') {
|
||||||
|
res = await addUserGroupDiscount(params)
|
||||||
|
} else {
|
||||||
|
params.append('id', discountForm.id)
|
||||||
|
res = await updateUserGroupDiscount(params)
|
||||||
|
}
|
||||||
|
if (res.data.code === 200) {
|
||||||
|
ElMessage.success(discountFormType.value === 'add' ? '新增成功' : '修改成功')
|
||||||
|
discountFormVisible.value = false
|
||||||
|
fetchDiscountList()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('操作失败:', error)
|
||||||
|
ElMessage.error(error.response?.data?.message || '操作失败')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchGroupList()
|
fetchGroupList()
|
||||||
@@ -696,6 +983,15 @@ onMounted(() => {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.discount-action-bar {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit-input-row { display: flex; align-items: center; gap: 6px; width: 100%; }
|
||||||
|
.unit-text { font-size: 13px; color: #606266; flex-shrink: 0; white-space: nowrap; }
|
||||||
|
|
||||||
/* 表格样式优化 */
|
/* 表格样式优化 */
|
||||||
:deep(.el-table) {
|
:deep(.el-table) {
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
Reference in New Issue
Block a user