Browse Source

fix: 修改AI助手相关

liaojiaxing 1 month ago
parent
commit
9e1c0da86c

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

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

+ 40 - 2
apps/flowchart-designer/src/components/NodeMenu.tsx

@@ -1,6 +1,6 @@
 import { useState, useEffect } from "react";
 import { NodeType } from "@/enum";
-import { nodes } from "@/components/nodes";
+import { nodes, aiNode } from "@/components/nodes";
 
 import start from "@/assets/wf_icon_start.gif";
 import end from "@/assets/wf_icon_end.gif";
@@ -45,9 +45,34 @@ export default function NodeMenu(props: {
     props?.onChange?.(n);
   };
 
+  const handleAddAiNode = (item: any) => {
+    const { width = 100, height = 100 } = graph?.getGraphArea() || {};
+    const rect = graph?.getAllCellsBBox();
+    const y = rect ? rect.y + rect.height - 80 : height / 2 - 100;
+
+    const n = graph?.addNode({
+      shape: item?.name,
+      position: props?.position || {
+        x: width / 2 - 150,
+        y,
+      },
+      data: {
+        name: item.data.name,
+        icon: item.icon,
+        color: item.color,
+        style: item.style,
+        port: item.port,
+        type: item.data.type,
+        hidePort: item.data.hidePort,
+      },
+    });
+
+    props?.onChange?.(n);
+  };
+
   return (
     <div className="w-280px flex flex-wrap gap-[8px 12px]">
-      {items.map((item) => {
+      {/* {items.map((item) => {
         return (
           <div
             className="w-[38%] border-box h-40px px-12px rounded-8px flex items-center cursor-pointer hover:bg-#eff0f8"
@@ -58,6 +83,19 @@ export default function NodeMenu(props: {
             <span>{item.text}</span>
           </div>
         );
+      })} */}
+      {aiNode.map((item) => {
+        return (
+          <div
+            className="w-[38%] border-box h-40px px-12px rounded-8px flex items-center cursor-pointer hover:bg-#eff0f8"
+            key={item.icon}
+            onClick={(e) => {handleAddAiNode(item)}}
+          >
+            {/* <img src={item.icon} className="w-24px" /> */}
+            <i className={"w-24px iconfont " + item.icon}/>
+            <span>{item.data.name}</span>
+          </div>
+        );
       })}
     </div>
   );

+ 7 - 16
apps/flowchart-designer/src/components/nodes/And.tsx

@@ -6,8 +6,6 @@ import Port from "./Port";
 import Content from "./Content";
 
 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);
 
@@ -16,22 +14,17 @@ export default ({ node, graph }: { node: Node; graph: Graph }) => {
     { key: "delete", label: "删除" },
   ];
 
-  useEffect(() => {
-    const { offsetWidth = 0, offsetHeight = 0 } = ref.current || {};
-    if (offsetHeight !== height || offsetWidth !== width) {
-      node.resize(offsetWidth, offsetHeight);
-    }
-  }, []);
-
   graph?.on("selection:changed", (args) => {
     setIsSelected(graph.isSelected(node));
   });
 
   return (
     <div
-      className="flow-node text-14px relative"
-      style={{ borderColor: isSelected || hovered ? "#0e52e0" : "#d9d9d9" }}
-      ref={ref}
+      className="flow-node"
+      style={{ 
+        
+        boxShadow: isSelected ? '0 0 0 8px #2941701a' : ''
+      }}
       onMouseEnter={() => setHovered(true)}
       onMouseLeave={() => setHovered(false)}
     >
@@ -40,10 +33,8 @@ export default ({ node, graph }: { node: Node; graph: Graph }) => {
         node={node}
         graph={graph}
         items={items}
-        headerStyle={{
-          backgroundImage:
-            "linear-gradient(to bottom, rgba(32, 139, 226, 0.2), #ffffff)",
-        }}
+        selected={isSelected}
+        hovered={hovered}
       />
 
       <Port hovered={hovered} out={false} style={{ left: -7, cursor: "default" }}  node={node} graph={graph} />

+ 7 - 16
apps/flowchart-designer/src/components/nodes/AutoHandle.tsx

@@ -6,8 +6,6 @@ import Port from "./Port";
 import Content from "./Content";
 
 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);
 
@@ -16,22 +14,17 @@ export default ({ node, graph }: { node: Node; graph: Graph }) => {
     { key: "delete", label: "删除" },
   ];
 
-  useEffect(() => {
-    const { offsetWidth = 0, offsetHeight = 0 } = ref.current || {};
-    if (offsetHeight !== height || offsetWidth !== width) {
-      node.resize(offsetWidth, offsetHeight);
-    }
-  }, []);
-
   graph?.on("selection:changed", (args) => {
     setIsSelected(graph.isSelected(node));
   });
 
   return (
     <div
-      className="flow-node text-14px relative"
-      style={{ borderColor: isSelected || hovered ? "#0e52e0" : "#d9d9d9" }}
-      ref={ref}
+      className="flow-node"
+      style={{ 
+        
+        boxShadow: isSelected ? '0 0 0 8px #2941701a' : ''
+      }}
       onMouseEnter={() => setHovered(true)}
       onMouseLeave={() => setHovered(false)}
     >
@@ -40,10 +33,8 @@ export default ({ node, graph }: { node: Node; graph: Graph }) => {
         node={node}
         graph={graph}
         items={items}
-        headerStyle={{
-          backgroundImage:
-            "linear-gradient(to bottom, rgba(32, 139, 226, 0.2), #ffffff)",
-        }}
+        selected={isSelected}
+        hovered={hovered}
       />
 
       <Port hovered={hovered} out={false} style={{ left: -7, cursor: "default" }}  node={node} graph={graph} />

+ 59 - 45
apps/flowchart-designer/src/components/nodes/Content.tsx

@@ -1,70 +1,84 @@
 import React, { useEffect, useRef, useState } from "react";
 import { Graph, Node } from "@antv/x6";
 import { Button, Dropdown, MenuProps, Input } from "antd";
+import {
+  CaretRightOutlined,
+  DeleteFilled,
+  DeleteOutlined,
+} from "@ant-design/icons";
 
 export default ({
   node,
   graph,
   img,
   items,
-  headerStyle,
+  hovered,
+  selected,
 }: {
   node: Node;
   graph: Graph;
-  img: string;
-  items: MenuProps["items"],
-  headerStyle: React.CSSProperties
+  img: string | React.ReactNode;
+  items: MenuProps["items"];
+  hovered: boolean;
+  selected: boolean;
 }) => {
-  const {name} = node.getData();
-  const [editing, setEditing] = useState(false);
+  const { name, type } = node.getData();
+
+  const handleDelete = () => {
+    graph.removeNode(node);
+  };
 
   return (
     <>
       <div
-        className="flex items-center justify-between px-8px pt-8px rounded-t-8px"
-        style={headerStyle}
+        className="w-full flex justify-end absolute top--34px left-0"
+        style={{ display: hovered || selected ? "flex" : "none" }}
       >
-        <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)}
-          >
-            { name }
-          </span>
-        )}
+        <Button
+          type="text"
+          className="text-primary-dark"
+          icon={<CaretRightOutlined />}
+          onClick={handleDelete}
+        />
+        <Button
+          type="text"
+          className="text-primary-dark"
+          icon={<DeleteFilled />}
+          onClick={handleDelete}
+        />
         <Dropdown menu={{ items }} overlayStyle={{ width: 120 }}>
-          <Button type="text" icon={<i className="iconfont icon-gengduo" />} />
+          <Button
+            type="text"
+            className="text-primary-dark"
+            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>
+      {type ? (
+        <div className="flex items-center leading-100px px-20px">
+          {typeof img === "string" ? (
+            <img src={img} alt="logo" className="w-50px" />
+          ) : (
+            img
+          )}
+          <div className="ml-10px">
+            <div className="text-#333 text-12px">{name}</div>
+          </div>
         </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>
+      ) : (
+        <>
+          <div className="w-full h-full relative flex items-center justify-center">
+            {typeof img === "string" ? (
+              <img src={img} alt="logo" className="w-50px" />
+            ) : (
+              img
+            )}
+          </div>
+          <div className="absolute w-full bottom--32px text-center text-#222">
+            {name}
+          </div>
+        </>
+      )}
     </>
   );
 };

+ 80 - 0
apps/flowchart-designer/src/components/nodes/Custom.tsx

@@ -0,0 +1,80 @@
+import { useEffect, useRef, useState } from "react";
+import { Graph, Node } from "@antv/x6";
+import img from "@/assets/wf_icon_handle.gif";
+import { MenuProps } from "antd";
+import Port from "./Port";
+import Content from "./Content";
+
+export default ({ node, graph }: { node: Node; graph: Graph }) => {
+  const [hovered, setHovered] = useState(false);
+  const [isSelected, setIsSelected] = useState(false);
+  const { icon, color, style, port, type, } = node.getData();
+
+  const items: MenuProps["items"] = [
+    { key: "copy", label: "复制" },
+    { key: "delete", label: "删除" },
+  ];
+
+  graph?.on("selection:changed", (args) => {
+    setIsSelected(graph.isSelected(node));
+  });
+
+  return (
+    <div
+      className="flow-node"
+      style={{
+        boxShadow: isSelected ? "0 0 0 8px #2941701a" : "",
+        ...style,
+      }}
+      onMouseEnter={() => setHovered(true)}
+      onMouseLeave={() => setHovered(false)}
+    >
+      <Content
+        img={
+          <i className={"iconfont " + icon} style={{ fontSize: 60, color }} />
+        }
+        node={node}
+        graph={graph}
+        items={items}
+        selected={isSelected}
+        hovered={hovered}
+      />
+
+      {!!port.in && (
+        <Port
+          hovered={hovered}
+          out={false}
+          style={{ left: -7, cursor: "default" }}
+          node={node}
+          graph={graph}
+        />
+      )}
+      {new Array(port.out).fill("").map((item, index) => (
+        <Port
+          key={index}
+          hovered={hovered}
+          style={{
+            right: -7,
+            cursor: "crosshair",
+            top: (100 / (port.out + 1)) * (index + 1),
+          }}
+          node={node}
+          graph={graph}
+        />
+      ))}
+      {type && (
+        <div style={{position: 'absolute', bottom: -10, left: 'calc(50% - 8px)', width: 16, height: 16}}>
+          <Port
+            hovered={hovered}
+            type='extra'
+            style={{
+              cursor: "default",
+            }}
+            node={node}
+            graph={graph}
+          />
+        </div>
+      )}
+    </div>
+  );
+};

+ 7 - 16
apps/flowchart-designer/src/components/nodes/Decision.tsx

@@ -6,8 +6,6 @@ import Port from "./Port";
 import Content from "./Content";
 
 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);
 
@@ -16,22 +14,17 @@ export default ({ node, graph }: { node: Node; graph: Graph }) => {
     { key: "delete", label: "删除" },
   ];
 
-  useEffect(() => {
-    const { offsetWidth = 0, offsetHeight = 0 } = ref.current || {};
-    if (offsetHeight !== height || offsetWidth !== width) {
-      node.resize(offsetWidth, offsetHeight);
-    }
-  }, []);
-
   graph?.on("selection:changed", (args) => {
     setIsSelected(graph.isSelected(node));
   });
 
   return (
     <div
-      className="flow-node text-14px relative"
-      style={{ borderColor: isSelected || hovered ? "#0e52e0" : "#d9d9d9" }}
-      ref={ref}
+      className="flow-node"
+      style={{ 
+        
+        boxShadow: isSelected ? '0 0 0 8px #2941701a' : ''
+      }}
       onMouseEnter={() => setHovered(true)}
       onMouseLeave={() => setHovered(false)}
     >
@@ -40,10 +33,8 @@ export default ({ node, graph }: { node: Node; graph: Graph }) => {
         node={node}
         graph={graph}
         items={items}
-        headerStyle={{
-          backgroundImage:
-            "linear-gradient(to bottom, rgba(32, 139, 226, 0.2), #ffffff)",
-        }}
+        selected={isSelected}
+        hovered={hovered}
       />
 
       <Port hovered={hovered} out={false} style={{ left: -7, cursor: "default" }}  node={node} graph={graph} />

+ 8 - 17
apps/flowchart-designer/src/components/nodes/End.tsx

@@ -6,8 +6,6 @@ import Port from "./Port";
 import Content from "./Content";
 
 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);
 
@@ -16,22 +14,17 @@ export default ({ node, graph }: { node: Node; graph: Graph }) => {
     { key: "delete", label: "删除" },
   ];
 
-  useEffect(() => {
-    const { offsetWidth = 0, offsetHeight = 0 } = ref.current || {};
-    if (offsetHeight !== height || offsetWidth !== width) {
-      node.resize(offsetWidth, offsetHeight);
-    }
-  }, []);
-
   graph?.on("selection:changed", (args) => {
     setIsSelected(graph.isSelected(node));
   });
 
   return (
     <div
-      className="flow-node text-14px relative"
-      style={{ borderColor: isSelected || hovered ? "#0e52e0" : "#d9d9d9" }}
-      ref={ref}
+      className="flow-node rounded-r-36px"
+      style={{ 
+        
+        boxShadow: isSelected ? '0 0 0 8px #2941701a' : ''
+      }}
       onMouseEnter={() => setHovered(true)}
       onMouseLeave={() => setHovered(false)}
     >
@@ -40,13 +33,11 @@ export default ({ node, graph }: { node: Node; graph: Graph }) => {
         node={node}
         graph={graph}
         items={items}
-        headerStyle={{
-          backgroundImage:
-            "linear-gradient(to bottom, rgba(250, 126, 123, 0.2), #ffffff)",
-        }}
+        selected={isSelected}
+        hovered={hovered}
       />
 
-      <Port out={false} hovered={hovered} style={{ left: -7 }} node={node} graph={graph} />
+      <Port hovered={hovered} out={false} style={{ left: -7, cursor: "default" }}  node={node} graph={graph} />
     </div>
   );
 };

+ 7 - 16
apps/flowchart-designer/src/components/nodes/Handle.tsx

@@ -6,8 +6,6 @@ import Port from "./Port";
 import Content from "./Content";
 
 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);
 
@@ -16,22 +14,17 @@ export default ({ node, graph }: { node: Node; graph: Graph }) => {
     { key: "delete", label: "删除" },
   ];
 
-  useEffect(() => {
-    const { offsetWidth = 0, offsetHeight = 0 } = ref.current || {};
-    if (offsetHeight !== height || offsetWidth !== width) {
-      node.resize(offsetWidth, offsetHeight);
-    }
-  }, []);
-
   graph?.on("selection:changed", (args) => {
     setIsSelected(graph.isSelected(node));
   });
 
   return (
     <div
-      className="flow-node text-14px relative"
-      style={{ borderColor: isSelected || hovered ? "#0e52e0" : "#d9d9d9" }}
-      ref={ref}
+      className="flow-node"
+      style={{ 
+        
+        boxShadow: isSelected ? '0 0 0 8px #2941701a' : ''
+      }}
       onMouseEnter={() => setHovered(true)}
       onMouseLeave={() => setHovered(false)}
     >
@@ -40,10 +33,8 @@ export default ({ node, graph }: { node: Node; graph: Graph }) => {
         node={node}
         graph={graph}
         items={items}
-        headerStyle={{
-          backgroundImage:
-            "linear-gradient(to bottom, rgba(32, 139, 226, 0.2), #ffffff)",
-        }}
+        selected={isSelected}
+        hovered={hovered}
       />
 
       <Port hovered={hovered} out={false} style={{ left: -7, cursor: "default" }}  node={node} graph={graph} />

+ 9 - 18
apps/flowchart-designer/src/components/nodes/Link.tsx

@@ -6,8 +6,6 @@ import Port from "./Port";
 import Content from "./Content";
 
 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);
 
@@ -16,22 +14,17 @@ export default ({ node, graph }: { node: Node; graph: Graph }) => {
     { key: "delete", label: "删除" },
   ];
 
-  useEffect(() => {
-    const { offsetWidth = 0, offsetHeight = 0 } = ref.current || {};
-    if (offsetHeight !== height || offsetWidth !== width) {
-      node.resize(offsetWidth, offsetHeight);
-    }
-  }, []);
-
   graph?.on("selection:changed", (args) => {
     setIsSelected(graph.isSelected(node));
   });
 
   return (
     <div
-      className="flow-node text-14px relative"
-      style={{ borderColor: isSelected || hovered ? "#0e52e0" : "#d9d9d9" }}
-      ref={ref}
+      className="flow-node"
+      style={{ 
+        
+        boxShadow: isSelected ? '0 0 0 8px #2941701a' : ''
+      }}
       onMouseEnter={() => setHovered(true)}
       onMouseLeave={() => setHovered(false)}
     >
@@ -40,14 +33,12 @@ export default ({ node, graph }: { node: Node; graph: Graph }) => {
         node={node}
         graph={graph}
         items={items}
-        headerStyle={{
-          backgroundImage:
-            "linear-gradient(to bottom, rgba(32, 139, 226, 0.2), #ffffff)",
-        }}
+        selected={isSelected}
+        hovered={hovered}
       />
 
-       <Port hovered={hovered} out={false} style={{ left: -7, cursor: "default" }}  node={node} graph={graph} />
-       <Port hovered={hovered} style={{ right: -7, cursor: "crosshair" }}  node={node} graph={graph} />
+      <Port hovered={hovered} out={false} style={{ left: -7, cursor: "default" }}  node={node} graph={graph} />
+      <Port hovered={hovered} style={{ right: -7, cursor: "crosshair" }}  node={node} graph={graph} />
     </div>
   );
 };

+ 109 - 0
apps/flowchart-designer/src/components/nodes/NoticeNode.tsx

@@ -0,0 +1,109 @@
+import React, { useEffect, useState, useRef } from "react";
+import { register } from "@antv/x6-react-shape";
+import { Graph, Node } from "@antv/x6";
+import { Input } from "antd";
+import { useSize } from "ahooks";
+import { Transform } from "@antv/x6-plugin-transform";
+const NoticeNode = ({ node, graph }: { node: Node; graph: Graph }) => {
+  const { name, text } = node.getData();
+  const boxRef = useRef<HTMLDivElement>(null);
+  const { width: w, height: h } = useSize(boxRef) || node.getSize();
+  const [showEdit, setShowEdit] = useState(false);
+  const tranform = useRef<Transform>();
+
+
+  const [content, setContent] = useState(text);
+
+  useEffect(() => {
+    setContent(text);
+  }, [text]);
+
+  useEffect(() => {
+    graph.on("node:selected", (args) => {
+      if(args.node.id === node.id) {
+        if(!tranform.current) {
+          tranform.current = new Transform({
+            resizing: { enabled: true }
+          });
+          graph.use(tranform.current);
+        } else {
+          tranform.current.enable();
+        }
+        graph.createTransformWidget(node);
+      } else {
+        graph.clearTransformWidgets();
+        tranform.current?.disable();
+      }
+    });
+  }, []);
+
+  const strokeWidth = 1;
+  return (
+    <>
+      <div
+        className="relative w-full h-full bg-#eff8ff"
+        ref={boxRef}
+        onMouseEnter={() => setShowEdit(true)}
+        onMouseLeave={() => setShowEdit(false)}
+      >
+        {/* <svg
+          className="absolute top-0 left-0"
+          viewBox={`0 0 ${w} ${h}`}
+          xmlns="http://www.w3.org/2000/svg"
+        >
+          <path
+            d={`
+              M ${strokeWidth},${strokeWidth}
+              L ${w - 20},${strokeWidth}
+              L ${w - strokeWidth},${20}
+              L ${w - strokeWidth},${h - strokeWidth}
+              L ${strokeWidth},${h - strokeWidth}
+              L ${strokeWidth},${strokeWidth}
+              Z
+            `}
+            fill='#eff8ff'
+            stroke="#666"
+            strokeWidth={strokeWidth}
+          />
+          <path
+            d={`
+              M ${w - 20},${strokeWidth}
+              L ${w - 20},${20}
+              L ${w - strokeWidth},${20}
+              L ${w - 20},${strokeWidth} Z
+            `}
+            fill='#eff8ff'
+            stroke="#666"
+            strokeWidth={strokeWidth}
+          />
+        </svg> */}
+        <div className="absolute w-full h-full flex flex-col">
+          <div className="color-#333 px-20px py-4px flex items-center justify-between">
+            <span className="truncate leading-22px">{name}</span>
+          </div>
+          <Input.TextArea
+            placeholder="请输入注释内容"
+            variant="borderless"
+            className="w-full resize-none! flex-1 color-#666"
+            value={content}
+            onChange={(e) => setContent(e.target.value)}
+            onBlur={() => {
+              node.prop("update:remark", {
+                ...node.getData(),
+                text: content,
+              });
+            }}
+          />
+        </div>
+      </div>
+    </>
+  );
+};
+
+register({
+  shape: "notice-node",
+  component: NoticeNode,
+  width: 300,
+  height: 300,
+  effect: ["data"],
+});

+ 169 - 75
apps/flowchart-designer/src/components/nodes/Port.tsx

@@ -8,10 +8,11 @@ export default function Port(props: {
   hovered: boolean;
   out?: boolean;
   style?: React.CSSProperties;
-  node?: Node,
-  graph?: Graph
+  node?: Node;
+  graph?: Graph;
+  type?: "in" | "out" | "extra";
 }) {
-  const { hovered, style = {}, out = true, node, graph } = props;
+  const { hovered, style = {}, out = true, node, graph, type } = props;
   const [canAdd, setCanAdd] = React.useState(false);
   const [open, setOpen] = React.useState(false);
   const isDown = useRef(false);
@@ -21,102 +22,153 @@ export default function Port(props: {
   const extraStyle = React.useMemo(() => {
     if (out && canAdd) {
       return {
-        transform: "scale(2) translateY(-50%)"
+        transform: "scale(2) translateY(-50%)",
       };
     }
-    return {};
+    if (!out) {
+      return {
+        width: 8,
+        height: 16,
+        borderRadius: 0,
+        border: "none",
+        left: -5,
+      };
+    } else {
+      return {};
+    }
   }, [canAdd]);
 
   const handleSetCanAdd = (value: boolean) => {
     out && setCanAdd(value);
-    node?.setData({ lock: !!value });
     graph?.togglePanning(!value);
   };
 
+  // step1: 鼠标按下拖拽开始
   const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
     isDown.current = true;
+    node?.setData({ lock: true });
   };
 
-  const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
-    
-  }
-
   useEffect(() => {
-    graph?.on("node:mouseup", (args) => {
-      console.log("node:mouseup");
-      isDown.current = false;
-      isMove.current = false;
-      // 拖拽过程中释放鼠标 检测当前是否有节点
-      if(newEdge.current) {
-        // 检测当前位置是否有元素
-        const nodes = graph.getNodesInArea(args.x, args.y, 5, 5);
-        if(!nodes.length) {
-          console.log('展示添加菜单')
-          graph.addNode({
-            shape: 'menu-popover',
-            x: args.x,
-            y: args.y,
-          })
-        }
-      }
-    });
-
+    // step2: 移动鼠标添加连线
     // 检测移动 添加边 设置边目标位置
     graph?.on("node:mousemove", (args) => {
-      if(isDown.current) {
+      if (isDown.current) {
         // 按下还没移动时创建边
-        if(!isMove.current && node) {
+        if (!isMove.current && node) {
           isMove.current = true;
           const ports = node.getPorts();
-          const rightPort = ports.find(item => item.group === 'right');
+          const rightPort = ports.find((item) => type ? item.group === 'bottom' : item.group === "right");
           newEdge.current = graph?.addEdge({
-            source: { cell: node.id, port: rightPort?.id},
-            connector: { name: 'smooth' },
+            source: { cell: node.id, port: rightPort?.id },
+            router: {
+              name: "manhattan",
+              args: {
+                padding: 20,
+                excludeShapes: ["notice-node"],
+              },
+            },
+            connector: { name: "rounded" },
             target: {
               x: args.x,
-              y: args.y
+              y: args.y,
             },
             attrs: {
               line: {
                 stroke: "#37d0ff",
                 strokeWidth: 2,
               },
-            }
+            },
           });
         } else {
-          // 否则修改边的终点
-          newEdge.current?.setTarget({
-            x: args.x,
-            y: args.y
+          // 判断是否进入其他节点内
+          // 查找到最顶层的节点 然后修改位置为节点
+          const nodes = graph.getNodesInArea(args.x, args.y, 5, 5);
+          const targetNode = nodes.length
+            ? (nodes || [])?.reduce((prev, curr) => {
+                const prevZ = prev.zIndex || -1;
+                const currZ = curr?.zIndex || -1;
+                return prevZ >= currZ ? prev : curr;
+              })
+            : null;
+
+          // 进入节点 不存在已连接边 则添加一条边
+          if (targetNode && targetNode.shape !== "notice-node") {
+            const ports = targetNode.getPorts();
+            const targetPort = ports.find((item) => item.group === "left");
+            newEdge.current?.setTarget({
+              cell: targetNode.id,
+              port: targetPort?.id,
+            });
+          } else {
+            // 否则修改边的终点
+            newEdge.current?.setTarget({
+              x: args.x,
+              y: args.y,
+            });
+          }
+        }
+      }
+    });
+
+    // step3: 鼠标抬起,展示节点菜单或者连线连接到目标节点
+    graph?.on("node:mouseup", (args) => {
+      console.log("node:mouseup");
+      node?.setData({ lock: false });
+      isDown.current = false;
+      isMove.current = false;
+      // 拖拽过程中释放鼠标 检测当前是否有节点
+      if (newEdge.current) {
+        // 判断是否连接到了节点
+        if (!Object.hasOwn(newEdge.current.target, "x")) {
+          newEdge.current.setAttrs({
+            line: {
+              stroke: "#7e8186",
+            },
           });
+          newEdge.current.setZIndex(0);
+          newEdge.current = undefined;
+          return;
         }
+        // 判断是否存在menu-popover
+        const els = document.querySelectorAll("[data-shape='menu-popover']");
+
+        if (els.length) {
+          return;
+        }
+        graph.addNode({
+          shape: "menu-popover",
+          x: args.x,
+          y: args.y,
+        });
       }
     });
 
-    // 连续添加节点完成
+    // 添加节点完成设置连线目标节点
     graph?.on("node:change:addedNode", (args: EventArgs["cell:change:*"]) => {
       const { current } = args;
-      if(newEdge.current && current) {
+      if (newEdge.current && current) {
         const addNode = current?.addNode as Node;
         const ports = addNode.getPorts();
-        const leftPort = ports?.find(item => item.group === 'left');
+        const leftPort = ports?.find((item) => item.group === "left");
+        const bottomPort = ports?.find((item) => item.group === "bottom");
         newEdge.current.setTarget({
           cell: addNode.id,
-          port: leftPort?.id
+          port: type ? bottomPort?.id : leftPort?.id,
         });
         newEdge.current.setAttrs({
           line: {
-            stroke: '#1b5cdf',
+            stroke: "#7e8186",
           },
         });
         newEdge.current = undefined;
       }
     });
 
-    // 连续添加popver关闭
+    // 节点菜单menu-popver关闭, 如果连线目标没有节点信息移除连线
     graph?.on("node:change:closedPopover", () => {
       setTimeout(() => {
-        if(Object.hasOwn(newEdge.current?.target || {}, 'x')) {
+        if (Object.hasOwn(newEdge.current?.target || {}, "x")) {
           graph?.removeCells([newEdge.current!]);
           newEdge.current = undefined;
         }
@@ -124,31 +176,39 @@ export default function Port(props: {
     });
   }, [graph]);
 
-  const {x, y} = node?.position() || {x: 0, y: 0};
+  const { x, y } = node?.position() || { x: 0, y: 0 };
   const x1 = (node?.getBBox()?.width || 0) + x + 50;
 
+  // 点击添加节点成功后,设置连线
   const handleAddChange = (addNode?: Node) => {
     setOpen(false);
-    if(addNode && node) {
-
+    if (addNode && node) {
       const sourcePorts = node?.getPorts();
-      const sourcePort = sourcePorts?.find(item => item.group === 'right');
+      const sourcePort = sourcePorts?.find((item) => type ? item.group === 'bottom' : item.group === "right");
       const targetPorts = addNode?.getPorts();
-      const targetPort = targetPorts?.find(item => item.group === 'left');
+      const targetPort = targetPorts?.find((item) => item.group === "left");
 
       graph?.addEdge({
         source: {
           cell: node.id,
-          port: sourcePort?.id
+          port: sourcePort?.id,
         },
         target: {
           cell: addNode.id,
-          port: targetPort?.id
+          port: targetPort?.id,
+        },
+        router: {
+          name: "manhattan",
+          args: {
+            padding: 20,
+            excludeShapes: ["notice-node"],
+          },
         },
-        connector: { name: 'smooth' },
+        zIndex: 0,
+        connector: { name: "rounded", args: {} },
         attrs: {
           line: {
-            stroke: '#1b5cdf',
+            stroke: "#7e8186",
             strokeWidth: 2,
           },
         },
@@ -158,7 +218,13 @@ export default function Port(props: {
 
   return (
     <Popover
-      content={<NodeMenu graph={graph} onChange={handleAddChange} position={{ x: x1, y}} />}
+      content={
+        <NodeMenu
+          graph={graph}
+          onChange={handleAddChange}
+          position={{ x: x1, y }}
+        />
+      }
       trigger={"click"}
       placement="right"
       arrow={false}
@@ -167,24 +233,52 @@ export default function Port(props: {
         setOpen(open);
       }}
     >
-      <div
-        className="node-port flex items-center justify-center"
-        style={{
-          transform: hovered ? "scale(1.2) translateY(-50%)" : "scale(1) translateY(-50%)",
-          opacity: hovered ? 1 : 0.5,
-          ...style,
-          ...extraStyle
-        }}
-        onMouseEnter={() => handleSetCanAdd(true)}
-        onMouseLeave={() => handleSetCanAdd(false)}
-        onMouseDown={handleMouseDown}
-        onMouseMove={handleMouseMove}
-      >
-        { out && <PlusOutlined
-          className="transform scale-0 transition duration-300 color-#fff text-8px"
-          style={{ transform: canAdd ? "scale(1)" : "scale(0)" }}
-        />}
-      </div>
+      {type === "extra" ? (
+        <div
+          className="node-port flex items-center justify-center"
+          style={{
+            transform: hovered
+              ? "scale(1.2) translateY(-50%) rotate(45deg)"
+              : "scale(1) translateY(-50%) rotate(45deg)",
+            width: 16,
+            height: 16,
+            border: 'none',
+            borderRadius: 0,
+            ...style,
+          }}
+          onMouseEnter={() => handleSetCanAdd(true)}
+          onMouseLeave={() => handleSetCanAdd(false)}
+          onMouseDown={handleMouseDown}
+        >
+          {out && (
+            <PlusOutlined
+              className="transform scale-0 transition duration-300 color-#fff text-8px"
+              style={{ transform: canAdd ? "scale(1)" : "scale(0)" }}
+            />
+          )}
+        </div>
+      ) : (
+        <div
+          className="node-port flex items-center justify-center"
+          style={{
+            transform: hovered
+              ? "scale(1.2) translateY(-50%)"
+              : "scale(1) translateY(-50%)",
+            ...style,
+            ...extraStyle,
+          }}
+          onMouseEnter={() => handleSetCanAdd(true)}
+          onMouseLeave={() => handleSetCanAdd(false)}
+          onMouseDown={handleMouseDown}
+        >
+          {out && (
+            <PlusOutlined
+              className="transform scale-0 transition duration-300 color-#fff text-8px"
+              style={{ transform: canAdd ? "scale(1)" : "scale(0)" }}
+            />
+          )}
+        </div>
+      )}
     </Popover>
   );
 }

+ 7 - 16
apps/flowchart-designer/src/components/nodes/Start.tsx

@@ -6,8 +6,6 @@ import Port from "./Port";
 import Content from "./Content";
 
 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);
 
@@ -16,22 +14,17 @@ export default ({ node, graph }: { node: Node; graph: Graph }) => {
     { key: "delete", label: "删除" },
   ];
 
-  useEffect(() => {
-    const { offsetWidth = 0, offsetHeight = 0 } = ref.current || {};
-    if (offsetHeight !== height || offsetWidth !== width) {
-      node.resize(offsetWidth, offsetHeight);
-    }
-  }, []);
-
   graph?.on("selection:changed", (args) => {
     setIsSelected(graph.isSelected(node));
   });
 
   return (
     <div
-      className="flow-node text-14px relative"
-      style={{ borderColor: isSelected || hovered ? "#0e52e0" : "#d9d9d9" }}
-      ref={ref}
+      className="flow-node rounded-l-36px"
+      style={{ 
+        
+        boxShadow: isSelected ? '0 0 0 8px #2941701a' : ''
+      }}
       onMouseEnter={() => setHovered(true)}
       onMouseLeave={() => setHovered(false)}
     >
@@ -40,10 +33,8 @@ export default ({ node, graph }: { node: Node; graph: Graph }) => {
         node={node}
         graph={graph}
         items={items}
-        headerStyle={{
-          backgroundImage:
-            "linear-gradient(to bottom, rgba(82, 196, 26, 0.2), #ffffff)",
-        }}
+        hovered={hovered}
+        selected={isSelected}
       />
 
       <Port hovered={hovered} style={{ right: -7, cursor: "crosshair" }} node={node} graph={graph} />

+ 42 - 3
apps/flowchart-designer/src/components/nodes/index.ts

@@ -9,6 +9,8 @@ import End from "./End";
 import Handle from "./Handle";
 import Link from "./Link";
 import PopoverNode from "./PopoverNode";
+import "./NoticeNode";
+import Custom from "./Custom";
 
 // 通用连接桩
 const ports = {
@@ -97,14 +99,51 @@ export const nodes = [
   { name: "decision-node", component: Decision, type: NodeType.DECISION, data: { name: '判断'} },
   { name: "end-node", component: End, type: NodeType.END, data: { name: '结束'} },
   { name: "handle-node", component: Handle, type: NodeType.PROCESS, data: { name: '处理'} },
-  { name: "link-node", component: Link, type: NodeType.LINK, data: { name: '连接'} },
+  { name: "link-node", component: Link, type: NodeType.LINK, data: { name: '连接'} }
 ];
 
+export const aiNode = [
+  {port: {in: 1, out: 1}, style: {}, color: '#00b2b2', icon: 'icon-wentifenlei', name: 'ai-category', component: Custom, data: { name: '问题分类'}},
+  {port: {in: 1, out: 1}, style: {}, color: '#00b2b2', icon: 'icon-xunhuan1', name: 'ai-loop', component: Custom, data: { name: '循环分支'}},
+  {port: {in: 1, out: 1}, style: {}, color: '#ff811a', icon: 'icon-shiyongwendang', name: 'ai-doc', component: Custom, data: { name: '文档提取器'}},
+  {port: {in: 1, out: 1}, style: {}, color: '#3071f3', icon: 'icon-http', name: 'ai-http', component: Custom, data: { name: 'HTTP'}},
+  {port: {in: 1, out: 0}, style: {borderTopRightRadius: 36, borderBottomRightRadius: 36}, color: '#d37d1d', icon: 'icon-flag', name: 'ai-end', component: Custom, data: { name: '结束'}},
+  {port: {in: 0, out: 1}, style: {borderTopLeftRadius: 36, borderBottomLeftRadius: 36}, color: '#5c62ff', icon: 'icon-message', name: 'ai-start', component: Custom, data: { name: '开始'}},
+  {port: {in: 1, out: 1}, style: {}, color: '#00b2b2', icon: 'icon-daima', name: 'ai-code', component: Custom, data: { name: '代码'}},
+  {port: {in: 1, out: 1}, style: {}, color: '#00b2b2', icon: 'icon-tiaojianfenzhi', name: 'ai-if', component: Custom, data: { name: '条件分支'}},
+  {port: {in: 1, out: 1}, style: {}, color: '#00b2b2', icon: 'icon-xunhuan', name: 'ai-iteration', component: Custom, data: { name: '迭代'}},
+  {port: {in: 1, out: 1}, style: {}, color: '#ff811a', icon: 'icon-data-update', name: 'ai-adddata', component: Custom, data: { name: '数据新增'}},
+  {port: {in: 1, out: 1}, style: {}, color: '#ff811a', icon: 'icon-data-Inquire', name: 'ai-querydata', component: Custom, data: { name: '数据查询'}},
+  {port: {in: 1, out: 1}, style: {}, color: '#ff811a', icon: 'icon-server-update-full', name: 'ai-updatedata', component: Custom, data: { name: '数据更新'}},
+  {port: {in: 1, out: 1}, style: {}, color: '#ff811a', icon: 'icon-server-update', name: 'ai-deldata', component: Custom, data: { name: '数据删除'}},
+  {port: {in: 1, out: 1}, style: {}, color: '#ff811a', icon: 'icon-book', name: 'ai-know', component: Custom, data: { name: '知识检索'}},
+  {port: {in: 1, out: 1}, style: {}, color: '#ff811a', icon: 'icon-image', name: 'ai-ocr', component: Custom, data: { name: '图像识别'}},
+  {port: {in: 1, out: 1}, style: {}, color: '#00b2b2', icon: 'icon-liebiaoguolvqi', name: 'ai-list', component: Custom, data: { name: '列表操作'}},
+  {port: {in: 1, out: 1}, style: {}, color: '#ca60fe', icon: 'icon-flow', name: 'ai-flow', component: Custom, data: { name: '工作流'}},
+  {port: {in: 1, out: 1}, style: {}, color: '#00b2b2', icon: 'icon-bianliangfuzhi', name: 'ai-equal', component: Custom, data: { name: '变量赋值'}},
+  {port: {in: 1, out: 1}, style: {}, color: '#1d44d3', icon: 'icon-zhinengti-opsasda', name: 'ai-llm', component: Custom, data: { name: '逻辑处理模型LLM', type: 'model'}, width: 256, height: 100},
+  {port: {in: 1, out: 1}, style: {}, color: '#00b2b2', icon: 'icon-kaifangAImoxingku', name: 'ai-params', component: Custom, data: { name: '参数提取'}},
+  {port: {in: 1, out: 1}, style: {}, color: '#00b2b2', icon: 'icon-canshutiqu', name: 'ai-var', component: Custom, data: { name: '变量聚合'}},
+  {port: {in: 1, out: 1}, style: {}, color: '#00b2b2', icon: 'icon-chajian1', name: 'ai-var', component: Custom, data: { name: '扩展插件', type: 'model'}, width: 256, height: 100},
+  {port: {in: 1, out: 1}, style: {}, color: '#4d6bfe', icon: 'icon-deepseek', name: 'ai-model', component: Custom, data: { name: 'DeepSeek R1', hidePort: true}},
+]
+
+aiNode.forEach((node) => {
+  register({
+    shape: node.name,
+    width: node?.width || 100,
+    height: node?.height || 100,
+    effect: ["data"],
+    component: node.component,
+    ports: {...ports}
+  });
+});
+
 nodes.forEach((node) => {
   register({
     shape: node.name,
-    width: 300,
-    height: 108,
+    width: 100,
+    height: 100,
     effect: ["data"],
     component: node.component,
     ports: {...ports}

+ 15 - 3
apps/flowchart-designer/src/models/flowModel.ts

@@ -29,7 +29,7 @@ export default function flowModel() {
         maxScale: 2,
       },
       background: {
-        color: "#f2f3f5",
+        color: "#fbfcfe",
       },
       grid: {
         size: 20,
@@ -57,11 +57,19 @@ export default function flowModel() {
       },
       // 连线配置
       connecting: {
-        snap: true
+        snap: true,
+        allowLoop: true,
+        allowPort: false,
+        allowNode: false,
+        allowMulti: false,
       }
     });
 
-    instance.use(new Transform());
+    // instance.use(new Transform({
+    //   resizing: {
+    //     enabled: true,
+    //   }
+    // }));
     instance.use(new Snapline({
       enabled: true
     }));
@@ -88,6 +96,10 @@ export default function flowModel() {
 
     setGraph(instance);
     graphRef.current = instance;
+
+    graph?.bindKey("del", () => {
+      graph?.removeCells(graph?.getSelectedCells());
+    });
   };
 
   return {

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

@@ -5,8 +5,8 @@ import React from 'react'
 export default function index() {
   return (
     <div className='flex justify-between items-center px-24px leading-[60px]'>
-      <div className="left w-200px text-#666">流程图 flowcode</div>
-      <div className="middle text-24px">流设计</div>
+      <div className="left w-200px text-#666"><span className='inline-block mr-4px w-2px h-14px bg-#1dd328'></span>发票助手</div>
+      <div className="middle text-24px">智能流设计</div>
       <div className="right w-200px text-right">
         <span className='text-10px text-#999 mr-12px'>自动保存 17:25:43</span>
         <Avatar size={32} icon={<UserOutlined />} />

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

@@ -47,6 +47,22 @@ export default function index() {
     }
   }, [graph, minimapRef.current]);
 
+  const handleAddNotice = () => {
+    const { width = 100, height = 100 } = graph?.getGraphArea() || {};
+    graph?.addNode({ 
+      shape: 'notice-node',
+      zIndex: -1,
+      position: {
+        x: width / 2 - 150,
+        y: height / 2 - 100,
+      },
+      data: {
+        name: '',
+        text: ''
+      }
+    });
+  }
+
   return (
     <div className="absolute left-32px bottom-32px z-2 flex gap-12px">
       <div className="w-120px h-40px rounded-12px bg-#fff box-shadow-sm flex items-center justify-between px-12px">
@@ -84,7 +100,7 @@ export default function index() {
           </Button>
         </Popover>
         <Tooltip title="添加文本">
-          <Button type="text" icon={<FileAddOutlined />} />
+          <Button type="text" icon={<FileAddOutlined />} onClick={handleAddNotice} />
         </Tooltip>
         <Divider type="vertical" />
         <Tooltip title="指针模式">

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

@@ -14,7 +14,7 @@ export default function index() {
 
   return (
     <div className='w-100vw h-100vh flex flex-col overflow-hidden'>
-      <div className="w-full h-60px bg-#f2f4f7 border-b border-#eaeaea border-b-solid box-shadow-sm">
+      <div className="w-full h-60px bg-#ffffff border-b border-#eaeaea border-b-solid box-shadow-sm">
         <Header />
       </div>
       <div className="flex-1 relative">

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

@@ -3,12 +3,24 @@ import {defineConfig, presetAttributify, presetUno} from 'unocss';
 export function createConfig({strict = true, dev = true} = {}) {
   return defineConfig({
     envMode: dev ? 'dev' : 'build', presets: [presetAttributify({strict}), presetUno()],
+    theme: {
+      colors: {
+        'primary': '#0e52e0',
+        'primary-light': '#e6f7ff',
+        'primary-dark': '#7e8186',
+        'primary-hover': '#40a9ff',
+        'primary-active': '#096dd9',
+        'primary-disabled': '#c6e2ff',
+        'primary-disabled-hover': '#c6e2ff',
+        'primary-disabled-active': '#c6e2ff',
+      }
+    },
     rules: [
       ['flex-important', {display: 'flex !important'}],
     ],
     shortcuts: {
-      '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'
+      'flow-node': 'w-full h-full relative text-14px bg-#fff border-2px border-#7e8186 border-solid rounded-8px',
+      'node-port': 'absolute w-10px h-10px rounded-full bg-#7e8186 top-1/2 border-2px border-solid border-#fff transition duration-300 hover:scale-150 origin-top hover:bg-#0e53e2'
     },
   });
 }