• 基于antd实现动态修改节点的Tree组件


    前言

    之前遇到一个需求,可对于任意节点添加或删除子节点。首先技术栈是基于react+ant design,ant提供了Tree组件,但都是根据固定的数据渲染出树结构,如果需要新增或删除节点,官网并未提供。

    实现过程

    新增节点

    首先,要记录选中节点,在有选中的情况下点击全局的新增按钮,就相当于在选中的节点下新增子节点,否则直接在最外层节点添加新的节点(此时的情况就是有多个并列的根节点)。当然也可以直接点击节点出现下拉菜单,选择操作
    在这里插入图片描述

    然后,实现新增功能,在点击新增按钮之后,相应的节点位置出现输入框,按回车或者输入框失去焦点代表输入完成。找到插入位置,将新增的节点插入。

    输入状态:
    在这里插入图片描述
    输入完成后:
    在这里插入图片描述
    需要自定义节点,点击节点(ant Dropdown组件也支持右键)显示下拉弹窗。
    这里的DropdownInput是自定义的组件,因为需要校验输入内容

    // DropdownInput组件
    import { Dropdown, Input } from "antd";
    import React, {
      forwardRef,
      useEffect,
      useImperativeHandle,
      useRef,
      useState,
    } from "react";
    import type { InputProps } from "antd";
    import _ from "lodash";
    
    interface DropdownInputType extends InputProps {
      errorInfo?: string;
      initValue?: string;
    }
    
    const DropdownInputFun: React.ForwardRefRenderFunction<
      unknown,
      DropdownInputType
    > = (props, ref) => {
      const { errorInfo, initValue, onChange, onBlur, onPressEnter } = props;
      const [open, setOpen] = useState<boolean>(false);
      const [errorText, setErrorText] = useState<string>("请输入中英文数字及下划线");
      const [value, setValue] = useState<string>(""); // 值
    
      const inputRef = useRef<any>(null);
    
      useImperativeHandle(ref, () => inputRef?.current);
    
      useEffect(() => {
        if (initValue) setValue(initValue);
      }, [initValue]);
    
      useEffect(() => {
        if (errorInfo) setErrorText(errorInfo);
      }, [errorInfo]);
    
      /** 监听输入报错 */
      const handleChange = _.debounce((e: any, isSure = false) => {
        const { value } = e?.target;
        const reg = /^[a-zA-Z0-9_\u4e00-\u9fa5]+$/;
        if (!reg.test(value)) {
          setOpen(true);
        } else {
          setOpen(false);
          onChange?.(value);
        }
      }, 300);
    
      return (
        <Dropdown
          overlay={
            <div
              style={{
                background: "#fff",
                padding: "8px 12px",
                height: 20,
                boxShadow: "0px 2px 12px 0px rgba(0,0,0,0.06)",
              }}
            >
              {errorText}
            </div>
          }
          open={open}
        >
          <Input
            ref={inputRef}
            value={value}
            onChange={(e) => {
              e?.persist();
              setValue(e?.target?.value);
              handleChange(e);
            }}
            onBlur={(e) => {
              !open && onBlur?.(e);
            }}
            onPressEnter={(e: any) => {
              !open && onPressEnter?.(e);
            }}
            style={{ width: 272, borderColor: open ? "red" : "" }}
          />
        </Dropdown>
      );
    };
    const DropdownInput = forwardRef(DropdownInputFun);
    export default DropdownInput;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    
     // 自定义节点
      const titleRender = (node: any) => {
        const { title, icon, key, isInput } = node;
        const paddingLeft = 16 * (node.level - 1);
        if (isInput)
          return (
            <DropdownInput
              ref={refInput}
              initValue={title}
              onPressEnter={(e) => onEnter(e, node)}
              onBlur={(e) => onEnter(e, node)}
            />
          );
        return (
          <Dropdown overlay={() =>  (
            <Menu
              onClick={(e) => {
                if (e?.key === "add") addItem(node);
                if (e?.key === "edit") editItem(node);
                if (e?.key === "del") {
                  const data = mergeChildrenToParent1(treeData, node?.key);
                  setTreeData(data); // 更新树 数据
                }
              }}
            >
              <Menu.Item key="del">刪除</Menu.Item>
              <Menu.Item key="add">新增</Menu.Item>
              <Menu.Item key="edit">编辑</Menu.Item>
            </Menu>
          )} 
          trigger={["click"]}>
            <div
              key={key}
              style={{ paddingLeft, display: "flex" }}
              className="titleRoot"
            >
              {icon}&nbsp;&nbsp;
              <div>{title}</div>
            </div>
          </Dropdown>
        );
      };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    添加节点的addItem函数

    // 添加节点
      const addItem = (node: any) => {
        const len = _.isEmpty(node?.children) ? 0 : node?.children?.length;
        // 插入节点isInput为true,渲染节点的判断条件
        const newChild = _.isEmpty(node.children)
          ? [{ isInput: true, key: `${node?.key}-${len}` }]
          : [
              {
                isInput: true,
                key: `${node?.key}-${len}`,
              },
              ...node.children,
            ];
        const data = updateTreeData(treeData, node, newChild);
        setTreeData(data);
        const expands = expandedKeys?.includes(node?.key)
          ? expandedKeys
          : [node?.key, ...expandedKeys];
        setExpandedKeys(expands);
        setIsAdd(true);
      };
    
    const updateTreeData = (tree: any, target: any, children: any) => {
      return tree.map((node: any) => {
        if (node.key === target.key) {
          return { ...node, children };
        } else if (node?.children) {
          return {
            ...node,
            children: updateTreeData(node?.children, target, children),
          };
        }
        return node;
      });
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    输入完成后的onEnter函数

    // 监听添加节点的输入
      const onEnter = (e: any, node: any) => {
        const value = e?.target?.value;
    
        setIsAdd(false);
        if (!value) {
        // 输入内容为空就回车,直接删除编辑框的节点
          const dele = deleteNodeByKey(treeData, node?.key);
          setTreeData(dele);
          return;
        }
        // 有输入内容就跟新
        const data = updateItem(treeData, node?.key, value);
        setTreeData(data);
      };
    
    // deleteNodeByKey
    // 根据key 找到要删除的节点
    const deleteNodeByKey: any = (treeData: any, keyToDelete: string) => {
      return _.map(treeData, (node) => {
        if (node.key === keyToDelete) {
          // 如果节点的key匹配要删除的key,则返回undefined,表示不包括该节点
          return undefined;
        } else if (node.children) {
          // 如果节点有子节点,则递归处理子节点
          return {
            ...node,
            children: deleteNodeByKey(node.children, keyToDelete),
          };
        }
        return node; // 其他情况下返回原始节点
      }).filter(Boolean); // 过滤掉undefined的节点
    };
    
    // updateItem 
    // 根据key 找到正在输入的节点,将输入内容跟新到title(显示节点的名字),并删除之前的isInput属性
    const updateItem: any = (tree: any, key: string, data: any) => {
      return _.map(tree, (item: any) => {
        if (item?.key === key) {
          item.title = data;
          return _.omit(item, "isInput");
        } else if (item?.children) {
          return { ...item, children: updateItem(item?.children, key, data) };
        }
        return item;
      });
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47

    这样一个新增节点的功能就完成了。

    编辑节点

    有了上面的新增功能,编辑就简单多啦,在将节点替换成编辑框时,只需要带上节点的title为输入框的默认值
    在这里插入图片描述

     const editItem = (node: any) => {
        const data = editTreeItem(treeData, node?.key);
        setTreeData(data);
        setIsAdd(true);
      };
    
    // 节点呈编辑状态
    export const editTreeItem: any = (tree: any, key: string) => {
      return _.map(tree, (item: any) => {
        if (item?.key === key) {
          item.isInput = true;
          console.log("进来啦",item);
          
          return item;
        } else if (item?.children) {
          return { ...item, children: editTreeItem(item?.children, key) };
        }
        return item;
      });
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    后面的逻辑就和新增一样啦,监听输入框的回车和失焦事件,完成编辑功能。

    删除节点

    删除节点要考虑是否删除节点下的子节点,如果直接删除子节点,逻辑就简单了,如果需要把删除节点的子节点给删除节点父节点,需要额外处理

    // 直接删除
    const deleteNodeByKey: any = (treeData: any, keyToDelete: string) => {
      return _.map(treeData, (node) => {
        if (node.key === keyToDelete) {
          // 如果节点的key匹配要删除的key,则返回undefined,表示不包括该节点
          return undefined;
        } else if (node.children) {
          // 如果节点有子节点,则递归处理子节点
          return {
            ...node,
            children: deleteNodeByKey(node.children, keyToDelete),
          };
        }
        return node; // 其他情况下返回原始节点
      }).filter(Boolean); // 过滤掉undefined的节点
    };
    
    // 删除节点,子节点合并到上级
    const mergeChildrenToParent: any = (
      treeData: any,
      keyToDelete: string
    ) => {
      return _.flatMap(treeData, (node) => {
        if (node.key === keyToDelete) {
          // 如果节点的key匹配要删除的key
          if (node.children) {
            // 如果有子节点,将子节点合并到当前节点的父节点中
            const parent = _.find(treeData, (parentNode) => {
              return _.some(parentNode.children, { key: keyToDelete });
            });
    
            if (parent) {
              parent.children = [
                ...(parent.children || []),
                ...(node.children || []),
              ];
            }
            return undefined; // 返回undefined,表示删除当前节点
          } else {
            return undefined; // 如果没有子节点,直接删除当前节点
          }
        } else if (node.children) {
          // 如果节点有子节点,则递归处理子节点
          return {
            ...node,
            children: mergeChildrenToParent(node.children, keyToDelete),
          };
        }
        return node; // 其他情况下返回原始节点
      }).filter(Boolean); // 过滤掉undefined的节点
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51

    附上Tree组件。里面的函数,上面都有,就不一一写完成了

    import React, { useEffect, useRef, useState } from "react";
    import { Button, Dropdown, Menu, Tree } from "antd";
    import { DownOutlined } from "@ant-design/icons";
    import DropdownInput from "@/components/DropdownInput";
    
    const DemoTree = () => {
      const [visible, setVisible] = useState<boolean>(false);
      const [treeData, setTreeData] = useState([
        {
          title: "根节点1",
          key: "1-0",
          children: [
            {
              title: "子节点1",
              key: "1-0-0",
            },
            {
              title: "子节点2",
              key: "1-0-1",
            },
            {
              title: "子节点3",
              key: "1-0-2",
            },
          ],
        },
        {
          title: "根节点2",
          key: "2-1",
          children: [
            {
              title: "子节点4",
              key: "2-1-0",
            },
            {
              title: "子节点5",
              key: "2-1-1",
            },
          ],
        },
        {
          title: "根节点3",
          key: "3-1",
          children: [
            {
              title: "子节点6",
              key: "3-1-0",
              children:[{
                title:'jjj',
                key:'dfv'
              }]
            },
            {
              title: "子节点7",
              key: "3-1-1",
            },
          ],
        },
      ]);
      const refInput = useRef<any>(null);
      const [expandedKeys, setExpandedKeys] = useState<any[]>([]);
    
      const editItem = (node: any) => {};
    
      // 添加节点
      const addItem = (node: any) => {};
    
      // 监听添加节点的输入
      const onEnter = (e: any, node: any) => {};
    
      // 自定义节点
      const titleRender = (node: any) => {
        const { title, icon, key, isInput } = node;
        const paddingLeft = 16 * (node.level - 1);
        if (isInput)
          return (
            <DropdownInput
              ref={refInput}
              initValue={title}
              onPressEnter={(e) => onEnter(e, node)}
              onBlur={(e) => onEnter(e, node)}
            />
          );
        return (
          <Dropdown overlay={() =>(
            <Menu
              onClick={(e) => {
                if (e?.key === "add") addItem(node);
                if (e?.key === "edit") editItem(node);
                if (e?.key === "del") {
                  const data = mergeChildrenToParent1(treeData, node?.key);
                  setTreeData(data);
                }
              }}
            >
              <Menu.Item key="del">刪除</Menu.Item>
              <Menu.Item key="add">新增</Menu.Item>
              <Menu.Item key="edit">编辑</Menu.Item>
            </Menu>
          )} trigger={["click"]}>
            <div
              key={key}
              style={{ paddingLeft, display: "flex" }}
              className="titleRoot"
            >
              {icon}&nbsp;&nbsp;
              <div>{title}</div>
            </div>
          </Dropdown>
        );
      };
    
      return (
        <div>
          <Tree
            treeData={treeData}
            expandedKeys={expandedKeys}
            switcherIcon={<DownOutlined />}
            titleRender={titleRender}
            onExpand={(keys: any[]) => setExpandedKeys(keys)}
          />
        </div>
      );
    };
    export default DemoTree;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125

    本文仅供参考,个人观点。

  • 相关阅读:
    SpringBoot核心注解
    SMART PLC飞剪控制算法
    双飞翼布局和圣杯布局
    基于simulink的三相PWM电压型逆变器系统建模与仿真
    FG6223EUUD系列模块选型参考
    【深入浅出 Yarn 架构与实现】4-5 RM 行为探究 - 启动 ApplicationMaster
    nacos配置中心及服务注册中心使用
    金仓数据库 KingbaseES插件参考手册 B
    【数据结构】线性表(六)堆栈:顺序栈及其基本操作(初始化、判空、判满、入栈、出栈、存取栈顶元素、清空栈)
    Something is wrong with your installed virtualenv version: 15.1.0
  • 原文地址:https://blog.csdn.net/study_way/article/details/133876858