基于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 (<Dropdownoverlay={<divstyle={{background: "#fff",padding: "8px 12px",height: 20,boxShadow: "0px 2px 12px 0px rgba(0,0,0,0.06)",}}>{errorText}</div>}open={open}><Inputref={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 (<DropdownInputref={refInput}initValue={title}onPressEnter={(e) => onEnter(e, node)}onBlur={(e) => onEnter(e, node)}/>);return (<Dropdown overlay={() =>  (<MenuonClick={(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"]}><divkey={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匹配要删除的keyif (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 (<DropdownInputref={refInput}initValue={title}onPressEnter={(e) => onEnter(e, node)}onBlur={(e) => onEnter(e, node)}/>);return (<Dropdown overlay={() =>(<MenuonClick={(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"]}><divkey={key}style={{ paddingLeft, display: "flex" }}className="titleRoot">{icon}&nbsp;&nbsp;<div>{title}</div></div></Dropdown>);};return (<div><TreetreeData={treeData}expandedKeys={expandedKeys}switcherIcon={<DownOutlined />}titleRender={titleRender}onExpand={(keys: any[]) => setExpandedKeys(keys)}/></div>);
};
export default DemoTree;

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

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

910数据结构(2013年真题)

算法设计题 问题1 已知元素数据类型为整数的顺序表SL&#xff08;a1,a2,…,am,b1,b2,…,bn&#xff09;&#xff0c;试设计算法将SL中元素的两部分互换为&#xff08;b1,b2,…,bn,a1,a2,…,am&#xff09;。要求&#xff1a;不能使用额外的数组空间。 &#xff08;1&#xff…

使用 Python 和蒙特卡罗计算未来股价走势以及历史波动率和隐含波动率

一、简介 预测金融市场是定量精度和全球经济细微差别的复杂融合。在这一探索中,蒙特卡罗模拟脱颖而出,成为首要的统计工具,指导我们对未来股票价格的理解。 这种方法以摩纳哥著名的蒙特卡洛赌场命名,并不依靠运气,而是植根于严格的概率模型。想象一下在受控环境中精心策划…

前端开发工具有哪些?17款前端工程师必备工具推荐!

软件开发是一个高度专业化的职业分工&#xff0c;根据所使用的编程语言的不同&#xff0c;会细分出多种岗位&#xff1a;前端开发、后端开发、客户端开发、iOS开发、Android开发、数据库开发等等&#xff0c;具体到每一个岗位&#xff0c;工作中常用的工具软件也存在着差别。 …

0基础学习VR全景平台篇 第108篇:全景图细节处理(下,航拍)

上课&#xff01;全体起立~ 大家好&#xff0c;欢迎观看蛙色官方系列全景摄影课程&#xff01; &#xff08;调色前图库&#xff09; &#xff08;原图-大图&#xff09; 一、导入文件 单击右下角导入按钮&#xff0c;选择航拍图片所在文件夹&#xff0c;选择图片&#xff0…

【前端】Js

目 录 一.前置知识第一个程序JavaScript 的书写形式注释输入输出 二.语法概览变量的使用理解 动态类型基本数据类型 三.运算符算术运算符赋值运算符 & 复合赋值运算符自增自减运算符比较运算符逻辑运算符位运算移位运算 四.条件语句if 语句三元表达式switch 五.循环语句whi…

基于php 进行每半小时钉钉预警

前言 业务场景&#xff1a;监控当前业务当出现并发情况时技术人员可以可以及时处理 使用技术栈&#xff1a; laravelredis 半小时触发一次报警信息实现思路 1、xshell脚本 具体参数就不详细解释了&#xff0c;想要详细了解可以自行百度 curl -H "Content-Type:appl…

论文阅读/写作扫盲

第一节&#xff1a;期刊科普 JCR分区和中科院分区是用于对期刊进行分类和评估的两种常见方法。它们的存在是为了帮助学术界和研究人员更好地了解期刊的学术质量、影响力和地位。 JCR分区&#xff08;Journal Citation Reports&#xff09;&#xff1a;JCR分区是由Clarivate Ana…

有哪些值得推荐的优秀 HTMLCSS 网站前端设计的网络资源(博客、论坛)?

前言 推荐几个有意思的CSS学习的网站和github上的学习类型的项目~ 网站推荐 1、CODEPEN 代码与所展示的页面相互对应&#xff0c;你可以在上面找到其他人已经写好的demo&#xff0c;参考 代码效果 网址&#xff1a;https://codepen.io 2、Coding Fantasy 通过游戏的形式来提…

多媒体应用设计师 第7章 多媒体数字压缩编码技术基础

1.多媒体数据压缩技术理论基础及压缩编码方法分类 必要性:大数据量的图像信息会给存储器的存储容量&#xff0c;通信线路的带宽&#xff0c;以及计算机的处理速度增加极大压力。如果没有多媒体编码压缩技术的发展&#xff0c;大容量图像&#xff0c;视频信息的存储和传输就难以…

自助查询小助手

嘿亲爱的老师们&#xff01;还在为成绩发布而烦恼吗&#xff1f;别担心&#xff0c;今天我们来聊聊如何利用免费的老师发布工具&#xff0c;让你轻松解决这一烦恼&#xff01; 成绩查询页面是什么&#xff1f;在很多学校里&#xff0c;成绩查询页面通常是一个网站或应用程序&am…

list.set交换数据需要(or不需要)添加其他中间变量,两个例子告诉你

说明&#xff1a;set()方法是来修改指定位置的元素。 两个参数&#xff0c;第一个参数是要修改的元素的索引&#xff0c;第二个参数是要设置的新值。 案例一&#xff1a;当链表中传入的是字符串时&#xff1a; public static void main(String[] args) {List list new Linke…

自动驾驶:控制算法概述

自动驾驶&#xff1a;控制算法概述 常见控制算法PID算法LQR算法MPC算法 自动驾驶控制算法横向控制纵向控制 参考文献 常见控制算法 PID算法 PID&#xff08;Proportional-Integral-Derivative&#xff09;控制是一种经典的反馈控制算法&#xff0c;通常用于稳定性和响应速度要…

docker数据卷+挂载(命令讲解+示例)

在容器中管理数据主要有两种方式&#xff1a; 数据卷&#xff08;Volumes&#xff09; 、挂载主机目录 (Bind mounts)。 一、数据卷 数据卷是一个可供一个或多个容器使用的特殊目录&#xff0c;可以在容器之间共享和重用。 特点&#xff1a; 对 数据卷 的修改会立马生效对 …

docker-compose部署elk(8.9.0)并开启ssl认证

docker部署elk并开启ssl认证 docker-compose部署elk部署所需yml文件 —— docker-compose-elk.yml部署配置elasticsearch和kibana并开启ssl配置基础数据认证配置elasticsearch和kibana开启https访问 配置logstash创建springboot项目进行测试kibana创建视图&#xff0c;查询日志…

Spring5应用之高级注解开发

作者简介&#xff1a;☕️大家好&#xff0c;我是Aomsir&#xff0c;一个爱折腾的开发者&#xff01; 个人主页&#xff1a;Aomsir_Spring5应用专栏,Netty应用专栏,RPC应用专栏-CSDN博客 当前专栏&#xff1a;Spring5应用专栏_Aomsir的博客-CSDN博客 文章目录 参考文献前言Conf…

那些年,我们追过的Java BUG

&#x1f337;&#x1f341; 博主猫头虎 带您 Go to New World.✨&#x1f341; &#x1f984; 博客首页——猫头虎的博客&#x1f390; &#x1f433;《面试题大全专栏》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33a; &a…

icg模块clock gating解析

// 两种形式&#xff1a; 与门形式 或门形式 三个用途&#xff1a; 用于关断时钟&#xff0c;降低功耗用于动态时钟切换是防止毛刺的产生用于时钟分频 解析&#xff1a;与门形式 解析&#xff1a;或门形式

安达发|制造企业生产排产现状和APS系统的解决方案

随着市场竞争的加剧&#xff0c;制造业企业面临着生产效率、成本控制和客户满意度等方面的巟大压力。在这种背景下&#xff0c;生产排产作为制造业的核心环节&#xff0c;对企业的生产经营具有重要意义。本文将针对制造业的生产排产现状进行分析&#xff0c;并提出相应的APS系统…

Android 远程调用服务之 AIDL

目录 一、AIDL 是什么&#xff1f;二、为什么要使用 AIDL&#xff1f; 1、使用 AIDL 是为了跨进程调用第三方服务&#xff1f; 2、使用 AIDL 是为了向第三方服务传输数据/参数&#xff1f; 3、使用 AIDL 是为了获取第三方服务直接或者异步返回的数据&#xff1f;三…

户外运动盛行,运动品牌如何利用软文推广脱颖而出?

全民健康意识的提升和城市居民对亲近自然的渴望带来户外运动的盛行&#xff0c;这也使运动品牌的市场保持强劲发展势头&#xff0c;那么在激烈的市场竞争中&#xff0c;运动品牌应该如何脱颖而出呢&#xff1f;下面就让媒介盒子告诉你&#xff01; 一、 分享户外运动干货 用户…