🎨 新增兑换码功能

This commit is contained in:
2025-09-09 23:54:01 +08:00
parent 3378e709cf
commit a631f6ea46
4 changed files with 1490 additions and 0 deletions

80
src/api/cdk/index.js Normal file
View File

@@ -0,0 +1,80 @@
import service from '@/utils/request'
// 获取兑换码库列表
export const mkList = (params) => {
return service({
url: '/cdk/mk/list',
method: 'get',
params
})
}
// 新增兑换码库
export const addMk = (data) => {
return service({
url: '/cdk/mk',
method: 'post',
data
})
}
// 编辑兑换码库
export const editMk = (data) => {
return service({
url: '/cdk/mk',
method: 'put',
data
})
}
// 删除兑换码库
export const delMk = (data) => {
return service({
url: '/cdk/mk',
method: 'delete',
data
})
}
// 获取兑换码库详情
export const mkDetail = (params) => {
return service({
url: '/cdk/mk',
method: 'get',
params
})
}
// =======================CDK相关===========================
// 获取兑换码列表
export const cdkList = (params) => {
return service({
url: '/cdk/list',
method: 'get',
params
})
}
// 新增兑换码
export const addCdk = (data) => {
return service({
url: '/cdk/generate',
method: 'post',
data
})
}
// 作废兑换码
export const delCdk = (data) => {
return service({
url: '/cdk',
method: 'delete',
data
})
}

View File

@@ -5,6 +5,7 @@
"/src/view/bot/bot/botForm.vue": "BotForm", "/src/view/bot/bot/botForm.vue": "BotForm",
"/src/view/category/category/category.vue": "Category", "/src/view/category/category/category.vue": "Category",
"/src/view/category/category/categoryForm.vue": "CategoryForm", "/src/view/category/category/categoryForm.vue": "CategoryForm",
"/src/view/cdk/index.vue": "CdkManagement",
"/src/view/dashboard/components/banner.vue": "Banner", "/src/view/dashboard/components/banner.vue": "Banner",
"/src/view/dashboard/components/card.vue": "Card", "/src/view/dashboard/components/card.vue": "Card",
"/src/view/dashboard/components/charts-content-numbers.vue": "ChartsContentNumbers", "/src/view/dashboard/components/charts-content-numbers.vue": "ChartsContentNumbers",
@@ -25,6 +26,7 @@
"/src/view/goods/article/edit.vue": "Edit", "/src/view/goods/article/edit.vue": "Edit",
"/src/view/goods/article/index.vue": "Index", "/src/view/goods/article/index.vue": "Index",
"/src/view/goods/index.vue": "goods", "/src/view/goods/index.vue": "goods",
"/src/view/goods/teacher_vip/index.vue": "Index",
"/src/view/goods/vip/index.vue": "VipList", "/src/view/goods/vip/index.vue": "VipList",
"/src/view/init/index.vue": "Init", "/src/view/init/index.vue": "Init",
"/src/view/layout/aside/asideComponent/asyncSubmenu.vue": "AsyncSubmenu", "/src/view/layout/aside/asideComponent/asyncSubmenu.vue": "AsyncSubmenu",

661
src/view/cdk/cdkManage.vue Normal file
View File

@@ -0,0 +1,661 @@
<template>
<div>
<!-- 页面头部 -->
<div class="page-header">
<div class="header-left">
<el-button @click="goBack" type="text" class="back-btn">
<el-icon><ArrowLeft /></el-icon>
返回兑换码库管理
</el-button>
<h2>{{ libraryInfo.codeName }} - 兑换码管理</h2>
</div>
<div class="header-right">
<el-button type="primary" @click="handleGenerateCdk">生成兑换码</el-button>
</div>
</div>
<!-- 搜索区域 -->
<div class="gva-search-box">
<el-form :inline="true" :model="searchForm" class="demo-form-inline">
<el-form-item label="兑换码">
<el-input v-model="searchForm.code" placeholder="请输入兑换码" clearable />
</el-form-item>
<el-form-item label="状态">
<el-select v-model="searchForm.status" placeholder="请选择状态" clearable>
<el-option label="未使用" :value="1" />
<el-option label="已使用" :value="2" />
<el-option label="已作废" :value="3" />
</el-select>
</el-form-item>
<el-form-item label="使用者">
<el-input v-model="searchForm.useName" placeholder="请输入使用者名称" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="searchData">查询</el-button>
<el-button @click="resetData">重置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 统计信息 -->
<div class="stats-box">
<el-row :gutter="20">
<el-col :span="6">
<div class="stat-item">
<div class="stat-value">{{ stats.total }}</div>
<div class="stat-label">总数量</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-item unused">
<div class="stat-value">{{ stats.unused }}</div>
<div class="stat-label">未使用</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-item used">
<div class="stat-value">{{ stats.used }}</div>
<div class="stat-label">已使用</div>
</div>
</el-col>
<el-col :span="6">
<div class="stat-item invalid">
<div class="stat-value">{{ stats.invalid }}</div>
<div class="stat-label">已作废</div>
</div>
</el-col>
</el-row>
</div>
<!-- 表格区域 -->
<div class="gva-table-box">
<el-table
:data="tableData"
v-loading="tableLoading"
style="width: 100%"
class="gva-table"
stripe
>
<el-table-column prop="ID" label="ID" width="80" />
<el-table-column prop="code" label="兑换码" min-width="180">
<template #default="{ row }">
<div class="code-cell">
<span>{{ row.code }}</span>
<el-button
type="text"
@click="copyCode(row.code)"
class="copy-btn"
title="复制兑换码"
>
<el-icon><CopyDocument /></el-icon>
</el-button>
</div>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="getStatusType(row.status)">
{{ getStatusText(row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="validDay" label="有效期(天)" width="120" />
<el-table-column prop="expireAt" label="到期时间" width="180">
<template #default="{ row }">
<span :class="{ 'text-danger': isExpiringSoon(row.expireAt), 'text-expired': isExpired(row.expireAt) }">
{{ formatDate(row.expireAt) }}
</span>
</template>
</el-table-column>
<el-table-column prop="useName" label="使用者" width="120">
<template #default="{ row }">
{{ row.useName || '-' }}
</template>
</el-table-column>
<el-table-column prop="useAt" label="使用时间" width="180">
<template #default="{ row }">
{{ row.useAt ? formatDate(row.useAt) : '-' }}
</template>
</el-table-column>
<el-table-column prop="CreatedAt" label="创建时间" width="180">
<template #default="{ row }">
{{ formatDate(row.CreatedAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button
type="text"
@click="handleDelete(row)"
style="color: #f56c6c"
:disabled="row.status !== 1"
>
作废
</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
class="gva-pagination"
/>
</div>
<!-- 生成兑换码弹窗 -->
<el-dialog
v-model="generateDialogVisible"
title="生成兑换码"
width="500px"
:before-close="handleGenerateClose"
>
<el-form
ref="generateFormRef"
:model="generateForm"
:rules="getDynamicRules()"
label-width="120px"
>
<el-form-item label="兑换码库">
<el-input v-model="libraryInfo.codeName" disabled />
</el-form-item>
<el-form-item label="生成数量" prop="number">
<el-input-number
v-model="generateForm.number"
:min="1"
:max="1000"
placeholder="请输入生成数量"
style="width: 100%"
/>
</el-form-item>
<el-form-item label="有效期" prop="validityType">
<el-radio-group v-model="generateForm.validityType" @change="handleValidityTypeChange">
<el-radio :label="'permanent'">永久有效</el-radio>
<el-radio :label="'custom'">自定义天数</el-radio>
</el-radio-group>
<div v-if="generateForm.validityType === 'custom'" style="margin-top: 10px;">
<el-input-number
v-model="generateForm.customDays"
:min="1"
:max="3650"
placeholder="请输入有效期天数"
style="width: 200px"
/>
<span style="margin-left: 8px;"></span>
</div>
<div style="font-size: 12px; color: #999; margin-top: 5px;">
{{ generateForm.validityType === 'permanent' ? '兑换码永久有效,不会过期' : '兑换码的有效期,超过此期限将无法使用' }}
</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleGenerateClose">取消</el-button>
<el-button type="primary" @click="submitGenerate">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { cdkList, addCdk, delCdk } from '@/api/cdk/index.js'
import { ref, onMounted, computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft, CopyDocument } from '@element-plus/icons-vue'
defineOptions({
name: 'CdkManage'
})
const route = useRoute()
const router = useRouter()
// 兑换码库信息
const libraryInfo = ref({
ID: '',
codeName: ''
})
// 表格相关数据
const tableLoading = ref(false)
const tableData = ref([])
const queryParams = ref({
page: 1,
pageSize: 10,
eid: 0
})
const total = ref(0)
// 搜索表单
const searchForm = ref({
code: '',
status: '',
useName: ''
})
// 统计数据
const stats = computed(() => {
const unused = tableData.value.filter(item => item.status === 1).length
const used = tableData.value.filter(item => item.status === 2).length
const invalid = tableData.value.filter(item => item.status === 3).length
return {
total: total.value,
unused,
used,
invalid
}
})
// 生成兑换码弹窗
const generateDialogVisible = ref(false)
const generateFormRef = ref(null)
const generateForm = ref({
eid: 0,
number: 1,
expirer: 30,
validityType: 'custom', // 'permanent' 或 'custom'
customDays: 30
})
// 表单验证规则
const generateRules = {
number: [
{ required: true, message: '请输入生成数量', trigger: 'blur' },
{ type: 'number', min: 1, max: 1000, message: '生成数量必须在1-1000之间', trigger: 'blur' }
],
validityType: [
{ required: true, message: '请选择有效期类型', trigger: 'change' }
]
}
// 动态验证规则
const getDynamicRules = () => {
const rules = { ...generateRules }
if (generateForm.value.validityType === 'custom') {
rules.customDays = [
{ required: true, message: '请输入有效期天数', trigger: 'blur' },
{ type: 'number', min: 1, max: 3650, message: '有效期必须在1-3650天之间', trigger: 'blur' }
]
}
return rules
}
// 格式化日期
const formatDate = (dateStr) => {
if (!dateStr) return '-'
return new Date(dateStr).toLocaleString('zh-CN')
}
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
1: '未使用',
2: '已使用',
3: '已作废'
}
return statusMap[status] || '未知'
}
// 获取状态类型
const getStatusType = (status) => {
const typeMap = {
1: 'success',
2: 'warning',
3: 'danger'
}
return typeMap[status] || 'info'
}
// 判断是否即将过期7天内
const isExpiringSoon = (expireTime) => {
if (!expireTime) return false
const expireDate = new Date(expireTime)
const now = new Date()
const diffTime = expireDate.getTime() - now.getTime()
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
return diffDays <= 7 && diffDays > 0
}
// 判断是否已过期
const isExpired = (expireTime) => {
if (!expireTime) return false
const expireDate = new Date(expireTime)
const now = new Date()
return expireDate.getTime() < now.getTime()
}
// 复制兑换码
const copyCode = async (code) => {
try {
await navigator.clipboard.writeText(code)
ElMessage.success('兑换码已复制到剪贴板')
} catch (error) {
// 降级方案
const textArea = document.createElement('textarea')
textArea.value = code
document.body.appendChild(textArea)
textArea.select()
document.execCommand('copy')
document.body.removeChild(textArea)
ElMessage.success('兑换码已复制到剪贴板')
}
}
// 搜索数据
const searchData = () => {
queryParams.value.page = 1
getList()
}
// 重置搜索
const resetData = () => {
searchForm.value = {
code: '',
status: '',
useName: ''
}
queryParams.value.page = 1
getList()
}
// 分页处理
const handleSizeChange = (size) => {
queryParams.value.pageSize = size
getList()
}
const handleCurrentChange = (page) => {
queryParams.value.page = page
getList()
}
// 获取兑换码列表
async function getList() {
tableLoading.value = true
try {
const params = {
...queryParams.value,
...searchForm.value
}
const res = await cdkList(params)
if (res.code === 0) {
tableData.value = res.data.list || []
total.value = res.data.total || 0
} else {
ElMessage.error(res.msg || '获取数据失败')
}
} catch (error) {
console.error('获取兑换码列表失败:', error)
ElMessage.error('获取数据失败')
} finally {
tableLoading.value = false
}
}
// 处理有效期类型变化
const handleValidityTypeChange = (type) => {
if (type === 'permanent') {
generateForm.value.expirer = 0
} else {
generateForm.value.expirer = generateForm.value.customDays || 30
}
}
// 生成兑换码
const handleGenerateCdk = () => {
generateForm.value = {
eid: Number(libraryInfo.value.ID),
number: 1,
expirer: 30,
validityType: 'custom',
customDays: 30
}
generateDialogVisible.value = true
}
// 关闭生成弹窗
const handleGenerateClose = () => {
generateDialogVisible.value = false
generateFormRef.value?.resetFields()
}
// 提交生成
const submitGenerate = async () => {
if (!generateFormRef.value) return
// 动态设置验证规则
generateFormRef.value.clearValidate()
// 根据选择的有效期类型设置expirer值
if (generateForm.value.validityType === 'permanent') {
generateForm.value.expirer = 0
} else {
generateForm.value.expirer = generateForm.value.customDays
}
// 验证表单
let isValid = true
// 验证生成数量
if (!generateForm.value.number || generateForm.value.number < 1 || generateForm.value.number > 1000) {
ElMessage.error('生成数量必须在1-1000之间')
isValid = false
}
// 验证自定义天数
if (generateForm.value.validityType === 'custom') {
if (!generateForm.value.customDays || generateForm.value.customDays < 1 || generateForm.value.customDays > 3650) {
ElMessage.error('有效期必须在1-3650天之间')
isValid = false
}
}
if (isValid) {
try {
// 准备提交的数据
const submitData = {
eid: generateForm.value.eid,
number: generateForm.value.number,
expirer: generateForm.value.expirer
}
const res = await addCdk(submitData)
if (res.code === 0) {
ElMessage.success('生成成功')
handleGenerateClose()
getList()
} else {
ElMessage.error(res.msg || '生成失败')
}
} catch (error) {
ElMessage.error('生成失败')
}
}
}
// 作废兑换码
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm('确定要作废这个兑换码吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const res = await delCdk({ ID: row.ID })
if (res.code === 0) {
ElMessage.success('作废成功')
getList()
} else {
ElMessage.error(res.msg || '作废失败')
}
} catch (error) {
// 用户取消作废
}
}
// 返回上一页
const goBack = () => {
router.go(-1)
}
// 页面初始化
onMounted(() => {
// 从路由参数获取兑换码库信息
const { id, name } = route.query
if (id && name) {
libraryInfo.value = {
ID: id,
codeName: decodeURIComponent(name)
}
queryParams.value.eid = id
getList()
} else {
ElMessage.error('缺少兑换码库信息')
goBack()
}
})
</script>
<style lang="scss" scoped>
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px;
background: #fff;
border-radius: 4px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
.header-left {
display: flex;
align-items: center;
.back-btn {
margin-right: 15px;
color: #409eff;
&:hover {
color: #66b1ff;
}
}
h2 {
margin: 0;
font-size: 18px;
color: #303133;
}
}
}
.gva-search-box {
padding: 20px;
background: #fff;
border-radius: 4px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.stats-box {
padding: 20px;
background: #fff;
border-radius: 4px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
.stat-item {
text-align: center;
padding: 20px;
border-radius: 8px;
background: #f8f9fa;
&.unused {
background: #f0f9ff;
color: #0369a1;
}
&.used {
background: #fefce8;
color: #a16207;
}
&.invalid {
background: #fef2f2;
color: #dc2626;
}
.stat-value {
font-size: 28px;
font-weight: bold;
margin-bottom: 5px;
}
.stat-label {
font-size: 14px;
opacity: 0.8;
}
}
}
.gva-table-box {
background: #fff;
border-radius: 4px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.gva-table {
:deep(.el-table__header) {
background-color: #f5f7fa;
}
:deep(.el-table__row:hover > td) {
background-color: #f5f7fa !important;
}
}
.gva-pagination {
margin-top: 20px;
text-align: right;
}
.demo-form-inline {
.el-form-item {
margin-right: 20px;
}
}
.code-cell {
display: flex;
align-items: center;
justify-content: space-between;
.copy-btn {
margin-left: 8px;
padding: 4px;
&:hover {
color: #409eff;
}
}
}
.text-danger {
color: #f56c6c;
font-weight: bold;
}
.text-expired {
color: #909399;
text-decoration: line-through;
}
</style>

747
src/view/cdk/index.vue Normal file
View File

@@ -0,0 +1,747 @@
<template>
<div>
<div class="cdk-library-management">
<!-- 兑换码库管理 -->
<div class="gva-search-box">
<el-form :inline="true" :model="mkSearchForm" class="demo-form-inline">
<el-form-item label="兑换码库名称">
<el-input v-model="mkSearchForm.codeName" placeholder="请输入兑换码库名称" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="searchMkData">查询</el-button>
<el-button @click="resetMkData">重置</el-button>
</el-form-item>
</el-form>
</div>
<div class="gva-table-box">
<div class="gva-btn-list">
<el-button type="primary" @click="handleAddMk">新增兑换码库</el-button>
</div>
<el-table
:data="mkTableData"
v-loading="mkTableLoading"
style="width: 100%"
class="gva-table"
stripe
>
<el-table-column prop="ID" label="ID" width="80" />
<el-table-column prop="codeName" label="兑换码库名称" min-width="150" />
<el-table-column prop="type" label="类型" width="100">
<template #default="{ row }">
{{ getTypeText(row.type) }}
</template>
</el-table-column>
<el-table-column prop="item" label="关联项目" width="150">
<template #default="{ row }">
{{ getItemDisplayName(row) }}
</template>
</el-table-column>
<el-table-column prop="num" label="兑换码数量" width="120" />
<el-table-column prop="no" label="已使用数量" width="120" />
<el-table-column label="使用率" width="100">
<template #default="{ row }">
{{ getUsageRate(row.no, row.num) }}
</template>
</el-table-column>
<el-table-column prop="CreatedAt" label="创建时间" width="180">
<template #default="{ row }">
{{ formatDate(row.CreatedAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="180" fixed="right">
<template #default="{ row }">
<el-button type="text" @click="handleEditMk(row)">编辑</el-button>
<el-button type="text" @click="handleManageCdk(row)">兑换码管理</el-button>
<el-button type="text" @click="handleDeleteMk(row)" style="color: #f56c6c">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="mkQueryParams.page"
v-model:page-size="mkQueryParams.pageSize"
:page-sizes="[10, 20, 50, 100]"
:total="mkTotal"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleMkSizeChange"
@current-change="handleMkCurrentChange"
class="gva-pagination"
/>
</div>
</div>
<!-- 新增/编辑兑换码库弹窗 -->
<el-dialog
v-model="mkDialogVisible"
:title="mkDialogTitle"
width="500px"
:before-close="handleMkClose"
>
<el-form
ref="mkFormRef"
:model="mkForm"
:rules="mkRules"
label-width="120px"
>
<el-form-item label="兑换码库名称" prop="codeName">
<el-input v-model="mkForm.codeName" placeholder="请输入兑换码库名称" />
</el-form-item>
<el-form-item label="类型" prop="type">
<el-select v-model="mkForm.type" placeholder="请选择类型" style="width: 100%" @change="handleTypeChange">
<el-option label="VIP" :value="1" />
<el-option label="讲师VIP" :value="2" />
<el-option label="课程" :value="3" />
</el-select>
</el-form-item>
<el-form-item :label="getItemLabel(mkForm.type)" prop="item">
<el-button type="primary" @click="handleSelectItem" style="width: 100%">
{{ selectedItemName || '请选择' + getItemLabel(mkForm.type) }}
</el-button>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleMkClose">取消</el-button>
<el-button type="primary" @click="submitMk">确定</el-button>
</span>
</template>
</el-dialog>
<!-- 选择项目弹窗 -->
<el-dialog
v-model="selectItemDialogVisible"
:title="'选择' + getItemLabel(mkForm.type)"
width="900px"
:before-close="handleSelectItemClose"
>
<!-- 搜索框 -->
<div style="margin-bottom: 15px;">
<el-input
v-model="itemSearchKeyword"
placeholder="请输入关键词搜索"
clearable
@input="handleItemSearch"
style="width: 300px;"
>
<template #prefix>
<el-icon><Search /></el-icon>
</template>
</el-input>
</div>
<el-table
:data="filteredItemOptions"
v-loading="itemOptionsLoading"
style="width: 100%"
@row-click="handleItemRowClick"
highlight-current-row
>
<el-table-column prop="ID" label="ID" width="80" />
<el-table-column :prop="getItemNameProp(mkForm.type)" :label="getItemLabel(mkForm.type) + '名称'" min-width="200" />
<!-- VIP套餐相关列 -->
<template v-if="mkForm.type === 1">
<el-table-column prop="price" label="价格" width="100">
<template #default="{ row }">
¥{{ row.price }}
</template>
</el-table-column>
<el-table-column prop="duration" label="时长(天)" width="100" />
</template>
<!-- 讲师VIP相关列 -->
<template v-if="mkForm.type === 2">
<el-table-column prop="price" label="价格" width="100">
<template #default="{ row }">
¥{{ row.price }}
</template>
</el-table-column>
<el-table-column prop="teacher_name" label="讲师" width="120" />
<el-table-column prop="desc" label="描述" width="150" show-overflow-tooltip />
</template>
<!-- 课程相关列 -->
<template v-if="mkForm.type === 3">
<el-table-column prop="price" label="价格" width="100">
<template #default="{ row }">
¥{{ row.price }}
</template>
</el-table-column>
<el-table-column prop="teacherName" label="讲师" width="120" />
<el-table-column prop="isFree" label="是否免费" width="100">
<template #default="{ row }">
<el-tag :type="row.isFree === 1 ? 'success' : 'info'">
{{ row.isFree === 1 ? '免费' : '付费' }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'danger'">
{{ row.status === 1 ? '上架' : '下架' }}
</el-tag>
</template>
</el-table-column>
</template>
</el-table>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleSelectItemClose">取消</el-button>
<el-button type="primary" @click="confirmSelectItem" :disabled="!selectedItem">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup>
import {
mkList,
addMk,
editMk,
delMk
} from '@/api/cdk/index.js'
import { list as vipList } from '@/api/goods/vip.js'
import { list as teacherVipList } from '@/api/goods/teacherVip.js'
import { list as goodsList } from '@/api/goods/index.js'
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search } from '@element-plus/icons-vue'
defineOptions({
name: 'CdkLibraryManagement'
})
const router = useRouter()
// 格式化日期
const formatDate = (dateStr) => {
if (!dateStr) return '-'
return new Date(dateStr).toLocaleString('zh-CN')
}
// 兑换码库相关数据
const mkTableLoading = ref(false)
const mkTableData = ref([])
const mkQueryParams = ref({
page: 1,
pageSize: 10
})
const mkTotal = ref(0)
const mkSearchForm = ref({
codeName: ''
})
// 兑换码库弹窗相关
const mkDialogVisible = ref(false)
const mkFormRef = ref(null)
const mkForm = ref({
ID: '',
codeName: '',
type: 1,
item: ''
})
const mkDialogTitle = computed(() => mkForm.value.ID ? '编辑兑换码库' : '新增兑换码库')
// 选择项目相关
const selectItemDialogVisible = ref(false)
const itemOptions = ref([])
const itemOptionsLoading = ref(false)
const selectedItem = ref(null)
const selectedItemName = ref('')
const itemSearchKeyword = ref('')
// 过滤后的项目选项
const filteredItemOptions = computed(() => {
if (!itemSearchKeyword.value) {
return itemOptions.value
}
const keyword = itemSearchKeyword.value.toLowerCase()
return itemOptions.value.filter(item => {
const nameProp = getItemNameProp(mkForm.value.type)
const name = item[nameProp]?.toLowerCase() || ''
const teacherName = (item.teacher_name || item.teacherName || '').toLowerCase()
const desc = (item.desc || '').toLowerCase()
return name.includes(keyword) ||
teacherName.includes(keyword) ||
desc.includes(keyword) ||
item.ID.toString().includes(keyword)
})
})
// 项目名称缓存
const itemNameCache = ref({})
// 表单验证规则
const mkRules = {
codeName: [
{ required: true, message: '请输入兑换码库名称', trigger: 'blur' }
],
type: [
{ required: true, message: '请选择类型', trigger: 'change' }
],
item: [
{ required: true, message: '请选择对应的项目', trigger: 'change' }
]
}
// 获取类型文本
const getTypeText = (type) => {
const typeMap = {
1: 'VIP',
2: '讲师VIP',
3: '课程'
}
return typeMap[type] || '未知'
}
// 获取使用率
const getUsageRate = (used, total) => {
if (!total || total === 0) return '0%'
const rate = ((used || 0) / total * 100).toFixed(1)
return `${rate}%`
}
// 获取项目标签
const getItemLabel = (type) => {
const labelMap = {
1: 'VIP套餐',
2: '讲师VIP',
3: '课程'
}
return labelMap[type] || '项目'
}
// 获取项目名称属性
const getItemNameProp = (type) => {
const propMap = {
1: 'name',
2: 'title',
3: 'title'
}
return propMap[type] || 'name'
}
// 处理类型变化
const handleTypeChange = (type) => {
// 清空已选择的项目
mkForm.value.item = ''
selectedItem.value = null
selectedItemName.value = ''
console.log('类型变更为:', getTypeText(type))
}
// 处理选择项目
const handleSelectItem = async () => {
if (!mkForm.value.type) {
ElMessage.warning('请先选择类型')
return
}
await loadItemOptions()
selectItemDialogVisible.value = true
}
// 加载项目选项
const loadItemOptions = async () => {
itemOptionsLoading.value = true
try {
let res
const params = { page: 1, pageSize: 1000 }
switch (mkForm.value.type) {
case 1: // VIP
res = await vipList(params)
break
case 2: // 讲师VIP
res = await teacherVipList(params)
break
case 3: // 课程
res = await goodsList(params)
break
default:
itemOptions.value = []
return
}
if (res.code === 0) {
itemOptions.value = res.data.list || []
} else {
ElMessage.error(res.msg || '获取数据失败')
itemOptions.value = []
}
} catch (error) {
console.error('加载项目选项失败:', error)
ElMessage.error('加载数据失败')
itemOptions.value = []
} finally {
itemOptionsLoading.value = false
}
}
// 处理项目行点击
const handleItemRowClick = (row) => {
selectedItem.value = row
}
// 确认选择项目
const confirmSelectItem = () => {
if (!selectedItem.value) {
ElMessage.warning('请选择一个项目')
return
}
mkForm.value.item = selectedItem.value.ID
const nameProp = getItemNameProp(mkForm.value.type)
selectedItemName.value = selectedItem.value[nameProp]
handleSelectItemClose()
}
// 关闭选择项目弹窗
const handleSelectItemClose = () => {
selectItemDialogVisible.value = false
selectedItem.value = null
itemSearchKeyword.value = ''
}
// 处理项目搜索
const handleItemSearch = () => {
// 搜索逻辑已在 computed 中处理
}
// 获取项目显示名称
const getItemDisplayName = (row) => {
const cacheKey = `${row.type}_${row.item}`
if (itemNameCache.value[cacheKey]) {
return itemNameCache.value[cacheKey]
}
return `ID: ${row.item}`
}
// 加载项目名称缓存
const loadItemNameCache = async () => {
try {
// 获取所有类型的数据并缓存名称
const [vipRes, teacherVipRes, goodsRes] = await Promise.all([
vipList({ page: 1, pageSize: 1000 }),
teacherVipList({ page: 1, pageSize: 1000 }),
goodsList({ page: 1, pageSize: 1000 })
])
// 缓存VIP名称
if (vipRes.code === 0 && vipRes.data.list) {
vipRes.data.list.forEach(item => {
itemNameCache.value[`1_${item.ID}`] = item.name
})
}
// 缓存讲师VIP名称
if (teacherVipRes.code === 0 && teacherVipRes.data.list) {
teacherVipRes.data.list.forEach(item => {
itemNameCache.value[`2_${item.ID}`] = item.title
})
}
// 缓存课程名称
if (goodsRes.code === 0 && goodsRes.data.list) {
goodsRes.data.list.forEach(item => {
itemNameCache.value[`3_${item.ID}`] = item.title
})
}
} catch (error) {
console.error('加载项目名称缓存失败:', error)
}
}
// 兑换码库相关方法
const searchMkData = () => {
mkQueryParams.value.page = 1
getMkList()
}
const resetMkData = () => {
mkSearchForm.value = {
codeName: ''
}
mkQueryParams.value = {
page: 1,
pageSize: 10
}
getMkList()
}
const handleMkSizeChange = (size) => {
mkQueryParams.value.pageSize = size
getMkList()
}
const handleMkCurrentChange = (page) => {
mkQueryParams.value.page = page
getMkList()
}
async function getMkList() {
mkTableLoading.value = true
try {
const params = {
...mkQueryParams.value,
...mkSearchForm.value
}
const res = await mkList(params)
if (res.code === 0) {
mkTableData.value = res.data.list || []
mkTotal.value = res.data.total || 0
}
} finally {
mkTableLoading.value = false
}
}
// 获取兑换码库选项
async function getMkOptions() {
try {
const res = await mkList({ page: 1, pageSize: 1000 })
if (res.code === 0) {
mkOptions.value = res.data.list || []
}
} catch (error) {
console.error('获取兑换码库选项失败:', error)
}
}
const handleAddMk = () => {
mkForm.value = {
ID: '',
codeName: '',
type: 1,
item: ''
}
selectedItemName.value = ''
selectedItem.value = null
mkDialogVisible.value = true
}
const handleEditMk = async (row) => {
mkForm.value = {
ID: row.ID,
codeName: row.codeName,
type: row.type,
item: row.item
}
// 获取已选择项目的名称
await loadItemOptions()
const nameProp = getItemNameProp(row.type)
const selectedItemData = itemOptions.value.find(item => item.ID === row.item)
selectedItemName.value = selectedItemData ? selectedItemData[nameProp] : `ID: ${row.item}`
mkDialogVisible.value = true
}
const handleManageCdk = (row) => {
// 跳转到兑换码管理页面传递兑换码库ID和名称
router.push({
path: 'cdkManage',
query: {
id: row.ID,
name: encodeURIComponent(row.codeName)
}
})
}
const handleDeleteMk = async (row) => {
try {
await ElMessageBox.confirm('确定要删除这个兑换码库吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const res = await delMk({ ID: row.ID })
if (res.code === 0) {
ElMessage.success('删除成功')
getMkList()
} else {
ElMessage.error(res.msg || '删除失败')
}
} catch (error) {
// 用户取消删除
}
}
const handleMkClose = () => {
mkDialogVisible.value = false
mkFormRef.value?.resetFields()
selectedItemName.value = ''
selectedItem.value = null
}
const submitMk = async () => {
if (!mkFormRef.value) return
await mkFormRef.value.validate(async (valid) => {
if (valid) {
try {
const isEdit = !!mkForm.value.ID
const res = isEdit ? await editMk(mkForm.value) : await addMk(mkForm.value)
if (res.code === 0) {
ElMessage.success(isEdit ? '编辑成功' : '新增成功')
handleMkClose()
getMkList()
} else {
ElMessage.error(res.msg || (isEdit ? '编辑失败' : '新增失败'))
}
} catch (error) {
ElMessage.error('操作失败')
}
}
})
}
// 兑换码相关方法
const searchCdkData = () => {
cdkQueryParams.value.page = 1
getCdkList()
}
const resetCdkData = () => {
cdkSearchForm.value = {
code: '',
mkId: '',
status: ''
}
cdkQueryParams.value = {
page: 1,
pageSize: 10
}
getCdkList()
}
const handleCdkSizeChange = (size) => {
cdkQueryParams.value.pageSize = size
getCdkList()
}
const handleCdkCurrentChange = (page) => {
cdkQueryParams.value.page = page
getCdkList()
}
async function getCdkList() {
cdkTableLoading.value = true
try {
const params = {
...cdkQueryParams.value,
...cdkSearchForm.value
}
const res = await cdkList(params)
if (res.code === 0) {
cdkTableData.value = res.data.list || []
cdkTotal.value = res.data.total || 0
}
} finally {
cdkTableLoading.value = false
}
}
const handleDeleteCdk = async (row) => {
try {
await ElMessageBox.confirm('确定要作废这个兑换码吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
})
const res = await delCdk({ ID: row.ID })
if (res.code === 0) {
ElMessage.success('作废成功')
getCdkList()
} else {
ElMessage.error(res.msg || '作废失败')
}
} catch (error) {
// 用户取消作废
}
}
// 页面初始化
onMounted(() => {
loadItemNameCache()
getMkList()
})
</script>
<style lang="scss" scoped>
.cdk-library-management {
// 兑换码库管理样式
}
.gva-search-box {
padding: 20px;
background: #fff;
border-radius: 4px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.gva-table-box {
background: #fff;
border-radius: 4px;
padding: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
.gva-btn-list {
margin-bottom: 20px;
}
}
.gva-table {
:deep(.el-table__header) {
background-color: #f5f7fa;
}
:deep(.el-table__row:hover > td) {
background-color: #f5f7fa !important;
}
}
.gva-pagination {
margin-top: 20px;
text-align: right;
}
.demo-form-inline {
.el-form-item {
margin-right: 20px;
}
}
.text-danger {
color: #f56c6c;
font-weight: bold;
}
</style>