antd Table 实现树形交互操作(类似 tree select 交互)

前言

需求是:

  • table 按照树形结构展示
  • 每个节点有个开关操作 switch
  • 父节点勾选后,所有的子节点也勾选
  • 父节点取消勾选后,所有子节点也取消勾选
  • 子节点勾选后,其祖先节点也必须勾选
  • 子节点取消勾选后,需要判断最外层父节点是否取消勾选
    • 如果此时最外层父节点的所有子节点都取消勾选,则该最外层节点取消勾选;
    • 如果此时最外层父节点的任一子节点处于勾选,则不处理最外层节点的勾选状态

实现

整体流程

  • permissionTree 是 Table 的数据源
  • 每一项的 switch 的状态根据 record.checked 展示
  • 打开关闭 switch 去修改 permissionTree 相关节点的 checked

table 树形结构展示

  • 直接用 table 的 virtual api
  • expandPermissionKeys 是 switch 打开后,需要展开的项 id 集合
  • handleSetPermissionTreeSwitch 是 switch 切换时的回调
  • handleSavePermission 是调接口保存 swtich 后的权限
tsx 复制代码
<div className='w-[98%] ml-auto'>
      <AntdTable
        loading={runGetPermissionTree.loading || runSetPermission.loading}
        columns={[
          {
            title: '权限名称',
            dataIndex: 'menuName',
            key: 'menuName',
          },
          {
            title: '操作',
            width: 300,
            key: 'action',
            render: (_, record) => {
              return (
                <div className='flex justify-start items-center'>
                  <Switch
                    checkedChildren='开启'
                    unCheckedChildren='关闭'
                    checked={record.checked}
                    onChange={(checked: boolean) => {
                      // 切换开关的回调
                      handleSetPermissionTreeSwitch(checked, record);
                    }}
                  />
                </div>
              );
            },
          },
        ]}
        dataSource={searchPermissionTree}
        virtual
        scroll={{
          x: 1000,
          y: 580,
        }}
        rowKey='id'
        expandable={{
          childrenColumnName: 'childList',
          expandedRowKeys: expandPermissionKeys,
          onExpandedRowsChange: handleExpandRowsChange,
        }}
        pagination={false}
      />
      <div className='flex justify-end items-center mt-3'>
        <span className='text-red-500'>注意:在编辑完权限后请点击此按钮进行保存</span>
        <Button className='ml-3' type='primary' onClick={() => handleSavePermission()}>保存</Button>
      </div>
    </div>

界面显示:

handleSetPermissionTreeSwitch 处理逻辑

处理当前节点及其所有子节点

tsx 复制代码
function findAndModifyChildListNode(tree: any[], targetId: any, checked: boolean) {
  function traverse(node: any) {
    const newNode = { ...node };

    if ((newNode.id === targetId)) {
      newNode.checked = checked;
      // 当前节点如果存在子节点,则所有子节点的状态同步更改
      if (
        newNode.childList && Array.isArray(newNode.childList)
      ) {
        newNode.childList = dfsUnsetChecked(newNode.childList, checked);
      }
    } else {
      if (newNode.childList && Array.isArray(newNode.childList)) {
        newNode.childList = newNode.childList.map((child: any) => traverse(child));
      }
    }

    return newNode;
  }

  return tree.map((node) => traverse(node));
}

handleSetPermissionTreeSwitch(checked: boolean, record: any) {
      const { id } = record;
      // 处理当前节点及其子节点的 checked status
      const childListTree = findAndModifyChildListNode(
        // [...state.searchPermissionTree],
        [...state.permissionTree],
        id,
        checked,
      );
      
      // 后续操作
    },

处理当前节点的所有祖先节点

tsx 复制代码
const findAndModifyParentListNode = (tree: any[], targetId: any, checked: boolean) => {
  const newTree = [...tree];

  function traverseAndModifyAncestors(node: any, ancestors: any[] = []) {
    if (node.id !== targetId) {
      ancestors.push(node);
      if (node.childList && node.childList.length > 0) {
        node.childList.forEach((child: any) => {
          traverseAndModifyAncestors(child, [...ancestors]);
        });
      }
    } else {
      // 没有子节点时,回溯并修改祖先节点的状态
      ancestors.forEach((ancestor) => {
        // 如果是选中,则直接选
        if (checked) {
          ancestor.checked = true;
        } else {
          /**
           * 如果是取消选中,需要判断当前节点是不是最外层的节点,如果是,判断它的下一级子阶段是不是都取消勾选了
           * 如果都取消勾选了,则把当前这个最外层节点关掉,否则不处理
           */
          if (!ancestor.parentCode) {
            const isAllCancel = ancestor.childList.every((node: any) => !node.checked);
            ancestor.checked = isAllCancel ? false : true;
          }
        }
      });
    }
  }

  newTree.map((root) => {
    traverseAndModifyAncestors(root);
  });

  return newTree;
};
handleSetPermissionTreeSwitch(checked: boolean, record: any) {
      const { id } = record;
      // 处理当前节点及其子节点的 checked status
      // ...

      // 再处理处理当前节点祖先节点的 checked status,返回最终的 tree
      const finalTree = findAndModifyParentListNode(
        [...childListTree],
        id,
        checked,
      );
    },

根据处理后的结果,设置需要展开的 ids

tsx 复制代码
function collectCheckedIds(tree: any[]) {
  const result: any[] = [];

  function traverse(node: any) {
    if (node.checked) {
      result.push(node.id);
    }
    if (node.childList && node.childList.length > 0) {
      node.childList.forEach((child: any) => traverse(child));
    }
  }

  tree.forEach((node) => traverse(node));
  return result;
}
handleSetPermissionTreeSwitch(checked: boolean, record: any) {
      const { id } = record;
      // 处理当前节点及其子节点的 checked status
      // ...
      
      
      // 再处理处理当前节点祖先节点的 checked status,返回最终的 tree
      // ...
      
      
      // 根据 checked status 更改后,设置需要展开的项
      const ids: string[] = collectCheckedIds(finalTree);
    },

设置相关 state

tsx 复制代码
handleSetPermissionTreeSwitch(checked: boolean, record: any) {
      const { id } = record;
      // 处理当前节点及其子节点的 checked status
      // ...
      
      
      // 再处理处理当前节点祖先节点的 checked status,返回最终的 tree
      // ...
      
      
      // 根据 checked status 更改后,设置需要展开的项
      // ...
      
      // 设置状态
      setState((d) => {
        d.permissionTree = finalTree;
        d.searchPermissionTree = finalTree;
        d.expandPermissionKeys = ids;
      });
    },

最终结果

相关推荐
小墨宝16 分钟前
js 生成pdf 并上传文件
前端·javascript·pdf
HED31 分钟前
用扣子快速手撸人生中第一个AI智能应用!
前端·人工智能
DN金猿35 分钟前
使用npm install或cnpm install报错解决
前端·npm·node.js
丘山子36 分钟前
一些鲜为人知的 IP 地址怪异写法
前端·后端·tcp/ip
志存高远661 小时前
Kotlin 的 suspend 关键字
前端
www_pp_1 小时前
# 构建词汇表:自然语言处理中的关键步骤
前端·javascript·自然语言处理·easyui
天天扭码2 小时前
总所周知,JavaScript中有很多函数定义方式,如何“因地制宜”?(ˉ﹃ˉ)
前端·javascript·面试
一个专注写代码的程序媛2 小时前
为什么vue的key值,不用index?
前端·javascript·vue.js
장숙혜2 小时前
ElementUi的Dropdown下拉菜单的详细介绍及使用
前端·javascript·vue.js
火柴盒zhang2 小时前
websheet之 编辑器
开发语言·前端·javascript·编辑器·spreadsheet·websheet