🎨 新增讲师申请审核页,新增vip管理页面

This commit is contained in:
2025-09-02 20:32:51 +08:00
parent f3ac0bb511
commit 5f470478cd
5 changed files with 716 additions and 4 deletions

View File

@@ -4,4 +4,4 @@ ENV = 'production'
VITE_BASE_API = /api
VITE_FILE_API = /api
#下方修改为你的线上ip如果需要在线使用表单构建工具时使用其余情况无需使用以下环境变量
VITE_BASE_PATH = https://demo.gin-vue-admin.com
VITE_BASE_PATH = http://lckt.hnlc5588.cn

42
src/api/goods/vip.js Normal file
View File

@@ -0,0 +1,42 @@
import service from '@/utils/request'
// @tag goods
// @summary 获取vip列表
// @param {object} params
// @return {object} data
// @router get /vip/list
export const list = (params) => {
return service({
url: '/vip/list',
method: 'get',
params
})
}
export const add = (data) => {
return service({
url: '/vip',
method: 'post',
data
})
}
export const edit = (data) => {
return service({
url: '/vip',
method: 'put',
data
})
}
export const del = (data) => {
return service({
url: '/vip',
method: 'delete',
data
})
}
export const detail = (id) => {
return service({
url: '/vip/'+ id,
method: 'get',
})
}

View File

@@ -1,5 +1,9 @@
import {getDict} from '@/utils/dictionary'
export let userStatus = await getDict('user-status')
// 移除 top-level await改为静态配置
export const userStatus = [
{ label: '启用', value: 1 },
{ label: '禁用', value: 0 }
]
export const ORDER_SEARCH_CONFIG = [
{
type: 'input',

View File

@@ -0,0 +1,329 @@
<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 />
</el-form-item>
<el-form-item label="等级">
<el-select v-model="searchInfo.level" placeholder="按等级搜索" clearable style="width: 180px">
<el-option :value="1" label="Vip" />
<el-option :value="2" label="Svip" />
</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="150" />
<el-table-column sortable align="left" label="等级" prop="level" width="120">
<template #default="scope">{{ formatLevel(scope.row.level) }}</template>
</el-table-column>
<el-table-column sortable align="left" label="价格" prop="price" width="120">
<template #default="scope">{{ Number(scope.row.price).toFixed(2) }}</template>
</el-table-column>
<el-table-column sortable align="left" label="有效期(天)" prop="expiration" width="120" />
<el-table-column align="left" label="描述" prop="des" min-width="200" show-overflow-tooltip />
<el-table-column align="left" label="操作" fixed="right" :min-width="appStore.operateMinWith">
<template #default="scope">
<el-button type="primary" link icon="edit" class="table-button" @click="updateRow(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'?'新增VIP':'编辑VIP'}}</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="level">
<el-select v-model="formData.level" placeholder="请选择等级">
<el-option :value="1" label="Vip" />
<el-option :value="2" label="Svip" />
</el-select>
</el-form-item>
<el-form-item label="价格(元)" prop="price">
<el-input v-model.number="formData.price" placeholder="请输入价格(数字)" />
</el-form-item>
<el-form-item label="有效期(天)" prop="expiration">
<el-input v-model.number="formData.expiration" placeholder="请输入有效期天数(数字)" />
</el-form-item>
<el-form-item label="描述" prop="des">
<el-input v-model="formData.des" placeholder="请输入描述" type="textarea" :rows="3" />
</el-form-item>
</el-form>
</el-drawer>
</div>
</template>
<script setup>
import { list as apiList, add as apiAdd, edit as apiEdit, del as apiDel, detail as apiDetail } from '@/api/goods/vip'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ref, reactive } from 'vue'
import { useAppStore } from '@/pinia'
import { formatDate } from '@/utils/format'
defineOptions({
name: 'VipList'
})
const appStore = useAppStore()
const btnLoading = ref(false)
const elFormRef = ref()
const elSearchFormRef = ref()
const showAllQuery = ref(false)
const formData = ref({
ID: undefined,
name: '',
level: undefined,
price: undefined,
expiration: undefined,
des: ''
})
const rule = reactive({
name: [
{ required: true, message: '请输入名称', trigger: ['blur','input'] },
{ whitespace: true, message: '不能只输入空格', trigger: ['blur','input'] }
],
level: [ { required: true, message: '请输入等级', trigger: ['blur','input'] } ],
price: [ { required: true, message: '请输入价格', trigger: ['blur','input'] } ],
expiration: [ { required: true, message: '请输入有效期', trigger: ['blur','input'] } ]
})
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 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',
level: 'level',
price: 'price',
expiration: 'expiration',
}
let sort = sortMap[prop] || prop
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 res = await apiList({ page: page.value, pageSize: pageSize.value, ...searchInfo.value })
if (res.code === 0) {
tableData.value = res.data.list
total.value = res.data.total
page.value = res.data.page
pageSize.value = res.data.pageSize
}
}
getTableData()
const multipleSelection = ref([])
const handleSelectionChange = (val) => {
multipleSelection.value = val
}
const type = ref('')
const formatLevel = (level) => {
if (level === 1) return 'Vip'
if (level === 2) return 'Svip'
return level
}
const updateRow = async (row) => {
const res = await apiDetail(row.ID)
type.value = 'update'
if (res.code === 0) {
formData.value = { ...res.data }
dialogFormVisible.value = true
}
}
const deleteRow = async (row) => {
ElMessageBox.confirm('确定要删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const res = await apiDel({ ID: row.ID })
if (res.code === 0) {
ElMessage({ type: 'success', message: '删除成功' })
if (tableData.value.length === 1 && page.value > 1) {
page.value--
}
getTableData()
}
})
}
const onDelete = async () => {
ElMessageBox.confirm('确定要删除吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
const IDs = []
if (multipleSelection.value.length === 0) {
ElMessage({ type: 'warning', message: '请选择要删除的数据' })
return
}
multipleSelection.value.forEach(item => IDs.push(item.ID))
// 后端若支持批量删除可改为批量接口;这里逐个删除
for (const id of IDs) {
await apiDel({ ID: id })
}
ElMessage({ type: 'success', message: '删除成功' })
if (tableData.value.length === IDs.length && 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 = { ID: undefined, name: '', level: undefined, price: undefined, expiration: undefined, des: '' }
}
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 apiAdd(formData.value)
break
case 'update':
res = await apiEdit(formData.value)
break
default:
res = await apiAdd(formData.value)
break
}
btnLoading.value = false
if (res.code === 0) {
ElMessage({ type: 'success', message: '创建/更改成功' })
closeDialog()
getTableData()
}
})
}
</script>
<style>
</style>

View File

@@ -0,0 +1,337 @@
<template>
<div>
<div class="gva-table-box">
<div class="gva-btn-list">
<el-button type="primary" @click="refreshList">刷新列表</el-button>
</div>
<!-- 统计信息 -->
<div style="margin-bottom: 20px; padding: 15px; background: #f8f9fa; border-radius: 8px;">
<el-row :gutter="20">
<el-col :span="6">
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: bold; color: #409eff;">{{ getStats().total }}</div>
<div style="color: #666;">总申请数</div>
</div>
</el-col>
<el-col :span="6">
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: bold; color: #e6a23c;">{{ getStats().pending }}</div>
<div style="color: #666;">待审核</div>
</div>
</el-col>
<el-col :span="6">
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: bold; color: #67c23a;">{{ getStats().approved }}</div>
<div style="color: #666;">已通过</div>
</div>
</el-col>
<el-col :span="6">
<div style="text-align: center;">
<div style="font-size: 24px; font-weight: bold; color: #f56c6c;">{{ getStats().rejected }}</div>
<div style="color: #666;">已拒绝</div>
</div>
</el-col>
</el-row>
</div>
<!-- 讲师申请列表 -->
<el-table
:data="tableData"
v-loading="tableLoading"
border
stripe
style="width: 100%"
>
<el-table-column type="index" label="序号" width="60" align="center" />
<el-table-column prop="nickname" label="申请人" align="center" width="100" />
<el-table-column prop="phone" label="手机号" align="center" width="120" />
<el-table-column prop="reason" label="申请理由" align="center" min-width="150" show-overflow-tooltip />
<el-table-column prop="expectRate" label="期望分成比例" align="center" width="120">
<template #default="{ row }">
<span class="text-blue-600 font-bold">{{ row.expectRate }}%</span>
</template>
</el-table-column>
<el-table-column label="申请状态" align="center" width="100">
<template #default="{ row }">
<el-tag :type="getStatusTag(row.status).type">
{{ getStatusTag(row.status).label }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="CreatedAt" label="申请时间" align="center" width="180">
<template #default="{ row }">
{{ formatDate(row.CreatedAt) }}
</template>
</el-table-column>
<el-table-column label="操作" align="center" width="200">
<template #default="{ row }">
<el-button
v-if="row.status === 0"
type="success"
size="small"
@click="handleReview(row, 1)"
>
通过
</el-button>
<el-button
v-if="row.status === 0"
type="danger"
size="small"
@click="handleReview(row, 2)"
>
拒绝
</el-button>
<el-button
type="primary"
size="small"
@click="handleDetail(row)"
>
详情
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div style="margin-top: 20px; text-align: right;">
<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="changePage"
/>
</div>
</div>
<!-- 审核弹窗 -->
<el-dialog
v-model="reviewVisible"
:title="reviewData.status === 1 ? '通过申请' : '拒绝申请'"
width="500px"
destroy-on-close
>
<el-form :model="reviewForm" label-width="100px">
<el-form-item label="申请人">
<span>{{ reviewData.nickname }}</span>
</el-form-item>
<el-form-item label="申请理由">
<span>{{ reviewData.reason }}</span>
</el-form-item>
<el-form-item label="期望分成">
<span>{{ reviewData.expectRate }}%</span>
</el-form-item>
<el-form-item label="审核备注" v-if="reviewData.status === 2">
<el-input
v-model="reviewForm.note"
type="textarea"
:rows="3"
placeholder="请输入拒绝理由"
maxlength="200"
show-word-limit
/>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="reviewVisible = false">取消</el-button>
<el-button
:type="reviewData.status === 1 ? 'success' : 'danger'"
@click="confirmReview"
>
{{ reviewData.status === 1 ? '确认通过' : '确认拒绝' }}
</el-button>
</span>
</template>
</el-dialog>
<!-- 详情弹窗 -->
<el-dialog
v-model="detailVisible"
title="申请详情"
width="600px"
destroy-on-close
>
<el-descriptions :column="2" border>
<el-descriptions-item label="申请人">{{ detailData.nickname || EMPTY_STR }}</el-descriptions-item>
<el-descriptions-item label="手机号">{{ detailData.phone || EMPTY_STR }}</el-descriptions-item>
<el-descriptions-item label="申请理由" :span="2">{{ detailData.reason || EMPTY_STR }}</el-descriptions-item>
<el-descriptions-item label="期望分成比例">{{ (detailData.expectRate || 0) }}%</el-descriptions-item>
<el-descriptions-item label="申请状态">
<el-tag :type="getStatusTag(detailData.status).type">
{{ getStatusTag(detailData.status).label }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="申请时间">{{ formatDate(detailData.CreatedAt) }}</el-descriptions-item>
<el-descriptions-item label="更新时间">{{ formatDate(detailData.UpdatedAt) }}</el-descriptions-item>
<el-descriptions-item label="审核备注" :span="2">{{ detailData.note || '无' }}</el-descriptions-item>
</el-descriptions>
</el-dialog>
</div>
</template>
<script setup>
import { getTeacherApplyList, reviewTeacherApply } from '@/api/user/index.js'
import { ref, onMounted } from 'vue'
import { formatDate } from '@/utils/format'
import { ElMessage } from 'element-plus'
const tableLoading = ref(true)
const tableData = ref([])
const detailVisible = ref(false)
const detailData = ref({})
const reviewVisible = ref(false)
const reviewData = ref({})
const queryParams = ref({
page: 1,
pageSize: 10
})
const total = ref(0)
const EMPTY_STR = '- -'
// 获取状态标签
const getStatusTag = (status) => {
const map = {
0: { type: 'warning', label: '待审核' },
1: { type: 'success', label: '已通过' },
2: { type: 'danger', label: '已拒绝' }
}
return map[status] || { type: '', label: EMPTY_STR }
}
// 获取统计数据
const getStats = () => {
const stats = {
total: total.value,
pending: tableData.value.filter(item => item.status === 0).length,
approved: tableData.value.filter(item => item.status === 1).length,
rejected: tableData.value.filter(item => item.status === 2).length
}
return stats
}
// 获取列表数据
const getList = async () => {
tableLoading.value = true
try {
const res = await getTeacherApplyList(queryParams.value)
if (res.code === 0) {
tableData.value = res.data.list
total.value = res.data.total
console.log('讲师申请数据获取成功:', {
total: res.data.total,
list: res.data.list
})
} else {
ElMessage.error(res.msg || '获取讲师申请列表失败')
}
} catch (error) {
console.error('获取讲师申请列表失败:', error)
ElMessage.error('获取讲师申请列表失败')
} finally {
tableLoading.value = false
}
}
// 查看详情
const handleDetail = (row) => {
detailData.value = { ...row }
detailVisible.value = true
}
// 审核操作
const handleReview = (row, status) => {
reviewData.value = { ...row, status }
reviewVisible.value = true
}
// 确认审核
const confirmReview = async () => {
try {
const data = {
ID: reviewData.value.ID,
status: reviewData.value.status,
note: reviewData.value.status === 2 ? reviewForm.value.note : ''
}
const res = await reviewTeacherApply(data)
if (res.code === 0) {
ElMessage.success(reviewData.value.status === 1 ? '审核通过成功' : '审核拒绝成功')
reviewVisible.value = false
getList() // 刷新列表
} else {
ElMessage.error(res.msg || '审核操作失败')
}
} catch (error) {
console.error('审核操作失败:', error)
ElMessage.error('审核操作失败')
}
}
// 分页相关
const changePage = (page) => {
queryParams.value.page = page
getList()
}
const handleSizeChange = (size) => {
queryParams.value.pageSize = size
queryParams.value.page = 1
getList()
}
// 刷新列表
const refreshList = () => {
getList()
}
// 审核表单
const reviewForm = ref({
note: ''
})
onMounted(() => {
getList()
})
</script>
<style lang="scss" scoped>
.gva-table-box {
.gva-btn-list {
margin-bottom: 20px;
}
.el-row {
.el-col {
.el-statistic {
text-align: center;
padding: 20px;
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;
}
}
}
}
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
</style>