效果:
代码:
// 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