这里分享的实现方法优点就是代码量少、简洁,不需要写一堆 if-else 语句、checked 和 halfChecked 赋值语句。
核心实现代码
以下是在 App.vue 中的关键代码,分为四个步骤:
-
1扩展 Permission 接口在接口中增加可选属性
selfPerm,用于记录该角色是否真正拥有此节点权限。 -
2checkRolePermissions — 初始化权限勾选遍历所有权限条目,同时给
checked和selfPerm赋值。 -
3checkParentNode — 更新父节点状态递归计算父节点应为全选还是半选,核心逻辑在此函数中。
-
4checkNode — 用户手动勾选时同步 selfPerm确保在前端交互修改权限时,
selfPerm也同步更新。
// 1、接口 Permission 中增加属性 selfPerm,?表示可选 interface Permission { id: number; name: string; menuType: string; children: Permission[]; checked: boolean; halfChecked: boolean; selfPerm?: boolean; } // 2、在函数(打√当前角色所拥有的权限)中增加 i.selfPerm = rp const checkRolePermissions = ( allPermissions: Permission[], rolePermissions: [], ) => { allPermissions.forEach((i) => { const rp = rolePermissions.includes(i.id); i.checked = rp; i.selfPerm = rp; if (i.children) { checkRolePermissions(i.children, rolePermissions); } }); }; // 3、更新父节点的选中状态(全选还是半选?) const checkParentNode = (arr: Permission[]) => { arr.forEach((p) => { if (p.children) { checkParentNode(p.children); const allCheck = p.children.every((c) => c.checked || c.halfChecked); const someCheck = p.children.some((c) => c.checked || c.halfChecked); p.checked = allCheck; p.halfChecked = !allCheck && (someCheck || !!p.selfPerm); } }); }; // 4、在实现打√的函数里面别忘记增加 node.selfPerm = isChecked const checkNode = (node: Permission, isChecked: boolean) => { node.checked = isChecked; node.selfPerm = isChecked; node.halfChecked = false; if (node.children) { node.children.forEach((i) => { checkNode(i, isChecked); }); } };
以"角色管理"为例解析
使用三层树形结构(如"角色管理")来解释上述代码:
需要清楚的是,我们约定只有父节点才有半选状态,比如"角色管理"和"角色信息"它们都是父节点,而"新增角色"没有子节点,所以"新增角色"是子节点。
解析 checkParentNode 函数
const checkParentNode = (arr: Permission[]) => { arr.forEach((p) => { if (p.children) { checkParentNode(p.children); const allCheck = p.children.every((c) => c.checked || c.halfChecked); const someCheck = p.children.some((c) => c.checked || c.halfChecked); p.checked = allCheck; p.halfChecked = !allCheck && (someCheck || !!p.selfPerm); } }); };
p 是一个 Permission 类型的对象(节点),p.children 是一个数组,里面存放的是 p 节点的所有直接子节点(比如"角色管理"节点的 children 数组里面只有"角色信息",而"角色信息"节点的 children 数组里面包括新增角色、编辑角色、删除角色、分配角色)。
p.children.every((c) => c.checked || c.halfChecked) 表示:如果父节点的所有(every)子节点都被选中(不管是全选 checked 还是半选 halfChecked),every 方法返回 true,否则为 false。
同理,p.children.some((c) => c.checked || c.halfChecked) 表示:如果父节点中部分(some)子节点被选中,some 方法返回 true,否则为 false。
为什么 every 和 some 中要加 || c.halfChecked?例如遍历到"角色管理"时,它的子节点"角色信息"是半选状态。
p.checked = allCheck
如果父节点 p 的所有子节点都被选中了(全选和半选均算选中),那么 allCheck 为 true,p 节点应该是打勾 ✔,而不是半选。
例如账号管理如图:
p.halfChecked = !allCheck && (someCheck || !!p.selfPerm)
这行代码表示:如果父节点 p 的所有子节点没有被全选(即 !allCheck),且 someCheck 为 true 或 !!p.selfPerm 为 true,那么父节点 p 应该为半选。
(someCheck || !!p.selfPerm) 的理解是:在不是全选的情况下,一种可能是父节点有部分子节点被选中(对应 someCheck 为 true);另一种可能是父节点没有任何子节点被选中,但自身有权限(对应 !!p.selfPerm 为 true),比如有"账号管理"目录权限但没有"用户信息"菜单权限。
这里使用三元运算符只是为了简洁,可能有些难以理解,当然也可以改写为 if-else 语句。
!!p.selfPerm 里的 !! 表示双重否定(即肯定),加 !! 只是为了避免 TypeScript 报红色波浪线,可以把 p.selfPerm 转换为 Boolean 类型,真假性质不变。
为什么需要 selfPerm 属性?
selfPerm 的目的是记录该角色是否拥有此权限:true 表示有,false 表示没有。
checkRolePermissions遍历所有权限时,通过const rp = rolePermissions.includes(i.id);和i.selfPerm = rp;给selfPerm赋值。- 在前端界面修改权限时,也需要更新
selfPerm的值。 - 不加
selfPerm会导致无法正确识别"有父节点权限但没有任何子节点权限"这种特殊情况。
比如店长角色拥有"内容管理"目录权限,但没有"新闻信息"等菜单权限。虽然这种情况在某些场景下不太常见,但仍需考虑: