44 KiB
系统角色权限配置实现逻辑分析
1. 引言
1.1 功能概述
系统角色权限配置模块负责管理系统用户的菜单访问权限和数据操作权限。该功能允许管理员为不同角色配置:
- 菜单权限:控制用户可以访问哪些菜单页面和功能按钮
- 数据权限:控制用户对特定模块数据的读写操作权限
1.2 核心概念
- 数据权限(DataScope):控制用户对不同模块数据的读写权限,分为"只读"和"读写"两种级别
- 菜单权限(MenuIds):控制用户可访问的菜单和按钮列表,通过勾选菜单ID实现权限控制
1.3 文档分析范围
本文档详细分析项目中角色权限配置的实现逻辑和流程,主要分析以下文件:
src/views/system/role/index.vue- 角色权限配置主界面组件src/views/system/role/utils/hook.tsx- 角色管理业务逻辑钩子函数
2. 权限配置架构
2.1 技术栈与依赖
- 前端框架:Vue 3.3.4,使用Composition API(
<script setup>语法) - UI组件库:Element Plus 2.3.6(el-form、el-radio-group、el-checkbox-group等)
- 状态管理:Pinia(通过useRole钩子函数管理局部状态)
- 路由:Vue Router 4.2.2
- 构建工具:Vite 4.3.9
2.2 核心数据类型
2.2.1 角色相关类型
// 角色查询参数
export interface RoleQuery extends BasePageQuery {
roleKey?: string; // 角色标识
roleName?: string; // 角色名称
status?: string; // 角色状态
timeRangeColumn?: string; // 时间范围列名
}
// 角色数据传输对象
export interface RoleDTO {
createTime: Date; // 创建时间
dataScope: number; // 数据范围
remark: string; // 备注
roleId: number; // 角色ID
roleKey: string; // 角色标识
roleName: string; // 角色名称
roleSort: number; // 角色排序
selectedDeptList: number[]; // 选择的部门列表
selectedMenuList: number[]; // 选择的菜单列表(权限相关)
status: number; // 角色状态
}
// 新增角色命令
export interface AddRoleCommand {
dataScope?: string; // 数据范围
menuIds: number[]; // 菜单ID列表(权限相关)
remark?: string; // 备注
roleKey: string; // 角色标识
roleName: string; // 角色名称
roleSort: number; // 角色排序
status?: string; // 角色状态
}
// 更新角色命令
export interface UpdateRoleCommand extends AddRoleCommand {
roleId: number; // 角色ID
}
2.2.2 菜单相关类型
// 菜单数据传输对象
export interface MenuDTO extends Tree {
createTime?: Date; // 创建时间
isButton?: number; // 是否是按钮
id?: number; // 菜单ID
menuName?: string; // 菜单名称
parentId?: number; // 父菜单ID
menuType: number; // 菜单类型
menuTypeStr: string; // 菜单类型字符串
path?: string; // 菜单路径
permission?: string; // 权限标识
routerName?: string; // 路由名称
status?: number; // 菜单状态
statusStr?: string; // 菜单状态字符串
}
2.2.3 表格菜单数据类型
interface MenuTableItem extends MenuDTO {
_level: number; // 层级深度(0: 顶层)
_rootId?: number; // 所属顶层菜单ID
_rootName?: string; // 所属顶层菜单名称
hasWritePermission?: boolean; // 是否有"读写"权限项
}
interface PermissionTableRow {
_rootId: number; // 顶层菜单ID(用于数据权限关联)
_rootName: string; // 顶层菜单名称
_level: number; // 当前层级深度
_hasWritePermission: boolean; // 是否有"读写"权限项
_parentKey: string; // 父级菜单键(用于合并单元格)
_rowspanMap: Record<string, number>; // 每个层级的rowspan值
level0: { id: number; name: string } | null; // 一级菜单数据
level1: { id: number; name: string } | null; // 二级菜单数据
level2: { id: number; name: string } | null; // 三级菜单数据
level3: { id: number; name: string } | null; // 四级菜单数据
level4: { id: number; name: string } | null; // 五级菜单数据
}
2.2.4 权限数据存储格式
- 数据权限(dataScope):
Record<number, string>类型,键为菜单分类ID,值为"${menuId}-${permission}"格式字符串- permission=0:表示只读权限
- permission=1:表示读写权限
- 菜单权限(menuIds):
number[]类型,直接存储菜单/按钮的ID列表 - 特殊权限项:"读写"权限项(
menuName === '读写')作为特殊按钮权限单独处理
2.3 组件结构
2.3.1 主组件(index.vue)
- 模板布局:左侧角色列表 + 右侧权限配置表单
- 状态管理:通过
useRole()钩子获取业务逻辑和状态 - 表单结构:
- 角色基本信息(角色名称、权限字符)
- 表格权限配置区:使用
el-table扁平化表格展示菜单权限和数据权限,通过:span-method实现单元格合并,包含:- 数据权限列:只在每个顶层分类的第一行显示,提供"只读"/"读写"单选按钮
- N级菜单列:动态生成,根据菜单最大层级确定列数,每列显示菜单名称和checkbox水平排列
2.3.2 业务逻辑钩子(hook.tsx)
- 状态管理:管理form查询条件、dataList角色列表、loading状态、menuTree菜单树
- 操作方法:提供onSearch、resetForm、getMenuTree、handleDelete等方法
- 职责分离:将业务逻辑与UI组件分离,提高代码可维护性
3. 权限配置实现逻辑
3.1 数据权限(DataScope)实现
3.1.1 数据权限定义与存储
数据权限通过dataScope对象存储,结构为Record<number, string>:
- 键:菜单分类ID(如1、2、3等)
- 值:
"${menuId}-${permission}"格式字符串permission=0:只读权限permission=1:读写权限
示例:
dataScope.value = {
1: "1-0", // 分类1:只读权限
2: "2-1", // 分类2:读写权限
3: null // 分类3:无权限
};
新增数据结构:writePermissionMap
const writePermissionMap = ref<Record<number, number>>({}); // 顶层菜单ID -> 读写项ID
在菜单树转换过程中,记录每个顶层菜单对应的"读写"权限项ID,用于数据权限与菜单权限的精确关联。
3.1.2 UI渲染逻辑
数据权限通过el-table的列渲染,仅当行属于某个顶层分类且该分类包含"读写"权限项(_hasWritePermission === true)且是该分类的第一行时显示:
<el-table-column label="数据权限" width="120" align="center">
<template #default="{ row, $index }">
<template v-if="row._hasWritePermission && ($index === 0 || permissionTableData[$index - 1]._rootId !== row._rootId)">
<el-radio-group v-model="dataScope[row._rootId]" size="small">
<el-radio :label="`${row._rootId}-0`">只读</el-radio>
<el-radio :label="`${row._rootId}-1`">读写</el-radio>
</el-radio-group>
</template>
</template>
</el-table-column>
关键变化:
- 从基于
_level的判断改为基于_rootId和行索引的判断 - 数据权限只在该顶层分类的第一行显示
- 通过
span-method实现的rowspan合并,数据权限单选框会自动跨行显示
3.1.3 数据权限计算规则
数据权限值通过updateDataScope函数计算,基于多级菜单结构的权限选择:
核心逻辑:
- 递归检查:
hasAnySelectedInCategory函数递归检查某个顶层分类下是否有选中的非"读写"菜单权限 - 权限关联:通过
writePermissionMap映射表关联顶层菜单ID与"读写"权限项ID - 自动设置:
- 当某个顶层分类下有选中的非"读写"菜单权限时,默认设置数据权限为"只读"(
${menu.id}-0) - 当同时选中了"读写"权限项时,数据权限自动设置为"读写"(
${menu.id}-1) - 当未选中任何权限时,清除该分类的数据权限(设置为
null),并自动取消勾选对应的"读写"权限项
- 当某个顶层分类下有选中的非"读写"菜单权限时,默认设置数据权限为"只读"(
关键改进:
- 支持多级菜单结构的权限统计,不再依赖扁平化的
processedMenuOptions - 数据权限与菜单权限的关联更加精确,通过
writePermissionMap实现ID映射 - 递归检查确保子菜单权限的正确统计
3.2 菜单权限(MenuIds)实现
3.2.1 菜单权限定义与存储
菜单权限通过menuIds数组存储,包含用户有权访问的菜单和按钮ID:
formData.menuIds = [1, 2, 3, 4, 5]; // 菜单ID列表
3.2.2 UI渲染逻辑
菜单权限通过el-table的动态列渲染,使用el-checkbox组件,排除"读写"权限项:
<el-table ref="menuTableRef" :data="permissionTableData" row-key="id" border class="menu-permission-table" :span-method="getSpanMethod">
<el-table-column label="数据权限" width="120" align="center">
<!-- 数据权限单选框 -->
</el-table-column>
<el-table-column v-for="level in maxLevel" :key="level - 1" :label="`${level}级菜单`" :width="180">
<template #default="{ row }">
<template v-if="row[`level${level - 1}`]">
<div class="menu-cell">
<span class="menu-name">{{ row[`level${level - 1}`].name }}</span>
<el-checkbox
:checked="formData.menuIds.includes(row[`level${level - 1}`].id)"
@change="checked => handleMenuCheckChange(row[`level${level - 1}`].id, checked)"
class="menu-checkbox"
/>
</div>
</template>
</template>
</el-table-column>
</el-table>
关键变化:
- 从树形表格改为扁平化多列表格
- 使用
v-for动态生成菜单层级列,每列显示菜单名称和checkbox水平排列 - 通过
:span-method实现单元格合并,相同父级菜单在连续行中合并显示 - 使用
row-key确保合并单元格后checkbox状态正确
3.2.3 菜单树转换逻辑
原始菜单树通过convertToFlatTableData函数转换为适合扁平化表格展示的permissionTableData:
const convertToFlatTableData = (menuTree: MenuDTO[], writeMap: Record<number, number>) => {
const rows: PermissionTableRow[] = [];
let maxLvl = 0;
function traverse(nodes: MenuDTO[], level: number, rootId: number, rootName: string, path: { id: number; name: string }[]) {
nodes.forEach(node => {
if (node.menuName === '读写') {
return;
}
const currentPath = [...path, { id: node.id!, name: node.menuName! }];
const currentLevel = level;
maxLvl = Math.max(maxLvl, currentLevel);
if (!node.children || node.children.length === 0) {
const row: PermissionTableRow = {
_rootId: rootId,
_rootName: rootName,
_level: currentLevel,
_hasWritePermission: !!writeMap[rootId],
_parentKey: '',
_rowspanMap: {},
level0: currentPath[0] || null,
level1: currentPath[1] || null,
level2: currentPath[2] || null,
level3: currentPath[3] || null,
level4: currentPath[4] || null
};
rows.push(row);
} else {
node.children.forEach(child => {
traverse([child], level + 1, rootId, rootName, currentPath);
});
}
});
}
menuTree.forEach(node => {
if (node.menuName === '读写') return;
traverse([node], 0, node.id!, node.menuName!, []);
});
// 计算rowspan值用于合并单元格
if (rows.length > 0) {
const rowspanMap: Record<string, number> = {};
rows.forEach(row => {
for (let i = 0; i < maxLvl + 1; i++) {
const key = `level${i}`;
const levelData = row[key as keyof PermissionTableRow];
if (levelData && typeof levelData === 'object' && 'id' in levelData) {
const id = (levelData as { id: number }).id;
const mapKey = `${key}-${id}`;
rowspanMap[mapKey] = (rowspanMap[mapKey] || 0) + 1;
}
}
});
rows.forEach(row => {
row._rowspanMap = {};
for (let i = 0; i < maxLvl + 1; i++) {
const key = `level${i}`;
const levelData = row[key as keyof PermissionTableRow];
if (levelData && typeof levelData === 'object' && 'id' in levelData) {
const id = (levelData as { id: number }).id;
const mapKey = `${key}-${id}`;
row._rowspanMap[key] = rowspanMap[mapKey];
}
}
});
}
maxLevel.value = maxLvl + 1;
return rows;
};
关键特点:
- 扁平化结构:将树形菜单转换为扁平的多层级表格数据,每行代表一条完整的菜单路径
- 层级存储:使用
level0到level4字段分别存储各层级菜单的ID和名称 - 合并单元格:计算每个菜单ID出现的次数(rowspan值),用于
getSpanMethod实现单元格合并 - 过滤"读写"项:在转换过程中移除名为"读写"的菜单项
3.3 权限联动机制
3.3.1 正向联动:菜单权限 → 数据权限
当用户选择菜单权限时,自动更新对应的数据权限:
触发条件:formData.menuIds数组变化
处理逻辑(updateDataScope函数):
- 递归检查:使用
hasAnySelectedInCategory函数递归检查每个顶层分类下是否有选中的非"读写"菜单权限 - 权限关联:通过
writePermissionMap映射表关联顶层菜单ID与"读写"权限项ID - 自动设置:
- 当某个顶层分类下有选中的非"读写"菜单权限时,默认设置数据权限为"只读"
- 当同时选中了"读写"权限项时,数据权限自动设置为"读写"
- 当未选中任何权限时,清除该分类的数据权限,并自动取消勾选对应的"读写"权限项
代码实现(index.vue:220-252):
// 统计某个顶层分类下是否有选中的菜单(遍历扁平表格)
const hasAnySelectedInCategory = (rootId: number): boolean => {
return permissionTableData.value.some(row => {
if (row._rootId !== rootId) return false;
for (let i = 0; i < maxLevel.value; i++) {
const key = `level${i}` as keyof PermissionTableRow;
const levelData = row[key];
if (levelData && typeof levelData === 'object' && 'id' in levelData &&
formData.menuIds.includes((levelData as { id: number }).id)) {
return true;
}
}
return false;
});
};
// 监听菜单选择变化,更新数据权限
const updateDataScope = (updateAll: boolean) => {
if (updateAll) dataScope.value = {};
const topLevelMenuIds = [...new Set(permissionTableData.value.map(row => row._rootId))];
topLevelMenuIds.forEach(rootId => {
const hasSelected = hasAnySelectedInCategory(rootId);
const writeItemId = writePermissionMap.value[rootId];
if (hasSelected) {
dataScope.value[rootId] = dataScope.value[rootId] || `${rootId}-0`;
} else {
dataScope.value[rootId] = null;
if (writeItemId && formData.menuIds.includes(writeItemId)) {
formData.menuIds = formData.menuIds.filter(id => id !== writeItemId);
}
}
});
};
关键改进:
- 递归统计:
hasAnySelectedInCategory函数支持多级菜单结构的权限统计 - 映射关联:通过
writePermissionMap实现数据权限与"读写"菜单项的精确关联 - 自动清理:当分类下无任何选中权限时,自动清理对应的"读写"权限项
3.3.2 反向联动:数据权限 → 菜单权限
当用户选择数据权限时,自动更新对应的菜单权限:
触发条件:dataScope对象变化
处理逻辑(watch(dataScope)监听器):
- 遍历映射:遍历
dataScope对象的所有条目 - 权限解析:将权限值分割为菜单分类ID和权限类型(0:只读, 1:读写)
- 精确关联:通过
writePermissionMap查找对应的"读写"权限项ID - 自动更新:
- 权限类型=1(读写) → 自动勾选该分类的"读写"按钮权限
- 权限类型=0(只读) → 自动取消勾选该分类的"读写"按钮权限
代码实现(index.vue:222-245):
// 监听dataScope变化,反向更新menuIds
watch(
dataScope,
(newDataScope) => {
Object.entries(newDataScope).forEach(([categoryId, scopeValue]) => {
if (!scopeValue) {
return;
}
const [id, permission] = scopeValue.split('-');
const writeItemId = writePermissionMap.value[Number(id)];
if (!writeItemId) return;
if (permission === '1') {
if (!formData.menuIds.includes(writeItemId)) {
formData.menuIds.push(writeItemId);
}
} else {
formData.menuIds = formData.menuIds.filter(menuId => menuId !== writeItemId);
}
});
},
{ deep: true }
);
关键改进:
- 直接映射:通过
writePermissionMap直接关联数据权限与"读写"菜单项,无需查找processedMenuOptions - 简化逻辑:移除对
processedMenuOptions的依赖,代码更加简洁高效 - 精确控制:确保数据权限变化时,对应的"读写"菜单权限精确同步
3.3.3 联动触发条件
- 初始化时:
updateDataScope(true)清除现有数据权限并重新计算 - 菜单权限变化时:
watch(formData.menuIds)触发正向联动 - 数据权限变化时:
watch(dataScope)触发反向联动
3.4 权限数据转换
3.4.1 数据转换过程
- 菜单树转换:通过
convertToFlatTableData函数将原始菜单树转换为扁平化表格数据permissionTableData - 映射表生成:在转换过程中生成
writePermissionMap,记录顶层菜单ID与"读写"权限项ID的对应关系 - 合并单元格计算:计算每个菜单ID出现的次数,生成
_rowspanMap用于单元格合并 - 动态层级列:根据菜单最大层级动态生成
level0到levelN字段,最多支持5级
3.4.2 关键特点
- 扁平化结构:将树形菜单转换为多列表格,每行代表一条完整菜单路径
- 动态列生成:根据菜单深度自动确定列数
- 单元格合并:通过
getSpanMethod和_rowspanMap实现相同父级菜单的单元格合并 - 过滤"读写"项:在转换过程中移除名为"读写"的菜单项
4. 权限配置工作流程
4.1 初始化流程
-
页面加载:
- 调用
onSearch()加载角色列表 - 调用
getMenuTree()获取完整菜单树
- 调用
-
数据处理:
- 菜单树通过
watch(menuTree)触发convertToFlatTableData函数,转换为扁平化表格数据permissionTableData - 同时生成
writePermissionMap映射表,记录顶层菜单ID与"读写"权限项ID的对应关系 - 计算
_rowspanMap用于单元格合并 - 角色列表排序:base角色排第一,admin角色排最后
- 菜单树通过
-
UI渲染:
- 左侧显示角色列表
- 右侧表单显示第一个角色的权限配置
- 使用
el-table扁平化表格展示permissionTableData,包含数据权限列和N级菜单列 - 通过
:span-method="getSpanMethod"实现单元格合并
4.2 角色切换流程
- 标签页切换:用户点击左侧角色列表项
- 触发监听器:
watch(activeTab)检测到角色变化 - 加载角色详情:
- 调用
getRoleInfo("update", row)获取角色权限信息 - 设置
formData.menuIds为角色的selectedMenuList
- 调用
- 更新数据权限:调用
updateDataScope(true)重新计算数据权限,基于permissionTableData扁平化结构统计权限选择
4.3 权限配置流程
-
用户操作:
- 在表格的各层级菜单列勾选/取消勾选菜单权限(通过
handleMenuCheckChange函数处理) - 在表格的"数据权限"列选择"只读"或"读写"权限(仅在每个顶层分类的第一行显示)
- 在表格的各层级菜单列勾选/取消勾选菜单权限(通过
-
联动更新:
- 菜单权限变化触发
updateDataScope(false),基于扁平化结构统计权限并更新数据权限 - 数据权限变化触发
watch(dataScope),通过writePermissionMap映射表更新对应的"读写"菜单权限
- 菜单权限变化触发
-
数据准备:
dataScope对象转换为逗号拼接的字符串:Object.values(dataScope.value).join(',')menuIds数组直接作为菜单权限数据,包含所有选中的菜单/按钮ID(除"读写"项)
4.4 保存提交流程
-
表单验证:
- 验证角色名称(roleName)不能为空
- 验证权限字符(roleKey)不能为空
- 验证角色顺序(roleSort)不能为空
-
权限数据格式化:
formData.dataScope = Object.values(dataScope.value).join(',')formData.menuIds保持数组格式
-
API调用:
- 新增角色:
addRoleApi(formData as AddRoleCommand) - 更新角色:
updateRoleApi(formData as UpdateRoleCommand)
- 新增角色:
-
结果反馈与界面更新:
- 成功:显示"提交成功"消息,刷新角色列表,切换到第一个角色
- 失败:显示错误消息,保持当前编辑状态
5. 关键函数与方法分析
5.1 useRole() 钩子函数(hook.tsx:17-209)
功能:封装角色管理的所有业务逻辑
主要状态:
form:角色查询条件(RoleQuery类型)dataList:角色列表(RoleDTO[]类型)loading:加载状态menuTree:菜单树结构activeTab:当前选中的角色标识pagination:分页配置
主要方法:
onSearch():搜索角色列表resetForm():重置查询表单getMenuTree():获取菜单树handleDelete():删除角色
5.2 核心监听器
5.2.1 watch(activeTab)(index.vue:83-93)
功能:处理角色切换 触发条件:用户点击左侧角色列表项 处理逻辑:
- 根据
roleKey查找对应的角色对象 - 调用
getRoleInfo("update", row)加载角色权限详情 - 将角色权限数据回填到
formData - 调用
updateDataScope(true)重新计算数据权限
5.2.2 watch(menuTree)(index.vue:215-218)
功能:转换菜单树为扁平化表格数据 触发条件:菜单树数据加载完成或变化 处理逻辑:
- 调用
convertToFlatTableData函数,将原始菜单树转换为扁平化表格数据permissionTableData - 生成
writePermissionMap映射表,记录顶层菜单ID与"读写"权限项ID的对应关系 - 计算
_rowspanMap用于单元格合并 - 过滤"读写"菜单项,避免在表格中重复显示
5.2.3 watch(dataScope)(index.vue:267-290)
功能:实现反向权限联动 触发条件:数据权限选择变化 处理逻辑:
- 遍历
dataScope对象,解析权限值(${menuId}-${permission}格式) - 通过
writePermissionMap查找对应的"读写"权限项ID - 根据权限类型(0:只读, 1:读写)自动更新
formData.menuIds数组中的"读写"权限项
5.2.4 watch(formData.menuIds)(index.vue:292-298)
功能:实现正向权限联动
触发条件:菜单权限选择变化(通过handleMenuCheckChange函数更新menuIds数组)
处理逻辑:
- 监听
formData.menuIds数组变化 - 触发
updateDataScope(false)函数,基于扁平化表格结构统计权限 - 自动更新
dataScope对象中的数据权限设置
5.3 数据处理函数
5.3.1 getRoleInfo()(index.vue:94-108)
功能:加载角色权限信息 参数:
type:操作类型("add"或"update")row:角色对象(可选) 处理逻辑:
- 调用
getMenuTree()确保菜单树已加载 - 如果是更新操作,调用
getRoleInfoApi()获取角色详情 - 设置
opType和opRow状态
5.3.2 updateDataScope()(index.vue:234-252)
功能:更新数据权限(正向联动) 参数:
updateAll:是否清除现有数据权限(true: 初始化时清空重建, false: 仅更新变化部分) 处理逻辑:
- 从
permissionTableData获取所有顶层菜单ID(通过_rootId) - 对每个顶层分类,使用
hasAnySelectedInCategory检查是否有选中的菜单权限 - 根据检查结果设置
dataScope值:- 有选中权限 → 默认设置为只读(
${rootId}-0) - 无选中权限 → 设置为
null,并自动移除对应的"读写"权限项
- 有选中权限 → 默认设置为只读(
- 通过
writePermissionMap实现数据权限与菜单权限的精确关联
5.3.3 getSpanMethod()(index.vue:231-248)
功能:实现el-table单元格合并 参数:
{ row, column, rowIndex, columnIndex }:表格单元格信息 处理逻辑:
- 数据权限列(columnIndex === 0)不合并
- 对于菜单层级列,根据
_rowspanMap计算rowspan值 - 如果当前单元格与上一行的对应层级单元格内容相同,返回
{ rowspan: 0, colspan: 0 }隐藏当前单元格 - 否则返回正常的
{ rowspan: N, colspan: 1 }进行合并
5.3.4 resetFromData()(index.vue:50-61)
功能:重置表单数据
处理逻辑:将formData重置为初始值
5.3.5 hasAnySelectedInCategory()(index.vue:220-232)
功能:检查某个顶层分类下是否有选中的菜单权限 参数:
rootId:顶层菜单分类ID 处理逻辑:
- 遍历
permissionTableData扁平化表格数据 - 筛选属于该顶层分类的行
- 检查该分类下各层级菜单ID是否在
formData.menuIds数组中 返回值:boolean,表示该分类下是否有选中的菜单权限
5.3.6 handleMenuCheckChange()(index.vue:255-265)
功能:处理菜单勾选框变化事件 参数:
menuId:菜单项IDchecked:勾选状态(CheckboxValueType类型) 处理逻辑:
- 将
checked参数转换为布尔值 - 如果为勾选状态且
menuIds数组中不包含该ID,则添加到数组 - 如果为取消勾选状态,则从数组中过滤移除该ID
- 更新后的
menuIds数组会触发watch(formData.menuIds)监听器,进而触发正向权限联动
5.3.7 handleConfirm()(index.vue:300-320)
功能:提交权限配置 处理逻辑:实现保存提交流程,详细逻辑见4.4节
6. API接口分析
6.1 角色相关API(@/api/system/role)
6.1.1 getRoleAllApi()
功能:获取所有角色列表
参数:RoleQuery类型查询条件
返回值:Promise<ResponseData<RoleDTO[]>>
使用场景:初始化时加载角色列表
6.1.2 getRoleInfoApi()
功能:获取角色详细信息
参数:roleId角色ID
返回值:Promise<ResponseData<RoleDTO>>
使用场景:切换角色时加载权限详情
6.1.3 addRoleApi()
功能:创建新角色
参数:AddRoleCommand类型角色数据
返回值:Promise<ResponseData<void>>
使用场景:保存新增角色
6.1.4 updateRoleApi()
功能:更新角色信息
参数:UpdateRoleCommand类型角色数据
返回值:Promise<ResponseData<void>>
使用场景:保存角色修改
6.1.5 deleteRoleApi()
功能:删除角色
参数:roleId角色ID
返回值:Promise<ResponseData<void>>
使用场景:删除角色操作
6.2 菜单相关API(@/api/system/menu)
6.2.1 getMenuListApi()
功能:获取菜单列表
参数:MenuQuery类型查询条件
返回值:Promise<ResponseData<MenuDTO[]>>
使用场景:加载菜单树数据
6.2.2 toTree()转换函数
功能:将扁平菜单列表转换为树形结构 参数:
data:扁平菜单数组idKey:ID字段名(默认"id")parentKey:父ID字段名(默认"parentId") 返回值:树形结构的菜单数组 使用场景:菜单树构建
7. 边界情况与特殊处理
7.1 空菜单树处理
场景:菜单API返回空数组或菜单树转换后为空
处理:menuTableData值为空数组,UI表格不显示任何菜单项
影响:用户无法配置任何权限,需要确保菜单数据正确加载
7.2 无"读写"权限的菜单分类
场景:某些菜单分类下没有名为"读写"的按钮权限项
处理:表格的数据权限列对该分类不显示单选框组(row.hasWritePermission === false)
影响:用户无法为该分类配置数据权限,仅能配置菜单访问权限
7.3 base和admin角色的特殊排序
场景:角色列表需要特定的显示顺序
处理:dataList.value.sort()自定义排序逻辑:
- base角色始终排在第一位
- admin角色始终排在最后一位
- 其他角色按默认顺序排列
7.4 权限清空的处理逻辑
场景:用户取消所有菜单权限选择 处理:
updateDataScope()函数将对应分类的dataScope设置为null- 通过
writePermissionMap查找对应的"读写"权限项ID,自动从menuIds数组中移除 - 确保数据权限与菜单权限的同步清理 影响:确保权限配置的一致性,避免残留无效权限项
7.5 首次加载和切换角色的差异处理
场景:
- 首次加载:需要初始化菜单树和默认角色
- 切换角色:需要加载特定角色的权限详情 处理:
- 首次:
getMenuTree()+onSearch()+ 设置第一个角色为activeTab - 切换:
watch(activeTab)触发getRoleInfo()加载详情
8. 数据流图与交互时序
8.1 完整数据流图
┌─────────────┐ ┌──────────────┐ ┌──────────────────┐
│ API数据 │────▶│ 角色列表/菜单树 │────▶│ permissionTableData │
└─────────────┘ └──────────────┘ └──────────────────┘
│ │ │
│ │ ▼
│ │ ┌──────────────────┐
│ │ │ 表格UI渲染 │
│ │ │ - 数据权限列 │
│ │ │ - N级菜单列 │
│ │ │ - 单元格合并 │
│ │ └──────────────────┘
│ │ │
│ │ ▼
│ │ ┌──────────────────┐
│ │ │ getSpanMethod │
│ │ │ 单元格合并计算 │
│ │ └──────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌──────────────┐ ┌──────────────────┐
│ API提交 │◀────│ 格式化权限数据 │◀────│ 表单验证 │
└─────────────┘ └──────────────┘ └──────────────────┘
│ ▲ ▲
│ │ │
▼ │ │
┌─────────────┐ ┌─────────────────────┐ ┌──────────────────┐
│ 结果反馈 │ │ formData.menuIds ←→ │ │ dataScope │
└─────────────┘ │ writePermissionMap │ └──────────────────┘
│ _rowspanMap │
└─────────────────────┘
双向联动
数据流说明:
- 原始数据获取:通过API获取角色列表和菜单树数据
- 数据转换:
convertToFlatTableData函数将菜单树转换为permissionTableData扁平化表格数据,同时生成writePermissionMap映射表和_rowspanMap合并计算 - UI渲染:
el-table扁平化表格展示,包含数据权限列和N级菜单列,通过getSpanMethod实现单元格合并 - 权限联动:通过
writePermissionMap实现dataScope与menuIds的双向精确同步 - 数据提交:权限数据格式化后通过API提交保存
8.2 关键交互时序
8.2.1 页面初始化时序
- 组件挂载:执行
onMounted钩子,调用onSearch() - 加载角色列表:调用
getRoleAllApi()获取所有角色 - 加载菜单树:调用
getMenuTree(),内部调用getMenuListApi() - 数据处理:菜单树通过
watch(menuTree)触发convertToFlatTableData函数,转换为permissionTableData扁平化表格数据 - 合并计算:计算
_rowspanMap用于单元格合并 - UI渲染:
el-table扁平化表格渲染,包含数据权限列和动态生成的N级菜单列 - 默认选中:设置
activeTab.value = dataList.value[0].roleKey
8.2.2 权限配置交互时序
- 用户操作:在表格的各层级菜单列勾选/取消勾选菜单权限,或选择"数据权限"列的单选按钮
- 状态更新:
- 菜单权限变化:
handleMenuCheckChange函数更新formData.menuIds数组 - 数据权限变化:
watch(dataScope)监听器更新对应的"读写"权限项
- 菜单权限变化:
- 正向联动:
watch(formData.menuIds)触发updateDataScope(false),基于扁平化结构统计权限并更新数据权限 - 反向联动:
watch(dataScope)通过writePermissionMap更新menuIds中的"读写"权限项 - UI更新:表格中的勾选框、单选按钮和合并单元格状态实时同步更新
8.2.3 保存提交时序
- 用户点击确认:触发
handleConfirm() - 表单验证:调用
formRef.value?.validate() - 数据格式化:
formData.dataScope = Object.values(dataScope.value).join(',') - API调用:根据
opType调用addRoleApi()或updateRoleApi() - 结果处理:成功刷新列表,失败显示错误信息
9. 总结与最佳实践
9.1 实现特点总结
9.1.1 架构优势
- 职责分离:业务逻辑与UI组件分离,提高代码可维护性
- 响应式设计:基于Vue 3 Composition API,状态管理清晰
- 组件化:复用Element Plus组件,保证UI一致性
- 界面设计优化:从树形表格改为扁平化多列表格,提升多级菜单的展示效果
- 数据流清晰:通过
writePermissionMap实现权限精确映射,确保数据权限与菜单权限双向同步 - 单元格合并:通过
getSpanMethod和_rowspanMap实现相同父级菜单的单元格合并,避免重复显示
9.1.2 权限联动机制
- 双向自动同步:数据权限和菜单权限通过
writePermissionMap实现精确双向同步 - 扁平化统计:基于扁平化表格进行权限统计,无需递归遍历
- 智能默认值:根据菜单权限选择智能设置数据权限默认值
- 边界处理完善:处理各种边缘情况,保证系统稳定性
9.1.3 数据处理
- 扁平化转换:
convertToFlatTableData函数将树形菜单转换为多列表格数据 - 动态列生成:根据菜单深度自动确定列数,最多支持5级
- 合并计算:自动计算
_rowspanMap用于单元格合并 - 过滤特殊项:在转换过程中移除"读写"菜单项,避免表格中重复显示
9.2 设计模式应用分析
9.2.1 观察者模式(Observer Pattern)
- 应用场景:
watch()监听器监听状态变化 - 实现方式:Vue 3的响应式系统 + watch API
- 效果:实现状态变化时的自动更新和联动
9.2.2 策略模式(Strategy Pattern)
- 应用场景:
updateDataScope()根据不同条件选择不同处理策略 - 实现方式:条件判断 + 不同的数据处理逻辑
- 效果:灵活处理各种权限配置场景
9.2.3 工厂模式(Factory Pattern)
- 应用场景:
toTree()函数创建树形结构、convertToFlatTableData函数创建扁平化表格 - 实现方式:输入扁平数据,输出树形结构或扁平化表格
- 效果:封装复杂的结构转换逻辑
9.2.4 组合模式(Composite Pattern)
- 应用场景:多级菜单树形结构展示
- 实现方式:
MenuDTO类型通过children属性形成树形结构 - 效果:统一处理单个菜单项和菜单树,支持多级权限联动
9.2.5 转换器模式(Adapter Pattern)
- 应用场景:
convertToFlatTableData函数将菜单树转换为扁平化表格数据 - 实现方式:适配树形结构为多列表格结构,添加层级字段和合并计算
- 效果:使菜单树数据适配扁平化表格渲染需求,实现UI组件与数据结构的解耦
9.2.6 表格合并模式(Table Span Pattern)
- 应用场景:相同父级菜单在连续行中的单元格合并
- 实现方式:通过
_rowspanMap记录每个菜单ID的出现次数,配合getSpanMethod实现合并 - 效果:避免重复显示相同父级菜单,提升表格可读性
9.3 可能的改进方向建议
9.3.1 性能优化
- 菜单树缓存:避免重复加载菜单树数据
- 虚拟滚动:菜单层级过多时使用虚拟滚动提升性能
- 按需加载:角色详情数据按需加载,减少初始请求数据量
9.3.2 功能增强
- 权限模板:提供预定义的权限模板,快速配置常见角色权限
- 权限继承:支持角色权限继承,减少重复配置
- 权限对比:提供角色权限对比功能,便于权限审计
- 批量操作:支持按分类或层级批量选择/取消权限
- 权限搜索过滤:在表格中支持关键词搜索和权限过滤
9.3.3 用户体验
- 权限预览:提供权限配置的预览模式
- 折叠控制:提供展开/折叠各层级菜单的交互控制
- 权限状态提示:清晰显示权限配置状态和关联关系
- 操作引导:提供权限配置的向导或操作指引
9.3.4 代码质量
- 单元测试:增加关键函数的单元测试覆盖率
- 类型安全:进一步强化TypeScript类型定义
- 代码文档:完善函数和复杂逻辑的代码注释
9.4 最佳实践建议
- 权限配置原则:遵循最小权限原则,只授予必要的权限
- 角色设计:基于岗位职责设计角色,避免角色过多或权限重叠
- 定期审计:定期审查角色权限配置,确保权限配置符合业务需求
- 变更记录:记录权限配置变更历史,便于追溯和审计
- 测试验证:权限配置变更后,进行充分的测试验证
文档版本:1.2
最后更新:2025-12-27
分析文件:src/views/system/role/index.vue(扁平化表格版本)、src/views/system/role/utils/hook.tsx
相关API:@/api/system/role、@/api/system/menu
改造说明:将树形表格改为扁平化多列表格,通过动态列和单元格合并实现多级菜单的横向展示
- 响应式设计:基于Vue 3 Composition API,状态管理清晰
- 组件化:复用Element Plus组件,保证UI一致性
- 界面设计优化:从扁平菜单结构改为树形表格展示,保留完整层级关系,提升用户体验
- 数据流清晰:通过
writePermissionMap实现权限精确映射,确保数据权限与菜单权限双向同步
9.1.2 权限联动机制
- 双向自动同步:数据权限和菜单权限通过
writePermissionMap实现精确双向同步 - 递归权限统计:支持多级菜单结构的权限检查和统计
- 智能默认值:根据菜单权限选择智能设置数据权限默认值
- 边界处理完善:处理各种边缘情况,保证系统稳定性
9.1.3 数据处理
- 树形结构保留:
convertMenuTreeToTableData函数保持菜单的完整层级关系 - 映射表生成:自动生成
writePermissionMap实现数据权限与菜单权限的精确关联 - 过滤特殊项:在转换过程中移除"读写"菜单项,避免表格中重复显示
- 层级标记:为每个菜单项添加
_level、_rootId、_rootName等字段,便于UI渲染和权限统计
9.2 设计模式应用分析
9.2.1 观察者模式(Observer Pattern)
- 应用场景:
watch()监听器监听状态变化 - 实现方式:Vue 3的响应式系统 + watch API
- 效果:实现状态变化时的自动更新和联动
9.2.2 策略模式(Strategy Pattern)
- 应用场景:
updateDataScope()根据不同条件选择不同处理策略 - 实现方式:条件判断 + 不同的数据处理逻辑
- 效果:灵活处理各种权限配置场景
9.2.3 工厂模式(Factory Pattern)
- 应用场景:
toTree()函数创建树形结构 - 实现方式:输入扁平数据,输出树形结构
- 效果:封装复杂的结构转换逻辑
9.2.4 组合模式(Composite Pattern)
- 应用场景:多级菜单树形结构展示和权限管理
- 实现方式:
MenuDTO类型通过children属性形成树形结构,el-table树形表格支持递归渲染 - 效果:统一处理单个菜单项和菜单树,支持多级权限联动和递归权限检查
9.2.5 转换器模式(Adapter Pattern)
- 应用场景:
convertMenuTreeToTableData函数将菜单树转换为表格数据 - 实现方式:适配
MenuDTO树形结构为MenuTableItem表格数据结构,添加层级标记和权限映射 - 效果:使菜单树数据适配
el-table的树形表格渲染需求,实现UI组件与数据结构的解耦
9.3 可能的改进方向建议
9.3.1 性能优化
- 菜单树缓存:避免重复加载菜单树数据
- 虚拟滚动:菜单权限选项过多时使用虚拟滚动提升性能
- 按需加载:角色详情数据按需加载,减少初始请求数据量
- 树形表格虚拟化:深层次菜单树使用虚拟化渲染,避免大量DOM节点影响性能
- 懒加载子节点:支持异步加载子菜单节点,减少初始数据量
9.3.2 功能增强
- 权限模板:提供预定义的权限模板,快速配置常见角色权限
- 权限继承:支持角色权限继承,减少重复配置
- 权限对比:提供角色权限对比功能,便于权限审计
- 树形权限批量操作:支持按分类或层级批量选择/取消权限
- 权限搜索过滤:在树形表格中支持关键词搜索和权限过滤
9.3.3 用户体验
- 权限预览:提供权限配置的预览模式
- 树形展开控制:提供展开/折叠树形节点的交互控制
- 权限状态提示:清晰显示权限配置状态和关联关系
- 操作引导:提供权限配置的向导或操作指引
9.3.4 代码质量
- 单元测试:增加关键函数的单元测试覆盖率
- 类型安全:进一步强化TypeScript类型定义
- 代码文档:完善函数和复杂逻辑的代码注释
9.4 最佳实践建议
- 权限配置原则:遵循最小权限原则,只授予必要的权限
- 角色设计:基于岗位职责设计角色,避免角色过多或权限重叠
- 定期审计:定期审查角色权限配置,确保权限配置符合业务需求
- 变更记录:记录权限配置变更历史,便于追溯和审计
- 测试验证:权限配置变更后,进行充分的测试验证
文档版本:1.1
最后更新:2025-12-27
分析文件:src/views/system/role/index.vue(改造后版本)、src/views/system/role/utils/hook.tsx
相关API:@/api/system/role、@/api/system/menu
改造依据:doc/plans/parallel-wishing-fiddle.md