mindMap.tsx 10 KB

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