428 lines
16 KiB
Vue
428 lines
16 KiB
Vue
<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(() => ({ value: '' }))
|
||
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(() => ({ value: '' }))
|
||
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(() => ({ value: '' }))
|
||
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">★ 内置</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] }">▶</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 }">▶</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] }">▶</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>
|