Преглед изворни кода

feat: 添加横向泳道节点

liaojiaxing пре 8 месеци
родитељ
комит
20ec910d4b

+ 87 - 0
apps/designer/src/components/lane/horizontalLane.tsx

@@ -0,0 +1,87 @@
+import { CompoundedComponent } from "@/types";
+import { register } from "@antv/x6-react-shape";
+import { Node } from "@antv/x6";
+import { defaultData } from "../data";
+import { useSizeHook, useShapeProps } from "@/hooks";
+
+const component = ({ node }: { node: Node }) => {
+  const { fill, stroke, opacity } = node.getData();
+  const { size, ref } = useSizeHook();
+  const {
+    fillContent,
+    strokeColor,
+    strokeWidth,
+  } = useShapeProps(fill, size, stroke);
+
+  const handleStartMove = () => {
+    node.setData({
+      ignoreDrag: false,
+    });
+  };
+
+  const handleEndMove = () => {
+    node.setData({
+      ignoreDrag: true,
+    });
+  };
+
+  return (
+    <>
+      <div
+        className="relative text-0 w-full h-full"
+        style={{
+          opacity: opacity / 100,
+          border: `solid ${strokeWidth}px ${strokeColor}`,
+        }}
+        ref={ref}
+      >
+        <div
+          className="w-40px h-full relative cursor-move"
+          style={{
+            background: fillContent,
+            borderRight: `solid ${strokeWidth}px ${strokeColor}`,
+          }}
+          onMouseEnter={handleStartMove}
+          onMouseLeave={handleEndMove}
+        >
+        </div>
+        <div className="pool">
+          <div className="stage"></div>
+          <div className="lane"></div>
+        </div>
+      </div>
+    </>
+  );
+};
+
+register({
+  shape: "custom-react-horizontalLane",
+  width: 540,
+  height: 250,
+  effect: ["data"],
+  component: component,
+});
+
+const horizontalLane: CompoundedComponent = {
+  name: "泳道(垂直)",
+  icon: require("./image/horizontalLane.png"),
+  node: {
+    shape: "custom-react-horizontalLane",
+    data: {
+      label: "",
+      // 泳池名称
+      poolName: "",
+      // 泳道
+      lane: [],
+      // 阶段
+      stage: [],
+      isLane: true,
+      laneDirection: "horizontal",
+      ignoreDrag: true,
+      noCreate: true,
+      ...defaultData,
+    },
+  },
+};
+
+export default horizontalLane;

+ 453 - 0
apps/designer/src/components/lane/horizontalPool.tsx

@@ -0,0 +1,453 @@
+import { CompoundedComponent } from "@/types";
+import { register } from "@antv/x6-react-shape";
+import { Graph, Node } from "@antv/x6";
+import { defaultData } from "../data";
+import CustomInput from "../CustomInput";
+import { useSizeHook, useShapeProps } from "@/hooks";
+import { useEffect, useRef, useState } from "react";
+import Setting from "./setting";
+import { LaneItem, StageItem } from "@/types";
+
+const component = ({ node, graph }: { node: Node; graph: Graph }) => {
+  const {
+    poolName,
+    text,
+    fill,
+    stroke,
+    opacity,
+    lane,
+    stage,
+    direction,
+    textDirection,
+    headerHeight,
+    stageWidth,
+    laneHeadHeight,
+  } = node.getData();
+  const [showSetting, setShowSetting] = useState(false);
+  const { size, ref } = useSizeHook();
+  const lister = useRef<any>();
+  const { fillContent, strokeColor, strokeWidth } = useShapeProps(
+    fill,
+    size,
+    stroke
+  );
+
+  graph.on("node:selected", (args) => {
+    setShowSetting(
+      graph.getSelectedCells().length === 1 && node.id === args.node.id
+    );
+  });
+  graph.on("node:unselected", (args) => {
+    if (node.id === args.node.id) setShowSetting(false);
+  });
+
+  useEffect(() => {
+    let width = node.getSize().width;
+    let height = node.getSize().height;
+
+    if (lane.length) {
+      height = lane.reduce(
+        (a: number, b: LaneItem) => a + b.width + strokeWidth,
+        0
+      );
+    }
+    if (stage.length) {
+      height += stageWidth;
+    }
+
+    node.setSize({ width, height });
+  }, [lane.length]);
+
+  // 监听宽高变化
+  if (!lister.current) {
+    lister.current = node.on("change:size", (args) => {
+      const lane = node.data.lane;
+      const stage = node.data.stage;
+      // 更新泳道宽度
+      if (lane.length) {
+        const originWidth = lane.reduce(
+          (a: number, b: LaneItem) => a + b.width,
+          0
+        );
+        const offsetY =
+          (args?.current?.height || 0) -
+          originWidth -
+          (stage.length ? stageWidth : 0);
+
+        if (offsetY) {
+          node.setData({
+            lane: lane.map((item: LaneItem) => {
+              return {
+                ...item,
+                width: item.width + offsetY / lane.length - strokeWidth,
+              };
+            }),
+          });
+        }
+      }
+      // 更新阶段高度
+      if (stage.length) {
+        const originHeight = stage.reduce(
+          (a: number, b: StageItem) => a + b.height,
+          0
+        );
+        const offsetX =
+          (args?.current?.width || 0) -
+          originHeight -  headerHeight;
+
+        if (offsetX) {
+          node.setData({
+            stage: stage.map((item: StageItem) => {
+              return {
+                ...item,
+                height: item.height + offsetX / stage.length - strokeWidth,
+              };
+            }),
+          });
+        }
+      }
+    });
+  }
+
+  const handleStartMove = () => {
+    node.setData({
+      ignoreDrag: false,
+    });
+  };
+
+  const handleEndMove = () => {
+    node.setData({
+      ignoreDrag: true,
+    });
+  };
+
+  // 插入阶段
+  const handleInsertStage = (x: number, y: number) => {
+    const stage = node.data.stage || [];
+    const { width, height } = node.getSize();
+    // 新阶段位置
+    const w1 = x - node.getPosition().x - headerHeight;
+    // 无阶段 从中切开
+    if (!stage.length) {
+      const w2 = width - w1 - headerHeight;
+      node.setData({
+        stage: [
+          {
+            name: "阶段",
+            height: w1,
+          },
+          {
+            name: "阶段",
+            height: w2,
+          },
+        ],
+      });
+      node.setSize({
+        width,
+        height: height + stageWidth
+      })
+    } else {
+      // 有阶段 则找出在哪一段内 再分成两段
+      let temp = 0; // 记录长度
+      for (let i = 0; i < stage.length; i++) {
+        const item = stage[i];
+        temp += item.height;
+        if (temp > w1) {
+          // 插入
+          const w3 = temp - w1;
+          const w4 = item.height - w3;
+          const newStages = [
+            {
+              name: "阶段",
+              height: w4,
+            },
+            {
+              ...item,
+              height: w3,
+            },
+          ];
+          stage.splice(i, 1, ...newStages);
+          node.setData(
+            {
+              stage: [...stage],
+            },
+            {
+              deep: false,
+            }
+          );
+          break;
+        }
+      }
+    }
+  };
+
+  // 插入泳道
+  const handleInsertLane = (x: number, _y: number) => {
+    console.log('插入泳道');
+    const lane = node.data.lane || [];
+    const stage = node.data.stage || [];
+    if(!lane.length) {
+      node.setData({
+        lane: [{
+          name: "",
+          width: node.getSize().height - (stage.length ? stageWidth : 0) 
+        }]
+      })
+    } else {
+      // 判断在哪一个泳道内
+      let temp = 0;
+      const w1 = x - node.getPosition().y;
+      for (let i = 0; i < lane.length; i++) {
+        const item = lane[i];
+        temp += item.width;
+        if (temp > w1) {
+          // 插入
+          lane.splice(i, 0, {
+            name: "",
+            width: lane[lane.length - 1].width
+          })
+          node.setData({
+            lane: [...lane]
+          });
+          break;
+        }
+      }
+    }
+  }
+
+  const listerEmbedded = useRef<any>();
+  if (!listerEmbedded.current) {
+    listerEmbedded.current = graph.on("node:embedded", (args) => {
+      const {isSeparator, isLane, separatorDiration, laneDirection } = args.node.data || {};
+      // 分隔符
+      if (isSeparator && separatorDiration !== direction) {
+        const bbox = node.getBBox();
+        if (bbox.isIntersectWithRect(args.node.getBBox())) {
+          handleInsertStage(args.x, args.y);
+        }
+      }
+      // 泳道
+      if (isLane && laneDirection === direction) {
+        const bbox = node.getBBox();
+        if (bbox.isIntersectWithRect(args.node.getBBox())) {
+          handleInsertLane(args.x, args.y);
+        }
+      }
+    });
+  }
+
+  const handleChangeLane = (val: number | null) => {
+    if (!val) {
+      node.setData({ lane: [] }, { deep: false });
+      return;
+    }
+    const currentLanes = node.data.lane || [];
+    const currentLength = currentLanes.length;
+
+    if (currentLength < val) {
+      let width = node.getSize().width;
+      if (currentLength > 0) {
+        width = currentLanes[currentLength - 1].width;
+      }
+      const newLanes = new Array(val - currentLength).fill({ width, name: "" });
+      node.setData({ lane: [...(node.data.lane || []), ...newLanes] });
+    } else {
+      node.updateData({
+        lane: currentLanes.slice(0, val),
+      });
+    }
+  };
+  return (
+    <>
+      {showSetting && <Setting node={node} onChangeLane={handleChangeLane} />}
+      <div
+        className="relative text-0 w-full h-full flex"
+        style={{
+          opacity: opacity / 100,
+          border: `solid ${strokeWidth}px ${strokeColor}`,
+        }}
+        ref={ref}
+      >
+        <div
+          className="h-full relative cursor-move"
+          style={{
+            width: headerHeight,
+            background: fillContent,
+            borderRight: `solid ${strokeWidth}px ${strokeColor}`,
+          }}
+          onMouseEnter={handleStartMove}
+          onMouseLeave={handleEndMove}
+        >
+          <CustomInput
+            value={poolName}
+            styles={text}
+            node={node}
+            onChange={(val) => node.setData({ poolName: val })}
+            txtStyle={{
+              transform: `rotate(-90deg) translateX(-${size.height}px)`,
+              transformOrigin: '0 0',
+              width: size.height,
+              height: headerHeight,
+            }}
+          />
+        </div>
+        <div
+          className="relative content h-full"
+          style={{ width: `calc(100% - ${headerHeight}px)` }}
+        >
+          {/* 阶段 */}
+          {stage.length ? (
+            <div
+              className="stage h-full w-full absolute top-0 left-0 flex"
+            >
+              {stage.map((stageItem: StageItem, index: number) => {
+                return (
+                  <div key={index} 
+                  style={{
+                    width: stageItem.height - strokeWidth,
+                    height: '100%',
+                    borderRight:
+                      index < stage.length - 1
+                        ? `solid ${strokeWidth}px ${strokeColor}`
+                        : "node",
+                  }}
+                  >
+                    <div
+                      className="relative stage-item cursor-move"
+                      style={{
+                        width: stageItem.height - 2 * strokeWidth,
+                        background: fillContent,
+                        height: stageWidth,
+                        borderBottom: `solid ${strokeWidth}px ${strokeColor}`,
+                      }}
+                      onMouseEnter={handleStartMove}
+                      onMouseLeave={handleEndMove}
+                    >
+                      <CustomInput
+                        value={stageItem.name}
+                        styles={text}
+                        node={node}
+                        onChange={(val) => {
+                          node.setData({
+                            stage: stage.map((item: StageItem, i: number) => {
+                              if (index === i) {
+                                return {
+                                  ...item,
+                                  name: val,
+                                };
+                              }
+                            }),
+                          });
+                        }}
+                      />
+                    </div>
+                  </div>
+                );
+              })}
+            </div>
+          ) : (
+            ""
+          )}
+          {/* 泳道 */}
+          <div className="lane absolute left-0" style={{ 
+              top: stage.length ? stageWidth : 0, 
+              width: `100%`,
+              height: `calc(100% - ${stage.length ? stageWidth : 0}px)`
+            }}>
+            {lane.map((item: LaneItem, index: number) => (
+              <div
+                key={index}
+                style={{
+                  width: '100%',
+                  height: item.width,
+                  borderBottom:
+                    index === lane.length - 1
+                      ? "none"
+                      : `solid ${strokeWidth}px ${strokeColor}`,
+                }}
+              >
+                <div
+                  className="flex-1 w-full relative cursor-move"
+                  style={{
+                    width: laneHeadHeight,
+                    height: '100%',
+                    background: fillContent,
+                    borderRight: `solid ${strokeWidth}px ${strokeColor}`,
+                  }}
+                  onMouseEnter={handleStartMove}
+                  onMouseLeave={handleEndMove}
+                >
+                  <CustomInput
+                    value={item.name}
+                    styles={text}
+                    node={node}
+                    txtStyle={{
+                      transform: `rotate(-90deg) translateX(-${item.width}px)`,
+                      transformOrigin: '0 0',
+                      width: item.width - strokeWidth,
+                      height: laneHeadHeight,
+                    }}
+                    onChange={(val) => {
+                      node.setData({
+                        lane: lane.map((item: LaneItem, i: number) => {
+                          if (index === i) {
+                            return {
+                              ...item,
+                              name: val,
+                            };
+                          }
+                          return item;
+                        }),
+                      });
+                    }}
+                  />
+                </div>
+              </div>
+            ))}
+          </div>
+        </div>
+      </div>
+    </>
+  );
+};
+
+register({
+  shape: "custom-react-horizontalPool",
+  width: 540,
+  height: 250,
+  effect: ["data"],
+  component: component,
+});
+
+const horizontalPool: CompoundedComponent = {
+  name: "泳池(垂直)",
+  icon: require("./image/horizontalPool.png"),
+  node: {
+    shape: "custom-react-horizontalPool",
+    data: {
+      label: "",
+      // 泳池名称
+      poolName: "",
+      // 泳道
+      lane: [],
+      // 阶段
+      stage: [],
+      // 泳道名称高度
+      headerHeight: 40,
+      // 阶段宽度
+      stageWidth: 20,
+      // 泳道名称高度
+      laneHeadHeight: 40,
+      // 泳池方向
+      direction: "horizontal",
+      // 文字方向
+      textDirection: "vertical",
+      // 忽略拖拽
+      ignoreDrag: true,
+      // 其他
+      ...defaultData,
+    },
+  },
+};
+
+export default horizontalPool;

+ 5 - 0
apps/designer/src/components/lane/horizontalSeparator.tsx

@@ -31,9 +31,14 @@ const horizontalSeparator: CompoundedComponent = {
     shape: "custom-react-horizontalSeparator",
     data: {
       label: "",
+      // 分割符
       isSeparator: true,
+      // 分割方向
       separatorDiration: "horizontal",
+      // 忽略拖动
       ignoreDrag: true,
+      // 不用创建
+      noCreate: true,
       ...defaultData,
     },
   },

apps/designer/src/components/lane/image/verticalSeparator.png → apps/designer/src/components/lane/image/vertialSeparator.png


+ 6 - 0
apps/designer/src/components/lane/index.ts

@@ -1,9 +1,15 @@
 import verticalPool from "./verticalPool"
 import verticalLane from "./verticalLane"
 import horizontalSeparator from "./horizontalSeparator"
+import vertialSeparator from "./verticalSeparator"
+import horizontalPool from "./horizontalPool"
+import horizontalLane from "./horizontalLane"
 
 export const lane = [
   verticalPool,
   verticalLane,
   horizontalSeparator,
+  vertialSeparator,
+  horizontalPool,
+  horizontalLane
 ]

+ 4 - 7
apps/designer/src/components/lane/verticalLane.tsx

@@ -2,21 +2,16 @@ import { CompoundedComponent } from "@/types";
 import { register } from "@antv/x6-react-shape";
 import { Node } from "@antv/x6";
 import { defaultData } from "../data";
-import CustomInput from "../CustomInput";
 import { useSizeHook, useShapeProps } from "@/hooks";
-import { useRef } from "react";
 
 const component = ({ node }: { node: Node }) => {
-  const { label, text, fill, stroke, opacity } = node.getData();
+  const { fill, stroke, opacity } = node.getData();
   const { size, ref } = useSizeHook();
   const {
     fillContent,
-    defsContent,
     strokeColor,
     strokeWidth,
-    strokeDasharray,
   } = useShapeProps(fill, size, stroke);
-  const { width: w, height: h } = size;
 
   const handleStartMove = () => {
     node.setData({
@@ -49,7 +44,6 @@ const component = ({ node }: { node: Node }) => {
           onMouseEnter={handleStartMove}
           onMouseLeave={handleEndMove}
         >
-          <CustomInput value={label} styles={text} node={node} />
         </div>
         <div className="pool">
           <div className="stage"></div>
@@ -81,7 +75,10 @@ const verticalLane: CompoundedComponent = {
       lane: [],
       // 阶段
       stage: [],
+      isLane: true,
+      laneDirection: "vertical",
       ignoreDrag: true,
+      noCreate: true,
       ...defaultData,
     },
   },

+ 187 - 47
apps/designer/src/components/lane/verticalPool.tsx

@@ -58,9 +58,11 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
   }, [lane.length]);
 
   // 监听宽高变化
-  if(!lister.current) {
+  if (!lister.current) {
     lister.current = node.on("change:size", (args) => {
       const lane = node.data.lane;
+      const stage = node.data.stage;
+      // 更新泳道宽度
       if (lane.length) {
         const originWidth = lane.reduce(
           (a: number, b: LaneItem) => a + b.width,
@@ -70,7 +72,7 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
           (args?.current?.width || 0) -
           originWidth -
           (stage.length ? stageWidth : 0);
-        
+
         if (offsetX) {
           node.setData({
             lane: lane.map((item: LaneItem) => {
@@ -82,6 +84,27 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
           });
         }
       }
+      // 更新阶段高度
+      if (stage.length) {
+        const originHeight = stage.reduce(
+          (a: number, b: StageItem) => a + b.height,
+          0
+        );
+        const offsetY =
+          (args?.current?.height || 0) -
+          originHeight -  headerHeight;
+
+        if (offsetY) {
+          node.setData({
+            stage: stage.map((item: StageItem) => {
+              return {
+                ...item,
+                height: item.height + offsetY / stage.length - strokeWidth,
+              };
+            }),
+          });
+        }
+      }
     });
   }
 
@@ -99,18 +122,118 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
 
   // 插入阶段
   const handleInsertStage = (x: number, y: number) => {
+    const stage = node.data.stage || [];
+    const { width, height } = node.getSize();
+    // 新阶段位置
+    const h1 = y - node.getPosition().y - headerHeight;
+    // 无阶段 从中切开
+    if (!stage.length) {
+      const h2 = height - h1 - headerHeight;
+      node.setData({
+        stage: [
+          {
+            name: "阶段",
+            height: h1,
+          },
+          {
+            name: "阶段",
+            height: h2,
+          },
+        ],
+      });
+      node.setSize({
+        width: width + stageWidth,
+        height
+      })
+    } else {
+      // 有阶段 则找出在哪一段内 再分成两段
+      let temp = 0; // 记录长度
+      for (let i = 0; i < stage.length; i++) {
+        const item = stage[i];
+        temp += item.height;
+        if (temp > h1) {
+          // 插入
+          const h3 = temp - h1;
+          const h4 = item.height - h3;
+          const newStages = [
+            {
+              name: "阶段",
+              height: h4,
+            },
+            {
+              ...item,
+              height: h3,
+            },
+          ];
+          stage.splice(i, 1, ...newStages);
+          node.setData(
+            {
+              stage: [...stage],
+            },
+            {
+              deep: false,
+            }
+          );
+          break;
+        }
+      }
+    }
+  };
 
+  // 插入泳道
+  const handleInsertLane = (x: number, _y: number) => {
+    console.log('插入泳道');
+    const lane = node.data.lane || [];
+    const stage = node.data.stage || [];
+    if(!lane.length) {
+      node.setData({
+        lane: [{
+          name: "",
+          width: node.getSize().width - (stage.length ? stageWidth : 0) 
+        }]
+      })
+    } else {
+      // 判断在哪一个泳道内
+      let temp = 0;
+      const w1 = x - node.getPosition().x;
+      for (let i = 0; i < lane.length; i++) {
+        const item = lane[i];
+        temp += item.width;
+        if (temp > w1) {
+          // 插入
+          lane.splice(i, 0, {
+            name: "",
+            width: lane[lane.length - 1].width
+          })
+          node.setData({
+            lane: [...lane]
+          });
+          break;
+        }
+      }
+    }
   }
 
-  graph.on("node:embedded", (args) => {
-    console.log("node:embedded",args);
-    if(args.node.data?.isSeparator) {
-      const bbox = node.getBBox();
-      if(bbox.isIntersectWithRect(args.node.getBBox())) {
-        handleInsertStage(args.e.offsetX, args.e.offsetY)
+  const listerEmbedded = useRef<any>();
+  if (!listerEmbedded.current) {
+    listerEmbedded.current = graph.on("node:embedded", (args) => {
+      const {isSeparator, isLane, separatorDiration, laneDirection } = args.node.data || {};
+      // 分隔符
+      if (isSeparator && separatorDiration !== direction) {
+        const bbox = node.getBBox();
+        if (bbox.isIntersectWithRect(args.node.getBBox())) {
+          handleInsertStage(args.x, args.y);
+        }
       }
-    }
-  })
+      // 泳道
+      if (isLane && laneDirection === direction) {
+        const bbox = node.getBBox();
+        if (bbox.isIntersectWithRect(args.node.getBBox())) {
+          handleInsertLane(args.x, args.y);
+        }
+      }
+    });
+  }
 
   const handleChangeLane = (val: number | null) => {
     if (!val) {
@@ -129,7 +252,7 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
       node.setData({ lane: [...(node.data.lane || []), ...newLanes] });
     } else {
       node.updateData({
-        lane: currentLanes.slice(0, val)
+        lane: currentLanes.slice(0, val),
       });
     }
   };
@@ -162,50 +285,61 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
           />
         </div>
         <div
-          className="content flex"
+          className="relative content"
           style={{ height: `calc(100% - ${headerHeight}px)` }}
         >
           {/* 阶段 */}
           {stage.length ? (
             <div
-              className="stage h-full"
-              style={{
-                width: stageWidth,
-                borderRight: `solid ${strokeWidth}px ${strokeColor}`,
-              }}
+              className="stage h-full w-full absolute left-0 top-0"
             >
               {stage.map((stageItem: StageItem, index: number) => {
                 return (
-                  <div
-                    key={index}
-                    className="relative stage-item w-full h-full"
-                    style={{
-                      background: fillContent,
-                    }}
+                  <div key={index} 
+                  style={{
+                    width: '100%',
+                    height: stageItem.height - strokeWidth,
+                    borderBottom:
+                      index < stage.length - 1
+                        ? `solid ${strokeWidth}px ${strokeColor}`
+                        : "node",
+                  }}
                   >
-                    <CustomInput
-                      value={stageItem.name}
-                      styles={text}
-                      node={node}
-                      txtStyle={{
-                        transform: `rotate(-90deg) translateX(-${stageItem.height}px)`,
-                        transformOrigin: "0 0",
-                        height: stageWidth,
-                        width: stageItem.height,
-                      }}
-                      onChange={(val) => {
-                        node.setData({
-                          stage: stage.map((item: StageItem, i: number) => {
-                            if (0 === i) {
-                              return {
-                                ...item,
-                                name: val,
-                              };
-                            }
-                          }),
-                        });
+                    <div
+                      className="relative stage-item cursor-move"
+                      style={{
+                        width: stageWidth,
+                        background: fillContent,
+                        height: stageItem.height - 2 * strokeWidth,
+                        borderRight: `solid ${strokeWidth}px ${strokeColor}`,
                       }}
-                    />
+                      onMouseEnter={handleStartMove}
+                      onMouseLeave={handleEndMove}
+                    >
+                      <CustomInput
+                        value={stageItem.name}
+                        styles={text}
+                        node={node}
+                        txtStyle={{
+                          transform: `rotate(-90deg) translateX(-${stageItem.height}px)`,
+                          transformOrigin: "0 0",
+                          height: stageWidth,
+                          width: stageItem.height,
+                        }}
+                        onChange={(val) => {
+                          node.setData({
+                            stage: stage.map((item: StageItem, i: number) => {
+                              if (index === i) {
+                                return {
+                                  ...item,
+                                  name: val,
+                                };
+                              }
+                            }),
+                          });
+                        }}
+                      />
+                    </div>
                   </div>
                 );
               })}
@@ -214,7 +348,7 @@ const component = ({ node, graph }: { node: Node; graph: Graph }) => {
             ""
           )}
           {/* 泳道 */}
-          <div className="lane flex h-full">
+          <div className="lane flex h-full absolute top-0" style={{ left: stage.length ? stageWidth : 0 }}>
             {lane.map((item: any, index: number) => (
               <div
                 key={index}
@@ -284,11 +418,17 @@ const verticalPool: CompoundedComponent = {
       lane: [],
       // 阶段
       stage: [],
+      // 泳道名称高度
       headerHeight: 40,
-      stageWidth: 40,
+      // 阶段宽度
+      stageWidth: 20,
+      // 泳池方向
       direction: "vertical",
+      // 文字方向
       textDirection: "horizontal",
+      // 忽略拖拽
       ignoreDrag: true,
+      // 其他
       ...defaultData,
     },
   },

+ 47 - 0
apps/designer/src/components/lane/verticalSeparator.tsx

@@ -0,0 +1,47 @@
+import { CompoundedComponent } from "@/types";
+import { register } from "@antv/x6-react-shape";
+import { Graph, Node } from "@antv/x6";
+import { defaultData } from "../data";
+
+const component = ({ node, graph }: { node: Node, graph: Graph }) => {
+
+  return (
+    <>
+      <div
+        className="relative text-0 w-0 h-full border-solid border-1 border-solid-color"
+      >
+       <span className="absolute w-14px left-2px top-0px text-12px">阶段</span>
+      </div>
+    </>
+  );
+};
+
+register({
+  shape: "custom-react-vertialSeparator",
+  width: 2,
+  height: 270,
+  effect: ["data"],
+  component: component,
+});
+
+const vertialSeparator: CompoundedComponent = {
+  name: "分隔符(需拖动到水平泳池上)",
+  icon: require("./image/vertialSeparator.png"),
+  node: {
+    shape: "custom-react-vertialSeparator",
+    data: {
+      label: "",
+      // 分割符
+      isSeparator: true,
+      // 分割方向
+      separatorDiration: "vertial",
+      // 忽略拖动
+      ignoreDrag: true,
+      // 不用创建
+      noCreate: true,
+      ...defaultData,
+    },
+  },
+};
+
+export default vertialSeparator;

+ 5 - 3
apps/designer/src/events/index.ts

@@ -76,10 +76,12 @@ export const handleGraphEvent = (graph: Graph) => {
     }
   );
 
-  // 处理添加节点事件
+  // 处理添加节点事件 noCreate为不用创建节点
   graph.on("node:added", ({ node }) => {
-    if(node.getData()?.isSeparator) {
-      graph.removeNode(node.id);
+    if(node.getData()?.noCreate) {
+      setTimeout(() => {
+        graph.removeNode(node.id);
+      }, 50);
     }
   })
 };

+ 3 - 3
apps/designer/src/models/graphModel.ts

@@ -113,10 +113,10 @@ export default function GraphModel() {
     .use(new History({
       enabled: true,
       beforeAddCommand: (event, args) => {
-        // @ts-ignore 排除分隔符节点
-        if(args?.cell?.data?.isSeparator) return false;
+        // @ts-ignore 排除不用创建的节点
+        if(args?.cell?.data?.noCreate) return false;
         // @ts-ignore 排除页面节点
-        return !(event === 'cell:added' && args?.cell?.getData()?.isPage)
+        return !(event === 'cell:added' && args?.cell?.getData()?.isPage);
       },
     }))
     .use(new Export());