Przeglądaj źródła

feat: 添加流程设计工具栏、节点

liaojiaxing 1 miesiąc temu
rodzic
commit
13777638e0

+ 1 - 1
apps/flowchart-designer/.umirc.ts

@@ -9,7 +9,7 @@ export default defineConfig({
     '/favicon.ico'
   ],
   styles: [
-    '//at.alicdn.com/t/c/font_4913485_roeqx1898k.css',
+    '//at.alicdn.com/t/c/font_4913485_l4dx2lu13w.css',
   ],
   metas: [
     { name: 'viewport', content: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no' }

+ 34 - 38
apps/flowchart-designer/src/components/nodes/And.tsx

@@ -1,12 +1,15 @@
-import { useEffect, useRef } from "react";
+import { useEffect, useRef, useState } from "react";
 import { Graph, Node } from "@antv/x6";
 import img from "@/assets/wf_icon_and.gif";
-import { Button, Dropdown, MenuProps } from "antd";
+import { MenuProps } from "antd";
+import Port from "./Port";
+import Content from "./Content";
 
-export default ({ node }: { node: Node }) => {
-  const {} = node.getData();
+export default ({ node, graph }: { node: Node; graph: Graph }) => {
   const ref = useRef<HTMLDivElement>(null);
   const { width, height } = node.getSize();
+  const [hovered, setHovered] = useState(false);
+  const [isSelected, setIsSelected] = useState(false);
 
   const items: MenuProps["items"] = [
     { key: "copy", label: "复制" },
@@ -14,43 +17,36 @@ export default ({ node }: { node: Node }) => {
   ];
 
   useEffect(() => {
-    const { width: w, height: h } = node.getSize();
-    if (w !== width || h !== height) {
-      node.resize(width, height);
+    const { offsetWidth = 0, offsetHeight = 0 } = ref.current || {};
+    if (offsetHeight !== height || offsetWidth !== width) {
+      node.resize(offsetWidth, offsetHeight);
     }
-  }, [width, height]);
+  }, []);
 
-  return (
-    <div className="flow-node text-14px" ref={ref}>
-      <div className="flex items-center justify-between">
-        <img src={img} alt="logo" className="w-32px mr-4px" />
-        <span className="text-16px color-#383743 flex-1">与节点</span>
-        <Dropdown menu={{ items }} overlayStyle={{ width: 120 }}>
-          <Button
-            type="text"
-            icon={<i className="iconfont icon-gengduo" />}
-          />
-        </Dropdown>
-      </div>
-
-      <div className="truncate text-12px color-#2029459e my-4px">
-        这是描述文本内容这是描述文本内容这是描述文本内容这是描述文本内容
-      </div>
-
-      <div className="text-12px color-#999">
-        <span className="mr-4px">代码</span>
-        <span className="color-#666">and</span>
-      </div>
+  graph?.on("selection:changed", (args) => {
+    setIsSelected(graph.isSelected(node));
+  });
 
-      <div className="text-12px color-#999">
-        <span className="mr-4px">关联</span>
-        <span className="color-#666">xxx页面+</span>
-      </div>
-
-      <div className="text-12px color-#999">
-        <span className="mr-4px">动作</span>
-        <span className="color-#666">xxx页面+</span>
-      </div>
+  return (
+    <div
+      className="flow-node text-14px relative"
+      style={{ borderColor: isSelected || hovered ? "#0e52e0" : "#d9d9d9" }}
+      ref={ref}
+      onMouseEnter={() => setHovered(true)}
+      onMouseLeave={() => setHovered(false)}
+    >
+      <Content
+        img={img}
+        node={node}
+        graph={graph}
+        items={items}
+        headerStyle={{
+          backgroundImage:
+            "linear-gradient(to bottom, rgba(32, 139, 226, 0.2), #ffffff)",
+        }}
+      />
+
+      <Port hovered={hovered} style={{ right: -7, cursor: "crosshair" }} />
     </div>
   );
 };

+ 34 - 38
apps/flowchart-designer/src/components/nodes/AutoHandle.tsx

@@ -1,12 +1,15 @@
-import { useEffect, useRef } from "react";
+import { useEffect, useRef, useState } from "react";
 import { Graph, Node } from "@antv/x6";
 import img from "@/assets/wf_icon_autohandle.gif";
-import { Button, Dropdown, MenuProps } from "antd";
+import { MenuProps } from "antd";
+import Port from "./Port";
+import Content from "./Content";
 
-export default ({ node }: { node: Node }) => {
-  const {} = node.getData();
+export default ({ node, graph }: { node: Node; graph: Graph }) => {
   const ref = useRef<HTMLDivElement>(null);
   const { width, height } = node.getSize();
+  const [hovered, setHovered] = useState(false);
+  const [isSelected, setIsSelected] = useState(false);
 
   const items: MenuProps["items"] = [
     { key: "copy", label: "复制" },
@@ -14,43 +17,36 @@ export default ({ node }: { node: Node }) => {
   ];
 
   useEffect(() => {
-    const { width: w, height: h } = node.getSize();
-    if (w !== width || h !== height) {
-      node.resize(width, height);
+    const { offsetWidth = 0, offsetHeight = 0 } = ref.current || {};
+    if (offsetHeight !== height || offsetWidth !== width) {
+      node.resize(offsetWidth, offsetHeight);
     }
-  }, [width, height]);
+  }, []);
 
-  return (
-    <div className="flow-node text-14px" ref={ref}>
-      <div className="flex items-center justify-between">
-        <img src={img} alt="logo" className="w-32px mr-4px" />
-        <span className="text-16px color-#383743 flex-1">自动处理节点</span>
-        <Dropdown menu={{ items }} overlayStyle={{ width: 120 }}>
-          <Button
-            type="text"
-            icon={<i className="iconfont icon-gengduo" />}
-          />
-        </Dropdown>
-      </div>
-
-      <div className="truncate text-12px color-#2029459e my-4px">
-        这是描述文本内容这是描述文本内容这是描述文本内容这是描述文本内容
-      </div>
-
-      <div className="text-12px color-#999">
-        <span className="mr-4px">代码</span>
-        <span className="color-#666">and</span>
-      </div>
+  graph?.on("selection:changed", (args) => {
+    setIsSelected(graph.isSelected(node));
+  });
 
-      <div className="text-12px color-#999">
-        <span className="mr-4px">关联</span>
-        <span className="color-#666">xxx页面+</span>
-      </div>
-
-      <div className="text-12px color-#999">
-        <span className="mr-4px">动作</span>
-        <span className="color-#666">xxx页面+</span>
-      </div>
+  return (
+    <div
+      className="flow-node text-14px relative"
+      style={{ borderColor: isSelected || hovered ? "#0e52e0" : "#d9d9d9" }}
+      ref={ref}
+      onMouseEnter={() => setHovered(true)}
+      onMouseLeave={() => setHovered(false)}
+    >
+      <Content
+        img={img}
+        node={node}
+        graph={graph}
+        items={items}
+        headerStyle={{
+          backgroundImage:
+            "linear-gradient(to bottom, rgba(32, 139, 226, 0.2), #ffffff)",
+        }}
+      />
+
+      <Port hovered={hovered} style={{ right: -7, cursor: "crosshair" }} />
     </div>
   );
 };

+ 69 - 0
apps/flowchart-designer/src/components/nodes/Content.tsx

@@ -0,0 +1,69 @@
+import React, { useEffect, useRef, useState } from "react";
+import { Graph, Node } from "@antv/x6";
+import { Button, Dropdown, MenuProps, Input } from "antd";
+
+export default ({
+  node,
+  graph,
+  img,
+  items,
+  headerStyle,
+}: {
+  node: Node;
+  graph: Graph;
+  img: string;
+  items: MenuProps["items"],
+  headerStyle: React.CSSProperties
+}) => {
+  const [editing, setEditing] = useState(false);
+
+  return (
+    <>
+      <div
+        className="flex items-center justify-between px-8px pt-8px rounded-t-8px"
+        style={headerStyle}
+      >
+        <img src={img} alt="logo" className="w-32px mr-4px" />
+        {editing ? (
+          <Input
+            placeholder="请输入节点名称"
+            className="flex-1"
+            autoFocus
+            onBlur={() => setEditing(false)}
+          />
+        ) : (
+          <span
+            className="text-16px color-#383743 flex-1"
+            onDoubleClick={() => setEditing(true)}
+          >
+            开始节点
+          </span>
+        )}
+        <Dropdown menu={{ items }} overlayStyle={{ width: 120 }}>
+          <Button type="text" icon={<i className="iconfont icon-gengduo" />} />
+        </Dropdown>
+      </div>
+
+      <div className="content px-8px pb-8px cursor-default">
+        <div className="truncate text-12px color-#2029459e my-4px">
+          这是描述文本内容这是描述文本内容这是描述文本内容这是描述文本内容
+        </div>
+
+        <div className="text-12px color-#999">
+          <span className="mr-4px">代码</span>
+          <span className="color-#666">START</span>
+        </div>
+
+        <div className="text-12px color-#999">
+          <span className="mr-4px">关联</span>
+          <span className="color-#666">xxx页面+</span>
+        </div>
+
+        <div className="text-12px color-#999">
+          <span className="mr-4px">动作</span>
+          <span className="color-#666">xxx页面+</span>
+        </div>
+      </div>
+    </>
+  );
+};

+ 34 - 38
apps/flowchart-designer/src/components/nodes/Decision.tsx

@@ -1,12 +1,15 @@
-import { useEffect, useRef } from "react";
+import { useEffect, useRef, useState } from "react";
 import { Graph, Node } from "@antv/x6";
 import img from "@/assets/wf_icon_or.gif";
-import { Button, Dropdown, MenuProps } from "antd";
+import { MenuProps } from "antd";
+import Port from "./Port";
+import Content from "./Content";
 
-export default ({ node }: { node: Node }) => {
-  const {} = node.getData();
+export default ({ node, graph }: { node: Node; graph: Graph }) => {
   const ref = useRef<HTMLDivElement>(null);
   const { width, height } = node.getSize();
+  const [hovered, setHovered] = useState(false);
+  const [isSelected, setIsSelected] = useState(false);
 
   const items: MenuProps["items"] = [
     { key: "copy", label: "复制" },
@@ -14,43 +17,36 @@ export default ({ node }: { node: Node }) => {
   ];
 
   useEffect(() => {
-    const { width: w, height: h } = node.getSize();
-    if (w !== width || h !== height) {
-      node.resize(width, height);
+    const { offsetWidth = 0, offsetHeight = 0 } = ref.current || {};
+    if (offsetHeight !== height || offsetWidth !== width) {
+      node.resize(offsetWidth, offsetHeight);
     }
-  }, [width, height]);
+  }, []);
 
-  return (
-    <div className="flow-node text-14px" ref={ref}>
-      <div className="flex items-center justify-between">
-        <img src={img} alt="logo" className="w-32px mr-4px" />
-        <span className="text-16px color-#383743 flex-1">判断节点</span>
-        <Dropdown menu={{ items }} overlayStyle={{ width: 120 }}>
-          <Button
-            type="text"
-            icon={<i className="iconfont icon-gengduo" />}
-          />
-        </Dropdown>
-      </div>
-
-      <div className="truncate text-12px color-#2029459e my-4px">
-        这是描述文本内容这是描述文本内容这是描述文本内容这是描述文本内容
-      </div>
-
-      <div className="text-12px color-#999">
-        <span className="mr-4px">代码</span>
-        <span className="color-#666">and</span>
-      </div>
+  graph?.on("selection:changed", (args) => {
+    setIsSelected(graph.isSelected(node));
+  });
 
-      <div className="text-12px color-#999">
-        <span className="mr-4px">关联</span>
-        <span className="color-#666">xxx页面+</span>
-      </div>
-
-      <div className="text-12px color-#999">
-        <span className="mr-4px">动作</span>
-        <span className="color-#666">xxx页面+</span>
-      </div>
+  return (
+    <div
+      className="flow-node text-14px relative"
+      style={{ borderColor: isSelected || hovered ? "#0e52e0" : "#d9d9d9" }}
+      ref={ref}
+      onMouseEnter={() => setHovered(true)}
+      onMouseLeave={() => setHovered(false)}
+    >
+      <Content
+        img={img}
+        node={node}
+        graph={graph}
+        items={items}
+        headerStyle={{
+          backgroundImage:
+            "linear-gradient(to bottom, rgba(32, 139, 226, 0.2), #ffffff)",
+        }}
+      />
+
+      <Port hovered={hovered} style={{ right: -7, cursor: "crosshair" }} />
     </div>
   );
 };

+ 34 - 38
apps/flowchart-designer/src/components/nodes/End.tsx

@@ -1,12 +1,15 @@
-import { useEffect, useRef } from "react";
+import { useEffect, useRef, useState } from "react";
 import { Graph, Node } from "@antv/x6";
 import img from "@/assets/wf_icon_end.gif";
-import { Button, Dropdown, MenuProps } from "antd";
+import { MenuProps } from "antd";
+import Port from "./Port";
+import Content from "./Content";
 
-export default ({ node }: { node: Node }) => {
-  const {} = node.getData();
+export default ({ node, graph }: { node: Node; graph: Graph }) => {
   const ref = useRef<HTMLDivElement>(null);
   const { width, height } = node.getSize();
+  const [hovered, setHovered] = useState(false);
+  const [isSelected, setIsSelected] = useState(false);
 
   const items: MenuProps["items"] = [
     { key: "copy", label: "复制" },
@@ -14,43 +17,36 @@ export default ({ node }: { node: Node }) => {
   ];
 
   useEffect(() => {
-    const { width: w, height: h } = node.getSize();
-    if (w !== width || h !== height) {
-      node.resize(width, height);
+    const { offsetWidth = 0, offsetHeight = 0 } = ref.current || {};
+    if (offsetHeight !== height || offsetWidth !== width) {
+      node.resize(offsetWidth, offsetHeight);
     }
-  }, [width, height]);
+  }, []);
 
-  return (
-    <div className="flow-node text-14px" ref={ref}>
-      <div className="flex items-center justify-between">
-        <img src={img} alt="logo" className="w-32px mr-4px" />
-        <span className="text-16px color-#383743 flex-1">结束节点</span>
-        <Dropdown menu={{ items }} overlayStyle={{ width: 120 }}>
-          <Button
-            type="text"
-            icon={<i className="iconfont icon-gengduo" />}
-          />
-        </Dropdown>
-      </div>
-
-      <div className="truncate text-12px color-#2029459e my-4px">
-        这是描述文本内容这是描述文本内容这是描述文本内容这是描述文本内容
-      </div>
-
-      <div className="text-12px color-#999">
-        <span className="mr-4px">代码</span>
-        <span className="color-#666">and</span>
-      </div>
+  graph?.on("selection:changed", (args) => {
+    setIsSelected(graph.isSelected(node));
+  });
 
-      <div className="text-12px color-#999">
-        <span className="mr-4px">关联</span>
-        <span className="color-#666">xxx页面+</span>
-      </div>
-
-      <div className="text-12px color-#999">
-        <span className="mr-4px">动作</span>
-        <span className="color-#666">xxx页面+</span>
-      </div>
+  return (
+    <div
+      className="flow-node text-14px relative"
+      style={{ borderColor: isSelected || hovered ? "#0e52e0" : "#d9d9d9" }}
+      ref={ref}
+      onMouseEnter={() => setHovered(true)}
+      onMouseLeave={() => setHovered(false)}
+    >
+      <Content
+        img={img}
+        node={node}
+        graph={graph}
+        items={items}
+        headerStyle={{
+          backgroundImage:
+            "linear-gradient(to bottom, rgba(250, 126, 123, 0.2), #ffffff)",
+        }}
+      />
+
+      <Port out={false} hovered={hovered} style={{ left: -7 }} />
     </div>
   );
 };

+ 34 - 38
apps/flowchart-designer/src/components/nodes/Handle.tsx

@@ -1,12 +1,15 @@
-import { useEffect, useRef } from "react";
+import { useEffect, useRef, useState } from "react";
 import { Graph, Node } from "@antv/x6";
 import img from "@/assets/wf_icon_handle.gif";
-import { Button, Dropdown, MenuProps } from "antd";
+import { MenuProps } from "antd";
+import Port from "./Port";
+import Content from "./Content";
 
-export default ({ node }: { node: Node }) => {
-  const {} = node.getData();
+export default ({ node, graph }: { node: Node; graph: Graph }) => {
   const ref = useRef<HTMLDivElement>(null);
   const { width, height } = node.getSize();
+  const [hovered, setHovered] = useState(false);
+  const [isSelected, setIsSelected] = useState(false);
 
   const items: MenuProps["items"] = [
     { key: "copy", label: "复制" },
@@ -14,43 +17,36 @@ export default ({ node }: { node: Node }) => {
   ];
 
   useEffect(() => {
-    const { width: w, height: h } = node.getSize();
-    if (w !== width || h !== height) {
-      node.resize(width, height);
+    const { offsetWidth = 0, offsetHeight = 0 } = ref.current || {};
+    if (offsetHeight !== height || offsetWidth !== width) {
+      node.resize(offsetWidth, offsetHeight);
     }
-  }, [width, height]);
+  }, []);
 
-  return (
-    <div className="flow-node text-14px" ref={ref}>
-      <div className="flex items-center justify-between">
-        <img src={img} alt="logo" className="w-32px mr-4px" />
-        <span className="text-16px color-#383743 flex-1">处理节点</span>
-        <Dropdown menu={{ items }} overlayStyle={{ width: 120 }}>
-          <Button
-            type="text"
-            icon={<i className="iconfont icon-gengduo" />}
-          />
-        </Dropdown>
-      </div>
-
-      <div className="truncate text-12px color-#2029459e my-4px">
-        这是描述文本内容这是描述文本内容这是描述文本内容这是描述文本内容
-      </div>
-
-      <div className="text-12px color-#999">
-        <span className="mr-4px">代码</span>
-        <span className="color-#666">and</span>
-      </div>
+  graph?.on("selection:changed", (args) => {
+    setIsSelected(graph.isSelected(node));
+  });
 
-      <div className="text-12px color-#999">
-        <span className="mr-4px">关联</span>
-        <span className="color-#666">xxx页面+</span>
-      </div>
-
-      <div className="text-12px color-#999">
-        <span className="mr-4px">动作</span>
-        <span className="color-#666">xxx页面+</span>
-      </div>
+  return (
+    <div
+      className="flow-node text-14px relative"
+      style={{ borderColor: isSelected || hovered ? "#0e52e0" : "#d9d9d9" }}
+      ref={ref}
+      onMouseEnter={() => setHovered(true)}
+      onMouseLeave={() => setHovered(false)}
+    >
+      <Content
+        img={img}
+        node={node}
+        graph={graph}
+        items={items}
+        headerStyle={{
+          backgroundImage:
+            "linear-gradient(to bottom, rgba(32, 139, 226, 0.2), #ffffff)",
+        }}
+      />
+
+      <Port hovered={hovered} style={{ right: -7, cursor: "crosshair" }} />
     </div>
   );
 };

+ 34 - 38
apps/flowchart-designer/src/components/nodes/Link.tsx

@@ -1,12 +1,15 @@
-import { useEffect, useRef } from "react";
+import { useEffect, useRef, useState } from "react";
 import { Graph, Node } from "@antv/x6";
 import img from "@/assets/wf_icon_judge.gif";
-import { Button, Dropdown, MenuProps } from "antd";
+import { MenuProps } from "antd";
+import Port from "./Port";
+import Content from "./Content";
 
-export default ({ node }: { node: Node }) => {
-  const {} = node.getData();
+export default ({ node, graph }: { node: Node; graph: Graph }) => {
   const ref = useRef<HTMLDivElement>(null);
   const { width, height } = node.getSize();
+  const [hovered, setHovered] = useState(false);
+  const [isSelected, setIsSelected] = useState(false);
 
   const items: MenuProps["items"] = [
     { key: "copy", label: "复制" },
@@ -14,43 +17,36 @@ export default ({ node }: { node: Node }) => {
   ];
 
   useEffect(() => {
-    const { width: w, height: h } = node.getSize();
-    if (w !== width || h !== height) {
-      node.resize(width, height);
+    const { offsetWidth = 0, offsetHeight = 0 } = ref.current || {};
+    if (offsetHeight !== height || offsetWidth !== width) {
+      node.resize(offsetWidth, offsetHeight);
     }
-  }, [width, height]);
+  }, []);
 
-  return (
-    <div className="flow-node text-14px" ref={ref}>
-      <div className="flex items-center justify-between">
-        <img src={img} alt="logo" className="w-32px mr-4px" />
-        <span className="text-16px color-#383743 flex-1">连接节点</span>
-        <Dropdown menu={{ items }} overlayStyle={{ width: 120 }}>
-          <Button
-            type="text"
-            icon={<i className="iconfont icon-gengduo" />}
-          />
-        </Dropdown>
-      </div>
-
-      <div className="truncate text-12px color-#2029459e my-4px">
-        这是描述文本内容这是描述文本内容这是描述文本内容这是描述文本内容
-      </div>
-
-      <div className="text-12px color-#999">
-        <span className="mr-4px">代码</span>
-        <span className="color-#666">and</span>
-      </div>
+  graph?.on("selection:changed", (args) => {
+    setIsSelected(graph.isSelected(node));
+  });
 
-      <div className="text-12px color-#999">
-        <span className="mr-4px">关联</span>
-        <span className="color-#666">xxx页面+</span>
-      </div>
-
-      <div className="text-12px color-#999">
-        <span className="mr-4px">动作</span>
-        <span className="color-#666">xxx页面+</span>
-      </div>
+  return (
+    <div
+      className="flow-node text-14px relative"
+      style={{ borderColor: isSelected || hovered ? "#0e52e0" : "#d9d9d9" }}
+      ref={ref}
+      onMouseEnter={() => setHovered(true)}
+      onMouseLeave={() => setHovered(false)}
+    >
+      <Content
+        img={img}
+        node={node}
+        graph={graph}
+        items={items}
+        headerStyle={{
+          backgroundImage:
+            "linear-gradient(to bottom, rgba(32, 139, 226, 0.2), #ffffff)",
+        }}
+      />
+
+      <Port hovered={hovered} style={{ right: -7, cursor: "crosshair" }} />
     </div>
   );
 };

+ 43 - 0
apps/flowchart-designer/src/components/nodes/Port.tsx

@@ -0,0 +1,43 @@
+import { PlusOutlined } from "@ant-design/icons";
+import React from "react";
+
+export default function Port(props: {
+  hovered: boolean;
+  out?: boolean;
+  style?: React.CSSProperties;
+}) {
+  const { hovered, style = {}, out = true } = props;
+  const [canAdd, setCanAdd] = React.useState(false);
+
+  const handleSetCanAdd = (value: boolean) => {
+    out && setCanAdd(value);
+  };
+
+  const extraStyle = React.useMemo(() => {
+    if (out && canAdd) {
+      return {
+        transform: "scale(2) translateY(-50%)"
+      };
+    }
+    return {};
+  }, [canAdd]);
+
+  return (
+    <div
+      className="node-port flex items-center justify-center"
+      style={{
+        transform: hovered ? "scale(1.5) translateY(-50%)" : "scale(1) translateY(-50%)",
+        opacity: hovered ? 1 : 0.5,
+        ...style,
+        ...extraStyle
+      }}
+      onMouseEnter={() => handleSetCanAdd(true)}
+      onMouseLeave={() => handleSetCanAdd(false)}
+    >
+      { out && <PlusOutlined
+        className="transform scale-0 transition duration-300 color-#fff text-8px"
+        style={{ transform: canAdd ? "scale(1)" : "scale(0)" }}
+      />}
+    </div>
+  );
+}

+ 34 - 38
apps/flowchart-designer/src/components/nodes/Start.tsx

@@ -1,12 +1,15 @@
-import { useEffect, useRef } from "react";
+import { useEffect, useRef, useState } from "react";
 import { Graph, Node } from "@antv/x6";
 import img from "@/assets/wf_icon_start.gif";
-import { Button, Dropdown, MenuProps } from "antd";
+import { MenuProps } from "antd";
+import Port from "./Port";
+import Content from "./Content";
 
-export default ({ node }: { node: Node }) => {
-  const {} = node.getData();
+export default ({ node, graph }: { node: Node; graph: Graph }) => {
   const ref = useRef<HTMLDivElement>(null);
   const { width, height } = node.getSize();
+  const [hovered, setHovered] = useState(false);
+  const [isSelected, setIsSelected] = useState(false);
 
   const items: MenuProps["items"] = [
     { key: "copy", label: "复制" },
@@ -14,43 +17,36 @@ export default ({ node }: { node: Node }) => {
   ];
 
   useEffect(() => {
-    const { width: w, height: h } = node.getSize();
-    if (w !== width || h !== height) {
-      node.resize(width, height);
+    const { offsetWidth = 0, offsetHeight = 0 } = ref.current || {};
+    if (offsetHeight !== height || offsetWidth !== width) {
+      node.resize(offsetWidth, offsetHeight);
     }
-  }, [width, height]);
+  }, []);
 
-  return (
-    <div className="flow-node text-14px" ref={ref}>
-      <div className="flex items-center justify-between">
-        <img src={img} alt="logo" className="w-32px mr-4px" />
-        <span className="text-16px color-#383743 flex-1">开始节点</span>
-        <Dropdown menu={{ items }} overlayStyle={{ width: 120 }}>
-          <Button
-            type="text"
-            icon={<i className="iconfont icon-gengduo" />}
-          />
-        </Dropdown>
-      </div>
-
-      <div className="truncate text-12px color-#2029459e my-4px">
-        这是描述文本内容这是描述文本内容这是描述文本内容这是描述文本内容
-      </div>
-
-      <div className="text-12px color-#999">
-        <span className="mr-4px">代码</span>
-        <span className="color-#666">START</span>
-      </div>
+  graph?.on("selection:changed", (args) => {
+    setIsSelected(graph.isSelected(node));
+  });
 
-      <div className="text-12px color-#999">
-        <span className="mr-4px">关联</span>
-        <span className="color-#666">xxx页面+</span>
-      </div>
-
-      <div className="text-12px color-#999">
-        <span className="mr-4px">动作</span>
-        <span className="color-#666">xxx页面+</span>
-      </div>
+  return (
+    <div
+      className="flow-node text-14px relative"
+      style={{ borderColor: isSelected || hovered ? "#0e52e0" : "#d9d9d9" }}
+      ref={ref}
+      onMouseEnter={() => setHovered(true)}
+      onMouseLeave={() => setHovered(false)}
+    >
+      <Content
+        img={img}
+        node={node}
+        graph={graph}
+        items={items}
+        headerStyle={{
+          backgroundImage:
+            "linear-gradient(to bottom, rgba(191, 131, 1, 0.2), #ffffff)",
+        }}
+      />
+
+      <Port hovered={hovered} style={{ right: -7, cursor: "crosshair" }} />
     </div>
   );
 };

+ 81 - 0
apps/flowchart-designer/src/components/nodes/index.ts

@@ -9,6 +9,86 @@ import End from "./End";
 import Handle from "./Handle";
 import Link from "./Link";
 
+// 通用连接桩
+const ports = {
+  groups: {
+    top: {
+      position: 'top',
+      attrs: {
+        circle: {
+          r: 4,
+          magnet: true,
+          stroke: '#5F95FF',
+          strokeWidth: 1,
+          fill: '#fff',
+          style: {
+            visibility: 'hidden',
+          },
+        },
+      },
+    },
+    right: {
+      position: 'right',
+      attrs: {
+        circle: {
+          r: 4,
+          magnet: true,
+          stroke: '#5F95FF',
+          strokeWidth: 1,
+          fill: '#fff',
+          style: {
+            visibility: 'hidden',
+          },
+        },
+      },
+    },
+    bottom: {
+      position: 'bottom',
+      attrs: {
+        circle: {
+          r: 4,
+          magnet: true,
+          stroke: '#5F95FF',
+          strokeWidth: 1,
+          fill: '#fff',
+          style: {
+            visibility: 'hidden',
+          },
+        },
+      },
+    },
+    left: {
+      position: 'left',
+      attrs: {
+        circle: {
+          r: 4,
+          magnet: true,
+          stroke: '#5F95FF',
+          strokeWidth: 1,
+          fill: '#fff',
+          style: {
+            visibility: 'hidden',
+          },
+        },
+      },
+    },
+  },
+  items: [
+    {
+      group: 'top',
+    },
+    {
+      group: 'right',
+    },
+    {
+      group: 'bottom',
+    },
+    {
+      group: 'left',
+    },
+  ],
+}
+
 export const nodes = [
   { name: "start-node", component: Start, type: NodeType.START },
   { name: "and-node", component: And, type: NodeType.AND },
@@ -26,5 +106,6 @@ nodes.forEach((node) => {
     height: 108,
     effect: ["data"],
     component: node.component,
+    ports: {...ports}
   });
 });

+ 2 - 1
apps/flowchart-designer/src/layouts/index.less

@@ -15,7 +15,7 @@ body {
 }
 
 .x6-widget-selection-box {
-  border: 2px dashed #239edd;
+  border: 2px solid #239edd;
   border-radius: 8px;
 }
 
@@ -25,6 +25,7 @@ body {
 
 .x6-widget-selection-selected {
   z-index: 1;
+  display: none;
 }
 
 .x6-widget-transform {

+ 27 - 8
apps/flowchart-designer/src/models/flowModel.ts

@@ -1,11 +1,12 @@
-import { Graph } from '@antv/x6';
-import { useRef, useState } from 'react';
+import { Graph } from "@antv/x6";
+import { useRef, useState } from "react";
 import { Dnd } from "@antv/x6-plugin-dnd";
 import { Transform } from "@antv/x6-plugin-transform";
 import { Snapline } from "@antv/x6-plugin-snapline";
 import { Clipboard } from "@antv/x6-plugin-clipboard";
 import { Selection } from "@antv/x6-plugin-selection";
 import { History } from "@antv/x6-plugin-history";
+import { Scroller } from "@antv/x6-plugin-scroller";
 import { Keyboard } from "@antv/x6-plugin-keyboard";
 import { Export } from "@antv/x6-plugin-export";
 
@@ -34,19 +35,37 @@ export default function flowModel() {
         type: "dot",
         visible: true,
         args: { background: true },
-      }
+      },
     });
 
     instance.use(new Transform());
-    instance.use(new Selection());
+    instance.use(
+      new Selection({
+        enabled: true,
+        multiple: true,
+        rubberband: true,
+        movable: true,
+        showNodeSelectionBox: true,
+        // showEdgeSelectionBox: true,
+        pointerEvents: "none",
+        strict: true
+      })
+    );
     instance.use(new History());
+    instance.use(
+      new Scroller({
+        enabled: true,
+        autoResize: true,
+        pannable: true,
+      })
+    );
 
     setGraph(instance);
     graphRef.current = instance;
-  }
+  };
 
   return {
     init,
-    graph
-  }
-}
+    graph,
+  };
+}

+ 5 - 2
apps/flowchart-designer/src/pages/designer/components/Nodes/index.tsx

@@ -20,20 +20,23 @@ const items = [
   { key: NodeType.LINK, icon: judge, text: "连接" },
   { key: NodeType.END, icon: end, text: "结束" },
 ];
+
 export default function index() {
   const { graph } = useModel("flowModel");
 
   const handleAddNode = (type: NodeType) => { 
     const node = nodes.find(item => item.type === type);
     const { width = 100, height = 100} = graph?.getGraphArea() || {};
+    const rect = graph?.getAllCellsBBox();
+    const y = rect ? rect.y + rect.height - 80 : height / 2 - 100;
 
     graph?.addNode({
       shape: node?.name,
       position: {
         x: width / 2 - 150,
-        y: height / 2 - 100,
+        y,
       },
-      data: {}
+      data: {},
     });
   };
 

+ 5 - 3
apps/flowchart-designer/src/pages/designer/components/Toolbar/index.tsx

@@ -34,7 +34,9 @@ export default function index() {
         width: 144,
         height: 100,
         padding: 10,
-        scalable: false,
+        scalable: true,
+        minScale: 0.2,
+        maxScale: 2,
         graphOptions: {
           background: {
             color: "#e9ebf0",
@@ -66,8 +68,8 @@ export default function index() {
       ></div>
 
       <div className="w-60px h-40px rounded-12px bg-#fff box-shadow-sm flex items-center justify-between px-12px">
-        <Button type="text" icon={<UndoOutlined />} disabled={true} />
-        <Button type="text" icon={<RedoOutlined />} />
+        <Button type="text" icon={<i className="iconfont icon-undo" />} disabled={true} />
+        <Button type="text" icon={<i className="iconfont icon-redo" />} />
       </div>
 
       <div className="w-260px h-40px rounded-12px bg-#fff box-shadow-sm flex items-center justify-between px-12px">

+ 1 - 1
apps/flowchart-designer/src/pages/designer/index.tsx

@@ -10,7 +10,7 @@ export default function index() {
   useEffect(() => {
     if (!ref.current) return;
     init(ref.current);
-  }, [ref.current]);
+  }, []);
 
   return (
     <div className='w-100vw h-100vh flex flex-col overflow-hidden'>

+ 2 - 1
apps/flowchart-designer/unocss.config.ts

@@ -7,7 +7,8 @@ export function createConfig({strict = true, dev = true} = {}) {
       ['flex-important', {display: 'flex !important'}],
     ],
     shortcuts: {
-      'flow-node': 'relative text-0 bg-#fff shadow-md w-full h-full border border-solid border-[#d9d9d9] rounded-[8px] p-8px hover:border-[#40a9ff]',
+      'flow-node': 'relative text-0 bg-#fff shadow-md border border-solid border-[#d9d9d9] rounded-[8px] hover:border-[#40a9ff]',
+      'node-port': 'absolute w-10px h-10px rounded-full bg-#0e53e2 top-1/2 opacity-50 border-2px border-solid border-#fff transition duration-300 hover:opacity-100 hover:scale-150 origin-top'
     },
   });
 }