mindMapModel.ts 9.7 KB

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