新增答辩用后台管理系统

This commit is contained in:
2026-06-29 03:08:42 +08:00
parent efd4d03037
commit e61fa6de00
98 changed files with 16022 additions and 420 deletions
+427
View File
@@ -0,0 +1,427 @@
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue'
import { getRolesByPage, createRole, updateRole, deleteRole, getRolePermissions, assignPermissionsToRole, getAllPermissions } from '@/api/modules/role'
import { ElMessage, ElMessageBox } from 'element-plus'
const loading = ref(false)
const list = ref<any[]>([])
const total = ref(0)
const dialogVisible = ref(false)
const permVisible = ref(false)
const isEdit = ref(false)
const editId = ref<number | null>(null)
const allPermissions = ref<any[]>([])
const selectedPermIds = ref<number[]>([])
const systemCollapsed = ref(true)
const collapsedGroups = ref<Record<string, boolean>>({})
// 内置超级管理员角色(id=1: '超级管理员', roleKey='admin'
const isBuiltinRole = (row: any) => row?.id === 1
function toggleGroup(label: string) {
collapsedGroups.value[label] = !collapsedGroups.value[label]
}
function toggleGroupAll(group: { label: string; items: any[] }) {
const ids = group.items.map((p: any) => p.id)
const allSelected = ids.every((id: number) => selectedPermIds.value.includes(id))
if (allSelected) {
selectedPermIds.value = selectedPermIds.value.filter((id: number) => !ids.includes(id))
} else {
const toAdd = ids.filter((id: number) => !selectedPermIds.value.includes(id))
selectedPermIds.value = [...selectedPermIds.value, ...toAdd]
}
}
function isGroupAllSelected(group: { items: any[] }) {
if (group.items.length === 0) return false
return group.items.every((p: any) => selectedPermIds.value.includes(p.id))
}
function isGroupPartialSelected(group: { items: any[] }) {
if (group.items.length === 0) return false
const some = group.items.some((p: any) => selectedPermIds.value.includes(p.id))
return some && !isGroupAllSelected(group)
}
const permCategories: { label: string; prefix: string }[] = [
{ label: '会员管理', prefix: 'business:member:' },
{ label: '会员卡管理', prefix: 'business:memberCard:' },
{ label: '团课管理', prefix: 'business:groupCourse:' },
{ label: '团课类型管理', prefix: 'business:groupCourseType:' },
{ label: '团课推荐管理', prefix: 'business:groupCourseRecommend:' },
{ label: '团课预约管理', prefix: 'business:groupCourseBooking:' },
{ label: '签到管理', prefix: 'business:checkIn:' },
{ label: '数据统计', prefix: 'business:dataCount:' },
{ label: '员工管理', prefix: 'business:employee:' },
]
const systemPermCategories: { label: string; prefix: string }[] = [
{ label: '用户管理', prefix: 'system:user:' },
{ label: '角色管理', prefix: 'system:role:' },
{ label: '权限管理', prefix: 'system:permission:' },
{ label: '菜单管理', prefix: 'system:menu:' },
{ label: '字典管理', prefix: 'system:dict:' },
{ label: '配置管理', prefix: 'system:config:' },
{ label: '日志管理', prefix: 'system:log:' },
{ label: '文件管理', prefix: 'system:file:' },
{ label: '公告管理', prefix: 'system:notice:' },
]
const businessPerms = computed(() => {
return permCategories.map(cat => ({
label: cat.label,
items: allPermissions.value.filter((p: any) => p.permissionCode?.startsWith(cat.prefix)),
})).filter(g => g.items.length > 0)
})
const systemPerms = computed(() => {
return systemPermCategories.map(cat => ({
label: cat.label,
items: allPermissions.value.filter((p: any) => p.permissionCode?.startsWith(cat.prefix)),
})).filter(g => g.items.length > 0)
})
const page = reactive({ page: 0, size: 10, sort: 'id', order: 'asc' })
const currentPage = computed({
get: () => page.page + 1,
set: (val: number) => { page.page = val - 1 }
})
const form = reactive({ roleName: '', roleKey: '', roleSort: 0, status: 1 })
async function fetchData() {
loading.value = true
try {
const res = await getRolesByPage(page)
if (res?.content) { list.value = res.content; total.value = res.totalElements || 0 }
else if (Array.isArray(res)) { list.value = res; total.value = res.length }
} catch { list.value = [] } finally { loading.value = false }
}
async function handleDelete(row: any) {
if (isBuiltinRole(row)) {
ElMessage.warning('超级管理员角色不可删除')
return
}
const { value: pwd } = await ElMessageBox.prompt('请输入管理员密码以确认删除', '验证密码', {
inputType: 'password',
confirmButtonText: '确定',
cancelButtonText: '取消',
}).catch(() => {})
if (!pwd) {
ElMessage.warning('请输入验证密码')
return
}
try {
await deleteRole(row.id, { params: { adminPassword: pwd } })
ElMessage.success('删除成功')
fetchData()
} catch (e: any) {
const msg = e?.response?.data || e?.message || '删除失败'
ElMessage.error(typeof msg === 'string' ? msg : '删除失败')
}
}
function openCreate() {
isEdit.value = false
editId.value = null
Object.assign(form, { roleName: '', roleKey: '', roleSort: 0, status: 1 })
dialogVisible.value = true
}
function openEdit(row: any) {
if (isBuiltinRole(row)) {
ElMessage.warning('超级管理员角色不可编辑')
return
}
isEdit.value = true
editId.value = row.id
Object.assign(form, row)
dialogVisible.value = true
}
async function handleSave() {
if (isEdit.value && isBuiltinRole(form)) {
ElMessage.warning('超级管理员角色不可编辑')
return
}
try {
if (isEdit.value) {
const { value: pwd } = await ElMessageBox.prompt('请输入管理员密码以确认编辑', '验证密码', {
inputType: 'password',
confirmButtonText: '确定',
cancelButtonText: '取消',
}).catch(() => {})
if (!pwd) {
ElMessage.warning('请输入验证密码')
return
}
await updateRole(editId.value!, form, { params: { adminPassword: pwd } })
} else {
await createRole(form)
}
ElMessage.success(isEdit.value ? '更新成功' : '创建成功')
dialogVisible.value = false
fetchData()
} catch (e: any) {
const msg = e?.response?.data || e?.message || '操作失败'
ElMessage.error(typeof msg === 'string' ? msg : '操作失败')
}
}
async function openAssignPerms(row: any) {
if (isBuiltinRole(row)) {
ElMessage.warning('超级管理员角色的权限不可被修改')
return
}
const perms = await getAllPermissions()
allPermissions.value = Array.isArray(perms) ? perms : []
try {
const rolePerms = await getRolePermissions(row.id)
selectedPermIds.value = Array.isArray(rolePerms) ? rolePerms.map((p: any) => p.id) : []
} catch { selectedPermIds.value = [] }
editId.value = row.id
permVisible.value = true
}
async function handleAssignPerms() {
const { value: pwd } = await ElMessageBox.prompt('请输入管理员密码以确认分配权限', '验证密码', {
inputType: 'password',
confirmButtonText: '确定',
cancelButtonText: '取消',
}).catch(() => {})
if (!pwd) {
ElMessage.warning('请输入验证密码')
return
}
try {
await assignPermissionsToRole(editId.value!, selectedPermIds.value, { params: { adminPassword: pwd } })
ElMessage.success('分配成功')
permVisible.value = false
} catch (e: any) {
const msg = e?.response?.data || e?.message || '分配失败'
ElMessage.error(typeof msg === 'string' ? msg : '分配失败')
}
}
onMounted(() => fetchData())
</script>
<template>
<div>
<h2 style="margin-bottom:16px">角色管理</h2>
<el-card class="search-card"><el-button type="primary" @click="openCreate" v-permission="'system:role:create'">新增角色</el-button></el-card>
<el-card>
<el-table :data="list" v-loading="loading" stripe>
<el-table-column prop="id" label="ID" width="70" align="center" />
<el-table-column label="角色信息" min-width="200">
<template #default="{ row }">
<div style="display:flex;align-items:center;gap:8px">
<span style="font-weight:500">{{ row.roleName }}</span>
<span v-if="isBuiltinRole(row)" style="font-size:12px;color:#e6a23c">&#9733; 内置</span>
<el-tag v-if="isBuiltinRole(row)" type="warning" size="small" effect="dark" style="margin-left:auto">内置</el-tag>
</div>
<div style="font-size:12px;color:#909399;margin-top:2px;font-family:monospace">{{ row.roleKey }}</div>
</template>
</el-table-column>
<el-table-column prop="roleSort" label="排序" width="70" align="center" />
<el-table-column label="状态" width="80" align="center">
<template #default="{ row }"><el-tag :type="row.status === 1 ? 'success' : 'danger'" size="small">{{ row.status === 1 ? '启用' : '禁用' }}</el-tag></template>
</el-table-column>
<el-table-column label="操作" width="240" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="openEdit(row)" v-permission="'system:role:edit'">编辑</el-button>
<el-button type="primary" link @click="openAssignPerms(row)" v-permission="'system:role:assign'">分配权限</el-button>
<el-button type="danger" link @click="handleDelete(row)" v-permission="'system:role:delete'">删除</el-button>
</template>
</el-table-column>
</el-table>
<div style="margin-top:16px;text-align:right">
<el-pagination v-model:current-page="currentPage" :page-size="page.size" :total="total" layout="prev,pager,next,total" @current-change="fetchData" />
</div>
</el-card>
<el-dialog v-model="dialogVisible" :title="isEdit ? '编辑角色' : '新增角色'" width="500px" :close-on-click-modal="false">
<el-form :model="form" label-width="90px">
<el-form-item label="角色名称"><el-input v-model="form.roleName" placeholder="请输入角色名称" /></el-form-item>
<el-form-item label="角色标识"><el-input v-model="form.roleKey" placeholder="如: admin" /></el-form-item>
<el-form-item label="排序"><el-input-number v-model="form.roleSort" :min="0" style="width:100%" /></el-form-item>
<el-form-item label="状态">
<el-switch v-model="form.status" :active-value="1" :inactive-value="0" active-text="启用" inactive-text="禁用" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSave">确定</el-button>
</template>
</el-dialog>
<el-dialog v-model="permVisible" title="分配权限" width="720px" :close-on-click-modal="false">
<div v-if="allPermissions.length === 0" style="color:#909399;text-align:center;padding:40px">暂无可用权限</div>
<template v-else>
<!-- 业务权限 -->
<div style="font-weight:600;font-size:14px;color:#303133;margin-bottom:12px">业务权限</div>
<el-checkbox-group v-model="selectedPermIds">
<div class="perm-scroll" style="max-height:320px;overflow-y:auto;padding-right:4px;margin-bottom:16px">
<div v-for="group in businessPerms" :key="group.label" class="perm-group">
<div class="perm-group-header" @click="toggleGroup(group.label)">
<span class="perm-group-arrow" :class="{ expanded: !collapsedGroups[group.label] }">&#9654;</span>
<span class="perm-group-label">{{ group.label }}</span>
<span class="perm-group-count">{{ group.items.length }}</span>
<span class="perm-group-toggle" @click.stop="toggleGroupAll(group)">
{{ isGroupAllSelected(group) ? '取消全选' : '全选' }}
</span>
</div>
<div v-show="!collapsedGroups[group.label]">
<div
v-for="perm in group.items" :key="perm.id"
class="perm-item"
>
<el-checkbox :label="perm.id" />
<span class="perm-name">{{ perm.permissionName }}</span>
<span class="perm-code">{{ perm.permissionCode }}</span>
</div>
</div>
</div>
</div>
</el-checkbox-group>
<!-- 系统权限 -->
<div
class="perm-system-header"
@click="systemCollapsed = !systemCollapsed"
>
<span class="perm-group-arrow" :class="{ expanded: !systemCollapsed }">&#9654;</span>
<span style="font-weight:600;font-size:14px;color:#303133">系统权限</span>
<span style="font-size:12px;color:#c0c4cc;margin-left:6px">非业务相关谨慎分配</span>
</div>
<div v-show="!systemCollapsed" class="perm-scroll" style="max-height:240px;overflow-y:auto;padding-right:4px">
<el-checkbox-group v-model="selectedPermIds">
<div v-for="group in systemPerms" :key="group.label" class="perm-group">
<div class="perm-group-header" @click="toggleGroup(group.label)">
<span class="perm-group-arrow" :class="{ expanded: !collapsedGroups[group.label] }">&#9654;</span>
<span class="perm-group-label">{{ group.label }}</span>
<span class="perm-group-count">{{ group.items.length }}</span>
<span class="perm-group-toggle" @click.stop="toggleGroupAll(group)">
{{ isGroupAllSelected(group) ? '取消全选' : '全选' }}
</span>
</div>
<div v-show="!collapsedGroups[group.label]">
<div
v-for="perm in group.items" :key="perm.id"
class="perm-item"
>
<el-checkbox :label="perm.id" />
<span class="perm-name">{{ perm.permissionName }}</span>
<span class="perm-code">{{ perm.permissionCode }}</span>
</div>
</div>
</div>
</el-checkbox-group>
</div>
</template>
<template #footer>
<el-button @click="permVisible = false">取消</el-button>
<el-button type="primary" @click="handleAssignPerms">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped>
.search-card { margin-bottom: 16px; }
/* 权限弹窗 */
.perm-group {
margin-bottom: 2px;
}
.perm-group-header {
display: flex;
align-items: center;
gap: 6px;
padding: 7px 10px;
background: #f5f7fa;
border-radius: 6px;
cursor: pointer;
user-select: none;
transition: background 0.15s;
}
.perm-group-header:hover {
background: #ebeef5;
}
.perm-group-arrow {
font-size: 10px;
color: #909399;
transition: transform 0.2s;
display: inline-block;
width: 12px;
}
.perm-group-arrow.expanded {
transform: rotate(90deg);
}
.perm-group-label {
font-weight: 500;
font-size: 13px;
color: #303133;
}
.perm-group-count {
font-size: 11px;
color: #c0c4cc;
background: #e9ecf1;
border-radius: 10px;
padding: 0 7px;
line-height: 18px;
}
.perm-group-toggle {
margin-left: auto;
font-size: 12px;
color: #409eff;
cursor: pointer;
padding: 0 4px;
}
.perm-group-toggle:hover {
color: #66b1ff;
}
.perm-item {
display: flex;
align-items: center;
padding: 5px 10px 5px 28px;
border-bottom: 1px solid #f5f5f5;
gap: 6px;
}
.perm-item:last-child {
border-bottom: none;
}
.perm-name {
font-size: 13px;
color: #303133;
}
.perm-code {
font-size: 11px;
color: #909399;
font-family: 'SF Mono', 'Menlo', 'Consolas', monospace;
margin-left: auto;
}
.perm-system-header {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 10px;
background: #fdf6ec;
border-radius: 6px;
cursor: pointer;
user-select: none;
margin-bottom: 4px;
transition: background 0.15s;
}
.perm-system-header:hover {
background: #faead7;
}
.perm-scroll::-webkit-scrollbar {
width: 5px;
}
.perm-scroll::-webkit-scrollbar-thumb {
background: #dcdfe6;
border-radius: 3px;
}
</style>