🎨 新增讲师管理&新增批量上传机器人&新增批量上传文章

This commit is contained in:
2025-09-17 18:26:25 +08:00
parent fe2aebb2b4
commit f1c8264878
13 changed files with 1628 additions and 53 deletions

View File

@@ -108,3 +108,12 @@ export const getBotPublic = () => {
method: 'get',
})
}
//BulkUpload
export const bulkCreateBot = (data) => {
return service({
url: '/bt/bulkBot',
method: 'post',
data
})
}

View File

@@ -37,3 +37,12 @@ export const detail = (params) => {
params
})
}
// 批量更新价格
export const batchUpdatePrice = (data) => {
return service({
url: '/app_user/teacher_vip/price',
method: 'put',
data
})
}

View File

@@ -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
})
@@ -103,4 +103,23 @@ export const getVipUserList = (params) => {
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
})
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 502 KiB

View File

@@ -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>

View File

@@ -578,6 +578,14 @@ export const ARTICLE_TABLE_CONFIG = {
},
slot: 'UpdatedAt'
},
{
attrs: {
label: '发布状态',
prop: 'status',
align: 'center'
},
slot: 'status'
},
{
attrs: {
label: '操作',

View File

@@ -3,6 +3,7 @@
"/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",
@@ -25,6 +26,7 @@
"/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",
@@ -96,6 +98,7 @@
"/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",

View File

@@ -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',

View File

@@ -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>

View 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>

View File

@@ -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) => {

View File

@@ -19,15 +19,14 @@
<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"
>
<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" />
@@ -35,13 +34,8 @@
<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"
/>
<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>
@@ -63,26 +57,74 @@
</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"
/>
<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>
<span class="text-lg">{{ editType === 'create' ? '新增讲师包月' : '编辑讲师包月' }}</span>
<div>
<el-button :loading="btnLoading" type="primary" @click="submitForm"> </el-button>
<el-button @click="closeDialog"> </el-button>
@@ -111,8 +153,8 @@
</template>
<script setup>
import { ref } from 'vue'
import { list as getTeacherVipList, add as createTeacherVip, edit as updateTeacherVip, del as deleteTeacherVip, detail as getDetail } from '@/api/goods/teacherVip'
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'
@@ -123,6 +165,22 @@ 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')
@@ -208,16 +266,16 @@ const formData = ref({
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'] }]
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) => {
const openEdit = async (type, row) => {
editType.value = type
if (type === 'update' && row?.ID) {
const res = await getDetail({ id: row.ID })
@@ -240,7 +298,7 @@ const closeDialog = () => {
}
const submitForm = () => {
formRef.value?.validate(async(valid)=>{
formRef.value?.validate(async (valid) => {
if (!valid) return
btnLoading.value = true
const payload = { ...formData.value }
@@ -263,9 +321,9 @@ const submitForm = () => {
})
}
const handleDelete = async(row) => {
const handleDelete = async (row) => {
try {
await ElMessageBox.confirm('确定要删除该记录吗?','提示',{ type:'warning' })
await ElMessageBox.confirm('确定要删除该记录吗?', '提示', { type: 'warning' })
const res = await deleteTeacherVip({ ID: row.ID })
if (res.code === 0) {
ElMessage.success('删除成功')
@@ -273,7 +331,146 @@ const handleDelete = async(row) => {
}
} 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>

View 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>