EditableTree.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. import React, { useState } from "react";
  2. import { useEffect } from "react";
  3. import { message, Modal, Tree } from "antd";
  4. import type { DataNode, TreeProps } from "antd/es/tree";
  5. import type { Key } from "antd/lib/table/interface";
  6. import { DeleteOutlined, SaveOutlined } from "@ant-design/icons";
  7. import { FileAddOutlined, LinkOutlined } from "@ant-design/icons";
  8. import { Button, Divider, Space } from "antd";
  9. import { useIntl } from "react-intl";
  10. import EditableTreeNode from "./EditableTreeNode";
  11. import { randomString } from "../../utils";
  12. export interface TreeNodeData {
  13. key: string;
  14. id: string;
  15. title: string | React.ReactNode;
  16. title_text?: string;
  17. icon?: React.ReactNode;
  18. children: TreeNodeData[];
  19. status?: number;
  20. deletedAt?: string | null;
  21. level: number;
  22. }
  23. export type ListNodeData = {
  24. key: string;
  25. title: string | React.ReactNode;
  26. title_text?: string;
  27. level: number;
  28. status?: number;
  29. children?: number;
  30. deletedAt?: string | null;
  31. };
  32. let tocActivePath: TreeNodeData[] = [];
  33. function tocGetTreeData(articles: ListNodeData[], active = "") {
  34. const treeData = [];
  35. const treeParents = [];
  36. const rootNode: TreeNodeData = {
  37. key: randomString(),
  38. id: "0",
  39. title: "root",
  40. title_text: "root",
  41. level: 0,
  42. children: [],
  43. };
  44. treeData.push(rootNode);
  45. let lastInsNode: TreeNodeData = rootNode;
  46. let iCurrLevel = 0;
  47. const keys: string[] = [];
  48. for (let index = 0; index < articles.length; index++) {
  49. const element = articles[index];
  50. const newNode: TreeNodeData = {
  51. key: randomString(),
  52. id: element.key,
  53. title: element.title,
  54. title_text: element.title_text,
  55. children: [],
  56. icon: keys.includes(element.key) ? <LinkOutlined /> : undefined,
  57. status: element.status,
  58. level: element.level,
  59. deletedAt: element.deletedAt,
  60. };
  61. if (!keys.includes(element.key)) {
  62. keys.push(element.key);
  63. }
  64. /*
  65. if (active == element.article) {
  66. newNode["extraClasses"] = "active";
  67. }
  68. */
  69. if (newNode.level > iCurrLevel) {
  70. //新的层级比较大,为上一个的子目录
  71. treeParents.push(lastInsNode);
  72. lastInsNode.children.push(newNode);
  73. } else if (newNode.level === iCurrLevel) {
  74. //目录层级相同,为平级
  75. treeParents[treeParents.length - 1].children.push(newNode);
  76. } else {
  77. // 小于 挂在上一个层级
  78. while (treeParents.length > 1) {
  79. treeParents.pop();
  80. if (treeParents[treeParents.length - 1].level < newNode.level) {
  81. break;
  82. }
  83. }
  84. treeParents[treeParents.length - 1].children.push(newNode);
  85. }
  86. lastInsNode = newNode;
  87. iCurrLevel = newNode.level;
  88. if (active === element.key) {
  89. tocActivePath = [];
  90. for (let index = 1; index < treeParents.length; index++) {
  91. tocActivePath.push(treeParents[index]);
  92. }
  93. }
  94. }
  95. return treeData[0].children;
  96. }
  97. function treeToList(treeNode: TreeNodeData[]): ListNodeData[] {
  98. let iTocTreeCurrLevel = 1;
  99. const arrTocTree: ListNodeData[] = [];
  100. for (const iterator of treeNode) {
  101. getTreeNodeData(iterator);
  102. }
  103. function getTreeNodeData(node: TreeNodeData) {
  104. let children = 0;
  105. if (typeof node.children != "undefined") {
  106. children = node.children.length;
  107. }
  108. arrTocTree.push({
  109. key: node.id,
  110. title: node.title,
  111. title_text: node.title_text,
  112. level: iTocTreeCurrLevel,
  113. children: children,
  114. deletedAt: node.deletedAt,
  115. });
  116. if (children > 0) {
  117. iTocTreeCurrLevel++;
  118. for (const iterator of node.children) {
  119. getTreeNodeData(iterator);
  120. }
  121. iTocTreeCurrLevel--;
  122. }
  123. }
  124. return arrTocTree;
  125. }
  126. interface IWidget {
  127. treeData: ListNodeData[];
  128. addFileButton?: React.ReactNode;
  129. addOnArticle?: TreeNodeData;
  130. updatedNode?: TreeNodeData;
  131. onChange?: Function;
  132. onSelect?: Function;
  133. onSave?: Function;
  134. onAddFile?: Function;
  135. onAppend?: Function;
  136. onTitleClick?: Function;
  137. }
  138. const EditableTreeWidget = ({
  139. treeData,
  140. addFileButton,
  141. addOnArticle,
  142. updatedNode,
  143. onChange,
  144. onSelect,
  145. onSave,
  146. onAppend,
  147. onTitleClick,
  148. }: IWidget) => {
  149. const intl = useIntl();
  150. const [checkKeys, setCheckKeys] = useState<string[]>([]);
  151. const [checkNodes, setCheckNodes] = useState<TreeNodeData[]>([]);
  152. const [gData, setGData] = useState<TreeNodeData[]>([]);
  153. const [listTreeData, setListTreeData] = useState<ListNodeData[]>();
  154. const [keys, setKeys] = useState<Key>("");
  155. useEffect(() => {
  156. if (typeof onChange !== "undefined") {
  157. onChange(listTreeData);
  158. }
  159. }, [listTreeData]);
  160. useEffect(() => {
  161. //找到节点并更新
  162. if (typeof updatedNode === "undefined") {
  163. return;
  164. }
  165. const update = (_node: TreeNodeData[]) => {
  166. _node.forEach((value, index, array) => {
  167. if (value.id === updatedNode.id) {
  168. array[index].title = updatedNode.title;
  169. array[index].title_text = updatedNode.title_text;
  170. console.log("key found");
  171. return;
  172. } else {
  173. update(array[index].children);
  174. }
  175. return;
  176. });
  177. };
  178. const newTree = [...gData];
  179. update(newTree);
  180. setGData(newTree);
  181. const list = treeToList(newTree);
  182. setListTreeData(list);
  183. }, [updatedNode]);
  184. const appendNode = (key: string, node: TreeNodeData) => {
  185. console.log("key", key);
  186. const append = (_node: TreeNodeData[]) => {
  187. _node.forEach((value, index, array) => {
  188. if (value.key === key) {
  189. array[index].children.push(node);
  190. console.log("key found");
  191. return;
  192. } else {
  193. append(array[index].children);
  194. }
  195. return;
  196. });
  197. };
  198. const newTree = [...gData];
  199. append(newTree);
  200. setGData(newTree);
  201. const list = treeToList(newTree);
  202. setListTreeData(list);
  203. };
  204. useEffect(() => {
  205. if (typeof addOnArticle === "undefined") {
  206. return;
  207. }
  208. console.log("add ", addOnArticle);
  209. const newTreeData = [...gData, addOnArticle];
  210. setGData(newTreeData);
  211. const list = treeToList(newTreeData);
  212. setListTreeData(list);
  213. }, [addOnArticle]);
  214. useEffect(() => {
  215. const data = tocGetTreeData(treeData);
  216. console.log("tree data", data);
  217. setGData(data);
  218. }, [treeData]);
  219. const onCheck: TreeProps["onCheck"] = (checkedKeys, info) => {
  220. console.log("onCheck", checkedKeys, info);
  221. setCheckKeys(checkedKeys as string[]);
  222. setCheckNodes(info.checkedNodes as TreeNodeData[]);
  223. };
  224. const onDragEnter: TreeProps["onDragEnter"] = (info) => {
  225. console.log(info);
  226. // expandedKeys 需要受控时设置
  227. // setExpandedKeys(info.expandedKeys)
  228. };
  229. const onDrop: TreeProps["onDrop"] = (info) => {
  230. console.log(info);
  231. const dropKey = info.node.key;
  232. const dragKey = info.dragNode.key;
  233. const dropPos = info.node.pos.split("-");
  234. const dropPosition =
  235. info.dropPosition - Number(dropPos[dropPos.length - 1]);
  236. const loop = (
  237. data: DataNode[],
  238. key: React.Key,
  239. callback: (node: DataNode, i: number, data: DataNode[]) => void
  240. ) => {
  241. for (let i = 0; i < data.length; i++) {
  242. if (data[i].key === key) {
  243. return callback(data[i], i, data);
  244. }
  245. if (data[i].children) {
  246. loop(data[i].children!, key, callback);
  247. }
  248. }
  249. };
  250. const data = [...gData];
  251. // Find dragObject
  252. let dragObj: DataNode;
  253. loop(data, dragKey, (item, index, arr) => {
  254. arr.splice(index, 1);
  255. dragObj = item;
  256. });
  257. if (!info.dropToGap) {
  258. // Drop on the content
  259. loop(data, dropKey, (item) => {
  260. item.children = item.children || [];
  261. // where to insert 示例添加到头部,可以是随意位置
  262. item.children.unshift(dragObj);
  263. });
  264. } else if (
  265. ((info.node as any).props.children || []).length > 0 && // Has children
  266. (info.node as any).props.expanded && // Is expanded
  267. dropPosition === 1 // On the bottom gap
  268. ) {
  269. loop(data, dropKey, (item) => {
  270. item.children = item.children || [];
  271. // where to insert 示例添加到头部,可以是随意位置
  272. item.children.unshift(dragObj);
  273. // in previous version, we use item.children.push(dragObj) to insert the
  274. // item to the tail of the children
  275. });
  276. } else {
  277. let ar: DataNode[] = [];
  278. let i: number;
  279. loop(data, dropKey, (_item, index, arr) => {
  280. ar = arr;
  281. i = index;
  282. });
  283. if (dropPosition === -1) {
  284. ar.splice(i!, 0, dragObj!);
  285. } else {
  286. ar.splice(i! + 1, 0, dragObj!);
  287. }
  288. }
  289. setGData(data);
  290. const list = treeToList(data);
  291. setListTreeData(list);
  292. };
  293. return (
  294. <>
  295. <Space>
  296. {addFileButton}
  297. <Button
  298. icon={<FileAddOutlined />}
  299. onClick={async () => {
  300. if (typeof onAppend !== "undefined") {
  301. const newNode = await onAppend({
  302. key: "",
  303. title: "",
  304. children: [],
  305. level: 0,
  306. });
  307. console.log("newNode", newNode);
  308. if (newNode) {
  309. const append = [...gData, newNode];
  310. setGData(append);
  311. const list = treeToList(append);
  312. setListTreeData(list);
  313. return true;
  314. } else {
  315. message.error("添加失败");
  316. return false;
  317. }
  318. } else {
  319. return false;
  320. }
  321. }}
  322. >
  323. {intl.formatMessage({ id: "buttons.create" })}
  324. </Button>
  325. <Button
  326. icon={<DeleteOutlined />}
  327. danger
  328. disabled={checkKeys.length === 0}
  329. onClick={() => {
  330. const delTree = (node: TreeNodeData[]): boolean => {
  331. for (let index = 0; index < node.length; index++) {
  332. if (checkKeys.includes(node[index].key)) {
  333. node.splice(index, 1);
  334. return true;
  335. } else {
  336. const cf = delTree(node[index].children);
  337. if (cf) {
  338. return cf;
  339. }
  340. }
  341. }
  342. return false;
  343. };
  344. Modal.confirm({
  345. title: "从文集移除下列文章吗?(文章不会被删除)",
  346. content: (
  347. <>
  348. {checkNodes.map((item, id) => (
  349. <div key={id}>
  350. {id + 1} {item.title}
  351. </div>
  352. ))}
  353. </>
  354. ),
  355. onOk() {
  356. const tmp = [...gData];
  357. const find = delTree(tmp);
  358. console.log("delete", keys, find, tmp);
  359. setGData(tmp);
  360. const list = treeToList(tmp);
  361. setListTreeData(list);
  362. },
  363. });
  364. }}
  365. >
  366. {intl.formatMessage({ id: "buttons.remove" })}
  367. </Button>
  368. <Button
  369. icon={<SaveOutlined />}
  370. onClick={() => {
  371. if (typeof onSave !== "undefined") {
  372. onSave(listTreeData);
  373. }
  374. }}
  375. type="primary"
  376. >
  377. {intl.formatMessage({ id: "buttons.save" })}
  378. </Button>
  379. </Space>
  380. <Divider></Divider>
  381. <Tree
  382. showLine
  383. showIcon
  384. checkable
  385. rootClassName="draggable-tree"
  386. draggable
  387. blockNode
  388. selectable={false}
  389. onDragEnter={onDragEnter}
  390. onDrop={onDrop}
  391. onCheck={onCheck}
  392. onSelect={(selectedKeys: Key[]) => {
  393. if (selectedKeys.length > 0) {
  394. setKeys(selectedKeys[0]);
  395. } else {
  396. setKeys("");
  397. }
  398. if (typeof onSelect !== "undefined") {
  399. onSelect(selectedKeys);
  400. }
  401. }}
  402. treeData={gData}
  403. titleRender={(node: TreeNodeData) => {
  404. return (
  405. <EditableTreeNode
  406. node={node}
  407. onAdd={async () => {
  408. if (typeof onAppend !== "undefined") {
  409. const newNode = await onAppend(node);
  410. console.log("newNode", newNode);
  411. if (newNode) {
  412. appendNode(node.key, newNode);
  413. return true;
  414. } else {
  415. message.error("添加失败");
  416. return false;
  417. }
  418. } else {
  419. return false;
  420. }
  421. }}
  422. onTitleClick={(e: React.MouseEvent<HTMLElement, MouseEvent>) => {
  423. if (typeof onTitleClick !== "undefined") {
  424. onTitleClick(e, node);
  425. }
  426. }}
  427. />
  428. );
  429. }}
  430. />
  431. </>
  432. );
  433. };
  434. export default EditableTreeWidget;