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

news2025/3/9 23:01:00

前言

之前遇到一个需求,可对于任意节点添加或删除子节点。首先技术栈是基于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;


 // 自定义节点
  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>
    );
  };

添加节点的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;
  });
};

输入完成后的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;
  });
};

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

编辑节点

有了上面的新增功能,编辑就简单多啦,在将节点替换成编辑框时,只需要带上节点的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;
  });
};

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

删除节点

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

// 直接删除
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的节点
};

附上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;

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1102213.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

elementui中el-select和el-tree实现下拉树形多选功能

实现效果如下&#xff1a; 代码如下&#xff1a; html中 <el-col :lg"12"><el-form-item label"可用单位" prop"useOrgListTemp"><div class"departAll"><el-selectref"selectTree"v-model"valu…

BUUCTF题解之[极客大挑战 2019]Havefun 1

1.题目分析 使用浏览器开发者工具查看网页源码&#xff0c;查看疑似flag的代码。 &#xff08;特别是注释了的源码&#xff0c;一般是HTML,JS,PHP的源码&#xff09; 修改统一资源定位符URL访问服务器后端接口&#xff0c;拿到flag。 1.URL URL是统一资源定位符&#xff08;…

“torch.load“中出现的“Unexpected key(s) in state_dict“报错问题

问题&#xff1a; 解决&#xff1a; 添加strictFalse&#xff0c;允许加载过程中出现不匹配的键。但请注意,仍然需要确保模型中的主要参数能够正确加载&#xff0c;以确保模型的有效性。 model.load_state_dict(state_dict) # 改为&#xff1a; model.load_state_dict(state…

接口自动化测试之HttpRunner测试框架

引言 接口自动化测试的实现方案有很多&#xff0c;没有编程基础的可以使用 PostmanNewman 或 JmeterAnt 来实现&#xff0c;有编程基础的则可以结合自动化测试框架来实现。基于Python的测试框架有&#xff1a;Unittest、HttpRunner、Robot Framework、Pytest等&#xff0c;本文…

跨境电商系统商城源码定制开发的优势与需求

随着互联网的快速发展&#xff0c;跨境电商成为了全球贸易的重要方式之一。为了满足不同企业的需求&#xff0c;跨境电商系统商城源码定制开发应运而生。这种定制开发的方式可以帮助企业打造适合自己的电商系统&#xff0c;提供个性化的功能和服务&#xff0c;迎合不断变化的市…

报错解决——AttributeError: ‘OpenpyxlWriter‘ object has no attribute ‘save‘

完整报错 Traceback (most recent call last):File "track_half.py", line 249, in <module>main(opt,File "track_half.py", line 153, in mainEvaluator.save_summary(summary, os.path.join(result_root, summary_{}.xlsx.format(exp_name)))Fil…

生产管理电子看板在制造业中的成功案例分享

生产管理电子看板是一种重要的生产现场管理工具&#xff0c;在制造业中已经取得了许多成功的应用案例。它通过实时采集生产现场的数据&#xff0c;包括设备状态、生产进度、产品质量等信息&#xff0c;并将这些信息以图表、数字等形式显示出来&#xff0c;同时还可以通过声音、…

Python武器库开发-基础篇(一)

前言 以Python编程为主&#xff0c;围绕渗透测试展开的一门专栏。专栏内容包括&#xff1a; Python基础编程&#xff08;Python基础、语法、对象、文件操作&#xff0c;错误和异常&#xff09;&#xff0c;Python高级编程&#xff08;正则表达式、网络编程、WEB编程&#xff0…

如何选择国产压力测试工具?

随着互联网的飞速发展&#xff0c;软件应用的性能和稳定性变得愈发重要。无论是在线购物网站、社交媒体平台还是移动应用程序&#xff0c;用户都期望能够快速、流畅地访问和使用它们。为了确保应用程序在高负载下仍能够正常运行&#xff0c;压力测试工具变得至关重要。在国内&a…

13.56M刷卡芯片522/523智能门锁超低功耗读卡方案

传统的机械锁具已走过了近百年历史&#xff0c;其功能及性能几乎已诠释到了极致。但仍不能满足现代人们对锁具有高可靠性、高安全性、信息化、智能化的要求&#xff0c;自50年代末&#xff0c;半导体技术问世后&#xff0c;人们便将该技术应用于锁具上&#xff0c;衍生出了形形…

华为云云耀云服务器L实例评测|在云耀云服务器L实例上无人直播,增加睡后收入

购买云耀云服务器 L 实例 华为云耀云服务器 L 实例是一款轻量级云服务器&#xff0c;开通选择实例即可立刻使用&#xff0c;不需要用户再对服务器进行基础配置。新用户还有专享优惠&#xff0c;2 核心 2G 内存 3M 带宽的服务器只要 89 元/年&#xff0c;可以点击华为云云耀云服…

常见的数据结构及应用

文章目录 前言数据结构介绍数组链表队列和栈树堆 总结 前言 数据结构是计算机存储、组织数据的方式。在工作中&#xff0c;我们通常会直接使用已经封装好的集合API&#xff0c;这样可以更高效地完成任务。但是作为一名程序员&#xff0c;掌握数据结构是非常重要的&#xff0c;…

【EI会议征稿】2024年第四届人工智能、自动化与高性能计算国际会议(AIAHPC 2024)

2024年第四届人工智能、自动化与高性能计算国际会议&#xff08;AIAHPC 2024&#xff09; 2024 4th International Conference on Artificial Intelligence, Automation and High Performance Computing 2024第四届人工智能、自动化与高性能计算国际会议(AIAHPC 2024)将于202…

搭建伪分布式Hadoop

文章目录 一、Hadoop部署模式&#xff08;一&#xff09;独立模式&#xff08;二&#xff09;伪分布式模式&#xff08;三&#xff09;完全分布式模式 二、搭建伪分布式Hadoop&#xff08;一&#xff09;登录虚拟机&#xff08;二&#xff09;上传安装包&#xff08;三&#xf…

通过 Splashtop Enterprise 实现更高的效率

远程工作的出现不仅重塑了我们的传统工作模式&#xff0c;而且还使远程访问和支持解决方案在确保运营连续性和效率方面的关键作用浮出水面。 Splashtop Enterprise 已帮助企业实现远程工作和 IT 远程支持的无缝远程访问&#xff0c;并已被证明是许多组织不可或缺的工具。 在本…

综合电商商城小程序的作用是什么

综合电商顾名思义就是什么商品都卖&#xff0c;涵盖的品牌、种类、数量非常庞大&#xff0c;线下门店规模也相当可以&#xff0c;在同城场景中有较高的需求度。 然而在实际经营中&#xff0c;综合商品经营商家还是会面临一些痛点。 通过【雨科】平台搭建综合电商商城小程序全面…

NProgress进度条的使用

1 下载nprogress npm install --save nprogress 2.然后在 router/index.js里写上以下几行代码 import NProgress from "nprogress"; // 导入 nprogress模块import "nprogress/nprogress.css"; // 导入样式&#xff0c;否则看不到效果NProgress.configure(…

如何规避企业内文件流转泄密风险?

由于企业内部业务流程复杂&#xff0c;研发、生产、销售等跨部门的不同人员有时需要交互数据&#xff0c;而不同的文件涉密程度不同&#xff0c;需要由不同涉密等级的人员进行处理。 为保障核心资料的安全&#xff0c;文件阅读权限管控系统为用户提供了灵活的内部文件流转功能&…

CleanMyMac X4.14.3中文版:时尚元素与高效清理的完美结合!

嗨&#xff01;小仙女们~&#x1f338;今天小编给大家介绍一款超级时尚的Mac清理神器——CleanMyMac X4.14.3中文版&#xff01; 想要让你的电脑焕然一新&#xff0c;提升操作速度&#xff0c;还能拥有时尚元素的体验&#xff0c;那就赶紧来看看吧&#xff01; CleanMyMac X 2…

重生奇迹mu获取宠物的方法

现在很多游戏都有宠物&#xff0c;因为宠物的加入让游戏更加有趣&#xff0c;玩家可以带着宠物玩游戏&#xff0c;宠物还可以做出呆萌的鬼脸&#xff0c;让玩家感受到游戏的魅力&#xff0c;重生奇迹MU游戏也是有各种宠物。 在重生奇迹MU之中有各种宠物&#xff0c;这些宠物非…