diff --git a/frontend/package.json b/frontend/package.json index c2fc9e8e..378c0a9f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -52,6 +52,7 @@ "@react-form-builder/designer-bundle": "^7.4.0", "@reduxjs/toolkit": "^2.0.1", "@tanstack/react-virtual": "^3.13.12", + "@tisoap/react-flow-smart-edge": "^4.0.1", "@types/recharts": "^1.8.29", "@types/uuid": "^10.0.0", "@xyflow/react": "^12.8.6", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index e2070d9b..b6f41752 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -131,6 +131,9 @@ importers: '@tanstack/react-virtual': specifier: ^3.13.12 version: 3.13.12(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tisoap/react-flow-smart-edge': + specifier: ^4.0.1 + version: 4.0.1(@xyflow/react@12.9.0(@types/react@18.3.18)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2) '@types/recharts': specifier: ^1.8.29 version: 1.8.29 @@ -2045,6 +2048,14 @@ packages: '@tanstack/virtual-core@3.13.12': resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==} + '@tisoap/react-flow-smart-edge@4.0.1': + resolution: {integrity: sha512-tqyQyaQFDc4QIL3Kw9UL9QVWId4cSuVNqKsDRzdPH1Mf8YMrwB8/dq/BMFEbuGaln9B3wlgeyhQx3vn6E9lpJA==} + peerDependencies: + '@xyflow/react': '>=12' + react: '>=18' + react-dom: '>=18' + typescript: '>=5' + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -6409,6 +6420,13 @@ snapshots: '@tanstack/virtual-core@3.13.12': {} + '@tisoap/react-flow-smart-edge@4.0.1(@xyflow/react@12.9.0(@types/react@18.3.18)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.7.2)': + dependencies: + '@xyflow/react': 12.9.0(@types/react@18.3.18)(immer@10.1.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + typescript: 5.7.2 + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.26.3 diff --git a/frontend/src/pages/Workflow/Design/components/FlowCanvas.tsx b/frontend/src/pages/Workflow/Design/components/FlowCanvas.tsx index 02320513..ad68e336 100644 --- a/frontend/src/pages/Workflow/Design/components/FlowCanvas.tsx +++ b/frontend/src/pages/Workflow/Design/components/FlowCanvas.tsx @@ -17,7 +17,7 @@ import '@xyflow/react/dist/style.css'; import type { FlowNode, FlowEdge } from '../types'; import { nodeTypes } from '../nodes'; -import CustomEdge from './CustomEdge'; +import SmartEdge from './SmartEdge'; import { generateEdgeId } from '../utils/idGenerator'; interface FlowCanvasProps { @@ -48,109 +48,6 @@ const FlowCanvas: React.FC = ({ const [nodes, , onNodesStateChange] = useNodesState(initialNodes); const [edges, setEdges, onEdgesStateChange] = useEdgesState(initialEdges); - // --- Auto adjust edge vertices when nodes move (preserve shape, avoid excessive length) --- - useEffect(() => { - // helper: get node position by id - const getNodePos = (id: string) => { - const n = nodes.find((nn) => nn.id === id); - return n?.position; - }; - - // geometry helpers - const recomputeVertices = ( - oldVerts: Array<{ x: number; y: number }> | undefined, - oldA: { x: number; y: number } | undefined, - oldB: { x: number; y: number } | undefined, - newA: { x: number; y: number }, - newB: { x: number; y: number } - ) => { - // default three vertices placed along the baseline - const defaultThree = () => [ - { x: newA.x + (newB.x - newA.x) * 0.25, y: newA.y + (newB.y - newA.y) * 0.25 }, - { x: (newA.x + newB.x) / 2, y: (newA.y + newB.y) / 2 }, - { x: newA.x + (newB.x - newA.x) * 0.75, y: newA.y + (newB.y - newA.y) * 0.75 }, - ]; - - // if no old endpoints or old vertices, return default - if (!oldA || !oldB || !oldVerts || oldVerts.length !== 3) { - return defaultThree(); - } - - const projOnLine = (p: { x: number; y: number }, a: { x: number; y: number }, b: { x: number; y: number }) => { - const dx = b.x - a.x, dy = b.y - a.y; - const len2 = dx * dx + dy * dy || 1; - const t = Math.max(0, Math.min(1, ((p.x - a.x) * dx + (p.y - a.y) * dy) / len2)); - const px = a.x + t * dx, py = a.y + t * dy; - const offx = p.x - px, offy = p.y - py; - // sign: which side of baseline (using cross product sign) - const cross = dx * (p.y - a.y) - dy * (p.x - a.x); - const sign = cross >= 0 ? 1 : -1; - const offMag = Math.hypot(offx, offy); - return { t, offMag, sign }; - }; - - const mapToNew = (t: number, offMag: number, sign: number, a: { x: number; y: number }, b: { x: number; y: number }) => { - const dx = b.x - a.x, dy = b.y - a.y; - const len = Math.hypot(dx, dy) || 1; - // unit normal for new baseline - const nx = -dy / len, ny = dx / len; - // clamp offset to avoid huge bulges (auto shrink if nodes far apart) - const maxOffset = Math.max(20, 0.35 * len); - const useOff = Math.min(offMag, maxOffset); - const baseX = a.x + t * dx, baseY = a.y + t * dy; - return { x: baseX + sign * useOff * nx, y: baseY + sign * useOff * ny }; - }; - - // Compute mapping (t, offset) from old baseline, then apply to new baseline - const tOff = oldVerts.map((v) => projOnLine(v, oldA, oldB)); - // enforce monotonic t order to keep vertex order stable - tOff.sort((a, b) => a.t - b.t); - const mapped = tOff.map((m) => mapToNew(m.t, m.offMag, m.sign, newA, newB)); - // ensure exactly 3 vertices - if (mapped.length === 3) return mapped as Array<{ x: number; y: number }>; - // fallback - return defaultThree(); - }; - - setEdges((eds) => { - return eds.map((ed) => { - const s = getNodePos(ed.source); - const t = getNodePos(ed.target); - if (!s || !t) return ed; - - const dataAny = (ed as any).data || {}; - const last = dataAny._lastEndpoints as { sx: number; sy: number; tx: number; ty: number } | undefined; - const current = { sx: s.x, sy: s.y, tx: t.x, ty: t.y }; - - // Initialize vertices if missing - const curVerts = (dataAny.vertices as any) as Array<{ x: number; y: number }> | undefined; - let vertices = curVerts; - let changed = false; - - if (!Array.isArray(vertices) || vertices.length !== 3) { - vertices = recomputeVertices(undefined, undefined, undefined, { x: current.sx, y: current.sy }, { x: current.tx, y: current.ty }); - changed = true; - } - - // Recompute when endpoints moved - if (!last || last.sx !== current.sx || last.sy !== current.sy || last.tx !== current.tx || last.ty !== current.ty) { - vertices = recomputeVertices(vertices, last ? { x: last.sx, y: last.sy } : undefined, last ? { x: last.tx, y: last.ty } : undefined, { x: current.sx, y: current.sy }, { x: current.tx, y: current.ty }); - changed = true; - } - - if (!changed) return ed; - return { - ...ed, - data: { - ...ed.data, - vertices, - _lastEndpoints: current, - }, - } as any; - }); - }); - }, [nodes, setEdges]); - // 处理边重连 const onReconnect = useCallback((oldEdge: Edge, newConnection: Connection) => { setEdges((els) => reconnectEdge(oldEdge, newConnection, els)); @@ -164,7 +61,7 @@ const FlowCanvas: React.FC = ({ source: params.source!, target: params.target!, type: 'smoothstep', - animated: true, + animated: false, style: { stroke: '#94a3b8', strokeWidth: 2, @@ -282,7 +179,7 @@ const FlowCanvas: React.FC = ({ onDragOver={handleDragOver} onMove={onViewportChange} nodeTypes={nodeTypes} - edgeTypes={{ smoothstep: CustomEdge }} + edgeTypes={{ smoothstep: SmartEdge }} isValidConnection={isValidConnection} defaultEdgeOptions={{ type: 'smoothstep',