graphModel.ts 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. import { useState, useRef, useEffect } from "react";
  2. import { Cell, Graph, Node } from "@antv/x6";
  3. import { Dnd } from "@antv/x6-plugin-dnd";
  4. import { Transform } from "@antv/x6-plugin-transform";
  5. import { Snapline } from "@antv/x6-plugin-snapline";
  6. import { Clipboard } from "@antv/x6-plugin-clipboard";
  7. import { Selection } from "@repo/x6-plugin-selection/src/index";
  8. import { History } from "@antv/x6-plugin-history";
  9. import { Keyboard } from "@antv/x6-plugin-keyboard";
  10. import { Export } from "@antv/x6-plugin-export";
  11. import { useModel } from "umi";
  12. import "@/components/PageContainer";
  13. import { handleGraphEvent, handleGraphApiEvent } from "@/events/flowEvent";
  14. import { pageMenu, nodeMenu, edgeMenu } from "@/utils/contentMenu";
  15. import { bindKeys } from "@/utils/fastKey";
  16. import { useSessionStorageState } from "ahooks";
  17. export default function GraphModel() {
  18. const [graph, setGraph] = useState<Graph>();
  19. const [dnd, setDnd] = useState<Dnd>();
  20. const pageNodeRef = useRef<Node>();
  21. const dndRef = useRef(dnd);
  22. // 画布
  23. const graphRef = useRef<Graph>();
  24. const { pageState } = useModel("appModel");
  25. // 当前选中的节点
  26. const [selectedCell, setSelectedCell] = useState<Cell[]>([]);
  27. const [canRedo, setCanRedo] = useState(false);
  28. const [canUndo, setCanUndo] = useState(false);
  29. const [projectInfo, setProjectInfo] = useSessionStorageState<{
  30. graph: Record<string, any>;
  31. elements: Cell.Properties[];
  32. }>("system-design-project");
  33. const handleChangeAll = () => {
  34. const cells = (graph?.toJSON().cells || []).filter(
  35. (cell) => !cell?.data?.isPage
  36. );
  37. projectInfo &&
  38. setProjectInfo({
  39. ...projectInfo,
  40. elements: cells,
  41. });
  42. };
  43. const addPageNode = () => {
  44. const graph = graphRef.current;
  45. pageNodeRef.current = graph?.addNode({
  46. shape: "page-container-node",
  47. width: pageState.width,
  48. height: pageState.height,
  49. zIndex: -1,
  50. data: {
  51. isPage: true,
  52. ignoreDrag: true,
  53. ...pageState,
  54. },
  55. tools: [
  56. {
  57. name: "contextmenu",
  58. args: {
  59. menu: pageMenu,
  60. },
  61. },
  62. ],
  63. });
  64. };
  65. const initCells = (cells: Cell.Properties[]) => {
  66. if (graphRef.current) {
  67. // 添加节点
  68. (cells || []).forEach((item) => {
  69. if (item.shape !== "edge") {
  70. graphRef.current?.addNode({
  71. ...item,
  72. data: JSON.parse(item?.data),
  73. ports: JSON.parse(item?.ports || "{}"),
  74. size: JSON.parse(item?.size || "{}"),
  75. position: JSON.parse(item?.position || "{}"),
  76. tools: [{
  77. name: "contextmenu",
  78. args: {
  79. menu: nodeMenu,
  80. },
  81. }]
  82. });
  83. }
  84. });
  85. // 添加边
  86. (cells || []).forEach((item) => {
  87. if (item.shape === "edge") {
  88. graphRef.current?.addEdge({
  89. ...item,
  90. data: JSON.parse(item?.data),
  91. attrs: JSON.parse(item?.attrs as unknown as string || "{}"),
  92. source: JSON.parse(item?.source),
  93. target: JSON.parse(item?.target),
  94. // router: JSON.parse(item?.router || "{}"),
  95. tools: [
  96. {
  97. name: "contextmenu",
  98. args: {
  99. menu: edgeMenu,
  100. },
  101. },
  102. ]
  103. });
  104. }
  105. })
  106. graphRef.current.on("cell:change:*", handleChangeAll);
  107. addPageNode();
  108. handleGraphApiEvent(graphRef.current);
  109. }
  110. };
  111. /**初始化页面节点 */
  112. useEffect(() => {
  113. if (pageState.width && graphRef.current && !pageNodeRef.current) {
  114. addPageNode();
  115. }
  116. }, [pageState.width, graphRef.current]);
  117. useEffect(() => {
  118. pageNodeRef.current?.setData({
  119. background: pageState.backgroundColor,
  120. ...pageState,
  121. });
  122. pageNodeRef.current?.setSize({
  123. width: pageState.width,
  124. height: pageState.height,
  125. });
  126. }, [pageState]);
  127. const enabledTransform = (node: Node) => {
  128. const data = node.getData<{ isPage: boolean; lock: boolean }>();
  129. return !data?.isPage && !data?.lock;
  130. };
  131. /**初始化画布 */
  132. const initGraph = (instance: Graph) => {
  133. // 添加插件
  134. instance
  135. .use(
  136. new Transform({
  137. resizing: {
  138. enabled: enabledTransform,
  139. minWidth: 20,
  140. minHeight: 20,
  141. },
  142. rotating: {
  143. enabled: enabledTransform,
  144. grid: 1,
  145. },
  146. })
  147. )
  148. .use(
  149. new Selection({
  150. enabled: true,
  151. multiple: true,
  152. rubberband: true,
  153. movable: true,
  154. showNodeSelectionBox: true,
  155. // showEdgeSelectionBox: true,
  156. pointerEvents: "none",
  157. strict: true,
  158. filter: (cell: Cell) => {
  159. const data = cell.getData<{ isPage: boolean; lock: boolean }>();
  160. return !data?.isPage && !data?.lock;
  161. },
  162. })
  163. )
  164. .use(
  165. new Snapline({
  166. sharp: true,
  167. resizing: true,
  168. })
  169. )
  170. .use(
  171. new Keyboard({
  172. enabled: true,
  173. global: true,
  174. })
  175. )
  176. .use(new Clipboard())
  177. .use(
  178. new History({
  179. enabled: true,
  180. beforeAddCommand: (event, args) => {
  181. // @ts-ignore 排除不用创建的节点
  182. if (args?.cell?.data?.noCreate) return false;
  183. // @ts-ignore 排除页面节点
  184. return !(event === "cell:added" && args?.cell?.getData()?.isPage);
  185. },
  186. })
  187. )
  188. .use(new Export());
  189. setGraph(instance);
  190. graphRef.current = instance;
  191. // 选中的节点/边发生改变(增删)时触发
  192. instance.on(
  193. "selection:changed",
  194. ({ selected }: { added: Cell[]; removed: Cell[]; selected: Cell[] }) => {
  195. setSelectedCell(selected);
  196. }
  197. );
  198. instance.on("history:change", () => {
  199. setCanRedo(instance.canRedo());
  200. setCanUndo(instance.canUndo());
  201. });
  202. // 通用事件处理
  203. handleGraphEvent(instance);
  204. // 绑定快捷键
  205. bindKeys(instance);
  206. };
  207. /**初始化拖拽 */
  208. const initDnd = (instance: Dnd) => {
  209. setDnd(instance);
  210. dndRef.current = instance;
  211. };
  212. /**组件库拖拽生成 */
  213. const startDrag = (
  214. e: React.MouseEvent<HTMLDivElement, MouseEvent>,
  215. node: Node.Metadata
  216. ) => {
  217. if (!dndRef.current || !graphRef.current) return;
  218. // 往画布添加节点
  219. const n = graphRef.current.createNode(node);
  220. // 右键菜单
  221. n.addTools({
  222. name: "contextmenu",
  223. args: {
  224. menu: nodeMenu,
  225. },
  226. });
  227. dndRef.current.start(n, e.nativeEvent as any);
  228. };
  229. // 撤销
  230. const onUndo = () => {
  231. graphRef.current?.undo();
  232. };
  233. // 重做
  234. const onRedo = () => {
  235. graphRef.current?.redo();
  236. };
  237. return {
  238. graph,
  239. dnd,
  240. initGraph,
  241. initDnd,
  242. startDrag,
  243. selectedCell,
  244. canRedo,
  245. canUndo,
  246. onUndo,
  247. onRedo,
  248. initCells,
  249. };
  250. }