mindmapHander.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. import { TopicType } from "@/enum";
  2. import { addTopic, getMindMapProjectByLocal } from "@/pages/mindmap/mindMap";
  3. import { MindMapProjectInfo, TopicItem } from "@/types";
  4. import { Cell, Graph, Node, Edge } from "@antv/x6";
  5. import { message } from "antd";
  6. import { cloneDeep } from "lodash-es";
  7. import { uuid } from "@/utils";
  8. import { ContextMenuTool } from "./contentMenu";
  9. import { MutableRefObject } from "react";
  10. import { exportImage } from "@/components/ExportImage";
  11. export const selectTopic = (graph: Graph, topic?: TopicItem) => {
  12. if (topic?.id) {
  13. setTimeout(() => {
  14. graph.resetSelection(topic.id);
  15. const node = graph.getCellById(topic?.id);
  16. node?.isNode() && graph.createTransformWidget(node);
  17. }, 100);
  18. }
  19. };
  20. export const selectTopics = (graph: Graph, topics?: TopicItem[]) => {
  21. setTimeout(() => {
  22. graph.resetSelection(topics?.map((item) => item.id));
  23. topics?.forEach((item) => {
  24. const node = graph.getCellById(item.id);
  25. node?.isNode() && graph.createTransformWidget(node);
  26. });
  27. }, 100);
  28. };
  29. /**
  30. * 添加同级主题
  31. * @param node
  32. * @param graph
  33. */
  34. export const addPeerTopic = (
  35. node: Cell,
  36. graph: Graph,
  37. setMindProjectInfo?: (info: MindMapProjectInfo) => void
  38. ) => {
  39. if (!setMindProjectInfo) return;
  40. const parentNode =
  41. node.data.type === TopicType.main || !node.data?.parentId
  42. ? node
  43. : graph.getCellById(node.data.parentId);
  44. const type =
  45. node.data.type === TopicType.main ? TopicType.branch : node.data.type;
  46. if (parentNode?.isNode()) {
  47. const topic = addTopic(type, setMindProjectInfo, parentNode);
  48. selectTopic(graph, topic);
  49. }
  50. };
  51. /**
  52. * 添加子主题
  53. * @param node
  54. * @param setMindProjectInfo
  55. * @param graph
  56. */
  57. export const addChildrenTopic = (
  58. node: Node,
  59. setMindProjectInfo?: (info: MindMapProjectInfo) => void,
  60. graph?: Graph
  61. ) => {
  62. if (!setMindProjectInfo) return;
  63. const type =
  64. node.data?.type === TopicType.main ? TopicType.branch : TopicType.sub;
  65. const topic = addTopic(type, setMindProjectInfo, node);
  66. graph && selectTopic(graph, topic);
  67. };
  68. /**
  69. * 添加父主题
  70. * @param node
  71. * @param setMindProjectInfo
  72. * @param graph
  73. */
  74. export const addParentTopic = (
  75. node: Node,
  76. setMindProjectInfo?: (info: MindMapProjectInfo) => void,
  77. graph?: Graph
  78. ) => {
  79. if (!setMindProjectInfo || !node.data?.parentId) return;
  80. const type =
  81. node.data?.type === TopicType.branch ? TopicType.branch : TopicType.sub;
  82. const parentNode = graph?.getCellById(node.data.parentId);
  83. // 删除原来的数据
  84. deleteTopics([node.data.id], setMindProjectInfo);
  85. // 加入新的父主题
  86. const topic = addTopic(type, setMindProjectInfo, parentNode as Node, {
  87. children: [
  88. {
  89. ...node.data,
  90. },
  91. ],
  92. });
  93. graph && selectTopic(graph, topic);
  94. };
  95. /**
  96. * 删除子主题
  97. * @param ids
  98. * @param setMindProjectInfo
  99. */
  100. export const deleteTopics = (
  101. ids: string[],
  102. setMindProjectInfo?: (info: MindMapProjectInfo) => void
  103. ) => {
  104. const mindProjectInfo = getMindMapProjectByLocal();
  105. if (!mindProjectInfo || !setMindProjectInfo) return;
  106. const topics = cloneDeep(mindProjectInfo.topics);
  107. const filterTopics = (list: TopicItem[]): TopicItem[] => {
  108. const result: TopicItem[] = [];
  109. for (const item of list) {
  110. if (!ids.includes(item.id) || item.type === TopicType.main) {
  111. if (item.children?.length) {
  112. item.children = filterTopics(item.children);
  113. }
  114. result.push(item);
  115. }
  116. }
  117. return result;
  118. };
  119. mindProjectInfo.topics = filterTopics(topics);
  120. setMindProjectInfo(mindProjectInfo); // TODO 这个方法删除更新有问题
  121. localStorage.setItem("minMapProjectInfo", JSON.stringify(mindProjectInfo));
  122. };
  123. /**
  124. * 删除当前主题
  125. * @param graph
  126. * @param nodes
  127. */
  128. export const handleDeleteCurrentTopic = (
  129. graph: Graph,
  130. nodes: Node[],
  131. ) => {
  132. const mindProjectInfo = getMindMapProjectByLocal();
  133. if (!mindProjectInfo) return;
  134. nodes.forEach((node) => {
  135. if (node.data.parentId) {
  136. traverseNode(mindProjectInfo.topics, (topic) => {
  137. if (topic.id === node.data.parentId) {
  138. const index = topic.children?.findIndex(
  139. (item) => item.id === node.id
  140. );
  141. if (typeof index === "number" && index >= 0) {
  142. const newChildren = (node.data.children || []).map(
  143. (childNode: TopicItem) => {
  144. return {
  145. ...childNode,
  146. type: topic.type === TopicType.main ? TopicType.branch : TopicType.sub,
  147. parentId: topic.id,
  148. };
  149. }
  150. );
  151. (topic.children || []).splice(
  152. index,
  153. 1,
  154. ...newChildren
  155. );
  156. }
  157. }
  158. });
  159. }
  160. console.log(mindProjectInfo)
  161. // @ts-ignore
  162. graph?.extendAttr?.setMindProjectInfo?.(mindProjectInfo);
  163. localStorage.setItem("minMapProjectInfo", JSON.stringify(mindProjectInfo));
  164. });
  165. };
  166. /**
  167. * 执行粘贴
  168. * @param graph
  169. * @param setMindProjectInfo
  170. */
  171. export const handleMindmapPaste = (
  172. graph: Graph,
  173. setMindProjectInfo: (info: MindMapProjectInfo) => void
  174. ) => {
  175. // 读取剪切板数据
  176. navigator.clipboard.read().then((items) => {
  177. console.log("剪切板内容:", items);
  178. const currentNode = graph.getSelectedCells().find((cell) => cell.isNode());
  179. if (!currentNode) {
  180. message.warning("请先选择一个主题");
  181. return;
  182. }
  183. const item = items?.[0];
  184. if (item) {
  185. /**读取图片数据 */
  186. if (item.types[0] === "image/png") {
  187. item.getType("image/png").then((blob) => {
  188. const reader = new FileReader();
  189. reader.readAsDataURL(blob);
  190. reader.onload = function (event) {
  191. const dataUrl = event.target?.result as string;
  192. // 获取图片大小
  193. const img = new Image();
  194. img.src = dataUrl;
  195. img.onload = function () {
  196. const width = img.width;
  197. const height = img.height;
  198. // 插入图片
  199. currentNode.setData({
  200. extraModules: {
  201. type: "image",
  202. data: {
  203. imageUrl: dataUrl,
  204. width,
  205. height,
  206. },
  207. },
  208. });
  209. };
  210. };
  211. });
  212. }
  213. /**读取文本数据 */
  214. if (item.types[0] === "text/plain") {
  215. item.getType("text/plain").then((blob) => {
  216. const reader = new FileReader();
  217. reader.readAsText(blob);
  218. reader.onload = function (event) {
  219. const text = event.target?.result as string;
  220. // 内部复制方法
  221. if (text === " ") {
  222. const nodes = localStorage.getItem("mindmap-copy-data");
  223. if (nodes) {
  224. JSON.parse(nodes)?.forEach((node: Node) => {
  225. const data = node.data;
  226. // 修改新的数据嵌套
  227. data.id = uuid();
  228. data.parentId = currentNode.id;
  229. if (data.children?.length) {
  230. data.children = traverseCopyData(data.children, data.id);
  231. }
  232. addTopic(
  233. currentNode.data?.type === TopicType.main
  234. ? TopicType.branch
  235. : TopicType.sub,
  236. setMindProjectInfo,
  237. currentNode,
  238. { ...data }
  239. );
  240. });
  241. }
  242. } else {
  243. const topic = addTopic(
  244. currentNode.data?.type === TopicType.main
  245. ? TopicType.branch
  246. : TopicType.sub,
  247. setMindProjectInfo,
  248. currentNode,
  249. { label: text }
  250. );
  251. selectTopic(graph, topic);
  252. }
  253. };
  254. });
  255. }
  256. }
  257. });
  258. };
  259. const traverseCopyData = (list: TopicItem[], parentId: string): TopicItem[] => {
  260. return list.map((item) => {
  261. item.id = uuid();
  262. item.parentId = parentId;
  263. if (item.children?.length) {
  264. item.children = traverseCopyData(item.children, item.id);
  265. }
  266. return item;
  267. });
  268. };
  269. /**
  270. * 遍历主题树
  271. * @param topics
  272. * @param callback
  273. * @returns
  274. */
  275. export const traverseNode = (
  276. topics: TopicItem[],
  277. callback: (topic: TopicItem, index: number) => void
  278. ): TopicItem[] => {
  279. return topics.map((topic, index) => {
  280. callback && callback(topic, index);
  281. if (topic.children?.length) {
  282. topic.children = traverseNode(topic.children, callback);
  283. }
  284. return topic;
  285. });
  286. };
  287. // 关联线
  288. const handleCorrelation = (
  289. e: MouseEvent,
  290. correlationEdgeRef: MutableRefObject<Edge | undefined>,
  291. graph: Graph
  292. ) => {
  293. if (correlationEdgeRef.current) {
  294. const point = graph?.clientToLocal(e.x, e.y);
  295. point && correlationEdgeRef.current?.setTarget(point);
  296. }
  297. };
  298. /**
  299. * 添加关联线
  300. * @param graph
  301. * @param correlationEdgeRef
  302. * @param sourceNode
  303. */
  304. export const handleCreateCorrelationEdge = (
  305. graph: Graph,
  306. correlationEdgeRef: MutableRefObject<Edge | undefined>,
  307. sourceNode?: Node
  308. ) => {
  309. if (sourceNode) {
  310. correlationEdgeRef.current = graph?.addEdge({
  311. source: { cell: sourceNode },
  312. target: {
  313. x: sourceNode.position().x,
  314. y: sourceNode.position().y,
  315. },
  316. connector: "normal",
  317. attrs: {
  318. line: {
  319. stroke: "#71cb2d",
  320. strokeWidth: 2,
  321. sourceMarker: {
  322. name: "",
  323. },
  324. targetMarker: {
  325. name: "",
  326. },
  327. style: {
  328. opacity: 0.6,
  329. },
  330. },
  331. },
  332. data: {
  333. ignoreDrag: true,
  334. },
  335. zIndex: 0,
  336. });
  337. document.body.addEventListener("mousemove", (e) =>
  338. handleCorrelation(e, correlationEdgeRef, graph)
  339. );
  340. } else {
  341. document.body.removeEventListener("mousemove", (e) =>
  342. handleCorrelation(e, correlationEdgeRef, graph)
  343. );
  344. if (correlationEdgeRef.current) {
  345. graph?.removeCell(correlationEdgeRef.current);
  346. correlationEdgeRef.current = undefined;
  347. }
  348. }
  349. };
  350. /**
  351. * 右键菜单处理方法
  352. */
  353. export const mindmapMenuHander = {
  354. addTopic(tool: ContextMenuTool) {
  355. const node = tool.cell;
  356. if (node.isNode())
  357. addChildrenTopic(node, node.data.setMindProjectInfo, tool.graph);
  358. },
  359. addPeerTopic(tool: ContextMenuTool) {
  360. const node = tool.cell;
  361. if (node.isNode())
  362. addPeerTopic(node, tool.graph, node.data.setMindProjectInfo);
  363. },
  364. addParentTopic(tool: ContextMenuTool) {
  365. const node = tool.cell;
  366. if (node.isNode())
  367. addParentTopic(node, node.data.setMindProjectInfo, tool.graph);
  368. },
  369. addCorrelationEdge(tool: ContextMenuTool) {
  370. if (tool.cell.isNode()) {
  371. // @ts-ignore
  372. const correlationEdgeRef = tool.graph?.extendAttr?.correlationEdgeRef;
  373. handleCreateCorrelationEdge(tool.graph, correlationEdgeRef, tool.cell);
  374. }
  375. },
  376. addRemark(tool: ContextMenuTool) {
  377. // @ts-ignore
  378. tool.graph?.extendAttr?.setRightToolbarActive("remark");
  379. selectTopic(tool.graph, tool.cell.data);
  380. },
  381. addHref(tool: ContextMenuTool) {
  382. console.log(tool.cell)
  383. // @ts-ignore
  384. tool.cell?.extendAttr?.showHrefConfig?.();
  385. },
  386. addTopicLink(tool: ContextMenuTool) {
  387. // todo
  388. },
  389. addImage(tool: ContextMenuTool) {
  390. // todo
  391. },
  392. addTag(tool: ContextMenuTool) {
  393. // @ts-ignore
  394. tool.graph?.extendAttr?.setRightToolbarActive("tag");
  395. selectTopic(tool.graph, tool.cell.data);
  396. },
  397. addIcon(tool: ContextMenuTool) {
  398. // @ts-ignore
  399. tool.graph?.extendAttr?.setRightToolbarActive("icon");
  400. selectTopic(tool.graph, tool.cell.data);
  401. },
  402. addCode(tool: ContextMenuTool) {
  403. tool.cell.setData({
  404. extraModules: {
  405. type: "code",
  406. data: {
  407. code: "",
  408. language: "javascript",
  409. },
  410. },
  411. });
  412. },
  413. chooseSameLevel(tool: ContextMenuTool) {
  414. const parentId = tool.cell.data?.parentId;
  415. if (!parentId) return;
  416. const parent = tool.graph.getCellById(parentId);
  417. selectTopics(tool.graph, parent.data?.children);
  418. },
  419. chooseAllSameLevel(tool: ContextMenuTool) {
  420. // todo
  421. },
  422. copy(tool: ContextMenuTool) {
  423. localStorage.setItem("mindmap-copy-data", JSON.stringify([tool.cell]));
  424. navigator.clipboard.writeText(" ");
  425. },
  426. cut(tool: ContextMenuTool) {
  427. tool.graph.cut([tool.cell]);
  428. },
  429. paste(tool: ContextMenuTool) {
  430. handleMindmapPaste(tool.graph, tool.cell.data.setMindProjectInfo);
  431. },
  432. delete(tool: ContextMenuTool) {
  433. deleteTopics([tool.cell.id], tool.cell.data.setMindProjectInfo);
  434. },
  435. deleteCurrent(tool: ContextMenuTool) {
  436. tool.cell.isNode() && handleDeleteCurrentTopic(tool.graph, [tool.cell]);
  437. },
  438. exportImage(tool: ContextMenuTool) {
  439. exportImage(tool.graph);
  440. },
  441. copyImage(tool: ContextMenuTool) {
  442. // TODO复制为图片
  443. },
  444. };