mindmapHander.tsx 17 KB

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