Files
gym-manage/gym-manage-cuit/src/views/role/index.vue
T
2026-06-29 20:34:25 +08:00

428 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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">&#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>