🎨 新增讲师管理&新增批量上传机器人&新增批量上传文章
This commit is contained in:
@@ -108,3 +108,12 @@ export const getBotPublic = () => {
|
||||
method: 'get',
|
||||
})
|
||||
}
|
||||
|
||||
//BulkUpload
|
||||
export const bulkCreateBot = (data) => {
|
||||
return service({
|
||||
url: '/bt/bulkBot',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
@@ -37,3 +37,12 @@ export const detail = (params) => {
|
||||
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
|
||||
})
|
||||
@@ -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 |
@@ -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>
|
||||
|
@@ -578,6 +578,14 @@ export const ARTICLE_TABLE_CONFIG = {
|
||||
},
|
||||
slot: 'UpdatedAt'
|
||||
},
|
||||
{
|
||||
attrs: {
|
||||
label: '发布状态',
|
||||
prop: 'status',
|
||||
align: 'center'
|
||||
},
|
||||
slot: 'status'
|
||||
},
|
||||
{
|
||||
attrs: {
|
||||
label: '操作',
|
||||
|
@@ -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",
|
||||
|
@@ -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>
|
||||
|
||||
|
@@ -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) => {
|
||||
|
@@ -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>
|
||||
|
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>
|
Reference in New Issue
Block a user