Port.tsx 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285
  1. import { PlusOutlined } from "@ant-design/icons";
  2. import { Edge, EventArgs, Graph, Node } from "@antv/x6";
  3. import React, { useEffect, useRef } from "react";
  4. import { Dropdown, Popover } from "antd";
  5. import NodeMenu from "../NodeMenu";
  6. export default function Port(props: {
  7. hovered: boolean;
  8. out?: boolean;
  9. style?: React.CSSProperties;
  10. node?: Node;
  11. graph?: Graph;
  12. type?: "in" | "out" | "extra";
  13. }) {
  14. const { hovered, style = {}, out = true, node, graph, type } = props;
  15. const [canAdd, setCanAdd] = React.useState(false);
  16. const [open, setOpen] = React.useState(false);
  17. const isDown = useRef(false);
  18. const isMove = useRef(false);
  19. const newEdge = React.useRef<Edge>();
  20. const extraStyle = React.useMemo(() => {
  21. if (out && canAdd) {
  22. return {
  23. transform: "scale(2) translateY(-50%)",
  24. };
  25. }
  26. if (!out) {
  27. return {
  28. width: 8,
  29. height: 16,
  30. borderRadius: 0,
  31. border: "none",
  32. left: -5,
  33. };
  34. } else {
  35. return {};
  36. }
  37. }, [canAdd]);
  38. const handleSetCanAdd = (value: boolean) => {
  39. out && setCanAdd(value);
  40. graph?.togglePanning(!value);
  41. };
  42. // step1: 鼠标按下拖拽开始
  43. const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
  44. isDown.current = true;
  45. node?.setData({ lock: true });
  46. };
  47. useEffect(() => {
  48. // step2: 移动鼠标添加连线
  49. // 检测移动 添加边 设置边目标位置
  50. graph?.on("node:mousemove", (args) => {
  51. if (isDown.current) {
  52. // 按下还没移动时创建边
  53. if (!isMove.current && node) {
  54. isMove.current = true;
  55. const ports = node.getPorts();
  56. const rightPort = ports.find((item) => type ? item.group === 'bottom' : item.group === "right");
  57. newEdge.current = graph?.addEdge({
  58. source: { cell: node.id, port: rightPort?.id },
  59. router: {
  60. name: "manhattan",
  61. args: {
  62. padding: 20,
  63. excludeShapes: ["notice-node"],
  64. },
  65. },
  66. connector: { name: "rounded" },
  67. target: {
  68. x: args.x,
  69. y: args.y,
  70. },
  71. attrs: {
  72. line: {
  73. stroke: "#37d0ff",
  74. strokeWidth: 2,
  75. },
  76. },
  77. });
  78. } else {
  79. // 判断是否进入其他节点内
  80. // 查找到最顶层的节点 然后修改位置为节点
  81. const nodes = graph.getNodesInArea(args.x, args.y, 5, 5);
  82. const targetNode = nodes.length
  83. ? (nodes || [])?.reduce((prev, curr) => {
  84. const prevZ = prev.zIndex || -1;
  85. const currZ = curr?.zIndex || -1;
  86. return prevZ >= currZ ? prev : curr;
  87. })
  88. : null;
  89. // 进入节点 不存在已连接边 则添加一条边
  90. if (targetNode && targetNode.shape !== "notice-node") {
  91. const ports = targetNode.getPorts();
  92. const targetPort = ports.find((item) => item.group === "left");
  93. newEdge.current?.setTarget({
  94. cell: targetNode.id,
  95. port: targetPort?.id,
  96. });
  97. } else {
  98. // 否则修改边的终点
  99. newEdge.current?.setTarget({
  100. x: args.x,
  101. y: args.y,
  102. });
  103. }
  104. }
  105. }
  106. });
  107. // step3: 鼠标抬起,展示节点菜单或者连线连接到目标节点
  108. graph?.on("node:mouseup", (args) => {
  109. console.log("node:mouseup", newEdge.current);
  110. node?.setData({ lock: false });
  111. isDown.current = false;
  112. isMove.current = false;
  113. // 拖拽过程中释放鼠标 检测当前是否有节点
  114. if (newEdge.current) {
  115. // 判断是否连接到了节点
  116. if (!Object.hasOwn(newEdge.current.target, "x")) {
  117. newEdge.current.setAttrs({
  118. line: {
  119. stroke: "#7e8186",
  120. },
  121. });
  122. newEdge.current.setZIndex(0);
  123. newEdge.current = undefined;
  124. return;
  125. }
  126. // 判断是否存在menu-popover
  127. const els = document.querySelectorAll("[data-shape='menu-popover']");
  128. if (els.length) {
  129. return;
  130. }
  131. graph.addNode({
  132. shape: "menu-popover",
  133. x: args.x,
  134. y: args.y,
  135. });
  136. }
  137. });
  138. // 添加节点完成设置连线目标节点
  139. graph?.on("node:change:addedNode", (args: EventArgs["cell:change:*"]) => {
  140. const { current } = args;
  141. if (newEdge.current && current) {
  142. const addNode = current?.addNode as Node;
  143. const ports = addNode.getPorts();
  144. const leftPort = ports?.find((item) => item.group === "left");
  145. const bottomPort = ports?.find((item) => item.group === "bottom");
  146. newEdge.current.setTarget({
  147. cell: addNode.id,
  148. port: type ? bottomPort?.id : leftPort?.id,
  149. });
  150. newEdge.current.setAttrs({
  151. line: {
  152. stroke: "#7e8186",
  153. },
  154. });
  155. newEdge.current = undefined;
  156. }
  157. });
  158. // 节点菜单menu-popver关闭, 如果连线目标没有节点信息移除连线
  159. graph?.on("node:change:closedPopover", () => {
  160. setTimeout(() => {
  161. if (Object.hasOwn(newEdge.current?.target || {}, "x")) {
  162. graph?.removeCells([newEdge.current!]);
  163. newEdge.current = undefined;
  164. }
  165. }, 300);
  166. });
  167. }, []);
  168. const { x, y } = node?.position() || { x: 0, y: 0 };
  169. const x1 = (node?.getBBox()?.width || 0) + x + 50;
  170. // 点击添加节点成功后,设置连线
  171. const handleAddChange = (addNode?: Node) => {
  172. setOpen(false);
  173. if (addNode && node) {
  174. const sourcePorts = node?.getPorts();
  175. const sourcePort = sourcePorts?.find((item) => type ? item.group === 'bottom' : item.group === "right");
  176. const targetPorts = addNode?.getPorts();
  177. const targetPort = targetPorts?.find((item) => item.group === "left");
  178. graph?.addEdge({
  179. source: {
  180. cell: node.id,
  181. port: sourcePort?.id,
  182. },
  183. target: {
  184. cell: addNode.id,
  185. port: targetPort?.id,
  186. },
  187. router: {
  188. name: "manhattan",
  189. args: {
  190. padding: 20,
  191. excludeShapes: ["notice-node"],
  192. },
  193. },
  194. zIndex: 0,
  195. connector: { name: "rounded", args: {} },
  196. attrs: {
  197. line: {
  198. stroke: "#7e8186",
  199. strokeWidth: 2,
  200. },
  201. },
  202. });
  203. }
  204. };
  205. return (
  206. <Popover
  207. content={
  208. <NodeMenu
  209. graph={graph}
  210. onChange={handleAddChange}
  211. position={{ x: x1, y }}
  212. />
  213. }
  214. trigger={"click"}
  215. placement="right"
  216. arrow={false}
  217. open={open}
  218. onOpenChange={(open) => {
  219. setOpen(open);
  220. }}
  221. >
  222. {type === "extra" ? (
  223. <div
  224. className="node-port flex items-center justify-center"
  225. style={{
  226. transform: hovered
  227. ? "scale(1.2) translateY(-50%) rotate(45deg)"
  228. : "scale(1) translateY(-50%) rotate(45deg)",
  229. width: 16,
  230. height: 16,
  231. border: 'none',
  232. borderRadius: 0,
  233. ...style,
  234. }}
  235. onMouseEnter={() => handleSetCanAdd(true)}
  236. onMouseLeave={() => handleSetCanAdd(false)}
  237. onMouseDown={handleMouseDown}
  238. >
  239. {out && (
  240. <PlusOutlined
  241. className="transform scale-0 transition duration-300 color-#fff text-8px"
  242. style={{ transform: canAdd ? "scale(1)" : "scale(0)" }}
  243. />
  244. )}
  245. </div>
  246. ) : (
  247. <div
  248. className="node-port flex items-center justify-center"
  249. style={{
  250. transform: hovered
  251. ? "scale(1.2) translateY(-50%)"
  252. : "scale(1) translateY(-50%)",
  253. ...style,
  254. ...extraStyle,
  255. }}
  256. onMouseEnter={() => handleSetCanAdd(true)}
  257. onMouseLeave={() => handleSetCanAdd(false)}
  258. onMouseDown={handleMouseDown}
  259. >
  260. {out && (
  261. <PlusOutlined
  262. className="transform scale-0 transition duration-300 color-#fff text-8px"
  263. style={{ transform: canAdd ? "scale(1)" : "scale(0)" }}
  264. />
  265. )}
  266. </div>
  267. )}
  268. </Popover>
  269. );
  270. }