From e95d12d76517481453cd25a41f22b0ca54c91087 Mon Sep 17 00:00:00 2001 From: sw3205933776 <3205933776@qq.com> Date: Wed, 19 Nov 2025 16:19:31 +0800 Subject: [PATCH] Add horizontal movement limits to workflow canvas --- src/components/WorkFlow/index.tsx | 78 ++++++++++++++++++++++++------- 1 file changed, 60 insertions(+), 18 deletions(-) diff --git a/src/components/WorkFlow/index.tsx b/src/components/WorkFlow/index.tsx index 626511b1a..45ad756c8 100644 --- a/src/components/WorkFlow/index.tsx +++ b/src/components/WorkFlow/index.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState, useCallback } from "react"; +import { useEffect, useRef, useState, useCallback, useMemo } from "react"; import { PanOnScrollMode, ReactFlow, @@ -49,6 +49,8 @@ export default function Workflow({ const [lastViewport, setLastViewport] = useState({ x: 0, y: 0, zoom: 1 }); const [nodes, setNodes, onNodesChange] = useNodesState([]); const workerList = useWorkerList(); + const containerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(0); const baseWorker: Agent[] = [ { tasks: [], @@ -279,34 +281,47 @@ export default function Workflow({ }, [taskAssigning, isEditMode, workerList]); const { setViewport, getViewport } = useReactFlow(); + const [isAnimating, setIsAnimating] = useState(false); + const totalNodesWidth = useMemo(() => { + if (!nodes.length) return 0; + + const widths = nodes.map((node) => (node.data.isExpanded ? 684 : 342)); + const spacing = Math.max(nodes.length - 1, 0) * 20; + + return widths.reduce((sum, width) => sum + width, 0) + spacing + 16; // padding buffer + }, [nodes]); + + const minViewportX = useMemo(() => { + if (!containerWidth) return 0; + const contentWidth = Math.max(totalNodesWidth, containerWidth); + return Math.min(0, containerWidth - contentWidth); + }, [containerWidth, totalNodesWidth]); + + const clampViewportX = useCallback( + (x: number) => Math.min(0, Math.max(minViewportX, x)), + [minViewportX] + ); + useEffect(() => { - const container: HTMLElement | null = - document.querySelector(".react-flow__pane"); - if (!container) return; - - const onWheel = (e: WheelEvent) => { - if (e.deltaY !== 0 && !isEditMode) { - e.preventDefault(); - - const { x, y, zoom } = getViewport(); - setViewport({ x: x - e.deltaY, y, zoom }, { duration: 0 }); + const updateWidth = () => { + if (containerRef.current) { + setContainerWidth(containerRef.current.clientWidth); } }; - container.addEventListener("wheel", onWheel, { passive: false }); + updateWidth(); + window.addEventListener("resize", updateWidth); return () => { - container.removeEventListener("wheel", onWheel); + window.removeEventListener("resize", updateWidth); }; - }, [getViewport, setViewport, isEditMode]); + }, []); - const [isAnimating, setIsAnimating] = useState(false); const moveViewport = (dx: number) => { if (isAnimating) return; const viewport = getViewport(); setIsAnimating(true); - // Prevent scrolling past x=0 (too far right) when moving left - const newX = dx > 0 ? Math.min(0, viewport.x + dx) : viewport.x + dx; + const newX = clampViewportX(viewport.x + dx); setViewport( { x: newX, y: viewport.y, zoom: viewport.zoom }, { @@ -322,6 +337,28 @@ export default function Workflow({ share(taskId); }; + useEffect(() => { + const container: HTMLElement | null = + document.querySelector(".react-flow__pane"); + if (!container) return; + + const onWheel = (e: WheelEvent) => { + if (e.deltaY !== 0 && !isEditMode) { + e.preventDefault(); + + const { x, y, zoom } = getViewport(); + const nextX = clampViewportX(x - e.deltaY); + setViewport({ x: nextX, y, zoom }, { duration: 0 }); + } + }; + + container.addEventListener("wheel", onWheel, { passive: false }); + + return () => { + container.removeEventListener("wheel", onWheel); + }; + }, [getViewport, setViewport, isEditMode, clampViewportX]); + return (
@@ -383,7 +420,7 @@ export default function Workflow({
-
+
{ + const clampedX = clampViewportX(viewport.x); + if (clampedX !== viewport.x) { + setViewport({ ...viewport, x: clampedX }); + return; + } if (isEditMode) { setLastViewport(viewport); }