mindMapEvent.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753
  1. import { Graph, Cell, Node, Edge, EventArgs } from "@antv/x6";
  2. import { BorderSize, StructureType, TopicType } from "@/enum";
  3. import { addTopic, updateTopic } from "@/pages/mindmap/mindMap";
  4. import { cellStyle, MindMapProjectInfo, TopicItem } from "@/types";
  5. import { Dnd } from "@antv/x6-plugin-dnd";
  6. import { selectTopic } from "@/utils/mindmapHander";
  7. import { uuid } from "@repo/utils";
  8. import { getTheme } from "@/pages/mindmap/theme";
  9. import { traverseNode } from "@/utils/mindmapHander";
  10. import { EditMindMapElement, AddMindMapElement } from "@/api/systemDesigner";
  11. import { debounce, isEqual } from "lodash-es";
  12. enum positionType {
  13. left = "left",
  14. right = "right",
  15. outside = "outside",
  16. }
  17. // 拖拽主题时指示器
  18. let indicatorNode: Node | undefined;
  19. let indicatorEdge: Edge | undefined;
  20. // 插入的节点位置
  21. let insertNode: Node | undefined;
  22. let moveStart: { x: number; y: number } | undefined;
  23. let dragging = false;
  24. let currentShadowNode: Node | undefined;
  25. export const bindMindMapEvents = (
  26. graph: Graph,
  27. setMindProjectInfo?: (
  28. info: MindMapProjectInfo,
  29. init?: boolean,
  30. isSetting?: boolean,
  31. ignoreRender?: boolean
  32. ) => void,
  33. setSelectedCell?: (cell: Cell[]) => void,
  34. dndRef?: React.MutableRefObject<Dnd | undefined>
  35. ) => {
  36. graph.on("selection:changed", ({ selected }: { selected: Cell[] }) => {
  37. setSelectedCell?.(selected);
  38. });
  39. graph.on("node:mouseenter", (args) => {
  40. if (args.node.data?.type !== TopicType.main) {
  41. graph.disablePanning();
  42. }
  43. });
  44. graph.on("node:mouseleave", (args) => {
  45. graph.enablePanning();
  46. });
  47. /*********************************** 拖拽开始 *********************************/
  48. graph.on("node:mousedown", (args) => {
  49. moveStart = {
  50. x: args.x,
  51. y: args.y,
  52. };
  53. });
  54. graph.on("node:mousemove", (args) => {
  55. if (!args.node.data?.parentId || args.node.data?.shadow) {
  56. return;
  57. }
  58. if (
  59. !dragging &&
  60. moveStart &&
  61. (Math.abs(args.x - moveStart.x) > 5 || Math.abs(args.y - moveStart.y) > 5)
  62. ) {
  63. // 判断开始点是否有值,有值时当拖拽距离>5时开启影子节点
  64. dragging = true;
  65. const node = args.node;
  66. const shadowNode = graph.createNode({
  67. shape: "rect",
  68. width: node.size().width,
  69. height: node.size().height,
  70. x: args.x,
  71. y: args.y,
  72. label: node.data?.label,
  73. attrs: {
  74. body: {
  75. rx: node.data?.borderSize,
  76. ry: node.data?.borderSize,
  77. stroke: "#666",
  78. strokeWidth: 1,
  79. },
  80. },
  81. data: {
  82. ...node.data,
  83. shadow: true,
  84. },
  85. });
  86. setShadowMode(true, node, graph);
  87. currentShadowNode = shadowNode;
  88. dndRef?.current?.init();
  89. dndRef?.current?.start(shadowNode, args.e as unknown as MouseEvent);
  90. }
  91. });
  92. graph.on("node:mousemove", (args) => {
  93. // 节点拖拽-处理影子节点吸附问题
  94. if (currentShadowNode && setMindProjectInfo) {
  95. topicDragHander(graph, { x: args.x, y: args.y }, args.node);
  96. }
  97. });
  98. graph.on("node:mouseup", (args) => {
  99. // 拖拽结束
  100. if (indicatorNode && insertNode) {
  101. graph.removeCell(args.node.id + "-edge");
  102. setMindProjectInfo &&
  103. handleSwitchPosition(
  104. setMindProjectInfo,
  105. args.node.id,
  106. insertNode.id,
  107. undefined,
  108. graph
  109. );
  110. }
  111. // 成为自由节点
  112. if (
  113. currentShadowNode &&
  114. !indicatorNode &&
  115. args.node.data?.parentId &&
  116. canBeFreeNode(args.x, args.y, args.node)
  117. ) {
  118. setMindProjectInfo &&
  119. handleSwitchPosition(
  120. setMindProjectInfo,
  121. args.node.id,
  122. undefined,
  123. {
  124. x: args.x,
  125. y: args.y,
  126. },
  127. graph
  128. );
  129. }
  130. currentShadowNode && setShadowMode(false, args.node, graph);
  131. moveStart = undefined;
  132. dragging = false;
  133. currentShadowNode = undefined;
  134. dndRef?.current?.remove();
  135. if (indicatorEdge && indicatorNode) {
  136. graph.removeCells([indicatorEdge, indicatorNode]);
  137. indicatorEdge = undefined;
  138. indicatorNode = undefined;
  139. }
  140. });
  141. graph.on("node:move", (args) => {
  142. // 自由节点拖拽
  143. if (!args.node.data?.parentId) {
  144. setShadowMode(true, args.node, graph);
  145. }
  146. });
  147. graph.on("node:moving", (args) => {
  148. if (!args.node.data?.parentId && setMindProjectInfo) {
  149. setShadowMode(false, args.node, graph);
  150. topicDragHander(graph, { x: args.x, y: args.y }, args.node);
  151. }
  152. });
  153. graph.on("node:moved", (args) => {
  154. if (!args.node.data?.parentId) {
  155. setShadowMode(false, args.node, graph);
  156. }
  157. });
  158. /*********************************** 拖拽结束 *********************************/
  159. // 双击画布空白-新增自由主题
  160. graph.on("blank:dblclick", (args) => {
  161. if (setMindProjectInfo) {
  162. const topic = addTopic(
  163. TopicType.branch,
  164. setMindProjectInfo,
  165. graph,
  166. undefined,
  167. {
  168. x: args.x,
  169. y: args.y,
  170. setMindProjectInfo,
  171. type: TopicType.branch,
  172. label: "自由主题",
  173. borderSize: BorderSize.medium,
  174. }
  175. );
  176. selectTopic(graph, topic);
  177. }
  178. });
  179. /**
  180. * 节点数据更改
  181. */
  182. graph.on("node:change:data", (args) => {
  183. const { current, previous } = args;
  184. console.log("数据变更:", current, previous);
  185. // 收折子项 setMindProjectInfo更新会重新渲染
  186. if (current.collapsed !== previous.collapsed) {
  187. setMindProjectInfo &&
  188. updateTopic(
  189. args.cell.id,
  190. { collapsed: current.collapsed },
  191. setMindProjectInfo,
  192. graph
  193. );
  194. return;
  195. }
  196. if (current?.links && current.links.length !== previous?.links?.length) {
  197. setMindProjectInfo &&
  198. updateTopic(
  199. args.cell.id,
  200. { links: current.links },
  201. setMindProjectInfo,
  202. graph
  203. );
  204. }
  205. if (current?.border !== previous?.border) {
  206. setMindProjectInfo &&
  207. updateTopic(
  208. args.cell.id,
  209. { border: current.border },
  210. setMindProjectInfo,
  211. graph
  212. );
  213. }
  214. if (current?.summary !== previous?.summary) {
  215. setMindProjectInfo &&
  216. updateTopic(
  217. args.cell.id,
  218. { summary: current.summary },
  219. setMindProjectInfo,
  220. graph
  221. );
  222. }
  223. if (current?.extraModules !== previous?.extraModules) {
  224. setMindProjectInfo &&
  225. updateTopic(
  226. args.cell.id,
  227. { extraModules: current?.extraModules },
  228. setMindProjectInfo,
  229. graph
  230. );
  231. }
  232. // 改线段
  233. if (current?.edge && !isEqual(current.edge, previous?.edge)) {
  234. setMindProjectInfo &&
  235. updateTopic(
  236. args.cell.id,
  237. { edge: current.edge },
  238. setMindProjectInfo,
  239. graph
  240. );
  241. }
  242. // 本地缓存更新不会重新渲染
  243. if (args.cell.id.includes("-border")) {
  244. updateTopic(
  245. args.current.origin,
  246. { border: current },
  247. (info) => {
  248. setMindProjectInfo?.(info, false, false, true);
  249. },
  250. graph
  251. );
  252. } else {
  253. updateTopic(
  254. args.cell.id,
  255. current,
  256. (info) => {
  257. setMindProjectInfo?.(info, false, false, true);
  258. },
  259. graph
  260. );
  261. }
  262. });
  263. graph.on("node:resized", (args) => {
  264. args.node.setData({
  265. fixedWidth: true,
  266. width: args.node.size().width,
  267. height: args.node.size().height,
  268. });
  269. });
  270. graph.on(
  271. "node:change:position",
  272. debounce((args) => {
  273. const { current } = args;
  274. if (
  275. args.cell.isNode() &&
  276. !args.cell.data?.parentId &&
  277. args.cell.data.type !== TopicType.main
  278. ) {
  279. updateTopic(
  280. args.cell.id,
  281. { ...args.cell.data, x: current?.x, y: current?.y },
  282. (info) => {
  283. // localStorage.setItem("minMapProjectInfo", JSON.stringify(info));
  284. setMindProjectInfo && setMindProjectInfo(info);
  285. },
  286. graph
  287. );
  288. EditMindMapElement({
  289. ...args.cell.data,
  290. ...args.current,
  291. graphId: sessionStorage.getItem("projectId"),
  292. tools: "",
  293. });
  294. }
  295. }, 500)
  296. );
  297. /**
  298. * 连接线更改
  299. */
  300. graph.on("edge:change:*", (args) => {
  301. if (args.key === "vertices" || args.key === "labels") {
  302. const link = args.edge.toJSON();
  303. const source = args.edge.getSourceCell();
  304. source?.setData({
  305. links: source.data?.links.map((item: Edge.Properties) => {
  306. if (item.id === link.id) return link;
  307. return item;
  308. }),
  309. });
  310. }
  311. });
  312. /**
  313. * 连接线删除
  314. */
  315. graph.on("edge:removed", (args) => {
  316. if (args.edge.data?.isLink) {
  317. // @ts-ignore
  318. const source = graph.getCellById(args.edge.source?.cell as string);
  319. source?.setData(
  320. {
  321. links: source.data?.links?.filter(
  322. (item: Edge.Properties) => item.id !== args.edge.id
  323. ),
  324. },
  325. {
  326. deep: false,
  327. }
  328. );
  329. }
  330. });
  331. };
  332. const canBeFreeNode = (x: number, y: number, node: Node): boolean => {
  333. if (!moveStart) return false;
  334. return Math.abs(x - moveStart.x) > 50 || Math.abs(y - moveStart.y) > 50;
  335. };
  336. // 判断当前点在主图的位置
  337. const atNodePosition = (
  338. position: { x: number; y: number },
  339. x: number,
  340. y: number,
  341. width: number,
  342. height: number
  343. ): positionType => {
  344. if (
  345. position.x < x + width / 2 &&
  346. position.x > x &&
  347. position.y < y + height &&
  348. position.y > y
  349. ) {
  350. return positionType.left;
  351. }
  352. if (
  353. position.x < x + width &&
  354. position.x > x + width / 2 &&
  355. position.y < y + height &&
  356. position.y > y
  357. ) {
  358. return positionType.right;
  359. }
  360. return positionType.outside;
  361. };
  362. // 创建一个指示器
  363. const addIndicator = (
  364. x: number,
  365. y: number,
  366. graph: Graph,
  367. targetNode: Node,
  368. atPosition: positionType
  369. ) => {
  370. if (indicatorEdge && indicatorNode) {
  371. graph.removeCells([indicatorEdge, indicatorNode]);
  372. }
  373. indicatorNode = graph.addNode({
  374. shape: "rect",
  375. width: 100,
  376. height: 26,
  377. x,
  378. y,
  379. attrs: {
  380. body: {
  381. fill: "#fecb99",
  382. stroke: "#fc7e00",
  383. strokeWidth: 1,
  384. rx: 2,
  385. ry: 2,
  386. style: {
  387. opacity: 0.6,
  388. },
  389. },
  390. },
  391. });
  392. indicatorEdge = graph.addEdge({
  393. source: {
  394. cell: targetNode.id,
  395. anchor: {
  396. name: atPosition === positionType.left ? "left" : "right",
  397. args: {
  398. dx: atPosition === positionType.left ? 3 : -3,
  399. },
  400. },
  401. },
  402. target: {
  403. cell: indicatorNode.id,
  404. anchor: {
  405. name: atPosition === positionType.left ? "right" : "left",
  406. args: {
  407. dx: atPosition === positionType.left ? -3 : 3,
  408. },
  409. },
  410. },
  411. connector: "normal",
  412. attrs: {
  413. line: {
  414. stroke: "#fc7e00",
  415. strokeWidth: 2,
  416. sourceMarker: {
  417. name: "",
  418. },
  419. targetMarker: {
  420. name: "",
  421. },
  422. style: {
  423. opacity: 0.6,
  424. },
  425. },
  426. },
  427. });
  428. };
  429. // 添加指示器
  430. const setIndicator = (
  431. atPosition: positionType,
  432. targetNode: Node,
  433. graph: Graph
  434. ) => {
  435. switch (atPosition) {
  436. // 1、左侧位置
  437. case positionType.left: {
  438. const x = targetNode.position().x - 120;
  439. const y = targetNode.position().y + targetNode.size().height / 2 - 13;
  440. // 判断是左侧节点还是右侧节点
  441. if (targetNode.data?.parentId) {
  442. const parentNode = graph.getCellById(targetNode.data.parentId);
  443. // 左侧朝向的结构
  444. if (
  445. parentNode?.isNode() &&
  446. targetNode.position().x < parentNode.position().x
  447. ) {
  448. addIndicator(x, y, graph, targetNode, atPosition);
  449. }
  450. } else {
  451. // @ts-ignore
  452. const mindProjectInfo = graph.extendAttr.getMindProjectInfo();
  453. if (mindProjectInfo?.structure === StructureType.left) {
  454. addIndicator(x, y, graph, targetNode, atPosition);
  455. }
  456. }
  457. break;
  458. }
  459. // 2、右侧位置
  460. case positionType.right: {
  461. const x = targetNode.position().x + targetNode.size().width + 20;
  462. const y = targetNode.position().y + targetNode.size().height / 2 - 13;
  463. // 判断是左侧节点还是右侧节点
  464. if (targetNode.data?.parentId) {
  465. const parentNode = graph.getCellById(targetNode.data.parentId);
  466. // 右侧朝向的结构
  467. if (
  468. parentNode?.isNode() &&
  469. targetNode.position().x > parentNode.position().x
  470. ) {
  471. addIndicator(x, y, graph, targetNode, atPosition);
  472. }
  473. } else {
  474. // @ts-ignore
  475. const mindProjectInfo = graph.extendAttr.getMindProjectInfo();
  476. if (mindProjectInfo?.structure === StructureType.right) {
  477. addIndicator(x, y, graph, targetNode, atPosition);
  478. }
  479. }
  480. break;
  481. }
  482. // 外部位置
  483. case positionType.outside: {
  484. }
  485. }
  486. };
  487. // 判断目标节点是不是后代节点
  488. const isDescendantNode = (targetNode: Node, originNode: Node, graph: Graph) => {
  489. if (targetNode.data?.parentId === originNode.id) {
  490. return true;
  491. }
  492. let isChild = false; // 是后代节点
  493. const findParent = (parentId: string) => {
  494. const cell = graph.getCellById(parentId);
  495. if (cell?.isNode() && cell.data.parentId) {
  496. if (cell.data.parentId === originNode.id) {
  497. isChild = true;
  498. } else {
  499. findParent(cell.data.parentId);
  500. }
  501. }
  502. };
  503. targetNode.data?.parentId && findParent(targetNode.data.parentId);
  504. return isChild;
  505. };
  506. /**
  507. * 主题拖拽放置
  508. * @param graph
  509. * @param setMindProjectInfo
  510. */
  511. export const topicDragHander = (
  512. graph: Graph,
  513. position: { x: number; y: number },
  514. originNode: Node
  515. ) => {
  516. if (indicatorEdge) graph.removeCell(indicatorEdge);
  517. if (indicatorNode) graph.removeCell(indicatorNode);
  518. indicatorEdge = undefined;
  519. indicatorNode = undefined;
  520. // 1、 获取位置的地方是否有节点
  521. const nodes = graph.getNodes();
  522. nodes.forEach((targetNode) => {
  523. // 目标节点是自己、自己的后代、节点不做处理
  524. if (
  525. targetNode.id === originNode.id ||
  526. isDescendantNode(targetNode, originNode, graph)
  527. ) {
  528. return;
  529. }
  530. const { x, y } = targetNode.position();
  531. const { width, height } = targetNode.size();
  532. // 2、 找到是在节点哪个区域内
  533. const atPosition = atNodePosition(position, x, y, width, height);
  534. // 3、添加插入指示
  535. setIndicator(atPosition, targetNode, graph);
  536. // 4、 根据位置确定插入位置
  537. if ([positionType.left, positionType.right].includes(atPosition)) {
  538. insertNode = targetNode;
  539. }
  540. });
  541. };
  542. /**
  543. * 拖拽完毕,切换主题位置
  544. * @param setMindProjectInfo
  545. * @param sourceId 拖拽的节点
  546. * @param targetId 放置的节点
  547. * @param position 自由节点放置的位置
  548. * @returns
  549. */
  550. const handleSwitchPosition = (
  551. setMindProjectInfo: (info: MindMapProjectInfo) => void,
  552. sourceId: string,
  553. targetId?: string,
  554. position?: { x: number; y: number },
  555. graph?: Graph
  556. ) => {
  557. // @ts-ignore
  558. const mindmapProjectInfo: MindMapProjectInfo = graph.extendAttr.getMindProjectInfo();
  559. if (!mindmapProjectInfo) return;
  560. // 找到要拖拽的节点并删除
  561. let source: (TopicItem & cellStyle) | undefined;
  562. mindmapProjectInfo.topics.forEach((topic) => {
  563. if (topic.id === sourceId) {
  564. source = topic;
  565. }
  566. mindmapProjectInfo.topics = mindmapProjectInfo.topics.filter(
  567. (item) => item.id !== sourceId
  568. );
  569. });
  570. const topics = source
  571. ? mindmapProjectInfo.topics
  572. : traverseNode(mindmapProjectInfo.topics, (topic) => {
  573. const findItem = topic?.children?.find((item) => item.id === sourceId);
  574. if (findItem) {
  575. source = findItem;
  576. topic.children = topic.children?.filter(
  577. (item) => item.id !== sourceId
  578. );
  579. }
  580. });
  581. if (!source) return;
  582. // 处理节点样式
  583. const targetNode = targetId ? graph?.getCellById(targetId) : undefined;
  584. const theme = getTheme(
  585. mindmapProjectInfo.theme,
  586. targetNode?.isNode() && targetNode.data.type !== TopicType.main
  587. ? targetNode.data
  588. : undefined
  589. );
  590. source.type =
  591. targetNode?.data?.type === TopicType.main
  592. ? TopicType.branch
  593. : TopicType.sub;
  594. const themeObj = targetId ? theme[source.type] : theme[TopicType.branch];
  595. source.fill = {
  596. ...source.fill,
  597. ...themeObj.fill,
  598. };
  599. source.text = {
  600. ...source.text,
  601. ...themeObj.text,
  602. };
  603. source.stroke = {
  604. ...source.stroke,
  605. ...themeObj.stroke,
  606. };
  607. source.edge = {
  608. ...source.edge,
  609. ...themeObj.edge,
  610. };
  611. // 后代节点样式
  612. if (source.children)
  613. source.children = traverseNode(source.children, (topic) => {
  614. const theme = getTheme(mindmapProjectInfo.theme, source);
  615. topic.type = TopicType.sub;
  616. topic.fill = {
  617. ...topic.fill,
  618. ...theme[topic.type].fill,
  619. };
  620. topic.text = {
  621. ...topic.text,
  622. ...theme[topic.type].text,
  623. };
  624. topic.stroke = {
  625. ...topic.stroke,
  626. ...theme[topic.type].stroke,
  627. };
  628. topic.edge = {
  629. ...topic.edge,
  630. ...theme[topic.type].edge,
  631. };
  632. });
  633. if (targetId) {
  634. // 加入到目标节点下
  635. mindmapProjectInfo.topics = traverseNode(topics, (topic) => {
  636. if (topic.id === targetId) {
  637. if (!topic.children) {
  638. topic.children = [];
  639. }
  640. source &&
  641. topic.children.push({
  642. ...source,
  643. parentId: topic.id,
  644. });
  645. }
  646. });
  647. } else {
  648. const id = uuid();
  649. // 成为自由节点
  650. const freeTopic = {
  651. ...source,
  652. id,
  653. children: (source.children || []).map((item) => {
  654. item.parentId = id;
  655. return item;
  656. }),
  657. opacity: 100,
  658. x: position?.x,
  659. y: position?.y,
  660. type: TopicType.branch,
  661. parentId: null,
  662. links: (source.links || []).map((item) => {
  663. // 修改sourceId
  664. item.source = {
  665. ...item.source,
  666. id,
  667. };
  668. return item;
  669. }),
  670. };
  671. mindmapProjectInfo.topics.push(freeTopic);
  672. AddMindMapElement({
  673. ...freeTopic,
  674. graphId: sessionStorage.getItem("projectId"),
  675. });
  676. }
  677. setMindProjectInfo(mindmapProjectInfo);
  678. };
  679. /**
  680. * 设置影子模式-拖拽时变灰
  681. * @param node
  682. * @param graph
  683. */
  684. export const setShadowMode = (enable: boolean, node: Node, graph: Graph) => {
  685. const data = node.getData();
  686. node.setData({
  687. opacity: enable ? 60 : 100,
  688. });
  689. if (data?.children?.length) {
  690. traverseNode(data.children, (topic) => {
  691. const cell = graph.getCellById(topic.id);
  692. const edge = graph.getCellById(topic.id + "-edge");
  693. if (cell && cell.isNode()) {
  694. cell.setData({
  695. opacity: enable ? 60 : 100,
  696. });
  697. }
  698. if (edge && edge.isEdge()) {
  699. edge.setAttrs({
  700. line: {
  701. style: {
  702. opacity: enable ? 0.6 : 1,
  703. },
  704. },
  705. });
  706. }
  707. });
  708. }
  709. };