react antd可编辑树

效果:
在这里插入图片描述

代码:

// index.tsx
import {
  ChangeEvent,
  forwardRef,
  useEffect,
  useImperativeHandle,
  useState,
} from "react";
import { Dropdown, Menu, Popconfirm, Tree, TreeProps, Input } from "antd";
import styles from "./index.module.scss";
import {
  CheckOutlined,
  CloseOutlined,
  EditOutlined,
  MinusOutlined,
  PlusOutlined,
} from "@ant-design/icons";
const { TreeNode } = Tree;

export interface TreeNodeType {
  value: string;
  defaultValue: string;
  key: string;
  isEditable: boolean;
  children?: any;
  title?: any;
  parentKey: string;
  type: number; // 0 根结点, 1 产品线, 2 子团队, 3 小组
}

const ADD_NODE_TYPE = {
  1: "产品线",
  2: "子团队",
  3: "小组",
};

const ADD_SUB_NODE_TYPE = {
  1: ["产品线"],
  2: ["子团队"],
  3: ["小组"],
};

interface Props {
  data: TreeNodeType[];
}

function EditableTree(props: Props, ref: any) {
  const { data: initData } = props;

  useImperativeHandle(ref, () => ({
    getTreeData: () => treeData,
  }));

  const [treeData, setTreeData] = useState<TreeNodeType[]>([]);
  const [expandedKeys, setExpandedKeys] = useState<React.Key[]>([]);

  useEffect(() => {
    if (initData) {
      setTreeData(initData);
    }
  }, [initData]);

  const onExpand = (expandedKeys: React.Key[]) => {
    console.log("onExpand", expandedKeys);
    setExpandedKeys(expandedKeys);
  };

  const renderAddMenu = (key: string, type: number) => {
    const items = ADD_SUB_NODE_TYPE[type];
    return (
      <Menu>
        {items.map((item: string) => (
          <Menu.Item key={item} onClick={() => onAdd(key, type)}>
            {item}
          </Menu.Item>
        ))}
      </Menu>
    );
  };

  const renderTreeNodes = (data: TreeNodeType[]) =>
    data.map((item: TreeNodeType) => {
      if (item.isEditable) {
        item.title = (
          <div className={styles.titleContainer}>
            <Input
              className={styles.inputField}
              value={item.value}
              onChange={(e) => onChangeInput(e, item.key)}
            />
            <span className={styles.operationField}>
              <CloseOutlined
                onClick={() => onClose(item.key, item.defaultValue)}
                style={{ marginLeft: 10 }}
              />
              <CheckOutlined
                onClick={() => onSave(item.key)}
                style={{ marginLeft: 10 }}
              />
            </span>
          </div>
        );
      } else {
        item.title = (
          <div className={styles.titleContainer}>
            <span style={{ wordBreak: "break-all" }}>{item.value}</span>
            <span className={styles.operationField}>
              <EditOutlined
                onClick={() => onEdit(item.key)}
                style={{ marginLeft: 10 }}
              />
              {item.type < 3 && (
                <Dropdown overlay={renderAddMenu(item.key, item.type + 1)}>
                  <PlusOutlined style={{ marginLeft: 10 }} />
                </Dropdown>
              )}
              {item.parentKey === "0" ? null : (
                <Popconfirm
                  title={`该组织【${item.value}】下的附属组织都会删除,是否确定删除?`}
                  onConfirm={() => onDelete(item.key)}
                  onCancel={() => {}}
                  okText="是"
                  cancelText="否"
                >
                  <MinusOutlined style={{ marginLeft: 10 }} />
                </Popconfirm>
              )}
            </span>
          </div>
        );
      }

      if (item.children) {
        return (
          <TreeNode title={item.title} key={item.key}>
            {renderTreeNodes(item.children)}
          </TreeNode>
        );
      }

      return <TreeNode {...item} />;
    });

  const onAdd = (key: string, type: number) => {
    console.log("add");
    if (expandedKeys.indexOf(key) === -1) {
      setExpandedKeys([...expandedKeys, key]);
    }
    setTreeData(addNode(key, treeData, type));
  };

  const addNode = (key: string, data: TreeNodeType[], type: number) =>
    data.map((item: TreeNodeType) => {
      if (item.key === key) {
        const defaultValue = {
          value: `${ADD_NODE_TYPE[type]}XXX`,
          defaultValue: `${ADD_NODE_TYPE[type]}XXX`,
          key: Math.random(), // 唯一 key
          parentKey: key,
          isEditable: false,
          type,
        };
        item.children
          ? item.children.push(defaultValue)
          : (item.children = [defaultValue]);
      } else {
        if (item.children) {
          addNode(key, item.children, type);
        }
      }
      return item;
    });

  const onDelete = (key: string) => {
    console.log("delete");
    // 为什么要加这一行,不然删除后数据不能回显到页面上
    setExpandedKeys(expandedKeys.filter((i) => i !== key));
    setTreeData(deleteNode(key, treeData));
  };

  const deleteNode = (key: string, data: TreeNodeType[]) => {
    data.forEach((item, index) => {
      if (item.key === key) {
        data.splice(index, 1);
      }
      if (item.children) {
        deleteNode(key, item.children);
      }
    });
    return data;
  };

  const onEdit = (key: string) => {
    console.log("edit");
    setTreeData(editNode(key, treeData));
  };

  const editNode = (key: string, data: TreeNodeType[]) =>
    data.map((item: TreeNodeType) => {
      if (item.key === key) {
        item.isEditable = true;
      } else {
        item.isEditable = false;
      }
      // 当某节点处于编辑状态,并改变数据,点击编辑其他节点时,此节点变成不可编辑状态,value 需要回退到 defaultvalue
      item.value = item.defaultValue;
      if (item.children) {
        editNode(key, item.children);
      }
      return item;
    });

  const onClose = (key: string, defaultValue: string) => {
    console.log("close");
    setTreeData(closeNode(key, defaultValue, treeData));
  };

  const closeNode = (key: string, defaultValue: string, data: TreeNodeType[]) =>
    data.map((item: TreeNodeType) => {
      item.isEditable = false;
      if (item.key === key) {
        item.value = defaultValue;
      }
      if (item.children) {
        closeNode(key, defaultValue, item.children);
      }
      return item;
    });

  const onSave = (key: string) => {
    console.log("save");
    setTreeData(saveNode(key, treeData));
  };

  const saveNode = (key: string, data: TreeNodeType[]) =>
    data.map((item: TreeNodeType) => {
      if (item.key === key) {
        item.defaultValue = item.value;
      }
      if (item.children) {
        saveNode(key, item.children);
      }
      item.isEditable = false;
      return item;
    });

  const onChangeInput = (event: ChangeEvent<HTMLInputElement>, key: string) => {
    console.log("onchange");
    setTreeData(changeNode(key, event.target.value, treeData));
  };

  const changeNode = (key: string, value: string, data: TreeNodeType[]) =>
    data.map((item: TreeNodeType) => {
      if (item.key === key) {
        item.value = value;
      }
      if (item.children) {
        changeNode(key, value, item.children);
      }
      return item;
    });

  const onDrop: TreeProps["onDrop"] = (info) => {
    console.log(info);
    const dropKey = info.node.key;
    const dragKey = info.dragNode.key;
    const dropPos = info.node.pos.split("-");
    const dropPosition =
      info.dropPosition - Number(dropPos[dropPos.length - 1]);

    const loop = (
      data: TreeNodeType[],
      key: React.Key,
      callback: (node: TreeNodeType, i: number, data: TreeNodeType[]) => void
    ) => {
      for (let i = 0; i < data.length; i++) {
        if (data[i].key === key) {
          return callback(data[i], i, data);
        }
        if (data[i].children) {
          loop(data[i].children!, key, callback);
        }
      }
    };
    const data = [...treeData];

    // Find dragObject
    let dragObj: TreeNodeType;
    loop(data, dragKey, (item, index, arr) => {
      arr.splice(index, 1);
      dragObj = item;
    });

    if (!info.dropToGap) {
      // Drop on the content
      loop(data, dropKey, (item) => {
        item.children = item.children || [];
        item.children.unshift(dragObj);
      });
    } else if (
      ((info.node as any).props.children || []).length > 0 && // Has children
      (info.node as any).props.expanded && // Is expanded
      dropPosition === 1 // On the bottom gap
    ) {
      loop(data, dropKey, (item) => {
        item.children = item.children || [];
        item.children.unshift(dragObj);
      });
    } else {
      let ar: TreeNodeType[] = [];
      let i: number;
      loop(data, dropKey, (_item, index, arr) => {
        ar = arr;
        i = index;
      });
      if (dropPosition === -1) {
        ar.splice(i!, 0, dragObj!);
      } else {
        ar.splice(i! + 1, 0, dragObj!);
      }
    }
    setTreeData(data);
  };

  return (
    <div style={{ width: 400 }}>
      {treeData.length > 0 && (
        <Tree
          //   expandedKeys={expandedKeys}
          //   onExpand={onExpand}
          defaultExpandAll={true}
          //   draggable
          //   onDrop={onDrop}
          blockNode
          selectable={false}
        >
          {renderTreeNodes(treeData)}
        </Tree>
      )}
    </div>
  );
}

export default forwardRef(EditableTree);
// index.module.scss
.inputField {
    border: none;
    border-bottom: 1px solid;
    background: none;
    line-height: normal;
}

.titleContainer{
    display: flex;
    justify-content: flex-start;
    .operationField{
        margin-left: auto;
        min-width: 100px;
    }
}
:global {
    .ant-tree li .ant-tree-node-content-wrapper:hover {
        background-color: unset;
    }
}

参考:https://blog.csdn.net/sujinchang939024/article/details/124719638,https://github.com/JerryMissTom/react-editable-tree

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值