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

简介: 基于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;
 // 自定义节点
  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}  
          <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}  
          <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;

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

目录
相关文章
|
JavaScript
antd树形控件遇到的一些坑
antd树形控件遇到的一些坑
antd-procomponent中编辑表格动态数据设置的使用
antd-procomponent中编辑表格动态数据设置的使用
1004 0
|
10月前
|
人工智能 运维 监控
MCP 打通AI大模型与 Zabbix,运维新时代来了!
管志勇,高级软件开发工程师、OceanBase认证专家,深耕软件开发多年,专注Zabbix运维开发与数据可视化。本文介绍其如何通过MCP协议实现大模型与Zabbix的智能联动,打造高效运维新范式。
1370 14
|
JavaScript 前端开发 开发者
"震撼揭秘!ElementUI Table组件大变身,一键自定义升序降序按钮,排序操作秒变炫酷舞台,点击即燃,你的列表排序从此不再平凡!"
【8月更文挑战第16天】在Vue.js开发中,ElementUI的Table组件因其实用性和灵活性深受喜爱。本文通过一个后台管理系统用户列表的案例,展示了如何自定义添加“升序”和“降序”按钮以增强排序功能。首先确保安装Vue与ElementUI;接着定义表格数据及初始排序状态;利用scoped slot自定义列模板,加入按钮并绑定点击事件;最后实现在点击按钮时更新排序状态并重新排序数据的逻辑。这种方法不仅改善了用户体验,还为开发者提供了灵活调整排序逻辑的空间。
825 3
|
JavaScript 前端开发 开发者
Vue 自定义进度条组件封装及使用方法详解
这是一篇关于自定义进度条组件的使用指南和开发文档。文章详细介绍了如何在Vue项目中引入、注册并使用该组件,包括基础与高级示例。组件支持分段配置(如颜色、文本)、动画效果及超出进度提示等功能。同时提供了完整的代码实现,支持全局注册,并提出了优化建议,如主题支持、响应式设计等,帮助开发者更灵活地集成和定制进度条组件。资源链接已提供,适合前端开发者参考学习。
816 17
|
JavaScript API
Antd——如何给树形控件增加编辑删除
Antd——如何给树形控件增加编辑删除
779 0
|
存储 索引
antd中table组件选中单行换样式(比如背景颜色)
在Ant Design (antd)的Table组件中,可以通过设置`onRow`属性来定义行点击事件,从而改变被点击行的样式,如背景颜色。`onRow`是一个函数,返回一个对象,对象包含事件处理函数,如`onClick`。同时,使用`rowClassName`属性来指定行的类名,结合状态管理,可以实现点击某行后改变其背景颜色的效果。具体实现时,需要在组件状态中保存当前被点击行的索引,然后通过`rowClassName`判断并返回相应的类名。
1947 2
antd中table组件选中单行换样式(比如背景颜色)
antd table合并行或者列(动态添加合并行、列)
在Ant Design的Table组件中实现行或列的合并,通过动态计算数据源中的`rowSpan`或`colSpan`属性来控制,支持对特定字段进行行或列合并,包括多选框列的合并处理。
2548 3
antd table合并行或者列(动态添加合并行、列)
|
JSON JavaScript 数据格式
打印插件 hiprint 使用、回单打印PDF保存本地、将列表数据打印成pdf文件保存到本地
这篇文章介绍了如何使用hiprint打印插件将列表数据打印成PDF文件并保存到本地,包括插件的配置、依赖安装、项目代码案例以及如何预览和打印数据。
打印插件 hiprint 使用、回单打印PDF保存本地、将列表数据打印成pdf文件保存到本地
|
Java 开发者 Spring
Spring项目中Ordered接口的应用:全局过滤器(GlobalFilter)的顺序控制
Spring项目中Ordered接口的应用:全局过滤器(GlobalFilter)的顺序控制
1111 2