mindmapHander.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603
  1. import { BorderSize, TopicType } from "@/enum";
  2. import { addTopic, buildTopic, getMindMapProjectByLocal } from "@/pages/mindmap/mindMap";
  3. import { HierarchyResult, 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. import TopicBorder from "@/components/mindMap/Border";
  12. import SummaryBorder from "@/components/mindMap/SummaryBorder";
  13. import { openInsertImageModal } from "@/components/ImageModal";
  14. export const selectTopic = (graph: Graph, topic?: TopicItem) => {
  15. if (topic?.id) {
  16. setTimeout(() => {
  17. graph.resetSelection(topic.id);
  18. const node = graph.getCellById(topic?.id);
  19. node?.isNode() && graph.createTransformWidget(node);
  20. }, 100);
  21. }
  22. };
  23. export const selectTopics = (graph: Graph, topics?: TopicItem[]) => {
  24. setTimeout(() => {
  25. graph.resetSelection(topics?.map((item) => item.id));
  26. topics?.forEach((item) => {
  27. const node = graph.getCellById(item.id);
  28. node?.isNode() && graph.createTransformWidget(node);
  29. });
  30. }, 100);
  31. };
  32. /**
  33. * 添加同级主题
  34. * @param node
  35. * @param graph
  36. */
  37. export const addPeerTopic = (
  38. node: Cell,
  39. graph: Graph,
  40. setMindProjectInfo?: (info: MindMapProjectInfo) => void
  41. ) => {
  42. if (!setMindProjectInfo) return;
  43. const parentNode =
  44. node.data.type === TopicType.main || !node.data?.parentId
  45. ? node
  46. : graph.getCellById(node.data.parentId);
  47. const type =
  48. node.data.type === TopicType.main ? TopicType.branch : node.data.type;
  49. if (parentNode?.isNode()) {
  50. const topic = addTopic(type, setMindProjectInfo, parentNode);
  51. selectTopic(graph, topic);
  52. }
  53. };
  54. /**
  55. * 添加子主题
  56. * @param node
  57. * @param setMindProjectInfo
  58. * @param graph
  59. */
  60. export const addChildrenTopic = (
  61. node: Node,
  62. setMindProjectInfo?: (info: MindMapProjectInfo) => void,
  63. graph?: Graph
  64. ) => {
  65. if (!setMindProjectInfo) return;
  66. const type =
  67. node.data?.type === TopicType.main ? TopicType.branch : TopicType.sub;
  68. const topic = addTopic(type, setMindProjectInfo, node);
  69. graph && selectTopic(graph, topic);
  70. };
  71. /**
  72. * 添加父主题
  73. * @param node
  74. * @param setMindProjectInfo
  75. * @param graph
  76. */
  77. export const addParentTopic = (
  78. node: Node,
  79. setMindProjectInfo?: (info: MindMapProjectInfo) => void,
  80. graph?: Graph
  81. ) => {
  82. if (!setMindProjectInfo || !node.data?.parentId) return;
  83. const type =
  84. node.data?.type === TopicType.branch ? TopicType.branch : TopicType.sub;
  85. const parentNode = graph?.getCellById(node.data.parentId);
  86. // 删除原来的数据
  87. deleteTopics([node.data.id], setMindProjectInfo);
  88. // 加入新的父主题
  89. const topic = addTopic(type, setMindProjectInfo, parentNode as Node, {
  90. children: [
  91. {
  92. ...node.data,
  93. },
  94. ],
  95. });
  96. graph && selectTopic(graph, topic);
  97. };
  98. /**
  99. * 删除子主题
  100. * @param ids
  101. * @param setMindProjectInfo
  102. */
  103. export const deleteTopics = (
  104. ids: string[],
  105. setMindProjectInfo?: (info: MindMapProjectInfo) => void
  106. ) => {
  107. const mindProjectInfo = getMindMapProjectByLocal();
  108. if (!mindProjectInfo || !setMindProjectInfo) return;
  109. const topics = cloneDeep(mindProjectInfo.topics);
  110. const filterTopics = (list: TopicItem[]): TopicItem[] => {
  111. const result: TopicItem[] = [];
  112. for (const item of list) {
  113. if (!ids.includes(item.id) || item.type === TopicType.main) {
  114. if (item.children?.length) {
  115. item.children = filterTopics(item.children);
  116. }
  117. result.push(item);
  118. }
  119. }
  120. return result;
  121. };
  122. mindProjectInfo.topics = filterTopics(topics);
  123. setMindProjectInfo(mindProjectInfo); // TODO 这个方法删除更新有问题
  124. localStorage.setItem("minMapProjectInfo", JSON.stringify(mindProjectInfo));
  125. };
  126. /**
  127. * 删除当前主题
  128. * @param graph
  129. * @param nodes
  130. */
  131. export const handleDeleteCurrentTopic = (graph: Graph, nodes: Node[]) => {
  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:
  147. topic.type === TopicType.main
  148. ? TopicType.branch
  149. : TopicType.sub,
  150. parentId: topic.id,
  151. };
  152. }
  153. );
  154. (topic.children || []).splice(index, 1, ...newChildren);
  155. }
  156. }
  157. });
  158. }
  159. // @ts-ignore
  160. graph?.extendAttr?.setMindProjectInfo?.(mindProjectInfo);
  161. localStorage.setItem("minMapProjectInfo", JSON.stringify(mindProjectInfo));
  162. });
  163. };
  164. /**
  165. * 执行粘贴
  166. * @param graph
  167. * @param setMindProjectInfo
  168. */
  169. export const handleMindmapPaste = (
  170. graph: Graph,
  171. setMindProjectInfo: (info: MindMapProjectInfo) => void
  172. ) => {
  173. // 读取剪切板数据
  174. navigator.clipboard.read().then((items) => {
  175. console.log("剪切板内容:", items);
  176. const currentNode = graph.getSelectedCells().find((cell) => cell.isNode());
  177. if (!currentNode) {
  178. message.warning("请先选择一个主题");
  179. return;
  180. }
  181. const item = items?.[0];
  182. if (item) {
  183. /**读取图片数据 */
  184. if (item.types[0] === "image/png") {
  185. item.getType("image/png").then((blob) => {
  186. const reader = new FileReader();
  187. reader.readAsDataURL(blob);
  188. reader.onload = function (event) {
  189. const dataUrl = event.target?.result as string;
  190. // 获取图片大小
  191. const img = new Image();
  192. img.src = dataUrl;
  193. img.onload = function () {
  194. const width = img.width;
  195. const height = img.height;
  196. // 插入图片
  197. currentNode.setData({
  198. extraModules: {
  199. type: "image",
  200. data: {
  201. imageUrl: dataUrl,
  202. width,
  203. height,
  204. },
  205. },
  206. });
  207. };
  208. };
  209. });
  210. }
  211. /**读取文本数据 */
  212. if (item.types[0] === "text/plain") {
  213. item.getType("text/plain").then((blob) => {
  214. const reader = new FileReader();
  215. reader.readAsText(blob);
  216. reader.onload = function (event) {
  217. const text = event.target?.result as string;
  218. // 内部复制方法
  219. if (text === " ") {
  220. const nodes = localStorage.getItem("mindmap-copy-data");
  221. if (nodes) {
  222. JSON.parse(nodes)?.forEach((node: Node) => {
  223. const data = node.data;
  224. // 修改新的数据嵌套
  225. data.id = uuid();
  226. data.parentId = currentNode.id;
  227. if (data.children?.length) {
  228. data.children = traverseCopyData(data.children, data.id);
  229. }
  230. addTopic(
  231. currentNode.data?.type === TopicType.main
  232. ? TopicType.branch
  233. : TopicType.sub,
  234. setMindProjectInfo,
  235. currentNode,
  236. { ...data }
  237. );
  238. });
  239. }
  240. } else {
  241. const topic = addTopic(
  242. currentNode.data?.type === TopicType.main
  243. ? TopicType.branch
  244. : TopicType.sub,
  245. setMindProjectInfo,
  246. currentNode,
  247. { label: text }
  248. );
  249. selectTopic(graph, topic);
  250. }
  251. };
  252. });
  253. }
  254. }
  255. });
  256. };
  257. const traverseCopyData = (list: TopicItem[], parentId: string): TopicItem[] => {
  258. return list.map((item) => {
  259. item.id = uuid();
  260. item.parentId = parentId;
  261. if (item.children?.length) {
  262. item.children = traverseCopyData(item.children, item.id);
  263. }
  264. return item;
  265. });
  266. };
  267. /**
  268. * 遍历主题树
  269. * @param topics
  270. * @param callback
  271. * @returns
  272. */
  273. export const traverseNode = (
  274. topics: TopicItem[],
  275. callback: (topic: TopicItem, index: number) => void
  276. ): TopicItem[] => {
  277. return topics.map((topic, index) => {
  278. callback && callback(topic, index);
  279. if (topic.children?.length) {
  280. topic.children = traverseNode(topic.children, callback);
  281. }
  282. return topic;
  283. });
  284. };
  285. // 关联线
  286. const handleCorrelation = (
  287. e: MouseEvent,
  288. correlationEdgeRef: MutableRefObject<Edge | undefined>,
  289. graph: Graph
  290. ) => {
  291. if (correlationEdgeRef.current) {
  292. const point = graph?.clientToLocal(e.x, e.y);
  293. point && correlationEdgeRef.current?.setTarget(point);
  294. }
  295. };
  296. /**
  297. * 添加关联线
  298. * @param graph
  299. * @param correlationEdgeRef
  300. * @param sourceNode
  301. */
  302. export const handleCreateCorrelationEdge = (
  303. graph: Graph,
  304. correlationEdgeRef: MutableRefObject<Edge | undefined>,
  305. sourceNode?: Node
  306. ) => {
  307. if (sourceNode) {
  308. correlationEdgeRef.current = graph?.addEdge({
  309. source: { cell: sourceNode },
  310. target: {
  311. x: sourceNode.position().x,
  312. y: sourceNode.position().y,
  313. },
  314. connector: "normal",
  315. attrs: {
  316. line: {
  317. stroke: "#71cb2d",
  318. strokeWidth: 2,
  319. sourceMarker: {
  320. name: "",
  321. },
  322. targetMarker: {
  323. name: "",
  324. },
  325. style: {
  326. opacity: 0.6,
  327. },
  328. },
  329. },
  330. data: {
  331. ignoreDrag: true,
  332. },
  333. zIndex: 0,
  334. });
  335. document.body.addEventListener("mousemove", (e) =>
  336. handleCorrelation(e, correlationEdgeRef, graph)
  337. );
  338. } else {
  339. document.body.removeEventListener("mousemove", (e) =>
  340. handleCorrelation(e, correlationEdgeRef, graph)
  341. );
  342. if (correlationEdgeRef.current) {
  343. graph?.removeCell(correlationEdgeRef.current);
  344. correlationEdgeRef.current = undefined;
  345. }
  346. }
  347. };
  348. export const addBorder = (nodes: Node[]) => {
  349. // 判断节点是否在当前存在父级以上节点
  350. nodes.forEach((node) => {
  351. let hasParent = false;
  352. traverseNode(node.data.children || [], (child) => {
  353. if (child.id === node.id) {
  354. hasParent = true;
  355. }
  356. });
  357. // 添加边框数据
  358. if (!hasParent && !node.data.border) {
  359. node.setData({
  360. border: {
  361. ...TopicBorder.data,
  362. },
  363. });
  364. }
  365. });
  366. };
  367. /**
  368. * 计算当前节点总大小
  369. * @param topItem
  370. * @param children
  371. * @returns
  372. */
  373. export const cacluculateExtremeValue = (topItem: HierarchyResult, children: HierarchyResult[]) => {
  374. let minX = topItem.x;
  375. let minY = topItem.y;
  376. let maxX = minX + topItem.data.width;
  377. let maxY = minY + topItem.data.height;
  378. children.forEach((child) => {
  379. const childXY = cacluculateExtremeValue(child, child.children);
  380. minX = Math.min(minX, child.x, childXY.minX);
  381. minY = Math.min(minY, child.y, childXY.minY);
  382. maxX = Math.max(maxX, child.x + child.data.width, childXY.maxX);
  383. maxY = Math.max(maxY, child.y + child.data.height, childXY.maxY);
  384. });
  385. return {
  386. minX,
  387. minY,
  388. maxX,
  389. maxY
  390. }
  391. };
  392. /**
  393. * 获取边框位置及大小
  394. * @param hierarchyItem
  395. * @returns
  396. */
  397. export const getBorderPositionAndSize = (hierarchyItem: HierarchyResult) => {
  398. const firstChild = hierarchyItem?.children?.[0];
  399. let totalHeigth = hierarchyItem?.totalHeight || 0;
  400. let totalWidth = hierarchyItem?.totalWidth || 0;
  401. let x = hierarchyItem?.x || 0;
  402. let y = hierarchyItem?.y || 0;
  403. // 是否存在子节点
  404. if (firstChild) {
  405. const position = cacluculateExtremeValue(hierarchyItem, hierarchyItem.children || []);
  406. x = position.minX;
  407. y = position.minY;
  408. totalHeigth = position.maxY - position.minY;
  409. totalWidth = position.maxX - position.minX;
  410. } else {
  411. totalWidth = hierarchyItem.data.width;
  412. totalHeigth = hierarchyItem.data.height;
  413. }
  414. return {
  415. x: x - 10,
  416. y: y - 10,
  417. width: totalWidth + 20,
  418. height: totalHeigth + 20,
  419. };
  420. };
  421. /**
  422. * 添加概要
  423. */
  424. export const addSummary = (nodes: Node[]) => {
  425. // 判断节点是否在当前存在父级以上节点
  426. nodes.forEach((node) => {
  427. let hasParent = false;
  428. traverseNode(node.data.children || [], (child) => {
  429. if (child.id === node.id) {
  430. hasParent = true;
  431. }
  432. });
  433. // 添加边框数据
  434. if (!hasParent && !node.data.summary) {
  435. const root = buildTopic(node.data.type, {
  436. setMindProjectInfo: node.data.setMindProjectInfo,
  437. type: TopicType.branch,
  438. label: "概要",
  439. borderSize: BorderSize.medium,
  440. isSummary: true,
  441. summarySource: node.id,
  442. });
  443. node.setData({
  444. summary: {
  445. topic: root,
  446. range: [node.id],
  447. border: {
  448. ...SummaryBorder.data,
  449. origin: root.id,
  450. summarySource: node.id,
  451. }
  452. }
  453. }, {
  454. deep: false
  455. });
  456. }
  457. });
  458. }
  459. /**
  460. * 插入图片
  461. */
  462. export const insertImage = (node?: Node) => {
  463. if(!node) return;
  464. openInsertImageModal((url) => {
  465. console.log('图片地址:', url);
  466. node.setData({
  467. extraModules: {
  468. type: "image",
  469. data: {
  470. imageUrl: url,
  471. width: 300,
  472. height: 300
  473. }
  474. }
  475. })
  476. })
  477. }
  478. /**
  479. * 右键菜单处理方法
  480. */
  481. export const mindmapMenuHander = {
  482. addTopic(tool: ContextMenuTool) {
  483. const node = tool.cell;
  484. if (node.isNode())
  485. addChildrenTopic(node, node.data.setMindProjectInfo, tool.graph);
  486. },
  487. addPeerTopic(tool: ContextMenuTool) {
  488. const node = tool.cell;
  489. if (node.isNode())
  490. addPeerTopic(node, tool.graph, node.data.setMindProjectInfo);
  491. },
  492. addParentTopic(tool: ContextMenuTool) {
  493. const node = tool.cell;
  494. if (node.isNode())
  495. addParentTopic(node, node.data.setMindProjectInfo, tool.graph);
  496. },
  497. addCorrelationEdge(tool: ContextMenuTool) {
  498. if (tool.cell.isNode()) {
  499. // @ts-ignore
  500. const correlationEdgeRef = tool.graph?.extendAttr?.correlationEdgeRef;
  501. handleCreateCorrelationEdge(tool.graph, correlationEdgeRef, tool.cell);
  502. }
  503. },
  504. addRemark(tool: ContextMenuTool) {
  505. // @ts-ignore
  506. tool.graph?.extendAttr?.setRightToolbarActive("remark");
  507. selectTopic(tool.graph, tool.cell.data);
  508. },
  509. addHref(tool: ContextMenuTool) {
  510. // @ts-ignore
  511. tool.cell?.extendAttr?.showHrefConfig?.();
  512. },
  513. addTopicLink(tool: ContextMenuTool) {
  514. // todo
  515. },
  516. addImage(tool: ContextMenuTool) {
  517. const cell = tool.cell;
  518. cell.isNode() && insertImage(cell);
  519. },
  520. addTag(tool: ContextMenuTool) {
  521. // @ts-ignore
  522. tool.graph?.extendAttr?.setRightToolbarActive("tag");
  523. selectTopic(tool.graph, tool.cell.data);
  524. },
  525. addIcon(tool: ContextMenuTool) {
  526. // @ts-ignore
  527. tool.graph?.extendAttr?.setRightToolbarActive("icon");
  528. selectTopic(tool.graph, tool.cell.data);
  529. },
  530. addCode(tool: ContextMenuTool) {
  531. tool.cell.setData({
  532. extraModules: {
  533. type: "code",
  534. data: {
  535. code: "",
  536. language: "javascript",
  537. },
  538. },
  539. });
  540. },
  541. chooseSameLevel(tool: ContextMenuTool) {
  542. const parentId = tool.cell.data?.parentId;
  543. if (!parentId) return;
  544. const parent = tool.graph.getCellById(parentId);
  545. selectTopics(tool.graph, parent.data?.children);
  546. },
  547. chooseAllSameLevel(tool: ContextMenuTool) {
  548. // todo
  549. },
  550. copy(tool: ContextMenuTool) {
  551. localStorage.setItem("mindmap-copy-data", JSON.stringify([tool.cell]));
  552. navigator.clipboard.writeText(" ");
  553. },
  554. cut(tool: ContextMenuTool) {
  555. tool.graph.cut([tool.cell]);
  556. },
  557. paste(tool: ContextMenuTool) {
  558. handleMindmapPaste(tool.graph, tool.cell.data.setMindProjectInfo);
  559. },
  560. delete(tool: ContextMenuTool) {
  561. deleteTopics([tool.cell.id], tool.cell.data.setMindProjectInfo);
  562. },
  563. deleteCurrent(tool: ContextMenuTool) {
  564. tool.cell.isNode() && handleDeleteCurrentTopic(tool.graph, [tool.cell]);
  565. },
  566. exportImage(tool: ContextMenuTool) {
  567. exportImage(tool.graph);
  568. },
  569. copyImage(tool: ContextMenuTool) {
  570. // TODO复制为图片
  571. },
  572. };