mindMapModel.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. import { cellStyle } from "@/types";
  2. import { Cell, Edge, EventArgs, Graph, Node } from "@antv/x6";
  3. import { message } from "antd";
  4. import { useEffect, useRef, useState } from "react";
  5. import { Selection } from "@repo/x6-plugin-selection";
  6. import { Keyboard } from "@antv/x6-plugin-keyboard";
  7. import { History } from "@antv/x6-plugin-history";
  8. import { Transform } from "@antv/x6-plugin-transform";
  9. import { Scroller } from "@antv/x6-plugin-scroller";
  10. import { Clipboard } from "@antv/x6-plugin-clipboard";
  11. import { Export } from "@antv/x6-plugin-export";
  12. import { MindMapProjectInfo } from "@/types";
  13. import { bindMindMapEvents } from "@/events/mindMapEvent";
  14. import { useSessionStorageState } from "ahooks";
  15. import { renderMindMap } from "@/pages/mindmap/mindMap";
  16. import { TopicType } from "@/enum";
  17. import { isEqual, cloneDeep } from "lodash-es";
  18. import { bindMindmapKeys } from "@/utils/fastKey";
  19. import { handleCreateCorrelationEdge } from "@/utils/mindmapHander";
  20. import { Dnd } from "@antv/x6-plugin-dnd";
  21. import { EditGraph, BatchEditMindMapElement } from "@/api/systemDesigner";
  22. export default function mindMapModel() {
  23. const [rightToobarActive, setRightToolbarActive] = useState<string>();
  24. // 格式刷启用
  25. const [enableFormatBrush, setEnableFormatBrush] = useState(false);
  26. // 格式刷样式
  27. const formatBrushStyle = useRef<cellStyle>();
  28. const graphRef = useRef<Graph>();
  29. const dndRef = useRef<Dnd>();
  30. const [graph, setGraph] = useState<Graph>();
  31. const [canRedo, setCanRedo] = useState(false);
  32. const [canUndo, setCanUndo] = useState(false);
  33. const [selectedCell, setSelectedCell] = useState<Cell[]>([]);
  34. const correlationEdgeRef = useRef<Edge>();
  35. const [mindProjectInfo, setProjectInfo] = useState<MindMapProjectInfo>();
  36. const projectInfoRef = useRef<MindMapProjectInfo>();
  37. const timer = useRef<any>();
  38. const setMindProjectInfo = (
  39. info: MindMapProjectInfo,
  40. init?: boolean,
  41. isSetting?: boolean,
  42. ignoreRender?: boolean
  43. ) => {
  44. setProjectInfo(cloneDeep(info));
  45. projectInfoRef.current = info;
  46. sessionStorage.setItem("mindMapProjectInfo", JSON.stringify(info));
  47. if (!init && !isSetting) {
  48. const graphId = sessionStorage.getItem("projectId");
  49. if (graphId && info?.topics) {
  50. // 清除定时器
  51. clearTimeout(timer.current);
  52. timer.current = setTimeout(() => {
  53. BatchEditMindMapElement(
  54. info.topics.map((topic) => {
  55. return {
  56. ...topic,
  57. graphId,
  58. };
  59. })
  60. );
  61. }, 500);
  62. }
  63. }
  64. // 配置更新
  65. if (isSetting) {
  66. const pageSetting = info?.pageSetting;
  67. if (sessionStorage.getItem("projectId") && pageSetting) {
  68. EditGraph({
  69. id: sessionStorage.getItem("projectId"),
  70. ...pageSetting,
  71. structure: info?.structure,
  72. theme: info?.theme,
  73. langNameList: [
  74. {
  75. name: "zh-CN",
  76. value: info.name,
  77. },
  78. ],
  79. });
  80. }
  81. }
  82. if(!ignoreRender && graphRef.current) {
  83. renderMindMap({
  84. graph: graphRef.current,
  85. setMindProjectInfo,
  86. pageSetting: info?.pageSetting,
  87. structure: info?.structure,
  88. theme: info?.theme,
  89. topics: info?.topics,
  90. });
  91. }
  92. if(init) {
  93. graph?.centerContent();
  94. }
  95. };
  96. const pageSettingRef = useRef<MindMapProjectInfo["pageSetting"]>();
  97. useEffect(() => {
  98. if (mindProjectInfo?.pageSetting && graph) {
  99. if (isEqual(pageSettingRef.current, mindProjectInfo?.pageSetting)) {
  100. return;
  101. }
  102. pageSettingRef.current = cloneDeep(
  103. mindProjectInfo?.pageSetting
  104. ) as MindMapProjectInfo["pageSetting"];
  105. const pageSetting = pageSettingRef.current;
  106. if (pageSetting?.backgroundType === "color") {
  107. graph.drawBackground({
  108. color: pageSetting?.backgroundColor,
  109. });
  110. } else {
  111. graph.drawBackground({
  112. image: pageSetting?.backgroundImage,
  113. repeat: "repeat",
  114. });
  115. }
  116. // 设置水印
  117. if (pageSetting.showWatermark && pageSetting.watermarkText) {
  118. const canvas = document.createElement("canvas");
  119. canvas.width = pageSetting.watermarkText.length * 16;
  120. canvas.height = 100;
  121. const ctx = canvas.getContext("2d");
  122. if (ctx) {
  123. ctx.fillStyle = "#aaa";
  124. ctx.font = "16px Arial";
  125. ctx.fillText(pageSetting.watermarkText, 1, 15);
  126. }
  127. const img = canvas.toDataURL();
  128. graph.drawBackground({
  129. image: img,
  130. repeat: "watermark",
  131. });
  132. }
  133. }
  134. }, [graph, mindProjectInfo?.pageSetting]);
  135. // 初始化脑图
  136. const initMindMap = (container: HTMLElement) => {
  137. const instance = new Graph({
  138. container,
  139. width: document.documentElement.clientWidth,
  140. height: document.documentElement.clientHeight,
  141. autoResize: true,
  142. async: false,
  143. mousewheel: {
  144. enabled: true,
  145. modifiers: "ctrl",
  146. minScale: 0.2,
  147. maxScale: 2,
  148. },
  149. connecting: {
  150. connectionPoint: "anchor",
  151. },
  152. interacting: {
  153. nodeMovable: (view) => {
  154. const data = view.cell.getData<{
  155. ignoreDrag: boolean;
  156. lock: boolean;
  157. type: TopicType;
  158. parentId: string;
  159. shadow: boolean;
  160. isSummary: boolean;
  161. }>();
  162. // 禁止拖拽或锁节点
  163. if (data?.ignoreDrag || data?.lock) return false;
  164. // 影子节点
  165. if (data?.shadow) return true;
  166. // 概要
  167. if (data?.isSummary) return false;
  168. // 自由节点
  169. return data?.type === TopicType.branch && !data?.parentId;
  170. },
  171. },
  172. });
  173. instance.use(new Selection());
  174. instance.use(new Keyboard());
  175. instance.use(new Clipboard());
  176. instance.use(new Export());
  177. instance.use(
  178. new Scroller({
  179. enabled: true,
  180. pannable: true,
  181. })
  182. );
  183. instance.use(
  184. new Transform({
  185. resizing: {
  186. enabled: true,
  187. orthogonal: true,
  188. },
  189. })
  190. );
  191. instance.use(
  192. new History({
  193. enabled: true,
  194. // beforeAddCommand: (e, args) => {
  195. // // @ts-ignore
  196. // return !args.cell.isEdge()
  197. // },
  198. })
  199. );
  200. instance.on("history:change", () => {
  201. setCanRedo(instance.canRedo());
  202. setCanUndo(instance.canUndo());
  203. });
  204. graphRef.current = instance;
  205. dndRef.current = new Dnd({
  206. target: instance,
  207. validateNode: () => {
  208. return false;
  209. },
  210. });
  211. // 绑定事件
  212. bindMindMapEvents(instance, setMindProjectInfo, setSelectedCell, dndRef);
  213. // 绑定键盘
  214. bindMindmapKeys(instance, mindProjectInfo, setMindProjectInfo);
  215. // 绑定实例方法
  216. // @ts-ignore
  217. instance.extendAttr = {
  218. setRightToolbarActive,
  219. correlationEdgeRef,
  220. setMindProjectInfo,
  221. getMindProjectInfo: () => cloneDeep(projectInfoRef.current),
  222. };
  223. setGraph(instance);
  224. };
  225. const handleBrushClick = (args: EventArgs & { cell: Cell }) => {
  226. // 取消格式刷
  227. if (!args?.cell || args?.cell?.data?.isPage) {
  228. formatBrushStyle.current = undefined;
  229. setEnableFormatBrush(false);
  230. graphRef.current?.off("cell:click", handleBrushClick);
  231. graphRef.current?.off("blank:click", handleBrushClick);
  232. } else {
  233. if (args.cell.data?.lock) return;
  234. // 应用格式刷
  235. const data = args.cell.data;
  236. args.cell.setData({
  237. text: formatBrushStyle.current?.text || data?.text,
  238. fill: formatBrushStyle.current?.fill || data?.fill,
  239. stroke: formatBrushStyle.current?.stroke || data?.stroke,
  240. opacity: formatBrushStyle.current?.opacity || data?.opacity,
  241. });
  242. }
  243. };
  244. // 开启格式刷
  245. const toggleFormatBrush = (graph: Graph) => {
  246. graphRef.current = graph;
  247. const cell = graph?.getSelectedCells()?.find((item) => item.isNode());
  248. setEnableFormatBrush((state) => {
  249. if (!state) {
  250. const data = cell?.getData();
  251. formatBrushStyle.current = data;
  252. message.info("格式刷已开启");
  253. graph.on("cell:click", handleBrushClick);
  254. graph.on("blank:click", handleBrushClick);
  255. } else {
  256. formatBrushStyle.current = undefined;
  257. graph.off("cell:click", handleBrushClick);
  258. graph.off("blank:click", handleBrushClick);
  259. }
  260. return !state;
  261. });
  262. };
  263. // 撤销
  264. const onUndo = () => {
  265. graphRef.current?.undo();
  266. };
  267. // 重做
  268. const onRedo = () => {
  269. graphRef.current?.redo();
  270. };
  271. // 设置右侧工具激活项
  272. const rightToolbarActive = (type: string) => {
  273. setRightToolbarActive(rightToobarActive === type ? undefined : type);
  274. };
  275. const setCorrelationEdgeInfo = (sourceNode?: Node) => {
  276. graph && handleCreateCorrelationEdge(graph, correlationEdgeRef, sourceNode);
  277. };
  278. // 添加连接线
  279. const handleAddCorrelation = (source: Cell, target: Cell) => {
  280. const link = graph
  281. ?.createEdge({
  282. source: {
  283. cell: source.id,
  284. connectionPoint: "rect",
  285. anchor: "center",
  286. },
  287. target: {
  288. cell: target.id,
  289. connectionPoint: "rect",
  290. anchor: "center",
  291. },
  292. zIndex: 0,
  293. connector: "smooth",
  294. attrs: {
  295. line: {
  296. stroke: "#71cb2d",
  297. strokeWidth: 2,
  298. },
  299. },
  300. data: {
  301. isLink: true,
  302. },
  303. tools: ["vertices", "edge-editor", "button-remove"],
  304. })
  305. .toJSON();
  306. if (link) {
  307. source.setData({
  308. links: [...(source.data?.links || []), link],
  309. });
  310. }
  311. };
  312. useEffect(() => {
  313. if (graph) {
  314. graph.on("node:click", (args) => {
  315. if (correlationEdgeRef.current) {
  316. handleAddCorrelation(
  317. correlationEdgeRef.current.getSourceCell()!,
  318. args.node
  319. );
  320. }
  321. });
  322. graph.on("blank:click", () => {
  323. setTimeout(() => {
  324. setCorrelationEdgeInfo(undefined);
  325. }, 50);
  326. });
  327. graph.on("cell:click", () => {
  328. setTimeout(() => {
  329. setCorrelationEdgeInfo(undefined);
  330. }, 50);
  331. });
  332. }
  333. }, [graph]);
  334. return {
  335. graph,
  336. selectedCell,
  337. initMindMap,
  338. rightToobarActive,
  339. rightToolbarActive,
  340. mindProjectInfo,
  341. setMindProjectInfo,
  342. canUndo,
  343. canRedo,
  344. onUndo,
  345. onRedo,
  346. enableFormatBrush,
  347. toggleFormatBrush,
  348. setCorrelationEdgeInfo,
  349. };
  350. }