Browse Source

feat: 添加边线编辑功能

liaojiaxing 7 months ago
parent
commit
0a49f48c0e

+ 7 - 0
apps/designer/src/enum/index.ts

@@ -10,4 +10,11 @@ export enum ConnectorType {
   Rounded, // 圆角
   Smooth,  // 平滑
   Normal,  // 直线
+}
+
+export enum LineType {
+  solid = "",
+  dashed = "5,5",
+  dotted = "1,5",
+  dashdot = "5,5,1,5",
 }

+ 79 - 6
apps/designer/src/events/index.ts

@@ -1,6 +1,79 @@
-/**
- * 事件处理
- */
-export function handleNodeClick(e: any) {
-  console.log(e);
-}
+import { Graph, Node } from "@antv/x6";
+
+export const handleGraphEvent = (graph: Graph) => {
+  const sourceArrowhead = {
+    name: "source-arrowhead",
+    args: {
+      attrs: {
+        d: 'M -5,-5 5,-5 5,5 -5,5 Z',
+        fill: '#fff',
+        stroke: '#239edd',
+        'stroke-width': 1,
+      },
+    }
+  }
+  const targetArrowhead = {
+    name: "target-arrowhead",
+    args: {
+      attrs: {
+        d: 'M -5,-5 5,-5 5,5 -5,5 Z',
+        fill: '#fff',
+        stroke: '#239edd',
+        'stroke-width': 1,
+      },
+    }
+  }
+  // 边选中
+  graph.on("edge:selected", (args) => {
+    args.edge.addTools(['edge-editor', sourceArrowhead, targetArrowhead]);
+  });
+  // 边取消选中
+  graph.on("edge:unselected", (args) => {
+    args.edge.removeTools(['edge-editor', sourceArrowhead, targetArrowhead]);
+  });
+
+  // 控制连接桩显示/隐藏
+  const showPorts = (ports: NodeListOf<SVGElement>, show: boolean) => {
+    for (let i = 0, len = ports.length; i < len; i += 1) {
+      ports[i].style.visibility = show ? "visible" : "hidden";
+    }
+  };
+  graph.on(
+    "node:mouseenter",
+    ({
+      _e,
+      node,
+    }: {
+      _e: React.MouseEvent<HTMLDivElement, MouseEvent>;
+      node: Node.Metadata;
+    }) => {
+      if (node.data?.isPage) return;
+
+      const container = document.querySelector(`[data-cell-id="${node.id}"]`)!;
+      if (!container) return;
+      const ports = container.querySelectorAll(
+        ".x6-port-body"
+      ) as NodeListOf<SVGElement>;
+      showPorts(ports, true);
+    }
+  );
+  graph.on(
+    "node:mouseleave",
+    ({
+      _e,
+      node,
+    }: {
+      _e: React.MouseEvent<HTMLDivElement, MouseEvent>;
+      node: Node.Metadata;
+    }) => {
+      if (node.data?.isPage) return;
+
+      const container = document.querySelector("#graph-container")!;
+      if (!container) return;
+      const ports = container.querySelectorAll(
+        ".x6-port-body"
+      ) as NodeListOf<SVGElement>;
+      showPorts(ports, false);
+    }
+  );
+};

+ 2 - 14
apps/designer/src/hooks/useShapeProps.tsx

@@ -1,4 +1,4 @@
-import { ImageFillType } from "@/enum";
+import { ImageFillType, LineType } from "@/enum";
 import { uniqueId } from "lodash-es";
 
 type PropType = {
@@ -78,19 +78,7 @@ export function useShapeProps(
     </>
   );
 
-  let strokeDasharray  = '';
-  switch(stroke.type) {
-    case 'dashed':
-      strokeDasharray = '5,5';
-      break;
-    case 'dotted':
-      strokeDasharray = '1,5';
-      break;
-    case 'dashdot':
-      strokeDasharray = '5,5,1,5';
-      break;
-    default: strokeDasharray = '';
-  }
+  const strokeDasharray  = LineType[stroke.type];
 
   return {
     strokeColor: stroke.color,

+ 14 - 0
apps/designer/src/loading.tsx

@@ -0,0 +1,14 @@
+export default function Loading() {
+  return (
+    <div className="fixed w-full h-full flex items-center justify-center">
+      <div className="flex">
+        <div className="relative mx-auto h-10 w-10 animate-bounce">
+          <div className="mx-auto h-16 w-16 animate-pulse rounded-full bg-gray-400"></div>
+          <span className="absolute flex h-5 w-5 animate-spin">
+            <span className="h-4 w-4 rounded-full bg-gray-400"> </span>
+          </span>
+        </div>
+      </div>
+    </div>
+  );
+}

+ 13 - 30
apps/designer/src/models/graphModel.ts

@@ -9,6 +9,7 @@ import { History } from '@antv/x6-plugin-history'
 import { Keyboard } from '@antv/x6-plugin-keyboard'
 import { useModel } from 'umi'
 import '@/components/PageContainer'
+import { handleGraphEvent } from '@/events'
 export default function GraphModel() {
   const [graph, setGraph] = useState<Graph>();
   const [dnd, setDnd] = useState<Dnd>();
@@ -35,6 +36,11 @@ export default function GraphModel() {
         data: {
           isPage: true,
           ...pageState
+        },
+        attrs: {
+          style: {
+            'pointer-events': 'none'
+          }
         }
       });
     };
@@ -75,8 +81,8 @@ export default function GraphModel() {
         multiple: true,
         rubberband: true,
         movable: true,
-        showNodeSelectionBox: true,
-        showEdgeSelectionBox: true,
+        // showNodeSelectionBox: true,
+        // showEdgeSelectionBox: true,
         pointerEvents: 'none',
         strict: true,
         filter: (cell: Cell) => {
@@ -100,44 +106,21 @@ export default function GraphModel() {
 
     }));
 
-    // 控制连接桩显示/隐藏
-    const showPorts = (ports: NodeListOf<SVGElement>, show: boolean) => {
-      for (let i = 0, len = ports.length; i < len; i += 1) {
-        ports[i].style.visibility = show ? 'visible' : 'hidden'
-      }
-    }
-    instance.on('node:mouseenter', ({_e, node}:{_e: React.MouseEvent<HTMLDivElement, MouseEvent>, node: Node.Metadata}) => {
-      if(node.data?.isPage) return;
-
-      const container = document.querySelector(`[data-cell-id="${node.id}"]`)!;
-      if(!container) return;
-      const ports = container.querySelectorAll(
-        '.x6-port-body',
-      ) as NodeListOf<SVGElement>;
-      showPorts(ports, true);
-    })
-    instance.on('node:mouseleave', ({_e, node}:{_e: React.MouseEvent<HTMLDivElement, MouseEvent>, node: Node.Metadata}) => {
-      if(node.data?.isPage) return;
+    setGraph(instance);
+    graphRef.current = instance;
 
-      const container = document.querySelector('#graph-container')!;
-      if(!container) return;
-      const ports = container.querySelectorAll(
-        '.x6-port-body',
-      ) as NodeListOf<SVGElement>;
-      showPorts(ports, false);
-    })
     // 选中的节点/边发生改变(增删)时触发
     instance.on('selection:changed', ({added, removed, selected}: {added: Cell[]; removed: Cell[]; selected: Cell[];}) => {
       setSelectedCell(selected);
     })
 
-    setGraph(instance);
-    graphRef.current = instance;
-
     instance.on('history:change', () => {
       setCanRedo(instance.canRedo());
       setCanUndo(instance.canUndo());
     })
+    
+    // 通用事件处理
+    handleGraphEvent(instance);
   }
 
   /**初始化拖拽 */

+ 61 - 1
apps/designer/src/pages/flow/components/Config/GraphStyle.tsx

@@ -26,7 +26,7 @@ import {
 import { arrowOptions } from "@/pages/flow/data";
 import { useModel } from "umi";
 import { useEffect, useRef, useState } from "react";
-import { ImageFillType, ConnectorType } from "@/enum";
+import { ImageFillType, ConnectorType, LineType } from "@/enum";
 import { set, cloneDeep } from "lodash-es";
 import { Cell } from "@antv/x6";
 import { fontFamilyOptions, alignOptionData } from '@/pages/flow/data';
@@ -113,6 +113,7 @@ export default function GraphStyle() {
 
   useEffect(() => {
     const firstNode = selectedCell?.find((item) => item.isNode());
+    const firstEdge = selectedCell?.find((item) => item.isEdge());
     eventNodeList.current = [];
     if (firstNode) {
       const position = firstNode.position();
@@ -169,6 +170,38 @@ export default function GraphStyle() {
       }
     }
 
+    if(firstEdge) {
+      const data = firstEdge.getData();
+      const attrs = firstEdge.attrs || {};
+      const sourceMarker = attrs.line?.sourceMarker as Record<string, any>;
+      const targetMarker = attrs.line?.targetMarker as Record<string, any>;
+      const lineType = attrs.line?.strokeDasharray === LineType.solid 
+        ? "solid"
+        : attrs.line?.strokeDasharray === LineType.dashed
+        ? "dashed"
+        : attrs.line?.strokeDasharray === LineType.dotted
+        ? "dotted"
+        : "dashdot";
+      let obj = {};
+      if(!firstNode) {
+        obj = {
+          stroke: {
+            type: lineType,
+            color: attrs.line?.stroke || "#000000",
+            width: attrs.line?.strokeWidth || 1,
+          }
+        }
+      }
+      setFormModel((state) => {
+        return {
+          ...state,
+          ...obj,
+          startArrow: sourceMarker?.name,
+          endArrow: targetMarker?.name,
+        }
+      })
+    }
+
     let nodeCount = 0;
     selectedCell?.forEach((cell) => {
       if (cell.isEdge()) {
@@ -195,6 +228,33 @@ export default function GraphStyle() {
           opacity: model.opacity,
         });
       }
+      if (cell.isEdge()) {
+        const attr = cell.attrs;
+        const sourceMarker = attr?.line?.sourceMarker as Record<string, any>;
+        const targetMarker = attr?.line?.targetMarker as Record<string, any>;
+        cell.setAttrs({
+          line: {
+            ...(attr?.line || {}),
+            stroke: model.stroke.color,
+            strokeWidth: model.stroke.width,
+            strokeDasharray: LineType[model.stroke.type],
+            targetMarker: {
+              ...(targetMarker || {}),
+              name: model.endArrow,
+              args: {
+                size: model.stroke.width + 8,
+              }
+            },
+            sourceMarker: {
+              ...(sourceMarker || {}),
+              name: model.startArrow,
+              args: {
+                size: model.stroke.width + 8,
+              }
+            }
+          }
+        })
+      }
     });
   };
 

+ 4 - 0
apps/designer/src/pages/flow/components/Content/index.less

@@ -32,3 +32,7 @@
 .x6-widget-transform-resize {
   border-radius: 0;
 }
+
+.x6-edge-selected {
+  filter: drop-shadow(0 0 2px #239edd);
+}

+ 27 - 15
apps/designer/src/pages/flow/components/Content/index.tsx

@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useRef } from "react";
+import { useEffect, useRef } from "react";
 import { Flex, Tabs } from "antd";
 import styles from "./index.less";
 import "./index.less";
@@ -6,6 +6,8 @@ import { Graph, Shape } from "@antv/x6";
 import { Scroller } from "@antv/x6-plugin-scroller";
 import Libary from "../Libary";
 import { useModel } from "umi";
+import { LineType } from '@/enum';
+import { defaultData } from '@/components/data';
 export default function Content() {
   const stageRef = useRef<HTMLDivElement | null>(null);
   const { initGraph } = useModel("graphModel");
@@ -51,49 +53,59 @@ export default function Content() {
         color: "#eaecee",
       },
       interacting: {
-        edgeLabelMovable: true,
+        edgeLabelMovable: false,
         edgeMovable: true,
         nodeMovable: (view) => {
           const data = view.cell.getData<{ isPage: boolean }>();
           return !data || !data.isPage;
         },
+        arrowheadMovable: true,
+        vertexMovable: true,
+        vertexAddable: true,
+        vertexDeletable: true,
+        useEdgeTools: true,
+        magnetConnectable: true,
+        stopDelegateOnDragging: true,
+        toolsAddable: true,
       },
+      // 连接配置
       connecting: {
-        router: "manhattan",
-        connector: {
-          name: "jumpover",
-          args: {
-            type: "arc",
-          },
-        },
         allowEdge: true,
+        allowLoop: true,
+        allowNode: true,
+        allowBlank: true,
+        allowPort: true,
+        allowMulti: true,
+        highlight: false,
         anchor: "center",
         connectionPoint: "anchor",
-        allowBlank: true,
         snap: {
           radius: 20,
         },
         createEdge() {
           return new Shape.Edge({
+            router: "manhattan",
             attrs: {
               line: {
                 stroke: "#323232",
+                strokeDasharray: LineType.solid,
                 strokeWidth: 2,
+                sourceMarker: {
+                  name: ''
+                },
                 targetMarker: {
-                  name: "block",
-                  width: 12,
-                  height: 8,
+                  name: 'block',
                 },
               },
+              text: defaultData.text
             },
-            tools: ["segments", "vertices"],
-            zIndex: 0,
           });
         },
         validateConnection({ targetMagnet }) {
           return !!targetMagnet;
         },
       },
+      // 高亮显示
       highlighting: {
         magnetAdsorbed: {
           name: "stroke",

+ 1 - 1
apps/designer/src/pages/flow/data.tsx

@@ -6,7 +6,7 @@ import {
 
 /** 箭头类型 */
 export const arrowOptions = [
-  { name: "none", icon: require("@/assets/icon/arrow0.png") },
+  { name: "", icon: require("@/assets/icon/arrow0.png") },
   { name: "block", icon: require("@/assets/icon/arrow1.png") },
   { name: "classic", icon: require("@/assets/icon/arrow2.png") },
   { name: "diamond", icon: require("@/assets/icon/arrow3.png") },