mindMap.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. import { StructureType, TopicType } from "@/enum";
  2. import { MindMapProjectInfo, TopicItem, HierarchyResult } from "@/types";
  3. import { Graph, Cell, Node } from "@antv/x6";
  4. import Topic from "@/components/mindMap/Topic";
  5. import TopicBorder from "@/components/mindMap/Border";
  6. import SummaryBorder from "@/components/mindMap/SummaryBorder";
  7. import { topicData } from "@/config/data";
  8. import { uuid } from "@/utils";
  9. import { hierarchyMethodMap } from "@/pages/mindmap/hierarchy";
  10. import { createEdge } from "./edge";
  11. import { getTheme } from "./theme";
  12. import { topicMenu } from "@/utils/contentMenu";
  13. import {
  14. cacluculateExtremeValue,
  15. getBorderPositionAndSize,
  16. } from "@/utils/mindmapHander";
  17. import { AddMindMapElement } from "@/api/systemDesigner";
  18. /**
  19. * 渲染思维导图项目
  20. * @param graph
  21. */
  22. export const renderMindMap = ({
  23. topics,
  24. pageSetting,
  25. structure,
  26. theme,
  27. graph,
  28. setMindProjectInfo,
  29. returnCells = false
  30. }: {
  31. topics: TopicItem[];
  32. pageSetting: MindMapProjectInfo["pageSetting"];
  33. structure: StructureType;
  34. theme: string;
  35. graph: Graph;
  36. setMindProjectInfo: (info: MindMapProjectInfo) => void;
  37. returnCells?: boolean;
  38. }) => {
  39. const cells: Cell[] = [];
  40. topics.forEach((topic) => {
  41. // 遍历出层次结构
  42. const result: HierarchyResult = hierarchyMethodMap[structure]?.(
  43. topic,
  44. pageSetting
  45. );
  46. let originPosition = { x: topic?.x ?? -10, y: topic?.y ?? -10 };
  47. if (graph.hasCell(topic.id) && !topic.isSummary) {
  48. const node = graph.getCellById(topic.id);
  49. if (node.isNode()) {
  50. originPosition = node.position();
  51. }
  52. }
  53. const offsetX = originPosition.x - result.x;
  54. const offsetY = originPosition.y - result.y;
  55. const traverse = (hierarchyItem: HierarchyResult, parent?: Node) => {
  56. if (hierarchyItem) {
  57. const { data, children, x, y } = hierarchyItem;
  58. const id = data?.id || uuid();
  59. // 创建主题
  60. const node = graph.createNode({
  61. ...Topic,
  62. width: data.width,
  63. height: data.height,
  64. data: {
  65. ...data,
  66. opacity: 100,
  67. // 节点内部执行数据更新方法
  68. setMindProjectInfo,
  69. },
  70. id,
  71. x: offsetX + x,
  72. y: offsetY + y,
  73. tools: [
  74. {
  75. name: "contextmenu",
  76. args: {
  77. menu: topicMenu,
  78. },
  79. },
  80. ],
  81. });
  82. // 渲染边框
  83. if (data.border) {
  84. cells.push(
  85. createBorderComponent(hierarchyItem, offsetX, offsetY, graph)
  86. );
  87. }
  88. // 渲染概要
  89. if (data.summary) {
  90. const summaryCells = createSummaryCells(
  91. hierarchyItem,
  92. data.summary,
  93. structure,
  94. pageSetting,
  95. theme,
  96. graph,
  97. setMindProjectInfo,
  98. offsetX,
  99. offsetY
  100. );
  101. cells.push(...(summaryCells || []));
  102. }
  103. cells.push(node);
  104. parent && parent.addChild(node);
  105. if (data?.links) {
  106. cells.push(...data.links.map((item) => graph.createEdge(item)));
  107. }
  108. if (children) {
  109. children.forEach((item: HierarchyResult, index) => {
  110. const isBracket = [
  111. StructureType.leftBracket,
  112. StructureType.rightBracket,
  113. ].includes(structure);
  114. // 括号图不绘制中间连线
  115. if (!isBracket || index === 0 || index === children.length - 1) {
  116. const edge = createEdge(graph, id, item, structure, theme, {
  117. onlyOneChild: children.length === 1,
  118. direction: item?.direction
  119. });
  120. cells.push(edge);
  121. node.addChild(edge);
  122. }
  123. // 递归遍历
  124. traverse(item, node);
  125. });
  126. }
  127. }
  128. };
  129. traverse(result);
  130. });
  131. // 概要节点 返回节点数据
  132. if(returnCells) {
  133. return cells;
  134. }
  135. const oldCells = graph.getCells();
  136. // 移除不要的节点及对应的边
  137. oldCells.forEach((cell) => {
  138. if (!cells.find((item) => cell.id === item.id)) {
  139. graph.removeCell(cell.id + "-edge");
  140. graph.removeCell(cell);
  141. }
  142. });
  143. // 添加或删除节点
  144. cells
  145. .filter((cell) => cell.isNode() && !graph.hasCell(cell.id))
  146. .forEach((cell) => {
  147. graph.addCell(cell);
  148. });
  149. // 更新老的节点
  150. cells
  151. .filter((cell) => cell.isNode() && graph.hasCell(cell.id))
  152. .forEach((cell) => {
  153. cell.isNode() && updateNode(cell, graph);
  154. });
  155. // 添加所需的节点
  156. const edgeCells = cells.filter((cell) => cell.isEdge());
  157. graph.removeCells(edgeCells);
  158. graph.addCell(edgeCells);
  159. };
  160. // 渲染概要
  161. const createSummaryCells = (
  162. hierarchyItem: HierarchyResult,
  163. summary: TopicItem['summary'],
  164. structure: StructureType,
  165. pageSetting: MindMapProjectInfo['pageSetting'],
  166. theme: string,
  167. graph: Graph,
  168. setMindProjectInfo: (info: MindMapProjectInfo) => void,
  169. offsetX: number,
  170. offsetY: number
  171. ): Cell[] => {
  172. let cells: Cell[] = [];
  173. if (summary) {
  174. const positionAndSize = cacluculateExtremeValue(
  175. hierarchyItem,
  176. hierarchyItem.children
  177. );
  178. const totalHeight = positionAndSize.maxY - positionAndSize.minY;
  179. const totalWidth = positionAndSize.maxX - positionAndSize.minX;
  180. // 概要边框
  181. const node = graph.createNode({
  182. ...SummaryBorder,
  183. data: summary.border,
  184. id: summary.topic.id + "-border",
  185. zIndex: 0,
  186. position: {
  187. x: offsetX + positionAndSize.minX - 2,
  188. y: offsetY + positionAndSize.minY - 2,
  189. },
  190. size: {
  191. width: totalWidth + 4,
  192. height: totalHeight + 4,
  193. },
  194. });
  195. cells.push(node);
  196. let position = {
  197. x: offsetX + hierarchyItem.x + totalWidth + 40,
  198. y: offsetY + hierarchyItem.y
  199. }
  200. if(
  201. [StructureType.left, StructureType.leftBracket, StructureType.leftFishbone, StructureType.leftTreeShape].includes(structure)
  202. || structure === StructureType.leftRight && hierarchyItem.direction === 'left'
  203. ) {
  204. position = {
  205. x: offsetX + hierarchyItem.x - 40 - totalWidth,
  206. y: offsetY + hierarchyItem.y
  207. }
  208. }
  209. const list = renderMindMap({
  210. topics: [{
  211. ...summary.topic,
  212. ...position
  213. }],
  214. pageSetting,
  215. structure,
  216. theme,
  217. graph,
  218. setMindProjectInfo,
  219. returnCells: true
  220. }) || [];
  221. // 概要节点
  222. cells.push(...list);
  223. }
  224. return cells;
  225. }
  226. // 创建外框组件
  227. const createBorderComponent = (
  228. hierarchyItem: HierarchyResult,
  229. offsetX: number,
  230. offsetY: number,
  231. graph: Graph
  232. ) => {
  233. const positionAndSize = getBorderPositionAndSize(hierarchyItem);
  234. return graph.createNode({
  235. ...TopicBorder,
  236. id: hierarchyItem.id + "-border",
  237. data: {
  238. ...hierarchyItem.data.border,
  239. origin: hierarchyItem.id,
  240. },
  241. zIndex: 0,
  242. position: {
  243. x: offsetX + positionAndSize.x,
  244. y: offsetY + positionAndSize.y,
  245. },
  246. size: {
  247. width: positionAndSize.width,
  248. height: positionAndSize.height,
  249. },
  250. });
  251. };
  252. // 更新现有节点
  253. const updateNode = (node: Node, graph: Graph) => {
  254. const oldCell = graph.getCellById(node.id);
  255. if (oldCell.isNode()) {
  256. oldCell.setData(node.data, { deep: false});
  257. oldCell.position(node.position().x, node.position().y);
  258. oldCell.setSize(node.size().width, node.size().height);
  259. // oldCell.setAttrs(node.attrs);
  260. // const cells = node.children?.map(item => graph.getCellById(item.id));
  261. // oldCell.setChildren(cells ?? null);
  262. }
  263. };
  264. /**
  265. * 添加分支主题
  266. */
  267. export const addTopic = (
  268. type: TopicType,
  269. setMindProjectInfo: (info: MindMapProjectInfo) => void,
  270. graph: Graph,
  271. node?: Node,
  272. otherData: Record<string, any> = {}
  273. ): TopicItem | undefined => {
  274. // @ts-ignore
  275. const projectInfo: MindMapProjectInfo = graph.extendAttr.getMindProjectInfo();
  276. if (!projectInfo || !setMindProjectInfo) return;
  277. const topic = buildTopic(
  278. type,
  279. {
  280. ...(otherData || {}),
  281. parentId: node?.id,
  282. isSummary: node?.data?.isSummary,
  283. summarySource: node?.data?.summarySource
  284. },
  285. graph,
  286. node
  287. );
  288. if (node) {
  289. const parentId = node.id;
  290. const traverse = (topics: TopicItem[]) => {
  291. topics.forEach((item) => {
  292. if (item.id === parentId) {
  293. if (item.children) {
  294. item.children?.push(topic);
  295. } else {
  296. item.children = [topic];
  297. }
  298. } else if (item.children) {
  299. traverse(item.children);
  300. }
  301. if (item.summary) {
  302. traverse([item.summary.topic])
  303. }
  304. });
  305. };
  306. traverse(projectInfo?.topics || []);
  307. } else {
  308. projectInfo.topics.push(topic);
  309. }
  310. if(sessionStorage.getItem("projectId") && !topic.parentId) {
  311. AddMindMapElement({
  312. ...topic,
  313. graphId: sessionStorage.getItem("projectId"),
  314. });
  315. }
  316. setMindProjectInfo(projectInfo);
  317. return topic;
  318. };
  319. const topicMap = {
  320. [TopicType.main]: {
  321. label: "中心主题",
  322. width: 176,
  323. height: 65,
  324. },
  325. [TopicType.branch]: {
  326. label: "分支主题",
  327. width: 104,
  328. height: 40,
  329. },
  330. [TopicType.sub]: {
  331. label: "子主题",
  332. width: 54,
  333. height: 26,
  334. },
  335. };
  336. /**
  337. * 构建一个主题数据
  338. * @param type 主题类型
  339. * @param options 配置项
  340. * @returns
  341. */
  342. export const buildTopic = (
  343. type: TopicType,
  344. options: Record<string, any> = {},
  345. graph?: Graph,
  346. parentNode?: Node
  347. ): TopicItem => {
  348. // @ts-ignore
  349. const projectInfo: MindMapProjectInfo = graph?.extendAttr?.getMindProjectInfo();
  350. const theme = getTheme(
  351. projectInfo?.theme || 'default',
  352. type === TopicType.sub ? parentNode?.data : undefined
  353. );
  354. const id = uuid();
  355. return {
  356. ...topicData,
  357. id,
  358. type,
  359. label: topicMap[type].label || "自由主题",
  360. width: topicMap[type].width || 206,
  361. height: topicMap[type].height || 70,
  362. fill: {
  363. ...topicData.fill,
  364. ...theme[type]?.fill,
  365. },
  366. text: {
  367. ...topicData.text,
  368. ...theme[type]?.text,
  369. },
  370. stroke: {
  371. ...topicData.stroke,
  372. ...theme[type]?.stroke,
  373. },
  374. edge: {
  375. ...topicData.edge,
  376. color: theme[type]?.edge.color,
  377. },
  378. ...options,
  379. children: (options?.children || topicData.children || []).map(
  380. (item: TopicItem) => {
  381. return {
  382. ...item,
  383. parentId: id,
  384. };
  385. }
  386. ),
  387. };
  388. };
  389. /**
  390. * 从本地获取项目信息
  391. * @returns
  392. */
  393. // export const getMindMapProjectByLocal = (): MindMapProjectInfo | null => {
  394. // return JSON.parse(sessionStorage.getItem("mindMapProjectInfo") || "null");
  395. // };
  396. /**
  397. * 更新主题数据
  398. * @param id 主题id
  399. * @param value 更新的数据
  400. * @param setMindProjectInfo 更新项目信息方法
  401. */
  402. export const updateTopic = (
  403. id: string,
  404. value: Partial<TopicItem>,
  405. setMindProjectInfo: (info: MindMapProjectInfo) => void,
  406. graph: Graph
  407. ) => {
  408. // @ts-ignore
  409. const projectInfo = graph.extendAttr.getMindProjectInfo;
  410. if (!projectInfo || !setMindProjectInfo) return;
  411. const traverse = (topics: TopicItem[]) => {
  412. topics.forEach((item) => {
  413. if (item.id === id) {
  414. Object.assign(item, value);
  415. }
  416. if (item.children) {
  417. traverse(item.children);
  418. }
  419. });
  420. };
  421. traverse(projectInfo?.topics || []);
  422. setMindProjectInfo(projectInfo);
  423. };