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;
      });
    },

最终结果

相关推荐
张拭心1 小时前
2024 总结,我的停滞与觉醒
android·前端
念九_ysl1 小时前
深入解析Vue3单文件组件:原理、场景与实战
前端·javascript·vue.js
Jenna的海糖1 小时前
vue3如何配置环境和打包
前端·javascript·vue.js
星之卡比*2 小时前
前端知识点---库和包的概念
前端·harmonyos·鸿蒙
灵感__idea2 小时前
Vuejs技术内幕:数据响应式之3.x版
前端·vue.js·源码阅读
烛阴2 小时前
JavaScript 构造器进阶:掌握 “new” 的底层原理,写出更优雅的代码!
前端·javascript
Alan-Xia2 小时前
使用jest测试用例之入门篇
前端·javascript·学习·测试用例
浪遏2 小时前
面试官😏 :文本太长,超出部分用省略号 ,怎么搞?我:🤡
前端·面试
昕er2 小时前
CefSharp 文件下载和保存功能-监听前端事件
前端
@PHARAOH2 小时前
WHAT - Tree Shaking 的前提是 ES Module
前端·webpack·ecmascript