Compare commits
9 Commits
568575d8da
...
main
Author | SHA1 | Date | |
---|---|---|---|
f1c8264878 | |||
fe2aebb2b4 | |||
04db549012 | |||
94a061691c | |||
36921300df | |||
856338950e | |||
a631f6ea46 | |||
3378e709cf | |||
773f13104b |
@@ -108,3 +108,12 @@ export const getBotPublic = () => {
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
//BulkUpload
|
||||
export const bulkCreateBot = (data) => {
|
||||
return service({
|
||||
url: '/bt/bulkBot',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
80
src/api/cdk/index.js
Normal file
80
src/api/cdk/index.js
Normal 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
|
||||
})
|
||||
}
|
||||
|
44
src/api/domain.js
Normal file
44
src/api/domain.js
Normal file
@@ -0,0 +1,44 @@
|
||||
import service from '@/utils/request'
|
||||
|
||||
export const list = (params) => {
|
||||
return service({
|
||||
url: '/domain/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// addDomain
|
||||
export const addDomain = (data) => {
|
||||
return service({
|
||||
url: '/domain',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// deleteDomain
|
||||
export const deleteDomain = (data) => {
|
||||
return service({
|
||||
url: '/domain',
|
||||
method: 'delete',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// updateDomain
|
||||
export const updateDomain = (data) => {
|
||||
return service({
|
||||
url: '/domain',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// getDomain
|
||||
export const getDomain = (id) => {
|
||||
return service({
|
||||
url: '/domain/' + id,
|
||||
method: 'get',
|
||||
})
|
||||
}
|
@@ -48,3 +48,12 @@ export const getTechers = (params) => {
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
//BulkUpload
|
||||
export const bulkUpload = (data) => {
|
||||
return service({
|
||||
url: '/article/bulk',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
48
src/api/goods/teacherVip.js
Normal file
48
src/api/goods/teacherVip.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import service from '@/utils/request'
|
||||
|
||||
|
||||
export const list = (params) => {
|
||||
return service({
|
||||
url: '/app_user/teacher_vips',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export const add = (data) => {
|
||||
return service({
|
||||
url: '/app_user/teacher_vip',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
export const edit = (data) => {
|
||||
return service({
|
||||
url: '/app_user/teacher_vip',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
export const del = (data) => {
|
||||
return service({
|
||||
url: '/app_user/teacher_vip',
|
||||
method: 'delete',
|
||||
data
|
||||
})
|
||||
}
|
||||
export const detail = (params) => {
|
||||
return service({
|
||||
url: '/app_user/teacher_vip',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// 批量更新价格
|
||||
export const batchUpdatePrice = (data) => {
|
||||
return service({
|
||||
url: '/app_user/teacher_vip/price',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
@@ -16,7 +16,7 @@ export const list = (params) => {
|
||||
// /app_user/status/:id
|
||||
export const status = (data) => {
|
||||
return service({
|
||||
url: '/app_user/status/'+ data.ID,
|
||||
url: '/app_user/status/' + data.ID,
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
@@ -75,3 +75,51 @@ export const setVip = (data) => {
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// GetLoginInfo
|
||||
export const getLoginInfo = (params) => {
|
||||
return service({
|
||||
url: '/app_user/login/log',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// RemoveUserVip
|
||||
export const removeUserVip = (data) => {
|
||||
return service({
|
||||
url: '/app_user/vip',
|
||||
method: 'delete',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// getVipUserList
|
||||
export const getVipUserList = (params) => {
|
||||
return service({
|
||||
url: '/app_user/vip/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// setTeacherWeight 设置讲师权重
|
||||
export const setTeacherWeight = (data) => {
|
||||
return service({
|
||||
url: '/app_user/weight',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// setTeacherRate 设置讲师分成比例
|
||||
export const setTeacherRate = (data) => {
|
||||
return service({
|
||||
url: '/app_user/rate',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
|
20
src/api/user/with.js
Normal file
20
src/api/user/with.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import service from '@/utils/request'
|
||||
|
||||
// list 获取提现列表
|
||||
export const list = (params) => {
|
||||
return service({
|
||||
url: '/sys/with/list',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
// update 更新提现状态
|
||||
export const update = (data) => {
|
||||
return service({
|
||||
url: '/sys/with',
|
||||
method: 'put',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 502 KiB |
@@ -58,7 +58,7 @@
|
||||
const userStore = useUserStore()
|
||||
|
||||
const toolbarConfig = {}
|
||||
|
||||
|
||||
// 创建自定义上传函数,直接检查当前 props 值
|
||||
const createCustomUpload = () => {
|
||||
return async (file, insertFn) => {
|
||||
@@ -66,13 +66,13 @@
|
||||
const shouldUseWatermark = props.useWatermark
|
||||
console.log('customUpload called, useWatermark:', shouldUseWatermark)
|
||||
console.log('props.useWatermark:', props.useWatermark)
|
||||
|
||||
|
||||
// 未开启水印则直接上传原图
|
||||
if (!shouldUseWatermark) {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
const resp = await fetch(basePath + '/fileUploadAndDownload/upload?noSave=1', {
|
||||
method: 'POST',
|
||||
const resp = await fetch(basePath + '/fileUploadAndDownload/upload?noSave=1', {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
'x-token': userStore.token,
|
||||
@@ -93,7 +93,7 @@
|
||||
const watermarkedBlob = await addBottomWatermark(file, {
|
||||
stripRatio: 0.18, // 水印条高度占原图高度比例
|
||||
background: 'rgba(255,255,255,0.96)',
|
||||
text: '老陈机器人',
|
||||
text: '好运助手',
|
||||
textColor: '#333',
|
||||
fontFamily: 'PingFang SC, Microsoft YaHei, Arial',
|
||||
logo: botLogo
|
||||
@@ -181,7 +181,7 @@
|
||||
const handleCreated = (editor) => {
|
||||
editorRef.value = editor
|
||||
valueHtml.value = props.modelValue
|
||||
|
||||
|
||||
// 动态更新上传配置
|
||||
if (editor && editor.getConfig) {
|
||||
const config = editor.getConfig()
|
||||
@@ -197,7 +197,7 @@
|
||||
// 确保整个编辑器区域都可以点击
|
||||
editorContainer.style.cursor = 'text'
|
||||
editorContainer.style.minHeight = '300px'
|
||||
|
||||
|
||||
// 添加点击事件监听器
|
||||
editorContainer.addEventListener('click', (e) => {
|
||||
if (e.target === editorContainer) {
|
||||
@@ -366,4 +366,4 @@
|
||||
min-height: 300px !important;
|
||||
cursor: text !important;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
181
src/config.js
181
src/config.js
@@ -174,6 +174,10 @@ export const USER_SEARCH_CONFIG = [
|
||||
{
|
||||
label: 'SVIP',
|
||||
value: 3
|
||||
},
|
||||
{
|
||||
label: '过期VIP用户',
|
||||
value: 4
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -291,17 +295,175 @@ export const ORDER_TABLE_CONFIG = {
|
||||
}
|
||||
]
|
||||
}
|
||||
// 用户列表
|
||||
export const USER_TABLE_CONFIG = {
|
||||
|
||||
// VIP用户搜索配置
|
||||
export const VIP_USER_SEARCH_CONFIG = [
|
||||
{
|
||||
type: 'input',
|
||||
prop: 'user_id',
|
||||
label: '用户ID',
|
||||
placeholder: '请输入用户ID',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
prop: 'name',
|
||||
label: '用户名称',
|
||||
placeholder: '请输入用户名称',
|
||||
}
|
||||
]
|
||||
|
||||
// VIP用户表格配置
|
||||
export const VIP_USER_TABLE_CONFIG = {
|
||||
index: true,
|
||||
schemes: [
|
||||
{
|
||||
attrs: {
|
||||
label: 'ID',
|
||||
prop: 'ID',
|
||||
label: '头像',
|
||||
prop: 'avatar',
|
||||
align: 'center'
|
||||
},
|
||||
slot: 'avatar'
|
||||
},
|
||||
{
|
||||
attrs: {
|
||||
label: '用户ID',
|
||||
prop: 'id',
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
attrs: {
|
||||
label: '用户名称',
|
||||
prop: 'nick_name',
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
attrs: {
|
||||
label: '手机号',
|
||||
prop: 'phone',
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
attrs: {
|
||||
label: 'VIP等级',
|
||||
prop: 'user_label',
|
||||
align: 'center'
|
||||
},
|
||||
slot: 'user_label'
|
||||
},
|
||||
{
|
||||
attrs: {
|
||||
label: 'VIP到期时间',
|
||||
prop: 'vip_expire_time',
|
||||
align: 'center'
|
||||
},
|
||||
slot: 'vip_expire_time'
|
||||
},
|
||||
{
|
||||
attrs: {
|
||||
label: '账户余额',
|
||||
prop: 'balance',
|
||||
align: 'center'
|
||||
},
|
||||
slot: 'balance'
|
||||
},
|
||||
{
|
||||
attrs: {
|
||||
label: '操作',
|
||||
prop: 'operate',
|
||||
align: 'center'
|
||||
},
|
||||
slot: 'operate'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 用户登录日志搜索配置
|
||||
export const LOGIN_LOG_SEARCH_CONFIG = [
|
||||
{
|
||||
type: 'input',
|
||||
prop: 'user_id',
|
||||
label: '用户ID',
|
||||
placeholder: '请输入用户ID',
|
||||
},
|
||||
{
|
||||
type: 'input',
|
||||
prop: 'user_name',
|
||||
label: '用户名称',
|
||||
placeholder: '请输入用户名称',
|
||||
}
|
||||
]
|
||||
|
||||
// 用户登录日志表格配置
|
||||
export const LOGIN_LOG_TABLE_CONFIG = {
|
||||
index: true,
|
||||
schemes: [
|
||||
{
|
||||
attrs: {
|
||||
label: '用户ID',
|
||||
prop: 'user_id',
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
attrs: {
|
||||
label: '用户名称',
|
||||
prop: 'user_name',
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
attrs: {
|
||||
label: '手机号',
|
||||
prop: 'phone',
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
attrs: {
|
||||
label: '登录IP',
|
||||
prop: 'ip',
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
attrs: {
|
||||
label: '登录地址',
|
||||
prop: 'address',
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
attrs: {
|
||||
label: '设备信息',
|
||||
prop: 'device',
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
attrs: {
|
||||
label: '登录方式',
|
||||
prop: 'mode',
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
attrs: {
|
||||
label: '登录时间',
|
||||
prop: 'login_time',
|
||||
align: 'center'
|
||||
},
|
||||
slot: 'login_time'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 用户列表
|
||||
export const USER_TABLE_CONFIG = {
|
||||
index: true,
|
||||
schemes: [
|
||||
{
|
||||
attrs: {
|
||||
label: '头像',
|
||||
@@ -365,6 +527,7 @@ export const USER_TABLE_CONFIG = {
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 文章列表
|
||||
export const ARTICLE_TABLE_CONFIG = {
|
||||
index: true,
|
||||
@@ -415,6 +578,14 @@ export const ARTICLE_TABLE_CONFIG = {
|
||||
},
|
||||
slot: 'UpdatedAt'
|
||||
},
|
||||
{
|
||||
attrs: {
|
||||
label: '发布状态',
|
||||
prop: 'status',
|
||||
align: 'center'
|
||||
},
|
||||
slot: 'status'
|
||||
},
|
||||
{
|
||||
attrs: {
|
||||
label: '操作',
|
||||
@@ -424,4 +595,4 @@ export const ARTICLE_TABLE_CONFIG = {
|
||||
slot: 'operate'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@@ -3,8 +3,11 @@
|
||||
"/src/view/banner/index.vue": "Index",
|
||||
"/src/view/bot/bot/bot.vue": "Bot",
|
||||
"/src/view/bot/bot/botForm.vue": "BotForm",
|
||||
"/src/view/bot/bot/bulkBot.vue": "BotBulk",
|
||||
"/src/view/category/category/category.vue": "Category",
|
||||
"/src/view/category/category/categoryForm.vue": "CategoryForm",
|
||||
"/src/view/cdk/cdkManage.vue": "CdkManage",
|
||||
"/src/view/cdk/index.vue": "CdkLibraryManagement",
|
||||
"/src/view/dashboard/components/banner.vue": "Banner",
|
||||
"/src/view/dashboard/components/card.vue": "Card",
|
||||
"/src/view/dashboard/components/charts-content-numbers.vue": "ChartsContentNumbers",
|
||||
@@ -15,6 +18,7 @@
|
||||
"/src/view/dashboard/components/table.vue": "Table",
|
||||
"/src/view/dashboard/components/wiki.vue": "Wiki",
|
||||
"/src/view/dashboard/index.vue": "Dashboard",
|
||||
"/src/view/domain/index.vue": "Domain",
|
||||
"/src/view/error/index.vue": "Error",
|
||||
"/src/view/error/reload.vue": "Reload",
|
||||
"/src/view/example/breakpoint/breakpoint.vue": "BreakPoint",
|
||||
@@ -22,9 +26,11 @@
|
||||
"/src/view/example/index.vue": "Example",
|
||||
"/src/view/example/upload/scanUpload.vue": "scanUpload",
|
||||
"/src/view/example/upload/upload.vue": "Upload",
|
||||
"/src/view/goods/article/bulkUpload.vue": "BulkUpload",
|
||||
"/src/view/goods/article/edit.vue": "Edit",
|
||||
"/src/view/goods/article/index.vue": "Index",
|
||||
"/src/view/goods/index.vue": "goods",
|
||||
"/src/view/goods/teacher_vip/index.vue": "Index",
|
||||
"/src/view/goods/vip/index.vue": "VipList",
|
||||
"/src/view/init/index.vue": "Init",
|
||||
"/src/view/layout/aside/asideComponent/asyncSubmenu.vue": "AsyncSubmenu",
|
||||
@@ -90,7 +96,11 @@
|
||||
"/src/view/systemTools/version/version.vue": "SysVersion",
|
||||
"/src/view/user/index.vue": "UserManage",
|
||||
"/src/view/user/user/index.vue": "Index",
|
||||
"/src/view/user/user/loginLog.vue": "LoginLog",
|
||||
"/src/view/user/user/teacherApply.vue": "TeacherApply",
|
||||
"/src/view/user/user/teacherManger.vue": "TeacherManagement",
|
||||
"/src/view/user/user/vipUser.vue": "VipUser",
|
||||
"/src/view/user/user/with.vue": "WithdrawManagement",
|
||||
"/src/plugin/announcement/form/info.vue": "InfoForm",
|
||||
"/src/plugin/announcement/view/info.vue": "Info",
|
||||
"/src/plugin/customerservice/view/chat/index.vue": "ServiceMain",
|
||||
|
@@ -5,6 +5,15 @@ const routes = [
|
||||
path: '/',
|
||||
redirect: '/login'
|
||||
},
|
||||
{
|
||||
path: '/bot/bulk',
|
||||
name: 'BotBulk',
|
||||
meta: {
|
||||
title: '批量新增机器人',
|
||||
keepAlive: true
|
||||
},
|
||||
component: () => import('@/view/bot/bot/bulkBot.vue')
|
||||
},
|
||||
{
|
||||
path: '/init',
|
||||
name: 'Init',
|
||||
|
@@ -33,6 +33,7 @@
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list">
|
||||
<el-button type="primary" icon="plus" @click="openDialog()">新增</el-button>
|
||||
<el-button type="primary" icon="upload" @click="goBulk" style="margin-left: 10px;">批量新增</el-button>
|
||||
<el-button icon="delete" style="margin-left: 10px;" :disabled="!multipleSelection.length" @click="onDelete">删除</el-button>
|
||||
|
||||
</div>
|
||||
@@ -46,13 +47,13 @@
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
|
||||
<el-table-column align="left" label="日期" prop="createdAt"width="180">
|
||||
<el-table-column align="left" label="日期" prop="createdAt" width="180">
|
||||
<template #default="scope">{{ formatDate(scope.row.CreatedAt) }}</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column align="left" label="关键词" prop="keyword" width="120" />
|
||||
<el-table-column label="内容" prop="content" width="200">
|
||||
<template #default="scope">
|
||||
<template #default>
|
||||
[富文本内容]
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -126,9 +127,10 @@ import RichEdit from '@/components/richtext/rich-edit.vue'
|
||||
import RichView from '@/components/richtext/rich-view.vue'
|
||||
|
||||
// 全量引入格式化工具 请按需保留
|
||||
import { getDictFunc, formatDate, formatBoolean, filterDict ,filterDataSource, returnArrImg, onDownloadFile } from '@/utils/format'
|
||||
import { formatDate } from '@/utils/format'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAppStore } from "@/pinia"
|
||||
|
||||
|
||||
@@ -141,6 +143,7 @@ defineOptions({
|
||||
// 提交按钮loading
|
||||
const btnLoading = ref(false)
|
||||
const appStore = useAppStore()
|
||||
const router = useRouter()
|
||||
|
||||
// 控制更多查询条件显示/隐藏状态
|
||||
const showAllQuery = ref(false)
|
||||
@@ -404,6 +407,11 @@ const closeDetailShow = () => {
|
||||
detailFrom.value = {}
|
||||
}
|
||||
|
||||
// 跳转批量新增
|
||||
const goBulk = () => {
|
||||
router.push({ path: '/bot/bulk' })
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
435
src/view/bot/bot/bulkBot.vue
Normal file
435
src/view/bot/bot/bulkBot.vue
Normal file
@@ -0,0 +1,435 @@
|
||||
<template>
|
||||
<div class="container-wrapper">
|
||||
<el-form
|
||||
ref="ruleFormRef"
|
||||
:model="formData"
|
||||
label-width="auto"
|
||||
style="margin-top: 0.5rem;"
|
||||
>
|
||||
<ColumnItem title="批量创建机器人回复">
|
||||
<!-- 文件上传区域 -->
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="上传文件">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
:action="`${getBaseUrl()}/fileUploadAndDownload/upload`"
|
||||
:before-upload="checkFile"
|
||||
:on-error="uploadError"
|
||||
:on-success="uploadSuccess"
|
||||
:on-remove="removeFile"
|
||||
:file-list="fileList"
|
||||
:data="{'classId': 3}"
|
||||
:headers="{'x-token': token}"
|
||||
multiple
|
||||
drag
|
||||
class="upload-dragger"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
将文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持批量上传文件(图片/文档等),单个文件不超过10MB
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 已上传文件列表 -->
|
||||
<el-row :gutter="20" v-if="uploadedFiles.length > 0">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="已上传文件">
|
||||
<div class="file-list">
|
||||
<div
|
||||
v-for="(file, index) in uploadedFiles"
|
||||
:key="index"
|
||||
class="file-item"
|
||||
>
|
||||
<el-image
|
||||
v-if="file.isImage"
|
||||
:src="file.url"
|
||||
:preview-src-list="uploadedFiles.filter(f=>f.isImage).map(f => f.url)"
|
||||
fit="cover"
|
||||
class="file-preview"
|
||||
/>
|
||||
<div v-else class="file-icon">{{ getExt(file.name) }}</div>
|
||||
<div class="file-info">
|
||||
<div class="file-name">{{ file.name }}</div>
|
||||
<div class="file-url">{{ file.url }}</div>
|
||||
</div>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="removeUploadedFile(index)"
|
||||
class="remove-btn"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</ColumnItem>
|
||||
</el-form>
|
||||
|
||||
<div class="gva-table-box footer-box">
|
||||
<el-button @click="$router.back()">返回</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="submit()"
|
||||
:loading="submitLoading"
|
||||
:disabled="uploadedFiles.length === 0"
|
||||
>
|
||||
批量创建 ({{ uploadedFiles.length }} 个文件)
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { bulkCreateBot } from '@/api/bot/bot.js'
|
||||
import ColumnItem from '@/components/columnItem/ColumnItem.vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
import { getBaseUrl } from '@/utils/format'
|
||||
import { useUserStore } from "@/pinia"
|
||||
import logoImg from '@/assets/bot_logo.png'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const token = userStore.token
|
||||
|
||||
const ruleFormRef = ref(null)
|
||||
const uploadRef = ref(null)
|
||||
const submitLoading = ref(false)
|
||||
const fileList = ref([])
|
||||
const uploadedFiles = ref([])
|
||||
|
||||
const formData = ref({})
|
||||
|
||||
// 文件类型工具
|
||||
const getExt = (name) => {
|
||||
const idx = name.lastIndexOf('.')
|
||||
return idx > -1 ? name.slice(idx + 1).toUpperCase() : 'FILE'
|
||||
}
|
||||
|
||||
// 文件上传前处理:校验大小并为图片添加底部水印
|
||||
const checkFile = async (file) => {
|
||||
const isLt10M = file.size / 1024 / 1024 < 10
|
||||
if (!isLt10M) {
|
||||
ElMessage.error('单个文件大小不能超过 10MB!')
|
||||
return false
|
||||
}
|
||||
const isImage = (file.type && file.type.startsWith('image/')) || /\.(png|jpg|jpeg|gif|webp|bmp)$/i.test(file.name)
|
||||
if (!isImage) {
|
||||
return true
|
||||
}
|
||||
try {
|
||||
const watermarked = await addWatermarkToImage(file)
|
||||
return watermarked
|
||||
} catch (e) {
|
||||
console.error('添加水印失败,使用原图上传:', e)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 为图片底部追加白底条 + 放大 logo + 文字“好运助手”
|
||||
async function addWatermarkToImage(rawFile) {
|
||||
const imageBitmap = await readImageFromFile(rawFile)
|
||||
const originWidth = imageBitmap.width
|
||||
const originHeight = imageBitmap.height
|
||||
|
||||
const barHeight = Math.max(80, Math.round(originHeight * 0.16))
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = originWidth
|
||||
canvas.height = originHeight + barHeight
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
// 原图
|
||||
ctx.drawImage(imageBitmap, 0, 0, originWidth, originHeight)
|
||||
// 底部白色背景
|
||||
ctx.fillStyle = '#FFFFFF'
|
||||
ctx.fillRect(0, originHeight, originWidth, barHeight)
|
||||
|
||||
// 绘制 logo(略大一些)
|
||||
const logo = await readImageFromUrl(logoImg)
|
||||
const logoTargetHeight = Math.min(barHeight * 0.8, 160)
|
||||
const logoScale = logoTargetHeight / logo.height
|
||||
const logoTargetWidth = Math.round(logo.width * logoScale)
|
||||
const paddingY = originHeight + Math.round((barHeight - logoTargetHeight) / 2)
|
||||
|
||||
// 绘制文字(先计算尺寸以便整体居中)
|
||||
const text = '好运助手'
|
||||
// 字号根据条高自适应
|
||||
const fontSize = Math.min(Math.max(Math.round(barHeight * 0.42), 26), 64)
|
||||
ctx.font = `${fontSize}px Microsoft YaHei, PingFang SC, Arial`
|
||||
ctx.fillStyle = '#333333'
|
||||
ctx.textBaseline = 'middle'
|
||||
const textMetrics = ctx.measureText(text)
|
||||
const textWidth = Math.ceil(textMetrics.width)
|
||||
const spacing = Math.round(barHeight * 0.2)
|
||||
|
||||
// 计算整体水平居中起点
|
||||
const groupWidth = logoTargetWidth + spacing + textWidth
|
||||
const startX = Math.max(0, Math.round((originWidth - groupWidth) / 2))
|
||||
|
||||
// 绘制 logo 和文字(水平居中)
|
||||
const logoX = startX
|
||||
const textX = logoX + logoTargetWidth + spacing
|
||||
const textY = originHeight + Math.round(barHeight / 2)
|
||||
|
||||
ctx.drawImage(logo, logoX, paddingY, logoTargetWidth, logoTargetHeight)
|
||||
ctx.fillText(text, textX, textY)
|
||||
|
||||
const mime = rawFile.type && rawFile.type.startsWith('image/') ? rawFile.type : 'image/jpeg'
|
||||
const blob = await new Promise((resolve) => canvas.toBlob(resolve, mime, 0.92))
|
||||
const wmFile = new File([blob], appendSuffixToFilename(rawFile.name, '_wm'), { type: blob.type })
|
||||
return wmFile
|
||||
}
|
||||
|
||||
function appendSuffixToFilename(name, suffix) {
|
||||
const idx = name.lastIndexOf('.')
|
||||
if (idx === -1) return name + suffix
|
||||
return name.slice(0, idx) + suffix + name.slice(idx)
|
||||
}
|
||||
|
||||
function readImageFromFile(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader()
|
||||
reader.onload = () => {
|
||||
const img = new Image()
|
||||
img.onload = () => resolve(img)
|
||||
img.onerror = reject
|
||||
img.src = reader.result
|
||||
}
|
||||
reader.onerror = reject
|
||||
reader.readAsDataURL(file)
|
||||
})
|
||||
}
|
||||
|
||||
function readImageFromUrl(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
img.crossOrigin = 'anonymous'
|
||||
img.onload = () => resolve(img)
|
||||
img.onerror = reject
|
||||
img.src = url
|
||||
})
|
||||
}
|
||||
|
||||
// 文件上传成功
|
||||
const uploadSuccess = (res, file) => {
|
||||
if (res.code === 0 && res.data.file) {
|
||||
const url = res.data.file.url
|
||||
uploadedFiles.value.push({
|
||||
name: file.name,
|
||||
url,
|
||||
isImage: (file.type && file.type.startsWith('image/')) || /\.(png|jpg|jpeg|gif|webp|bmp|svg)$/i.test(file.name)
|
||||
})
|
||||
ElMessage.success(`${file.name} 上传成功`)
|
||||
} else {
|
||||
ElMessage.error('上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 文件上传失败
|
||||
const uploadError = () => {
|
||||
ElMessage.error('上传失败')
|
||||
}
|
||||
|
||||
// 移除文件(同步两侧列表)
|
||||
const removeFile = (file) => {
|
||||
const index = fileList.value.findIndex(item => item.uid === file.uid)
|
||||
if (index > -1) {
|
||||
uploadedFiles.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 移除已上传文件
|
||||
const removeUploadedFile = (index) => {
|
||||
uploadedFiles.value.splice(index, 1)
|
||||
if (fileList.value[index]) {
|
||||
fileList.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 提交创建
|
||||
async function submit() {
|
||||
if (uploadedFiles.value.length === 0) {
|
||||
ElMessage.warning('请先上传文件')
|
||||
return
|
||||
}
|
||||
|
||||
submitLoading.value = true
|
||||
try {
|
||||
const payload = {
|
||||
files: uploadedFiles.value.map(f => f.url)
|
||||
}
|
||||
const res = await bulkCreateBot(payload)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('批量创建成功!')
|
||||
router.back()
|
||||
} else if (res.code === 5) {
|
||||
handlePartialFailure(res)
|
||||
} else {
|
||||
ElMessage.error(res.msg || '批量创建失败')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('批量创建失败:', e)
|
||||
ElMessage.error('批量创建失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理部分失败(与文章批量上传一致协议:data 为失败URL数组字符串)
|
||||
function handlePartialFailure(res) {
|
||||
try {
|
||||
const failedUrls = res.data.replace(/[[\]]/g, '').split(' ').filter(url => url.trim())
|
||||
if (failedUrls.length === 0) {
|
||||
ElMessage.error('解析失败文件列表出错')
|
||||
return
|
||||
}
|
||||
|
||||
const originalCount = uploadedFiles.value.length
|
||||
uploadedFiles.value = uploadedFiles.value.filter(file => failedUrls.includes(file.url))
|
||||
fileList.value = fileList.value.filter(file => failedUrls.some(url => file.url === url))
|
||||
|
||||
const successCount = originalCount - failedUrls.length
|
||||
const failedCount = failedUrls.length
|
||||
|
||||
ElMessage.warning({
|
||||
message: `部分创建失败!成功:${successCount}个,失败:${failedCount}个。请处理失败项后重试。`,
|
||||
duration: 5000
|
||||
})
|
||||
|
||||
ElMessageBox.alert(
|
||||
`失败的文件:\n${failedUrls.join('\n')}`,
|
||||
'创建失败详情',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('处理部分失败时出错:', error)
|
||||
ElMessage.error('处理结果时出错')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container-wrapper {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.upload-dragger {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
background: #fafafa;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.file-preview {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
margin-right: 8px;
|
||||
background: #f0f2f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.file-url {
|
||||
font-size: 10px;
|
||||
color: #909399;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.footer-box {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::deep(.el-row) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
::deep(.el-form) {
|
||||
flex: 1;
|
||||
background: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
::deep(.el-form-item) {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
::deep(.el-form-item__label) {
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
::deep(.el-upload-dragger) {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
776
src/view/cdk/cdkManage.vue
Normal file
776
src/view/cdk/cdkManage.vue
Normal file
@@ -0,0 +1,776 @@
|
||||
<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="openShare(row)"
|
||||
>
|
||||
分享
|
||||
</el-button>
|
||||
<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="shareDialogVisible"
|
||||
title="分享兑换码"
|
||||
width="520px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
>
|
||||
<el-form label-width="100px">
|
||||
<el-form-item label="选择域名">
|
||||
<el-select
|
||||
v-model="shareForm.domain"
|
||||
placeholder="请选择域名"
|
||||
filterable
|
||||
style="width: 100%"
|
||||
@change="updateShareLink"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in domainOptions"
|
||||
:key="item.ID"
|
||||
:label="item.name ? `${item.name}(${item.domain_url})` : item.domain_url"
|
||||
:value="item.domain_url"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="分享链接">
|
||||
<div class="flex" style="width: 100%">
|
||||
<el-input v-model="shareForm.link" readonly />
|
||||
<el-button style="margin-left: 10px" type="primary" @click="copyShareLink" :disabled="!shareForm.link">复制</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="二维码" v-if="shareForm.link">
|
||||
<div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap">
|
||||
<vue-qr :text="shareForm.link" :size="180" :margin="0" :autoColor="true" :dotScale="1"/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="closeShare">关 闭</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 生成兑换码弹窗 -->
|
||||
<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 { list as domainList } from '@/api/domain'
|
||||
import vueQr from 'vue-qr/src/packages/vue-qr.vue'
|
||||
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 components = { vueQr }
|
||||
|
||||
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 shareDialogVisible = ref(false)
|
||||
const shareForm = ref({
|
||||
domain: '',
|
||||
link: ''
|
||||
})
|
||||
const currentShareCode = ref('')
|
||||
const domainOptions = ref([])
|
||||
|
||||
const openShare = async (row) => {
|
||||
currentShareCode.value = row.code
|
||||
shareForm.value = { domain: '', link: '' }
|
||||
shareDialogVisible.value = true
|
||||
if (!domainOptions.value.length) {
|
||||
try {
|
||||
const res = await domainList({ page: 1, pageSize: 1000 })
|
||||
if (res.code === 0) {
|
||||
domainOptions.value = res.data.list || []
|
||||
}
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const stripProtocol = (url) => {
|
||||
if (!url) return ''
|
||||
return url.replace(/^https?:\/\//i, '').replace(/\/$/, '')
|
||||
}
|
||||
|
||||
const updateShareLink = () => {
|
||||
const host = stripProtocol(shareForm.value.domain)
|
||||
if (!host || !currentShareCode.value) {
|
||||
shareForm.value.link = ''
|
||||
return
|
||||
}
|
||||
shareForm.value.link = `${host}/pages/user/cdk/index?dhm=${encodeURIComponent(currentShareCode.value)}`
|
||||
}
|
||||
|
||||
const copyShareLink = async () => {
|
||||
if (!shareForm.value.link) return
|
||||
try {
|
||||
await navigator.clipboard.writeText(shareForm.value.link)
|
||||
ElMessage.success('链接已复制到剪贴板')
|
||||
} catch (error) {
|
||||
// 兼容不支持的情况
|
||||
const ta = document.createElement('textarea')
|
||||
ta.value = shareForm.value.link
|
||||
document.body.appendChild(ta)
|
||||
ta.select()
|
||||
document.execCommand('copy')
|
||||
document.body.removeChild(ta)
|
||||
ElMessage.success('链接已复制到剪贴板')
|
||||
}
|
||||
}
|
||||
|
||||
const closeShare = () => {
|
||||
shareDialogVisible.value = false
|
||||
}
|
||||
|
||||
|
||||
// 统计数据
|
||||
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>
|
746
src/view/cdk/index.vue
Normal file
746
src/view/cdk/index.vue
Normal file
@@ -0,0 +1,746 @@
|
||||
<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 = {
|
||||
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>
|
||||
|
||||
|
394
src/view/domain/index.vue
Normal file
394
src/view/domain/index.vue
Normal file
@@ -0,0 +1,394 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="gva-search-box">
|
||||
<el-form ref="elSearchFormRef" :inline="true" :model="searchInfo" class="demo-form-inline" :rules="searchRule" @keyup.enter="onSubmit">
|
||||
<el-form-item label="创建日期" prop="createdAt">
|
||||
<template #label>
|
||||
<span>
|
||||
创建日期
|
||||
<el-tooltip content="搜索范围是开始日期(包含)至结束日期(不包含)">
|
||||
<el-icon><QuestionFilled /></el-icon>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
</template>
|
||||
<el-date-picker v-model="searchInfo.startCreatedAt" type="datetime" placeholder="开始日期" :disabled-date="time=> searchInfo.endCreatedAt ? time.getTime() > searchInfo.endCreatedAt.getTime() : false"></el-date-picker>
|
||||
—
|
||||
<el-date-picker v-model="searchInfo.endCreatedAt" type="datetime" placeholder="结束日期" :disabled-date="time=> searchInfo.startCreatedAt ? time.getTime() < searchInfo.startCreatedAt.getTime() : false"></el-date-picker>
|
||||
</el-form-item>
|
||||
|
||||
<template v-if="showAllQuery">
|
||||
<el-form-item label="域名名称">
|
||||
<el-input v-model="searchInfo.name" placeholder="请输入名称" clearable style="width: 200px" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="searchInfo.status" placeholder="全部" clearable style="width: 180px">
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="类型">
|
||||
<el-select v-model="searchInfo.type" placeholder="全部" clearable style="width: 180px">
|
||||
<el-option label="炮灰域名" :value="1" />
|
||||
<el-option label="入口域名" :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</template>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="onSubmit">查询</el-button>
|
||||
<el-button icon="refresh" @click="onReset">重置</el-button>
|
||||
<el-button link type="primary" icon="arrow-down" @click="showAllQuery=true" v-if="!showAllQuery">展开</el-button>
|
||||
<el-button link type="primary" icon="arrow-up" @click="showAllQuery=false" v-else>收起</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list">
|
||||
<el-button type="primary" icon="plus" @click="openDialog()">新增</el-button>
|
||||
<el-button icon="delete" style="margin-left: 10px;" :disabled="!multipleSelection.length" @click="onDelete">删除</el-button>
|
||||
</div>
|
||||
|
||||
<el-table
|
||||
ref="multipleTable"
|
||||
style="width: 100%"
|
||||
tooltip-effect="dark"
|
||||
:data="tableData"
|
||||
row-key="ID"
|
||||
@selection-change="handleSelectionChange"
|
||||
@sort-change="sortChange"
|
||||
>
|
||||
<el-table-column type="selection" width="55" />
|
||||
|
||||
<el-table-column align="left" label="日期" prop="CreatedAt" width="180">
|
||||
<template #default="scope">{{ formatDate(scope.row.CreatedAt) }}</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column sortable align="left" label="名称" prop="name" min-width="140" />
|
||||
<el-table-column align="left" label="域名" prop="domain_url" min-width="220">
|
||||
<template #default="scope">
|
||||
<el-link :href="scope.row.domain_url" target="_blank" type="primary">{{ scope.row.domain_url }}</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="描述" prop="description" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column align="left" label="状态" prop="status" width="120">
|
||||
<template #default="scope">{{ formatStatus(scope.row.status) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="类型" prop="type" width="120">
|
||||
<template #default="scope">{{ formatType(scope.row.type) }}</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column align="left" label="操作" fixed="right" :min-width="appStore.operateMinWith">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" link class="table-button" @click="getDetails(scope.row)"><el-icon style="margin-right: 5px"><InfoFilled /></el-icon>查看</el-button>
|
||||
<el-button type="primary" link icon="edit" class="table-button" @click="updateDomainFunc(scope.row)">编辑</el-button>
|
||||
<el-button type="primary" link icon="delete" @click="deleteRow(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="gva-pagination">
|
||||
<el-pagination
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:current-page="page"
|
||||
:page-size="pageSize"
|
||||
:page-sizes="[10, 30, 50, 100]"
|
||||
:total="total"
|
||||
@current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-drawer destroy-on-close :size="appStore.drawerSize" v-model="dialogFormVisible" :show-close="false" :before-close="closeDialog">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">{{ type==='create' ? '新增' : '编辑' }}</span>
|
||||
<div>
|
||||
<el-button :loading="btnLoading" type="primary" @click="enterDialog">确 定</el-button>
|
||||
<el-button @click="closeDialog">取 消</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form :model="formData" label-position="top" ref="elFormRef" :rules="rule" label-width="80px">
|
||||
<el-form-item label="名称:" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="域名地址:" prop="domain_url">
|
||||
<el-input v-model="formData.domain_url" placeholder="例如:https://example.com" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述:" prop="description">
|
||||
<el-input v-model="formData.description" type="textarea" :rows="3" placeholder="请输入描述" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态:" prop="status">
|
||||
<el-select v-model="formData.status" placeholder="请选择" style="width: 100%">
|
||||
<el-option label="启用" :value="1" />
|
||||
<el-option label="禁用" :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="类型:" prop="type">
|
||||
<el-select v-model="formData.type" placeholder="请选择" style="width: 100%">
|
||||
<el-option label="炮灰域名" :value="1" />
|
||||
<el-option label="入口域名" :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-drawer>
|
||||
|
||||
<el-drawer destroy-on-close :size="appStore.drawerSize" v-model="detailShow" :show-close="true" :before-close="closeDetailShow" title="查看">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="名称">{{ detailFrom.name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="域名">
|
||||
<el-link :href="detailFrom.domain_url" target="_blank" type="primary">{{ detailFrom.domain_url }}</el-link>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="描述">{{ detailFrom.description }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">{{ formatStatus(detailFrom.status) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="类型">{{ formatType(detailFrom.type) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">{{ formatDate(detailFrom.CreatedAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">{{ formatDate(detailFrom.UpdatedAt) }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-drawer>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { list, addDomain, deleteDomain, updateDomain, getDomain } from '@/api/domain'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { ref, reactive } from 'vue'
|
||||
import { useAppStore } from "@/pinia"
|
||||
import { formatDate } from '@/utils/format'
|
||||
|
||||
defineOptions({ name: 'Domain' })
|
||||
|
||||
const appStore = useAppStore()
|
||||
const btnLoading = ref(false)
|
||||
const showAllQuery = ref(false)
|
||||
|
||||
const formData = ref({
|
||||
name: '',
|
||||
domain_url: '',
|
||||
description: '',
|
||||
status: undefined,
|
||||
type: undefined,
|
||||
})
|
||||
|
||||
const rule = reactive({
|
||||
name: [
|
||||
{ required: true, message: '请输入名称', trigger: ['input','blur'] },
|
||||
{ whitespace: true, message: '不能只输入空格', trigger: ['input','blur'] },
|
||||
],
|
||||
domain_url: [
|
||||
{ required: true, message: '请输入域名地址', trigger: ['input','blur'] },
|
||||
],
|
||||
status: [
|
||||
{ required: true, message: '请选择状态', trigger: ['change','blur'] },
|
||||
],
|
||||
type: [
|
||||
{ required: true, message: '请选择类型', trigger: ['change','blur'] },
|
||||
],
|
||||
})
|
||||
|
||||
const searchRule = reactive({
|
||||
createdAt: [
|
||||
{ validator: (rule, value, callback) => {
|
||||
if (searchInfo.value.startCreatedAt && !searchInfo.value.endCreatedAt) {
|
||||
callback(new Error('请填写结束日期'))
|
||||
} else if (!searchInfo.value.startCreatedAt && searchInfo.value.endCreatedAt) {
|
||||
callback(new Error('请填写开始日期'))
|
||||
} else if (searchInfo.value.startCreatedAt && searchInfo.value.endCreatedAt && (searchInfo.value.startCreatedAt.getTime() === searchInfo.value.endCreatedAt.getTime() || searchInfo.value.startCreatedAt.getTime() > searchInfo.value.endCreatedAt.getTime())) {
|
||||
callback(new Error('开始日期应当早于结束日期'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}, trigger: 'change' }
|
||||
],
|
||||
})
|
||||
|
||||
const elFormRef = ref()
|
||||
const elSearchFormRef = ref()
|
||||
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const tableData = ref([])
|
||||
const searchInfo = ref({})
|
||||
|
||||
const sortChange = ({ prop, order }) => {
|
||||
const sortMap = { name: 'name', CreatedAt: 'created_at' }
|
||||
let sort = sortMap[prop]
|
||||
if(!sort){
|
||||
sort = prop && prop.replace(/[A-Z]/g, match => `_${match.toLowerCase()}`)
|
||||
}
|
||||
searchInfo.value.sort = sort
|
||||
searchInfo.value.order = order
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const onReset = () => {
|
||||
searchInfo.value = {}
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const onSubmit = () => {
|
||||
elSearchFormRef.value?.validate(async(valid) => {
|
||||
if (!valid) return
|
||||
page.value = 1
|
||||
getTableData()
|
||||
})
|
||||
}
|
||||
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const getTableData = async() => {
|
||||
const table = await list({ page: page.value, pageSize: pageSize.value, ...searchInfo.value })
|
||||
if (table.code === 0) {
|
||||
tableData.value = table.data.list
|
||||
total.value = table.data.total
|
||||
page.value = table.data.page
|
||||
pageSize.value = table.data.pageSize
|
||||
}
|
||||
}
|
||||
|
||||
getTableData()
|
||||
|
||||
const multipleSelection = ref([])
|
||||
const handleSelectionChange = (val) => {
|
||||
multipleSelection.value = val
|
||||
}
|
||||
|
||||
const deleteRow = (row) => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
deleteDomainFunc(row)
|
||||
})
|
||||
}
|
||||
|
||||
const onDelete = async() => {
|
||||
ElMessageBox.confirm('确定要删除吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(async() => {
|
||||
if (multipleSelection.value.length === 0) {
|
||||
ElMessage({ type: 'warning', message: '请选择要删除的数据' })
|
||||
return
|
||||
}
|
||||
const results = await Promise.all(
|
||||
multipleSelection.value.map(item => deleteDomain({ ID: item.ID }))
|
||||
)
|
||||
const ok = results.every(r => r && r.code === 0)
|
||||
if (ok) {
|
||||
ElMessage({ type: 'success', message: '删除成功' })
|
||||
if (tableData.value.length === multipleSelection.value.length && page.value > 1) {
|
||||
page.value--
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const type = ref('')
|
||||
|
||||
const updateDomainFunc = async(row) => {
|
||||
const res = await getDomain(row.ID)
|
||||
type.value = 'update'
|
||||
if (res.code === 0) {
|
||||
formData.value = { ...res.data }
|
||||
dialogFormVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const deleteDomainFunc = async (row) => {
|
||||
const res = await deleteDomain({ ID: row.ID })
|
||||
if (res.code === 0) {
|
||||
ElMessage({ type: 'success', message: '删除成功' })
|
||||
if (tableData.value.length === 1 && page.value > 1) {
|
||||
page.value--
|
||||
}
|
||||
getTableData()
|
||||
}
|
||||
}
|
||||
|
||||
const dialogFormVisible = ref(false)
|
||||
|
||||
const openDialog = () => {
|
||||
type.value = 'create'
|
||||
dialogFormVisible.value = true
|
||||
}
|
||||
|
||||
const closeDialog = () => {
|
||||
dialogFormVisible.value = false
|
||||
formData.value = {
|
||||
name: '',
|
||||
domain_url: '',
|
||||
description: '',
|
||||
status: undefined,
|
||||
type: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
const enterDialog = async () => {
|
||||
btnLoading.value = true
|
||||
elFormRef.value?.validate( async (valid) => {
|
||||
if (!valid) return btnLoading.value = false
|
||||
let res
|
||||
switch (type.value) {
|
||||
case 'create':
|
||||
res = await addDomain(formData.value)
|
||||
break
|
||||
case 'update':
|
||||
res = await updateDomain(formData.value)
|
||||
break
|
||||
default:
|
||||
res = await addDomain(formData.value)
|
||||
break
|
||||
}
|
||||
btnLoading.value = false
|
||||
if (res.code === 0) {
|
||||
ElMessage({ type: 'success', message: '创建/更改成功' })
|
||||
closeDialog()
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const detailFrom = ref({})
|
||||
const detailShow = ref(false)
|
||||
const openDetailShow = () => { detailShow.value = true }
|
||||
|
||||
const getDetails = async (row) => {
|
||||
const res = await getDomain(row.ID)
|
||||
if (res.code === 0) {
|
||||
detailFrom.value = res.data
|
||||
openDetailShow()
|
||||
}
|
||||
}
|
||||
|
||||
const closeDetailShow = () => {
|
||||
detailShow.value = false
|
||||
detailFrom.value = {}
|
||||
}
|
||||
|
||||
const formatStatus = (s) => s === 1 ? '启用' : s === 2 ? '禁用' : '-'
|
||||
const formatType = (t) => t === 1 ? '炮灰域名' : t === 2 ? '入口域名' : '-'
|
||||
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
</style>
|
||||
|
||||
|
475
src/view/goods/article/bulkUpload.vue
Normal file
475
src/view/goods/article/bulkUpload.vue
Normal file
@@ -0,0 +1,475 @@
|
||||
<template>
|
||||
<div class="container-wrapper">
|
||||
<el-form
|
||||
ref="ruleFormRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
label-width="auto"
|
||||
style="margin-top: 0.5rem;"
|
||||
>
|
||||
<ColumnItem title="批量上传文章">
|
||||
<!-- 文件上传区域 -->
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="上传文件" prop="files">
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
:action="`${getBaseUrl()}/fileUploadAndDownload/upload`"
|
||||
:before-upload="checkFile"
|
||||
:on-error="uploadError"
|
||||
:on-success="uploadSuccess"
|
||||
:on-remove="removeFile"
|
||||
:file-list="fileList"
|
||||
:data="{'classId': 3}"
|
||||
:headers="{'x-token': token}"
|
||||
multiple
|
||||
drag
|
||||
class="upload-dragger"
|
||||
accept="image/*"
|
||||
>
|
||||
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||
<div class="el-upload__text">
|
||||
将文件拖到此处,或<em>点击上传</em>
|
||||
</div>
|
||||
<template #tip>
|
||||
<div class="el-upload__tip">
|
||||
支持批量上传图片文件,单个文件不超过5MB
|
||||
</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 已上传文件列表 -->
|
||||
<el-row :gutter="20" v-if="uploadedFiles.length > 0">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="已上传文件">
|
||||
<div class="file-list">
|
||||
<div
|
||||
v-for="(file, index) in uploadedFiles"
|
||||
:key="index"
|
||||
class="file-item"
|
||||
>
|
||||
<el-image
|
||||
:src="file.url"
|
||||
:preview-src-list="uploadedFiles.map(f => f.url)"
|
||||
fit="cover"
|
||||
class="file-preview"
|
||||
/>
|
||||
<div class="file-info">
|
||||
<div class="file-name">{{ file.name }}</div>
|
||||
<div class="file-url">{{ file.url }}</div>
|
||||
</div>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="small"
|
||||
@click="removeUploadedFile(index)"
|
||||
class="remove-btn"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<!-- 文章信息表单 -->
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input v-model="formData.title" placeholder="请输入文章标题" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="分类" prop="categoryId">
|
||||
<el-select v-model="formData.categoryId" placeholder="请选择文章分类" clearable style="width: 100%">
|
||||
<el-option
|
||||
v-for="item in articleCategoryList"
|
||||
:key="item.ID"
|
||||
:label="item.name"
|
||||
:value="item.ID"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="是否免费" prop="isFree">
|
||||
<el-radio-group v-model="formData.isFree">
|
||||
<el-radio :label="1">免费</el-radio>
|
||||
<el-radio :label="0">付费</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12" v-if="formData.isFree === 0">
|
||||
<el-form-item label="价格(元)" prop="price">
|
||||
<el-input v-model="formData.price" type="number" placeholder="请输入价格,单位:元" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="定时发布" prop="publishTime">
|
||||
<el-date-picker
|
||||
v-model="formData.publishTime"
|
||||
type="datetime"
|
||||
placeholder="选择发布时间(可选)"
|
||||
format="YYYY-MM-DD HH:mm:ss"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="文章描述" prop="desc">
|
||||
<RichEdit style="width: 100%" v-model="formData.desc"/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</ColumnItem>
|
||||
</el-form>
|
||||
|
||||
<div class="gva-table-box footer-box">
|
||||
<el-button @click="$router.back()">返回</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="submit(ruleFormRef)"
|
||||
:loading="submitLoading"
|
||||
:disabled="uploadedFiles.length === 0"
|
||||
>
|
||||
批量提交 ({{ uploadedFiles.length }} 个文件)
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { bulkUpload } from '@/api/goods/index.js'
|
||||
import { getCategoryList } from '@/api/category/category.js'
|
||||
import ColumnItem from '@/components/columnItem/ColumnItem.vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { UploadFilled } from '@element-plus/icons-vue'
|
||||
import { getBaseUrl } from '@/utils/format'
|
||||
import { useUserStore } from "@/pinia"
|
||||
import RichEdit from '@/components/richtext/rich-edit.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const userStore = useUserStore()
|
||||
const token = userStore.token
|
||||
|
||||
const ruleFormRef = ref(null)
|
||||
const uploadRef = ref(null)
|
||||
const submitLoading = ref(false)
|
||||
const fileList = ref([])
|
||||
const uploadedFiles = ref([])
|
||||
const categoryList = ref([])
|
||||
|
||||
const formData = ref({
|
||||
title: '',
|
||||
desc: '',
|
||||
price: 0,
|
||||
categoryId: 0,
|
||||
publishTime: '',
|
||||
isFree: 0
|
||||
})
|
||||
|
||||
const rules = ref({
|
||||
title: [
|
||||
{ required: true, message: '请输入文章标题', trigger: 'blur' }
|
||||
],
|
||||
categoryId: [
|
||||
{ required: true, message: '请选择文章分类', trigger: 'change' }
|
||||
],
|
||||
isFree: [
|
||||
{ required: true, message: '请选择是否免费', trigger: 'change' }
|
||||
],
|
||||
price: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入价格',
|
||||
trigger: 'blur',
|
||||
validator: (rule, value, callback) => {
|
||||
if (formData.value.isFree === 0 && (!value || value <= 0)) {
|
||||
callback(new Error('付费文章请输入大于0的价格'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 计算属性:筛选出文章分类
|
||||
const articleCategoryList = computed(() => {
|
||||
return categoryList.value.filter(item => !item.url || item.url === '')
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
getCategoryData()
|
||||
})
|
||||
|
||||
// 获取分类数据
|
||||
async function getCategoryData() {
|
||||
try {
|
||||
const res = await getCategoryList()
|
||||
if (res.code === 0) {
|
||||
categoryList.value = res.data.list || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取分类数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 文件上传前检查
|
||||
const checkFile = (file) => {
|
||||
const isLt5M = file.size / 1024 / 1024 < 5
|
||||
const isImage = file.type.startsWith('image/')
|
||||
|
||||
if (!isImage) {
|
||||
ElMessage.error('只能上传图片文件!')
|
||||
return false
|
||||
}
|
||||
if (!isLt5M) {
|
||||
ElMessage.error('上传图片大小不能超过 5MB!')
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 文件上传成功
|
||||
const uploadSuccess = (res, file) => {
|
||||
if (res.code === 0 && res.data.file) {
|
||||
uploadedFiles.value.push({
|
||||
name: file.name,
|
||||
url: res.data.file.url
|
||||
})
|
||||
ElMessage.success(`${file.name} 上传成功`)
|
||||
} else {
|
||||
ElMessage.error('上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 文件上传失败
|
||||
const uploadError = () => {
|
||||
ElMessage.error('上传失败')
|
||||
}
|
||||
|
||||
// 移除文件
|
||||
const removeFile = (file) => {
|
||||
const index = fileList.value.findIndex(item => item.uid === file.uid)
|
||||
if (index > -1) {
|
||||
uploadedFiles.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 移除已上传文件
|
||||
const removeUploadedFile = (index) => {
|
||||
uploadedFiles.value.splice(index, 1)
|
||||
// 同时移除fileList中对应的文件
|
||||
if (fileList.value[index]) {
|
||||
fileList.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
function submit(formRef) {
|
||||
if (!formRef) return
|
||||
|
||||
if (uploadedFiles.value.length === 0) {
|
||||
ElMessage.warning('请先上传文件')
|
||||
return
|
||||
}
|
||||
|
||||
// 校验
|
||||
formRef.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
submitLoading.value = true
|
||||
|
||||
try {
|
||||
// 处理价格字段:用户输入元,接口需要分
|
||||
let price = 0
|
||||
if (formData.value.isFree === 0 && formData.value.price) {
|
||||
price = Math.round(Number(formData.value.price) * 100)
|
||||
}
|
||||
|
||||
const submitData = {
|
||||
files: uploadedFiles.value.map(file => file.url),
|
||||
title: formData.value.title,
|
||||
desc: formData.value.desc,
|
||||
price: price,
|
||||
categoryId: formData.value.categoryId,
|
||||
publishTime: formData.value.publishTime || '',
|
||||
isFree: formData.value.isFree
|
||||
}
|
||||
|
||||
const res = await bulkUpload(submitData)
|
||||
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('批量上传成功!')
|
||||
router.back()
|
||||
} else if (res.code === 5) {
|
||||
// 部分文件上传失败,处理失败的文件
|
||||
handlePartialFailure(res)
|
||||
} else {
|
||||
ElMessage.error(res.msg || '批量上传失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量上传失败:', error)
|
||||
ElMessage.error('批量上传失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 处理部分上传失败的情况
|
||||
function handlePartialFailure(res) {
|
||||
try {
|
||||
// 解析失败的文件URL列表
|
||||
const failedUrls = res.data.replace(/[[\]]/g, '').split(' ').filter(url => url.trim())
|
||||
|
||||
if (failedUrls.length === 0) {
|
||||
ElMessage.error('解析失败文件列表出错')
|
||||
return
|
||||
}
|
||||
|
||||
// 从已上传文件中移除成功的文件,只保留失败的文件
|
||||
const originalCount = uploadedFiles.value.length
|
||||
uploadedFiles.value = uploadedFiles.value.filter(file =>
|
||||
failedUrls.includes(file.url)
|
||||
)
|
||||
|
||||
// 同时更新fileList
|
||||
fileList.value = fileList.value.filter(file =>
|
||||
failedUrls.some(url => file.url === url)
|
||||
)
|
||||
|
||||
const successCount = originalCount - failedUrls.length
|
||||
const failedCount = failedUrls.length
|
||||
|
||||
ElMessage.warning({
|
||||
message: `部分文件上传失败!成功:${successCount}个,失败:${failedCount}个。请重新上传失败的文件。`,
|
||||
duration: 5000
|
||||
})
|
||||
|
||||
// 显示失败文件的详细信息
|
||||
ElMessageBox.alert(
|
||||
`失败的文件:\n${failedUrls.join('\n')}`,
|
||||
'上传失败详情',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
} catch (error) {
|
||||
console.error('处理部分失败时出错:', error)
|
||||
ElMessage.error('处理上传结果时出错')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.container-wrapper {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.upload-dragger {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 4px;
|
||||
background: #fafafa;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.file-preview {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.file-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.file-name {
|
||||
font-size: 12px;
|
||||
color: #606266;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.file-url {
|
||||
font-size: 10px;
|
||||
color: #909399;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.remove-btn {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.footer-box {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
margin-top: 20px;
|
||||
padding: 20px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
:deep(.el-row) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
:deep(.el-form) {
|
||||
flex: 1;
|
||||
background: white;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.el-form-item) {
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
font-weight: 500;
|
||||
color: #606266;
|
||||
}
|
||||
|
||||
:deep(.el-upload-dragger) {
|
||||
width: 100%;
|
||||
height: 120px;
|
||||
}
|
||||
</style>
|
@@ -23,7 +23,7 @@
|
||||
:config="ARTICLE_TABLE_CONFIG"
|
||||
>
|
||||
<template #status="{ row }">
|
||||
<el-tag :type="tag(row.status).extend">{{ tag(row.status).label }}</el-tag>
|
||||
<el-tag :type="getStatusType(row.status)">{{ getStatusText(row.status) }}</el-tag>
|
||||
</template>
|
||||
<template #coverImg="{ row }">
|
||||
<el-image
|
||||
@@ -82,6 +82,9 @@
|
||||
/>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="文章标题">{{ detailData.title || EMPTY_STR }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="getStatusType(detailData.status)">{{ getStatusText(detailData.status) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="是否免费">
|
||||
<el-tag :type="detailData.isFree === 1 ? 'success' : 'warning'">
|
||||
{{ detailData.isFree === 1 ? '免费' : '付费' }}
|
||||
@@ -203,13 +206,24 @@
|
||||
return (price / 100).toFixed(2) + '元'
|
||||
}
|
||||
|
||||
// 状态标签
|
||||
const tag = (status) => {
|
||||
const map = {
|
||||
1: { extend: 'success', label: '已发布' },
|
||||
0: { extend: 'info', label: '草稿' }
|
||||
// 获取状态文本
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
1: '已发布',
|
||||
2: '待发布',
|
||||
3: '审核未通过'
|
||||
}
|
||||
return map[status] || { extend: '', label: EMPTY_STR }
|
||||
return statusMap[status] || EMPTY_STR
|
||||
}
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusType = (status) => {
|
||||
const typeMap = {
|
||||
1: 'success', // 已发布 - 绿色
|
||||
2: 'warning', // 待发布 - 橙色
|
||||
3: 'danger' // 审核未通过 - 红色
|
||||
}
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
const searchData = (data) => {
|
||||
@@ -387,7 +401,7 @@
|
||||
}
|
||||
}
|
||||
function changePage(data) {
|
||||
queryParams.value.pageNum = data
|
||||
queryParams.value.page = data
|
||||
getList()
|
||||
}
|
||||
</script>
|
||||
|
476
src/view/goods/teacher_vip/index.vue
Normal file
476
src/view/goods/teacher_vip/index.vue
Normal file
@@ -0,0 +1,476 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="gva-search-box">
|
||||
<el-form :inline="true" :model="searchInfo" class="demo-form-inline" @keyup.enter="onSubmit">
|
||||
<el-form-item label="讲师">
|
||||
<div style="display:flex;gap:8px;align-items:center">
|
||||
<el-input v-model="searchInfo.teacher_name" placeholder="请选择讲师" clearable style="width: 220px" readonly />
|
||||
<el-button type="primary" plain @click="openTeacherChoose('search')">选择讲师</el-button>
|
||||
<el-button @click="clearTeacher('search')">清空</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" icon="search" @click="onSubmit">查询</el-button>
|
||||
<el-button icon="refresh" @click="onReset">重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list">
|
||||
<el-button type="primary" icon="plus" @click="openEdit('create')">新增</el-button>
|
||||
<el-button type="warning" icon="edit" @click="openBatchEdit" :disabled="selectedRows.length === 0">
|
||||
批量编辑价格 ({{ selectedRows.length }})
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-table ref="multipleTable" style="width: 100%" tooltip-effect="dark" :data="tableData" row-key="ID"
|
||||
@selection-change="handleSelectionChange">
|
||||
<el-table-column type="selection" width="55" />
|
||||
<el-table-column type="index" label="#" width="60" />
|
||||
<el-table-column align="left" label="ID" prop="ID" width="100" />
|
||||
<el-table-column align="left" label="标题" prop="title" min-width="160" />
|
||||
<el-table-column align="left" label="讲师ID" prop="teacher_id" width="110" />
|
||||
<el-table-column align="left" label="讲师" prop="teacher_name" min-width="140" />
|
||||
<el-table-column align="left" label="头像" width="120">
|
||||
<template #default="scope">
|
||||
<el-image v-if="scope.row.avatar" style="width: 60px; height: 60px" :src="scope.row.avatar"
|
||||
:preview-src-list="[scope.row.avatar]" fit="cover" />
|
||||
<span v-else>无</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="价格" prop="price" width="120">
|
||||
<template #default="{ row }">
|
||||
{{ formatPrice(row.price) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="更新时间" prop="UpdatedAt" width="180">
|
||||
<template #default="{ row }">{{ formatDate(row.UpdatedAt) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="left" label="描述" prop="desc" min-width="200" show-overflow-tooltip />
|
||||
<el-table-column align="left" label="操作" fixed="right" width="160">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="openEdit('update', row)">编辑</el-button>
|
||||
<el-button type="danger" link @click="handleDelete(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="gva-pagination">
|
||||
<el-pagination layout="total, sizes, prev, pager, next, jumper" :current-page="page" :page-size="pageSize"
|
||||
:page-sizes="[10, 30, 50, 100]" :total="total" @current-change="handleCurrentChange"
|
||||
@size-change="handleSizeChange" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 选择讲师弹窗 -->
|
||||
<userChoose ref="userChooseRef" title="选择讲师人员" @getRecipientInfo="onChooseTeacher" />
|
||||
|
||||
<!-- 批量编辑价格弹窗 -->
|
||||
<el-dialog v-model="batchEditVisible" title="批量编辑价格" width="600px" :before-close="closeBatchEdit">
|
||||
<div style="margin-bottom: 20px;">
|
||||
<el-alert :title="`已选择 ${selectedRows.length} 个讲师包月服务`" type="info" show-icon :closable="false" />
|
||||
</div>
|
||||
|
||||
<div class="batch-edit-content">
|
||||
<el-form :model="batchForm" ref="batchFormRef" :rules="batchRules" label-width="120px">
|
||||
<el-form-item label="统一价格(元)" prop="adjustValue">
|
||||
<el-input-number v-model="batchForm.adjustValue" :precision="2" :step="0.01" :min="0.01"
|
||||
placeholder="请输入统一设置的价格" style="width: 200px" />
|
||||
<span style="margin-left: 8px; color: #666;">元</span>
|
||||
</el-form-item>
|
||||
<div style="font-size: 12px; color: #999; margin-top: -10px; margin-bottom: 20px;">
|
||||
所有选中的讲师包月服务将被设置为相同的价格
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<div style="margin-top: 20px;">
|
||||
<h4>预览效果:</h4>
|
||||
<el-table :data="previewData" style="width: 100%" max-height="300">
|
||||
<el-table-column prop="title" label="标题" min-width="150" />
|
||||
<el-table-column prop="teacher_name" label="讲师" width="100" />
|
||||
<el-table-column label="原价格" width="100">
|
||||
<template #default="{ row }">
|
||||
<span style="color: #909399;">{{ formatPrice(row.originalPrice) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="新价格" width="100">
|
||||
<template #default="{ row }">
|
||||
<span style="color: #67c23a; font-weight: bold;">{{ formatPrice(row.newPrice) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="变化" width="100">
|
||||
<template #default="{ row }">
|
||||
<span :style="{ color: row.change >= 0 ? '#67c23a' : '#f56c6c' }">
|
||||
{{ row.change >= 0 ? '+' : '' }}{{ formatPrice(row.change) }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="closeBatchEdit">取消</el-button>
|
||||
<el-button type="primary" @click="submitBatchEdit" :loading="batchLoading">
|
||||
确定更新 ({{ selectedRows.length }} 项)
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 新增/编辑 抽屉 -->
|
||||
<el-drawer destroy-on-close :size="'520px'" v-model="dialogVisible" :show-close="false" :before-close="closeDialog">
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">{{ editType === 'create' ? '新增讲师包月' : '编辑讲师包月' }}</span>
|
||||
<div>
|
||||
<el-button :loading="btnLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="closeDialog">取 消</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<el-form :model="formData" ref="formRef" label-position="top" :rules="rules">
|
||||
<el-form-item label="标题" prop="title">
|
||||
<el-input v-model="formData.title" placeholder="请输入标题" />
|
||||
</el-form-item>
|
||||
<el-form-item label="讲师" prop="teacher_id">
|
||||
<div style="display:flex;gap:8px;align-items:center;width:100%">
|
||||
<el-input v-model="formData.teacher_name" placeholder="请选择讲师" readonly />
|
||||
<el-button type="primary" plain @click="openTeacherChoose('form')">选择讲师</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="价格(元)" prop="price">
|
||||
<el-input v-model.number="formData.price" type="number" min="0" placeholder="请输入价格" />
|
||||
</el-form-item>
|
||||
<el-form-item label="描述" prop="desc">
|
||||
<el-input v-model="formData.desc" type="textarea" :rows="4" placeholder="请输入描述" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { list as getTeacherVipList, add as createTeacherVip, edit as updateTeacherVip, del as deleteTeacherVip, detail as getDetail, batchUpdatePrice } from '@/api/goods/teacherVip'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
import { formatDate } from '@/utils/format'
|
||||
import userChoose from '@/components/userChoose/index.vue'
|
||||
|
||||
const page = ref(1)
|
||||
const total = ref(0)
|
||||
const pageSize = ref(10)
|
||||
const tableData = ref([])
|
||||
const searchInfo = ref({ teacher_id: undefined, teacher_name: '' })
|
||||
|
||||
// 批量编辑相关
|
||||
const selectedRows = ref([])
|
||||
const batchEditVisible = ref(false)
|
||||
const batchLoading = ref(false)
|
||||
const batchFormRef = ref(null)
|
||||
const batchForm = ref({
|
||||
adjustValue: 0
|
||||
})
|
||||
|
||||
const batchRules = {
|
||||
adjustValue: [
|
||||
{ required: true, message: '请输入统一价格', trigger: 'blur' },
|
||||
{ type: 'number', min: 0.01, message: '价格必须大于0', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 讲师选择
|
||||
const userChooseRef = ref()
|
||||
const chooseScene = ref('search')
|
||||
const openTeacherChoose = (scene) => {
|
||||
chooseScene.value = scene
|
||||
userChooseRef.value && userChooseRef.value.open()
|
||||
}
|
||||
const clearTeacher = (scene) => {
|
||||
if (scene === 'search') {
|
||||
searchInfo.value.teacher_id = undefined
|
||||
searchInfo.value.teacher_name = ''
|
||||
} else {
|
||||
formData.value.teacher_id = undefined
|
||||
formData.value.teacher_name = ''
|
||||
}
|
||||
}
|
||||
const onChooseTeacher = (data) => {
|
||||
const id = data.id || data.ID
|
||||
const name = data.nick_name || data.username || data.name
|
||||
if (chooseScene.value === 'search') {
|
||||
searchInfo.value.teacher_id = id
|
||||
searchInfo.value.teacher_name = name
|
||||
} else {
|
||||
formData.value.teacher_id = id
|
||||
formData.value.teacher_name = name
|
||||
}
|
||||
}
|
||||
|
||||
const formatPrice = (val) => {
|
||||
if (val === null || val === undefined) return '-'
|
||||
const yuan = Number(val) / 100
|
||||
return `${yuan.toFixed(2)} 元`
|
||||
}
|
||||
|
||||
const onReset = () => {
|
||||
searchInfo.value = { teacher_id: undefined }
|
||||
page.value = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const onSubmit = () => {
|
||||
page.value = 1
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleSizeChange = (val) => {
|
||||
pageSize.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const handleCurrentChange = (val) => {
|
||||
page.value = val
|
||||
getTableData()
|
||||
}
|
||||
|
||||
const getTableData = async () => {
|
||||
const params = { page: page.value, pageSize: pageSize.value }
|
||||
if (searchInfo.value.teacher_id !== undefined && searchInfo.value.teacher_id !== '' && searchInfo.value.teacher_id !== null) {
|
||||
params.teacher_id = searchInfo.value.teacher_id
|
||||
}
|
||||
const res = await getTeacherVipList(params)
|
||||
if (res.code === 0) {
|
||||
tableData.value = res.data.list || []
|
||||
total.value = res.data.total || 0
|
||||
page.value = res.data.page || page.value
|
||||
pageSize.value = res.data.pageSize || pageSize.value
|
||||
}
|
||||
}
|
||||
|
||||
getTableData()
|
||||
|
||||
// ============== 新增/编辑/删除 ==============
|
||||
const dialogVisible = ref(false)
|
||||
const formRef = ref()
|
||||
const btnLoading = ref(false)
|
||||
const editType = ref('create')
|
||||
const formData = ref({
|
||||
ID: undefined,
|
||||
title: '',
|
||||
teacher_id: undefined,
|
||||
teacher_name: '',
|
||||
price: 0,
|
||||
desc: ''
|
||||
})
|
||||
const rules = {
|
||||
title: [{ required: true, message: '请输入标题', trigger: ['blur', 'input'] }],
|
||||
teacher_id: [{ required: true, message: '请选择讲师', trigger: ['change', 'blur'] }],
|
||||
price: [{ required: true, message: '请输入价格', trigger: ['blur', 'input'] }]
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
formData.value = { ID: undefined, title: '', teacher_id: undefined, teacher_name: '', price: 0, desc: '' }
|
||||
}
|
||||
|
||||
const openEdit = async (type, row) => {
|
||||
editType.value = type
|
||||
if (type === 'update' && row?.ID) {
|
||||
const res = await getDetail({ id: row.ID })
|
||||
if (res.code === 0) {
|
||||
const data = { ...res.data }
|
||||
if (data.price !== null && data.price !== undefined) {
|
||||
data.price = Number(data.price) / 100
|
||||
}
|
||||
formData.value = { ...formData.value, ...data }
|
||||
}
|
||||
} else {
|
||||
resetForm()
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const closeDialog = () => {
|
||||
dialogVisible.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
const submitForm = () => {
|
||||
formRef.value?.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
btnLoading.value = true
|
||||
const payload = { ...formData.value }
|
||||
// 元 -> 分
|
||||
if (payload.price !== null && payload.price !== undefined) {
|
||||
payload.price = Math.round(Number(payload.price) * 100)
|
||||
}
|
||||
let res
|
||||
if (editType.value === 'create') {
|
||||
res = await createTeacherVip(payload)
|
||||
} else {
|
||||
res = await updateTeacherVip(payload)
|
||||
}
|
||||
btnLoading.value = false
|
||||
if (res && res.code === 0) {
|
||||
ElMessage.success('保存成功')
|
||||
closeDialog()
|
||||
getTableData()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = async (row) => {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该记录吗?', '提示', { type: 'warning' })
|
||||
const res = await deleteTeacherVip({ ID: row.ID })
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('删除成功')
|
||||
getTableData()
|
||||
}
|
||||
} catch { /* 用户取消 */ }
|
||||
}
|
||||
|
||||
// ============== 批量编辑相关 ==============
|
||||
const handleSelectionChange = (selection) => {
|
||||
selectedRows.value = selection
|
||||
}
|
||||
|
||||
const openBatchEdit = () => {
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要编辑的记录')
|
||||
return
|
||||
}
|
||||
batchForm.value = {
|
||||
adjustValue: 0
|
||||
}
|
||||
batchEditVisible.value = true
|
||||
}
|
||||
|
||||
const closeBatchEdit = () => {
|
||||
batchEditVisible.value = false
|
||||
batchFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 计算预览数据
|
||||
const previewData = computed(() => {
|
||||
if (!selectedRows.value.length || !batchForm.value.adjustValue) {
|
||||
return selectedRows.value.map(row => ({
|
||||
...row,
|
||||
originalPrice: row.price,
|
||||
newPrice: row.price,
|
||||
change: 0
|
||||
}))
|
||||
}
|
||||
|
||||
return selectedRows.value.map(row => {
|
||||
const originalPrice = row.price
|
||||
// 统一设置为固定价格(元转分)
|
||||
const newPrice = Math.round(batchForm.value.adjustValue * 100)
|
||||
|
||||
return {
|
||||
...row,
|
||||
originalPrice,
|
||||
newPrice: Math.max(0, newPrice),
|
||||
change: newPrice - originalPrice
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const submitBatchEdit = async () => {
|
||||
if (!batchFormRef.value) return
|
||||
|
||||
await batchFormRef.value.validate(async (valid) => {
|
||||
if (!valid) return
|
||||
|
||||
if (selectedRows.value.length === 0) {
|
||||
ElMessage.warning('请先选择要编辑的记录')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要将选中的 ${selectedRows.value.length} 个讲师包月服务的价格统一设置为 ${batchForm.value.adjustValue} 元吗?`,
|
||||
'批量更新确认',
|
||||
{ type: 'warning' }
|
||||
)
|
||||
|
||||
batchLoading.value = true
|
||||
|
||||
// 准备批量更新数据 - 根据新的接口格式
|
||||
const ids = selectedRows.value.map(item => item.ID)
|
||||
const price = Math.round(batchForm.value.adjustValue * 100) // 统一价格(元转分)
|
||||
|
||||
const res = await batchUpdatePrice({
|
||||
ids: ids,
|
||||
price: price
|
||||
})
|
||||
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('批量更新成功')
|
||||
closeBatchEdit()
|
||||
selectedRows.value = []
|
||||
getTableData()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '批量更新失败')
|
||||
}
|
||||
} catch (error) {
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('批量更新失败')
|
||||
}
|
||||
} finally {
|
||||
batchLoading.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.batch-edit-content {
|
||||
.el-form-item {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.el-radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.el-radio {
|
||||
margin-right: 0;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.gva-btn-list {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.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-pagination {
|
||||
margin-top: 20px;
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
@@ -12,7 +12,7 @@
|
||||
<div class="gva-btn-list">
|
||||
<el-button type="info" @click="exportOrders">导出订单</el-button>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- 订单统计信息 -->
|
||||
<div style="margin-bottom: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px;">
|
||||
<el-row :gutter="20">
|
||||
@@ -66,7 +66,8 @@
|
||||
<el-table-column type="index" label="序号" width="60" align="center" />
|
||||
<el-table-column prop="order_no" label="订单号" align="center" min-width="180" />
|
||||
<el-table-column prop="title" label="商品名称" align="center" min-width="150" />
|
||||
<el-table-column prop="name" label="用户姓名" align="center" width="100" />
|
||||
<el-table-column prop="name" label="用户姓名" align="center" width="100" />、
|
||||
<el-table-column prop="user_id" label="用户ID" align="center" width="100" />
|
||||
<el-table-column label="价格" align="center" width="100">
|
||||
<template #default="{ row }">
|
||||
<span class="text-green-600 font-bold">{{ formatPrice(row.price) }}</span>
|
||||
@@ -106,7 +107,7 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
|
||||
<!-- 分页 -->
|
||||
<div style="margin-top: 20px; text-align: right;">
|
||||
<el-pagination
|
||||
@@ -308,7 +309,7 @@ function getOrderStats() {
|
||||
.gva-btn-list {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
|
||||
.el-row {
|
||||
.el-col {
|
||||
.el-statistic {
|
||||
@@ -317,12 +318,12 @@ function getOrderStats() {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
|
||||
|
||||
.el-statistic__number {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
.el-statistic__label {
|
||||
color: #666;
|
||||
margin-top: 8px;
|
||||
|
@@ -53,7 +53,7 @@
|
||||
|
||||
<!--user_label 1: 普通用户 2: VIP 3:SVIP-->
|
||||
<template #user_label="{ row }">
|
||||
{{ row.user_label === 1 ? '注册会员' : row.user_label === 2 ? 'VIP' : 'SVIP' }}
|
||||
{{ row.user_label === 1 ? '注册会员' : row.user_label === 2 ? 'VIP' : row.user_label === 3 ? 'SVIP':row.user_label === 4 ? '过期VIP':'未知' }}
|
||||
</template>
|
||||
|
||||
<!--balance 账户余额-->
|
||||
@@ -74,7 +74,7 @@
|
||||
<el-dialog
|
||||
v-model="balanceDialogVisible"
|
||||
title="修改余额"
|
||||
width="400px"
|
||||
width="450px"
|
||||
:before-close="handleBalanceClose"
|
||||
>
|
||||
<el-form
|
||||
@@ -84,17 +84,30 @@
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="当前余额">
|
||||
<span>{{ currentUser.balance }}</span>
|
||||
<span style="font-weight: bold; color: #409eff;">¥{{ currentUser.balance }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="新余额" prop="balance">
|
||||
<el-form-item label="操作类型" prop="change_type">
|
||||
<el-select v-model="balanceForm.change_type" placeholder="请选择操作类型" style="width: 100%">
|
||||
<el-option label="增加余额" :value="1" />
|
||||
<el-option label="减少余额" :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="balanceForm.change_type === 1 ? '增加金额' : '减少金额'" prop="balance">
|
||||
<el-input-number
|
||||
v-model="balanceForm.balance"
|
||||
:precision="2"
|
||||
:step="0.01"
|
||||
:min="0"
|
||||
placeholder="请输入新余额"
|
||||
:min="0.01"
|
||||
:max="balanceForm.change_type === 2 ? currentUser.balance : 999999"
|
||||
:placeholder="balanceForm.change_type === 1 ? '请输入要增加的金额' : '请输入要减少的金额'"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作后余额">
|
||||
<span style="font-weight: bold; color: #67c23a;">
|
||||
¥{{ getNewBalance() }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
@@ -118,13 +131,14 @@
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="当前标签">
|
||||
<span>{{ currentUser.user_label === 1 ? '注册会员' : currentUser.user_label === 2 ? 'VIP' : 'SVIP' }}</span>
|
||||
<span>{{ currentUser.user_label === 1 ? '注册会员' : currentUser.user_label === 2 ? 'VIP' : currentUser.user_label === 3 ? 'SVIP' : currentUser.user_label === 4 ? '过期VIP' :"未知" }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="VIP类型" prop="user_label">
|
||||
<el-select v-model="vipForm.user_label" placeholder="请选择VIP类型">
|
||||
<el-option label="注册会员" :value="1" />
|
||||
<el-option label="VIP" :value="2" />
|
||||
<el-option label="SVIP" :value="3" />
|
||||
<el-option label="过期VIP" :value="4" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="到期时间" prop="vip_expire_time">
|
||||
@@ -168,7 +182,7 @@
|
||||
{{ userDetail.user_type === 1 ? '普通用户' : '讲师' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="用户标签">
|
||||
{{ userDetail.user_label === 1 ? '注册会员' : userDetail.user_label === 2 ? 'VIP' : 'SVIP' }}
|
||||
{{ userDetail.user_label === 1 ? '注册会员' : userDetail.user_label === 2 ? 'VIP' : userDetail.user_label === 3 ? 'SVIP' : userDetail.user_label === 4 ? '过期VIP':"未知"}}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="账户余额">{{ userDetail.balance }}</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
@@ -210,6 +224,7 @@
|
||||
<el-option label="注册会员" :value="1" />
|
||||
<el-option label="VIP" :value="2" />
|
||||
<el-option label="SVIP" :value="3" />
|
||||
<el-option label="过期VIP用户" :value="4" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="昵称" prop="nick_name">
|
||||
@@ -362,14 +377,18 @@
|
||||
const balanceFormRef = ref(null)
|
||||
const currentUser = ref({})
|
||||
const balanceForm = ref({
|
||||
ID: '',
|
||||
balance: 0
|
||||
id: '',
|
||||
balance: 0,
|
||||
change_type: 1
|
||||
})
|
||||
|
||||
const balanceRules = {
|
||||
change_type: [
|
||||
{ required: true, message: '请选择操作类型', trigger: 'change' }
|
||||
],
|
||||
balance: [
|
||||
{ required: true, message: '请输入余额', trigger: 'blur' },
|
||||
{ type: 'number', min: 0, message: '余额不能小于0', trigger: 'blur' }
|
||||
{ required: true, message: '请输入金额', trigger: 'blur' },
|
||||
{ type: 'number', min: 0.01, message: '金额必须大于0', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -393,12 +412,31 @@
|
||||
function handleBalance(row) {
|
||||
currentUser.value = row
|
||||
balanceForm.value = {
|
||||
ID: row.ID,
|
||||
balance: row.balance
|
||||
id: row.ID,
|
||||
balance: 0,
|
||||
change_type: 1
|
||||
}
|
||||
balanceDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 计算操作后的新余额
|
||||
function getNewBalance() {
|
||||
if (!balanceForm.value.balance || !currentUser.value.balance) {
|
||||
return currentUser.value.balance || 0
|
||||
}
|
||||
|
||||
const currentBalance = parseFloat(currentUser.value.balance)
|
||||
const changeAmount = parseFloat(balanceForm.value.balance)
|
||||
|
||||
if (balanceForm.value.change_type === 1) {
|
||||
// 增加
|
||||
return (currentBalance + changeAmount).toFixed(2)
|
||||
} else {
|
||||
// 减少
|
||||
return Math.max(0, currentBalance - changeAmount).toFixed(2)
|
||||
}
|
||||
}
|
||||
|
||||
function handleBalanceClose() {
|
||||
balanceDialogVisible.value = false
|
||||
balanceFormRef.value?.resetFields()
|
||||
@@ -406,6 +444,18 @@
|
||||
|
||||
async function submitBalance() {
|
||||
if (!balanceFormRef.value) return
|
||||
|
||||
// 额外验证:减少余额时不能超过当前余额
|
||||
if (balanceForm.value.change_type === 2) {
|
||||
const currentBalance = parseFloat(currentUser.value.balance)
|
||||
const reduceAmount = parseFloat(balanceForm.value.balance)
|
||||
|
||||
if (reduceAmount > currentBalance) {
|
||||
ElMessage.error('减少金额不能超过当前余额')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
await balanceFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
const res = await setBalance(balanceForm.value)
|
||||
|
106
src/view/user/user/loginLog.vue
Normal file
106
src/view/user/user/loginLog.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="searchForm">
|
||||
<searchForm
|
||||
:search="LOGIN_LOG_SEARCH_CONFIG"
|
||||
@searchData="searchData"
|
||||
@resetData="resetData"
|
||||
class="search-box searchForm"
|
||||
/>
|
||||
</div>
|
||||
<div class="gva-table-box">
|
||||
<Content
|
||||
@changePage="changePage"
|
||||
:total="total"
|
||||
v-model:tabloading="tableLoading"
|
||||
v-model:currentPage="queryParams.page"
|
||||
v-model:pageSize="queryParams.pageSize"
|
||||
:data="tableData"
|
||||
:config="LOGIN_LOG_TABLE_CONFIG"
|
||||
>
|
||||
<template #login_time="{ row }">
|
||||
{{ row.login_time }}
|
||||
</template>
|
||||
</Content>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElMessage } from 'element-plus'
|
||||
import searchForm from '@/components/searchForm/index.vue'
|
||||
import Content from '@/components/content/index.vue'
|
||||
import { getLoginInfo } from '@/api/user/index.js'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { LOGIN_LOG_SEARCH_CONFIG, LOGIN_LOG_TABLE_CONFIG } from '@/config'
|
||||
|
||||
const tableLoading = ref(true)
|
||||
const tableData = ref([])
|
||||
const queryParams = ref({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
user_id: '',
|
||||
user_name: ''
|
||||
})
|
||||
const total = ref(0)
|
||||
|
||||
// 搜索数据
|
||||
const searchData = (data) => {
|
||||
queryParams.value.page = 1
|
||||
queryParams.value = { ...queryParams.value, ...data }
|
||||
getList()
|
||||
}
|
||||
|
||||
// 重置数据
|
||||
const resetData = () => {
|
||||
queryParams.value = {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
user_id: '',
|
||||
user_name: ''
|
||||
}
|
||||
getList()
|
||||
}
|
||||
|
||||
// 获取登录日志列表
|
||||
async function getList() {
|
||||
try {
|
||||
tableLoading.value = true
|
||||
const res = await getLoginInfo(queryParams.value)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
function changePage(data) {
|
||||
queryParams.value.page = data
|
||||
getList()
|
||||
}
|
||||
|
||||
// 页面加载时获取数据
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.searchForm {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.gva-table-box {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
864
src/view/user/user/teacherManger.vue
Normal file
864
src/view/user/user/teacherManger.vue
Normal file
@@ -0,0 +1,864 @@
|
||||
<template>
|
||||
<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.name" placeholder="请输入讲师昵称" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="手机号">
|
||||
<el-input v-model="searchForm.phone" 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="0" />
|
||||
</el-select>
|
||||
</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 active">
|
||||
<div class="stat-value">{{ stats.active }}</div>
|
||||
<div class="stat-label">启用讲师</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-item vip">
|
||||
<div class="stat-value">{{ stats.vip }}</div>
|
||||
<div class="stat-label">VIP讲师</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-item disabled">
|
||||
<div class="stat-value">{{ stats.disabled }}</div>
|
||||
<div class="stat-label">禁用讲师</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<!-- 表格区域 -->
|
||||
<div class="gva-table-box">
|
||||
<div class="gva-btn-list">
|
||||
<el-button type="primary" @click="handleAdd">新增讲师</el-button>
|
||||
</div>
|
||||
|
||||
<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="avatar" label="头像" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-avatar
|
||||
:size="50"
|
||||
:src="row.avatar"
|
||||
fit="cover"
|
||||
>
|
||||
<img src="https://cube.elemecdn.com/e/fd/0fc7d20532fdaf769a25683617711png.png" />
|
||||
</el-avatar>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="nick_name" label="讲师昵称" min-width="120" />
|
||||
<el-table-column prop="phone" label="手机号" width="130">
|
||||
<template #default="{ row }">
|
||||
{{ row.phone || '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="balance" label="账户余额" width="120">
|
||||
<template #default="{ row }">
|
||||
<span style="color: #67c23a; font-weight: bold;">¥{{ row.balance }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="weight" label="权重" width="80">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getWeightTagType(row.weight)">
|
||||
{{ row.weight }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="expect_rate" label="分成比例" width="100">
|
||||
<template #default="{ row }">
|
||||
<span style="color: #409eff; font-weight: bold;">{{ row.expect_rate }}%</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="is_vip" label="VIP状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="row.is_vip ? 'warning' : 'info'">
|
||||
{{ row.is_vip ? 'VIP讲师' : '普通讲师' }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
v-model="row.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
:loading="row.loading"
|
||||
:before-change="() => statusChangeBefore(row)"
|
||||
/>
|
||||
</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="200" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button type="text" @click="handleDetail(row)">详情</el-button>
|
||||
<el-button type="text" @click="handleEditWeight(row)">设置权重</el-button>
|
||||
<el-button type="text" @click="handleEditRate(row)">设置分成</el-button>
|
||||
<el-button type="text" @click="handleBalance(row)">修改余额</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="detailDialogVisible"
|
||||
title="讲师详情"
|
||||
width="600px"
|
||||
:before-close="handleDetailClose"
|
||||
>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="讲师头像">
|
||||
<el-avatar
|
||||
:size="80"
|
||||
:src="currentTeacher.avatar"
|
||||
fit="cover"
|
||||
/>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="讲师ID">{{ currentTeacher.ID }}</el-descriptions-item>
|
||||
<el-descriptions-item label="讲师昵称">{{ currentTeacher.nick_name }}</el-descriptions-item>
|
||||
<el-descriptions-item label="手机号">{{ currentTeacher.phone || '-' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="账户余额">
|
||||
<span style="color: #67c23a; font-weight: bold;">¥{{ currentTeacher.balance }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="权重">{{ currentTeacher.weight }}</el-descriptions-item>
|
||||
<el-descriptions-item label="分成比例">{{ currentTeacher.expect_rate }}%</el-descriptions-item>
|
||||
<el-descriptions-item label="VIP状态">
|
||||
<el-tag :type="currentTeacher.is_vip ? 'warning' : 'info'">
|
||||
{{ currentTeacher.is_vip ? 'VIP讲师' : '普通讲师' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="currentTeacher.status ? 'success' : 'danger'">
|
||||
{{ currentTeacher.status ? '启用' : '禁用' }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="注册时间">{{ formatDate(currentTeacher.CreatedAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="个人简介" :span="2">
|
||||
{{ currentTeacher.des || '暂无简介' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 设置权重弹窗 -->
|
||||
<el-dialog
|
||||
v-model="weightDialogVisible"
|
||||
title="设置讲师权重"
|
||||
width="400px"
|
||||
:before-close="handleWeightClose"
|
||||
>
|
||||
<el-form
|
||||
ref="weightFormRef"
|
||||
:model="weightForm"
|
||||
:rules="weightRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="讲师昵称">
|
||||
<span>{{ currentTeacher.nick_name }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="当前权重">
|
||||
<span>{{ currentTeacher.weight }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="新权重" prop="weight">
|
||||
<el-input-number
|
||||
v-model="weightForm.weight"
|
||||
:min="0"
|
||||
:max="100"
|
||||
placeholder="请输入权重值"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div style="font-size: 12px; color: #999; margin-top: 5px;">
|
||||
权重越高,推荐优先级越高(0-100)
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleWeightClose">取消</el-button>
|
||||
<el-button type="primary" @click="submitWeight">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 设置分成比例弹窗 -->
|
||||
<el-dialog
|
||||
v-model="rateDialogVisible"
|
||||
title="设置分成比例"
|
||||
width="400px"
|
||||
:before-close="handleRateClose"
|
||||
>
|
||||
<el-form
|
||||
ref="rateFormRef"
|
||||
:model="rateForm"
|
||||
:rules="rateRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="讲师昵称">
|
||||
<span>{{ currentTeacher.nick_name }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="当前分成">
|
||||
<span>{{ currentTeacher.expect_rate }}%</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="新分成比例" prop="expect_rate">
|
||||
<el-input-number
|
||||
v-model="rateForm.expect_rate"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:precision="2"
|
||||
placeholder="请输入分成比例"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div style="font-size: 12px; color: #999; margin-top: 5px;">
|
||||
讲师从课程销售中获得的分成比例(0-100%)
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleRateClose">取消</el-button>
|
||||
<el-button type="primary" @click="submitRate">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 修改余额弹窗 -->
|
||||
<el-dialog
|
||||
v-model="balanceDialogVisible"
|
||||
title="修改余额"
|
||||
width="450px"
|
||||
:before-close="handleBalanceClose"
|
||||
>
|
||||
<el-form
|
||||
ref="balanceFormRef"
|
||||
:model="balanceForm"
|
||||
:rules="balanceRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="当前余额">
|
||||
<span style="font-weight: bold; color: #409eff;">¥{{ currentTeacher.balance }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作类型" prop="change_type">
|
||||
<el-select v-model="balanceForm.change_type" placeholder="请选择操作类型" style="width: 100%">
|
||||
<el-option label="增加余额" :value="1" />
|
||||
<el-option label="减少余额" :value="2" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="balanceForm.change_type === 1 ? '增加金额' : '减少金额'" prop="balance">
|
||||
<el-input-number
|
||||
v-model="balanceForm.balance"
|
||||
:precision="2"
|
||||
:step="0.01"
|
||||
:min="0.01"
|
||||
:max="balanceForm.change_type === 2 ? currentTeacher.balance : 999999"
|
||||
:placeholder="balanceForm.change_type === 1 ? '请输入要增加的金额' : '请输入要减少的金额'"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="操作后余额">
|
||||
<span style="font-weight: bold; color: #67c23a;">
|
||||
¥{{ getNewBalance() }}
|
||||
</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleBalanceClose">取消</el-button>
|
||||
<el-button type="primary" @click="submitBalance">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 新增讲师弹窗 -->
|
||||
<el-dialog
|
||||
v-model="addDialogVisible"
|
||||
title="新增讲师"
|
||||
width="500px"
|
||||
:before-close="handleAddClose"
|
||||
>
|
||||
<el-form
|
||||
ref="addFormRef"
|
||||
:model="addForm"
|
||||
:rules="addRules"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="手机号" prop="phone">
|
||||
<el-input v-model="addForm.phone" placeholder="请输入手机号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input v-model="addForm.password" type="password" placeholder="请输入密码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="讲师昵称" prop="nick_name">
|
||||
<el-input v-model="addForm.nick_name" placeholder="请输入讲师昵称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="权重" prop="weight">
|
||||
<el-input-number
|
||||
v-model="addForm.weight"
|
||||
:min="0"
|
||||
:max="100"
|
||||
placeholder="请输入权重"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="分成比例" prop="expect_rate">
|
||||
<el-input-number
|
||||
v-model="addForm.expect_rate"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:precision="2"
|
||||
placeholder="请输入分成比例"
|
||||
style="width: 100%"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleAddClose">取消</el-button>
|
||||
<el-button type="primary" @click="submitAdd">确定</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { list, status, register, setBalance, setTeacherWeight, setTeacherRate } from '@/api/user/index.js'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'TeacherManagement'
|
||||
})
|
||||
|
||||
// 表格相关数据
|
||||
const tableLoading = ref(false)
|
||||
const tableData = ref([])
|
||||
const queryParams = ref({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
type: 2 // 固定为讲师类型
|
||||
})
|
||||
const total = ref(0)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
name: '',
|
||||
phone: '',
|
||||
status: '',
|
||||
is_vip: ''
|
||||
})
|
||||
|
||||
// 统计数据
|
||||
const stats = computed(() => {
|
||||
const active = tableData.value.filter(item => item.status === 1).length
|
||||
const disabled = tableData.value.filter(item => item.status === 0).length
|
||||
const vip = tableData.value.filter(item => item.is_vip === 1).length
|
||||
|
||||
return {
|
||||
total: total.value,
|
||||
active,
|
||||
disabled,
|
||||
vip
|
||||
}
|
||||
})
|
||||
|
||||
// 当前选中的讲师
|
||||
const currentTeacher = ref({})
|
||||
|
||||
// 详情弹窗
|
||||
const detailDialogVisible = ref(false)
|
||||
|
||||
// 权重设置弹窗
|
||||
const weightDialogVisible = ref(false)
|
||||
const weightFormRef = ref(null)
|
||||
const weightForm = ref({
|
||||
id: '',
|
||||
weight: 0
|
||||
})
|
||||
|
||||
// 分成比例设置弹窗
|
||||
const rateDialogVisible = ref(false)
|
||||
const rateFormRef = ref(null)
|
||||
const rateForm = ref({
|
||||
id: '',
|
||||
expect_rate: 0
|
||||
})
|
||||
|
||||
// 余额修改弹窗
|
||||
const balanceDialogVisible = ref(false)
|
||||
const balanceFormRef = ref(null)
|
||||
const balanceForm = ref({
|
||||
id: '',
|
||||
balance: 0,
|
||||
change_type: 1
|
||||
})
|
||||
|
||||
// 新增讲师弹窗
|
||||
const addDialogVisible = ref(false)
|
||||
const addFormRef = ref(null)
|
||||
const addForm = ref({
|
||||
phone: '',
|
||||
password: '',
|
||||
nick_name: '',
|
||||
user_type: 2,
|
||||
user_label: 1,
|
||||
weight: 0,
|
||||
expect_rate: 0
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const weightRules = {
|
||||
weight: [
|
||||
{ required: true, message: '请输入权重值', trigger: 'blur' },
|
||||
{ type: 'number', min: 0, max: 100, message: '权重值必须在0-100之间', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const rateRules = {
|
||||
expect_rate: [
|
||||
{ required: true, message: '请输入分成比例', trigger: 'blur' },
|
||||
{ type: 'number', min: 0, max: 100, message: '分成比例必须在0-100之间', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const balanceRules = {
|
||||
change_type: [
|
||||
{ required: true, message: '请选择操作类型', trigger: 'change' }
|
||||
],
|
||||
balance: [
|
||||
{ required: true, message: '请输入金额', trigger: 'blur' },
|
||||
{ type: 'number', min: 0.01, message: '金额必须大于0', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
const addRules = {
|
||||
phone: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
{ min: 6, message: '密码长度不能小于6位', trigger: 'blur' }
|
||||
],
|
||||
nick_name: [
|
||||
{ required: true, message: '请输入讲师昵称', trigger: 'blur' }
|
||||
],
|
||||
weight: [
|
||||
{ required: true, message: '请输入权重', trigger: 'blur' }
|
||||
],
|
||||
expect_rate: [
|
||||
{ required: true, message: '请输入分成比例', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '-'
|
||||
return new Date(dateStr).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 获取权重标签类型
|
||||
const getWeightTagType = (weight) => {
|
||||
if (weight >= 80) return 'danger'
|
||||
if (weight >= 50) return 'warning'
|
||||
if (weight >= 20) return 'primary'
|
||||
return 'info'
|
||||
}
|
||||
|
||||
// 计算操作后的新余额
|
||||
const getNewBalance = () => {
|
||||
if (!balanceForm.value.balance || !currentTeacher.value.balance) {
|
||||
return currentTeacher.value.balance || 0
|
||||
}
|
||||
|
||||
const currentBalance = parseFloat(currentTeacher.value.balance)
|
||||
const changeAmount = parseFloat(balanceForm.value.balance)
|
||||
|
||||
if (balanceForm.value.change_type === 1) {
|
||||
return (currentBalance + changeAmount).toFixed(2)
|
||||
} else {
|
||||
return Math.max(0, currentBalance - changeAmount).toFixed(2)
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索数据
|
||||
const searchData = () => {
|
||||
queryParams.value.page = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetData = () => {
|
||||
searchForm.value = {
|
||||
name: '',
|
||||
phone: '',
|
||||
status: '',
|
||||
is_vip: ''
|
||||
}
|
||||
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 list(params)
|
||||
if (res.code === 0) {
|
||||
tableData.value = res.data.list || []
|
||||
// 为每个讲师添加loading状态
|
||||
tableData.value.forEach(item => {
|
||||
item.loading = false
|
||||
})
|
||||
total.value = res.data.total || 0
|
||||
} else {
|
||||
ElMessage.error(res.msg || '获取数据失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取讲师列表失败:', error)
|
||||
ElMessage.error('获取数据失败')
|
||||
} finally {
|
||||
tableLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 状态切换
|
||||
async function statusChangeBefore(row) {
|
||||
row.loading = true
|
||||
try {
|
||||
const params = { ID: row.ID, status: row.status === 1 ? 0 : 1 }
|
||||
const res = await status(params)
|
||||
if (res.code === 0) {
|
||||
row.status = row.status === 1 ? 0 : 1
|
||||
ElMessage.success('状态修改成功')
|
||||
} else {
|
||||
ElMessage.error(res.msg || '状态修改失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('状态修改失败')
|
||||
} finally {
|
||||
row.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const handleDetail = (row) => {
|
||||
currentTeacher.value = row
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 设置权重
|
||||
const handleEditWeight = (row) => {
|
||||
currentTeacher.value = row
|
||||
weightForm.value = {
|
||||
id: row.ID,
|
||||
weight: row.weight
|
||||
}
|
||||
weightDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 设置分成比例
|
||||
const handleEditRate = (row) => {
|
||||
currentTeacher.value = row
|
||||
rateForm.value = {
|
||||
id: row.ID,
|
||||
expect_rate: row.expect_rate
|
||||
}
|
||||
rateDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 修改余额
|
||||
const handleBalance = (row) => {
|
||||
currentTeacher.value = row
|
||||
balanceForm.value = {
|
||||
id: row.ID,
|
||||
balance: 0,
|
||||
change_type: 1
|
||||
}
|
||||
balanceDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 新增讲师
|
||||
const handleAdd = () => {
|
||||
addForm.value = {
|
||||
phone: '',
|
||||
password: '',
|
||||
nick_name: '',
|
||||
user_type: 2,
|
||||
user_label: 1,
|
||||
weight: 0,
|
||||
expect_rate: 0
|
||||
}
|
||||
addDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 关闭弹窗方法
|
||||
const handleDetailClose = () => {
|
||||
detailDialogVisible.value = false
|
||||
}
|
||||
|
||||
const handleWeightClose = () => {
|
||||
weightDialogVisible.value = false
|
||||
weightFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
const handleRateClose = () => {
|
||||
rateDialogVisible.value = false
|
||||
rateFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
const handleBalanceClose = () => {
|
||||
balanceDialogVisible.value = false
|
||||
balanceFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
const handleAddClose = () => {
|
||||
addDialogVisible.value = false
|
||||
addFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 提交权重设置
|
||||
const submitWeight = async () => {
|
||||
if (!weightFormRef.value) return
|
||||
await weightFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const res = await setTeacherWeight(weightForm.value)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('权重设置成功')
|
||||
handleWeightClose()
|
||||
getList()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '权重设置失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('权重设置失败:', error)
|
||||
ElMessage.error('权重设置失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 提交分成比例设置
|
||||
const submitRate = async () => {
|
||||
if (!rateFormRef.value) return
|
||||
await rateFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const res = await setTeacherRate(rateForm.value)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('分成比例设置成功')
|
||||
handleRateClose()
|
||||
getList()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '分成比例设置失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('分成比例设置失败:', error)
|
||||
ElMessage.error('分成比例设置失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 提交余额修改
|
||||
const submitBalance = async () => {
|
||||
if (!balanceFormRef.value) return
|
||||
|
||||
// 额外验证:减少余额时不能超过当前余额
|
||||
if (balanceForm.value.change_type === 2) {
|
||||
const currentBalance = parseFloat(currentTeacher.value.balance)
|
||||
const reduceAmount = parseFloat(balanceForm.value.balance)
|
||||
|
||||
if (reduceAmount > currentBalance) {
|
||||
ElMessage.error('减少金额不能超过当前余额')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
await balanceFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const res = await setBalance(balanceForm.value)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('余额修改成功')
|
||||
handleBalanceClose()
|
||||
getList()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '余额修改失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('余额修改失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 提交新增讲师
|
||||
const submitAdd = async () => {
|
||||
if (!addFormRef.value) return
|
||||
await addFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const res = await register(addForm.value)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('新增讲师成功')
|
||||
handleAddClose()
|
||||
getList()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '新增讲师失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('新增讲师失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 页面初始化
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.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;
|
||||
|
||||
&.active {
|
||||
background: #f0f9ff;
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
&.vip {
|
||||
background: #fef9e7;
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background: #fef2f2;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.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-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;
|
||||
}
|
||||
}
|
||||
</style>
|
186
src/view/user/user/vipUser.vue
Normal file
186
src/view/user/user/vipUser.vue
Normal file
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="searchForm">
|
||||
<searchForm
|
||||
:search="VIP_USER_SEARCH_CONFIG"
|
||||
@searchData="searchData"
|
||||
@resetData="resetData"
|
||||
class="search-box searchForm"
|
||||
/>
|
||||
</div>
|
||||
<div class="gva-table-box">
|
||||
<Content
|
||||
@changePage="changePage"
|
||||
:total="total"
|
||||
v-model:tabloading="tableLoading"
|
||||
v-model:currentPage="queryParams.page"
|
||||
v-model:pageSize="queryParams.pageSize"
|
||||
:data="tableData"
|
||||
:config="VIP_USER_TABLE_CONFIG"
|
||||
>
|
||||
<template #avatar="{ row }">
|
||||
<el-image
|
||||
style="width: 60px; height: 60px"
|
||||
:src="row.avatar"
|
||||
:zoom-rate="1.2"
|
||||
:max-scale="7"
|
||||
:min-scale="0.2"
|
||||
:preview-src-list="[row.avatar]"
|
||||
show-progress
|
||||
:initial-index="4"
|
||||
fit="cover"
|
||||
/>
|
||||
</template>
|
||||
<template #user_label="{ row }">
|
||||
<el-tag :type="getVipLabelType(row.user_label)">
|
||||
{{ getVipLabelText(row.user_label) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
<template #vip_expire_time="{ row }">
|
||||
{{ row.vip_expire_time || '-' }}
|
||||
</template>
|
||||
<template #balance="{ row }">
|
||||
{{ formatBalance(row.balance) }}
|
||||
</template>
|
||||
<template #operate="{ row }">
|
||||
<el-button type="danger" link @click="handleRemoveVip(row)">
|
||||
移除VIP
|
||||
</el-button>
|
||||
</template>
|
||||
</Content>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import searchForm from '@/components/searchForm/index.vue'
|
||||
import Content from '@/components/content/index.vue'
|
||||
import { getVipUserList, removeUserVip } from '@/api/user/index.js'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { VIP_USER_SEARCH_CONFIG, VIP_USER_TABLE_CONFIG } from '@/config'
|
||||
|
||||
const tableLoading = ref(true)
|
||||
const tableData = ref([])
|
||||
const queryParams = ref({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
user_id: '',
|
||||
name: ''
|
||||
})
|
||||
const total = ref(0)
|
||||
|
||||
// 搜索数据
|
||||
const searchData = (data) => {
|
||||
queryParams.value.page = 1
|
||||
queryParams.value = { ...queryParams.value, ...data }
|
||||
getList()
|
||||
}
|
||||
|
||||
// 重置数据
|
||||
const resetData = () => {
|
||||
queryParams.value = {
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
user_id: '',
|
||||
name: ''
|
||||
}
|
||||
getList()
|
||||
}
|
||||
|
||||
// 获取VIP用户列表
|
||||
async function getList() {
|
||||
try {
|
||||
tableLoading.value = true
|
||||
const res = await getVipUserList(queryParams.value)
|
||||
if (res.code === 0) {
|
||||
tableData.value = res.data.list || []
|
||||
total.value = res.data.total || 0
|
||||
} else {
|
||||
ElMessage.error(res.msg || '获取VIP用户列表失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取VIP用户列表失败:', error)
|
||||
ElMessage.error('获取VIP用户列表失败')
|
||||
} finally {
|
||||
tableLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 分页处理
|
||||
function changePage(data) {
|
||||
queryParams.value.page = data
|
||||
getList()
|
||||
}
|
||||
|
||||
// 格式化余额显示
|
||||
function formatBalance(balance) {
|
||||
if (!balance || balance === 0) return '0.00元'
|
||||
return (balance / 100).toFixed(2) + '元'
|
||||
}
|
||||
|
||||
// 获取VIP等级标签类型
|
||||
function getVipLabelType(userLabel) {
|
||||
const typeMap = {
|
||||
1: 'info', // 注册会员
|
||||
2: 'warning', // VIP
|
||||
3: 'success' // SVIP
|
||||
}
|
||||
return typeMap[userLabel] || 'info'
|
||||
}
|
||||
|
||||
// 获取VIP等级文本
|
||||
function getVipLabelText(userLabel) {
|
||||
const textMap = {
|
||||
1: '注册会员',
|
||||
2: 'VIP',
|
||||
3: 'SVIP'
|
||||
}
|
||||
return textMap[userLabel] || '未知'
|
||||
}
|
||||
|
||||
// 移除用户VIP
|
||||
async function handleRemoveVip(row) {
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
`确定要移除用户 "${row.nick_name}" 的VIP身份吗?此操作不可撤销。`,
|
||||
'确认移除VIP',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
|
||||
const res = await removeUserVip({ id: row.id })
|
||||
if (res.code === 0) {
|
||||
ElMessage.success('移除VIP成功')
|
||||
getList() // 刷新列表
|
||||
} else {
|
||||
ElMessage.error(res.msg || '移除VIP失败')
|
||||
}
|
||||
} catch (error) {
|
||||
// 取消操作时不需要提示
|
||||
if (error !== 'cancel') {
|
||||
ElMessage.error('移除VIP失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时获取数据
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.searchForm {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.gva-table-box {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
676
src/view/user/user/with.vue
Normal file
676
src/view/user/user/with.vue
Normal file
@@ -0,0 +1,676 @@
|
||||
<template>
|
||||
<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.userName" placeholder="请输入用户名" clearable />
|
||||
</el-form-item>
|
||||
<el-form-item label="提现类型">
|
||||
<el-select v-model="searchForm.withType" 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-select v-model="searchForm.status" placeholder="请选择状态" clearable>
|
||||
<el-option label="待处理" :value="1" />
|
||||
<el-option label="已完成" :value="2" />
|
||||
<el-option label="已拒绝" :value="3" />
|
||||
<el-option label="已取消" :value="4" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="提现时间">
|
||||
<el-date-picker
|
||||
v-model="searchForm.dateRange"
|
||||
type="daterange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
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 pending">
|
||||
<div class="stat-value">{{ stats.pending }}</div>
|
||||
<div class="stat-label">待处理</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-item completed">
|
||||
<div class="stat-value">{{ stats.completed }}</div>
|
||||
<div class="stat-label">已完成</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="stat-item rejected">
|
||||
<div class="stat-value">{{ stats.rejected }}</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="userName" label="用户名" width="120" />
|
||||
<el-table-column prop="withType" label="提现类型" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getWithTypeTagType(row.withType)">
|
||||
{{ getWithTypeText(row.withType) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="amount" label="申请金额" width="120">
|
||||
<template #default="{ row }">
|
||||
<span style="color: #f56c6c; font-weight: bold;">¥{{ row.amount }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="realAmount" label="实际到账" width="120">
|
||||
<template #default="{ row }">
|
||||
<span style="color: #67c23a; font-weight: bold;">
|
||||
{{ row.realAmount > 0 ? `¥${row.realAmount}` : '-' }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="status" label="状态" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getStatusTagType(row.status)">
|
||||
{{ getStatusText(row.status) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="accountInfo" label="收款信息" width="120">
|
||||
<template #default="{ row }">
|
||||
<el-button type="text" @click="viewAccountInfo(row)">查看</el-button>
|
||||
</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 prop="arriveTime" label="到账时间" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ row.arriveTime ? formatDate(row.arriveTime) : '-' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="180" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
v-if="row.status === 1"
|
||||
type="text"
|
||||
@click="handleApprove(row)"
|
||||
>
|
||||
通过
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="row.status === 1"
|
||||
type="text"
|
||||
@click="handleReject(row)"
|
||||
style="color: #f56c6c"
|
||||
>
|
||||
拒绝
|
||||
</el-button>
|
||||
<el-button type="text" @click="viewDetail(row)">详情</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="approveDialogVisible"
|
||||
:title="approveType === 'approve' ? '通过提现申请' : '拒绝提现申请'"
|
||||
width="500px"
|
||||
:before-close="handleApproveClose"
|
||||
>
|
||||
<el-form
|
||||
ref="approveFormRef"
|
||||
:model="approveForm"
|
||||
:rules="approveRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="用户名">
|
||||
<span>{{ currentWithdraw.userName }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item label="申请金额">
|
||||
<span style="color: #f56c6c; font-weight: bold;">¥{{ currentWithdraw.amount }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="approveType === 'approve'" label="实际到账金额" prop="realAmount">
|
||||
<el-input-number
|
||||
v-model="approveForm.realAmount"
|
||||
:precision="2"
|
||||
:step="0.01"
|
||||
:min="0"
|
||||
:max="currentWithdraw.amount"
|
||||
placeholder="请输入实际到账金额"
|
||||
style="width: 100%"
|
||||
/>
|
||||
<div style="font-size: 12px; color: #999; margin-top: 5px;">
|
||||
可扣除手续费等费用,不能超过申请金额
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="approveForm.remark"
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
:placeholder="approveType === 'approve' ? '请输入通过备注(可选)' : '请输入拒绝原因'"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="handleApproveClose">取消</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="submitApprove"
|
||||
:class="approveType === 'reject' ? 'reject-btn' : ''"
|
||||
>
|
||||
{{ approveType === 'approve' ? '确认通过' : '确认拒绝' }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 收款信息弹窗 -->
|
||||
<el-dialog
|
||||
v-model="accountInfoDialogVisible"
|
||||
title="收款信息"
|
||||
width="600px"
|
||||
:before-close="handleAccountInfoClose"
|
||||
>
|
||||
<div class="account-info-content">
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="提现类型">
|
||||
<el-tag :type="getWithTypeTagType(currentWithdraw.withType)">
|
||||
{{ getWithTypeText(currentWithdraw.withType) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="申请金额">
|
||||
<span style="color: #f56c6c; font-weight: bold;">¥{{ currentWithdraw.amount }}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
|
||||
<div style="margin-top: 20px;">
|
||||
<h4>收款信息:</h4>
|
||||
<div v-if="isImageUrl(currentWithdraw.accountInfo)" class="image-info">
|
||||
<el-image
|
||||
:src="currentWithdraw.accountInfo"
|
||||
:preview-src-list="[currentWithdraw.accountInfo]"
|
||||
fit="contain"
|
||||
style="max-width: 300px; max-height: 300px;"
|
||||
/>
|
||||
<p style="margin-top: 10px; color: #666;">{{ getWithTypeText(currentWithdraw.withType) }}收款码</p>
|
||||
</div>
|
||||
<div v-else class="text-info">
|
||||
<el-input
|
||||
:value="currentWithdraw.accountInfo"
|
||||
readonly
|
||||
type="textarea"
|
||||
:rows="3"
|
||||
/>
|
||||
<p style="margin-top: 10px; color: #666;">银行卡号或账户信息</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<el-dialog
|
||||
v-model="detailDialogVisible"
|
||||
title="提现详情"
|
||||
width="600px"
|
||||
:before-close="handleDetailClose"
|
||||
>
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="提现ID">{{ currentWithdraw.ID }}</el-descriptions-item>
|
||||
<el-descriptions-item label="用户名">{{ currentWithdraw.userName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="提现类型">
|
||||
<el-tag :type="getWithTypeTagType(currentWithdraw.withType)">
|
||||
{{ getWithTypeText(currentWithdraw.withType) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="申请金额">
|
||||
<span style="color: #f56c6c; font-weight: bold;">¥{{ currentWithdraw.amount }}</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="实际到账">
|
||||
<span style="color: #67c23a; font-weight: bold;">
|
||||
{{ currentWithdraw.realAmount > 0 ? `¥${currentWithdraw.realAmount}` : '-' }}
|
||||
</span>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<el-tag :type="getStatusTagType(currentWithdraw.status)">
|
||||
{{ getStatusText(currentWithdraw.status) }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="申请时间">{{ formatDate(currentWithdraw.CreatedAt) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="到账时间">
|
||||
{{ currentWithdraw.arriveTime ? formatDate(currentWithdraw.arriveTime) : '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { list, update } from '@/api/user/with.js'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
defineOptions({
|
||||
name: 'WithdrawManagement'
|
||||
})
|
||||
|
||||
// 表格相关数据
|
||||
const tableLoading = ref(false)
|
||||
const tableData = ref([])
|
||||
const queryParams = ref({
|
||||
page: 1,
|
||||
pageSize: 10
|
||||
})
|
||||
const total = ref(0)
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = ref({
|
||||
userName: '',
|
||||
withType: '',
|
||||
status: '',
|
||||
dateRange: []
|
||||
})
|
||||
|
||||
// 统计数据
|
||||
const stats = computed(() => {
|
||||
const pending = tableData.value.filter(item => item.status === 1).length
|
||||
const completed = tableData.value.filter(item => item.status === 2).length
|
||||
const rejected = tableData.value.filter(item => item.status === 3).length
|
||||
|
||||
return {
|
||||
total: total.value,
|
||||
pending,
|
||||
completed,
|
||||
rejected
|
||||
}
|
||||
})
|
||||
|
||||
// 审核弹窗
|
||||
const approveDialogVisible = ref(false)
|
||||
const approveFormRef = ref(null)
|
||||
const approveType = ref('approve') // 'approve' 或 'reject'
|
||||
const currentWithdraw = ref({})
|
||||
const approveForm = ref({
|
||||
id: '',
|
||||
status: 2,
|
||||
realAmount: 0,
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 收款信息弹窗
|
||||
const accountInfoDialogVisible = ref(false)
|
||||
|
||||
// 详情弹窗
|
||||
const detailDialogVisible = ref(false)
|
||||
|
||||
// 表单验证规则
|
||||
const approveRules = {
|
||||
realAmount: [
|
||||
{ required: true, message: '请输入实际到账金额', trigger: 'blur' },
|
||||
{ type: 'number', min: 0, message: '金额不能小于0', trigger: 'blur' }
|
||||
],
|
||||
remark: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
if (approveType.value === 'reject' && !value) {
|
||||
callback(new Error('拒绝时必须填写拒绝原因'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return '-'
|
||||
return new Date(dateStr).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 获取提现类型文本
|
||||
const getWithTypeText = (type) => {
|
||||
const typeMap = {
|
||||
1: '微信',
|
||||
2: '支付宝',
|
||||
3: '银行卡'
|
||||
}
|
||||
return typeMap[type] || '未知'
|
||||
}
|
||||
|
||||
// 获取提现类型标签类型
|
||||
const getWithTypeTagType = (type) => {
|
||||
const typeMap = {
|
||||
1: 'success',
|
||||
2: 'primary',
|
||||
3: 'warning'
|
||||
}
|
||||
return typeMap[type] || 'info'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status) => {
|
||||
const statusMap = {
|
||||
1: '待处理',
|
||||
2: '已完成',
|
||||
3: '已拒绝',
|
||||
4: '已取消'
|
||||
}
|
||||
return statusMap[status] || '未知'
|
||||
}
|
||||
|
||||
// 获取状态标签类型
|
||||
const getStatusTagType = (status) => {
|
||||
const typeMap = {
|
||||
1: 'warning',
|
||||
2: 'success',
|
||||
3: 'danger',
|
||||
4: 'info'
|
||||
}
|
||||
return typeMap[status] || 'info'
|
||||
}
|
||||
|
||||
// 判断是否为图片URL
|
||||
const isImageUrl = (url) => {
|
||||
if (!url) return false
|
||||
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
|
||||
return imageExtensions.some(ext => url.toLowerCase().includes(ext))
|
||||
}
|
||||
|
||||
// 搜索数据
|
||||
const searchData = () => {
|
||||
queryParams.value.page = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetData = () => {
|
||||
searchForm.value = {
|
||||
userName: '',
|
||||
withType: '',
|
||||
status: '',
|
||||
dateRange: []
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
// 处理日期范围
|
||||
if (searchForm.value.dateRange && searchForm.value.dateRange.length === 2) {
|
||||
params.startTime = searchForm.value.dateRange[0]
|
||||
params.endTime = searchForm.value.dateRange[1]
|
||||
}
|
||||
delete params.dateRange
|
||||
|
||||
const res = await list(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 handleApprove = (row) => {
|
||||
currentWithdraw.value = row
|
||||
approveType.value = 'approve'
|
||||
approveForm.value = {
|
||||
id: row.ID,
|
||||
status: 2,
|
||||
realAmount: row.amount,
|
||||
remark: ''
|
||||
}
|
||||
approveDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 拒绝申请
|
||||
const handleReject = (row) => {
|
||||
currentWithdraw.value = row
|
||||
approveType.value = 'reject'
|
||||
approveForm.value = {
|
||||
id: row.ID,
|
||||
status: 3,
|
||||
realAmount: 0,
|
||||
remark: ''
|
||||
}
|
||||
approveDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 查看收款信息
|
||||
const viewAccountInfo = (row) => {
|
||||
currentWithdraw.value = row
|
||||
accountInfoDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetail = (row) => {
|
||||
currentWithdraw.value = row
|
||||
detailDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 关闭审核弹窗
|
||||
const handleApproveClose = () => {
|
||||
approveDialogVisible.value = false
|
||||
approveFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 关闭收款信息弹窗
|
||||
const handleAccountInfoClose = () => {
|
||||
accountInfoDialogVisible.value = false
|
||||
}
|
||||
|
||||
// 关闭详情弹窗
|
||||
const handleDetailClose = () => {
|
||||
detailDialogVisible.value = false
|
||||
}
|
||||
|
||||
// 提交审核
|
||||
const submitApprove = async () => {
|
||||
if (!approveFormRef.value) return
|
||||
|
||||
await approveFormRef.value.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
const res = await update(approveForm.value)
|
||||
if (res.code === 0) {
|
||||
ElMessage.success(approveType.value === 'approve' ? '审核通过成功' : '拒绝成功')
|
||||
handleApproveClose()
|
||||
getList()
|
||||
} else {
|
||||
ElMessage.error(res.msg || '操作失败')
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error('操作失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 页面初始化
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.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;
|
||||
|
||||
&.pending {
|
||||
background: #fef9e7;
|
||||
color: #e6a23c;
|
||||
}
|
||||
|
||||
&.completed {
|
||||
background: #f0f9ff;
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
&.rejected {
|
||||
background: #fef2f2;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.account-info-content {
|
||||
.image-info {
|
||||
text-align: center;
|
||||
|
||||
.el-image {
|
||||
border: 1px solid #dcdfe6;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.text-info {
|
||||
.el-textarea {
|
||||
:deep(.el-textarea__inner) {
|
||||
background-color: #f5f7fa;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.reject-btn {
|
||||
background-color: #f56c6c !important;
|
||||
border-color: #f56c6c !important;
|
||||
|
||||
&:hover {
|
||||
background-color: #f78989 !important;
|
||||
border-color: #f78989 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user