技术文档 · Vue 3 + TypeScript

父节点半选状态实现

用最少的代码,优雅地处理树形权限组件中父节点的全选与半选逻辑

Note

这里分享的实现方法优点就是代码量少、简洁,不需要写一堆 if-else 语句、checkedhalfChecked 赋值语句。

核心实现代码

以下是在 App.vue 中的关键代码,分为四个步骤:

App.vue TypeScript
// 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);
    });
  }
};

以"角色管理"为例解析

使用三层树形结构(如"角色管理")来解释上述代码:

Note

需要清楚的是,我们约定只有父节点才有半选状态,比如"角色管理"和"角色信息"它们都是父节点,而"新增角色"没有子节点,所以"新增角色"是子节点。

三层树形结构示意图
图1 — 三层树形权限结构示例(角色管理)

解析 checkParentNode 函数

checkParentNode TypeScript
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);
    }
  });
};
Note

p 是一个 Permission 类型的对象(节点),p.children 是一个数组,里面存放的是 p 节点的所有直接子节点(比如"角色管理"节点的 children 数组里面只有"角色信息",而"角色信息"节点的 children 数组里面包括新增角色、编辑角色、删除角色、分配角色)。

Note

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。

Note

为什么 everysome 中要加 || c.halfChecked?例如遍历到"角色管理"时,它的子节点"角色信息"是半选状态。

tips:"角色管理"为什么不是半选?因为"角色管理"只有一个子节点(角色信息),"角色信息"已经被选中(半选),所以"角色管理"的所有子节点都被选中了,那么"角色管理"节点应该是打勾 ✔,而不是半选。
tips:"角色信息"的半选表示当前角色(店长)拥有"角色信息"菜单权限,但是没有"角色信息"菜单下的任何子权限(按钮权限)。
角色信息半选示意图
图2 — 角色信息半选示意
Note

p.checked = allCheck

如果父节点 p 的所有子节点都被选中了(全选和半选均算选中),那么 allCheck 为 true,p 节点应该是打勾 ✔,而不是半选。

例如账号管理如图:

账号管理全选示意图
图3 — 账号管理节点全选示意

p.halfChecked = !allCheck && (someCheck || !!p.selfPerm)

这行代码表示:如果父节点 p 的所有子节点没有被全选(即 !allCheck),且 someCheck 为 true !!p.selfPerm 为 true,那么父节点 p 应该为半选。

(someCheck || !!p.selfPerm) 的理解是:在不是全选的情况下,一种可能是父节点有部分子节点被选中(对应 someCheck 为 true);另一种可能是父节点没有任何子节点被选中,但自身有权限(对应 !!p.selfPerm 为 true),比如有"账号管理"目录权限但没有"用户信息"菜单权限。

仅有父节点权限的半选示意图
图4 — 仅有父节点权限时的半选状态
Note

这里使用三元运算符只是为了简洁,可能有些难以理解,当然也可以改写为 if-else 语句。

!!p.selfPerm 里的 !! 表示双重否定(即肯定),加 !! 只是为了避免 TypeScript 报红色波浪线,可以把 p.selfPerm 转换为 Boolean 类型,真假性质不变。

为什么需要 selfPerm 属性?

Note

selfPerm 的目的是记录该角色是否拥有此权限:true 表示有,false 表示没有。

  • checkRolePermissions 遍历所有权限时,通过 const rp = rolePermissions.includes(i.id);i.selfPerm = rp;selfPerm 赋值。
  • 在前端界面修改权限时,也需要更新 selfPerm 的值。
  • 不加 selfPerm 会导致无法正确识别"有父节点权限但没有任何子节点权限"这种特殊情况。

比如店长角色拥有"内容管理"目录权限,但没有"新闻信息"等菜单权限。虽然这种情况在某些场景下不太常见,但仍需考虑:

只有父节点权限无子节点权限的特殊情况
图5 — 只有父节点权限、无子节点权限的特殊情况