import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { buildGraphLayout } from './edit/variableUtils'; import './StudioRunNodeGraph.css'; const NODE_STATUS_LABEL = { pending: '待执行', active: '进行中', completed: '已完成', skipped: '已跳过', }; function defaultEdgePath(from, to, nodeW, nodeH) { const x1 = from.x + nodeW / 2; const y1 = from.y + nodeH; const x2 = to.x + nodeW / 2; const y2 = to.y; const dy = Math.max(24, (y2 - y1) * 0.45); return `M ${x1} ${y1} C ${x1} ${y1 + dy}, ${x2} ${y2 - dy}, ${x2} ${y2}`; } function StudioRunNodeGraph({ pipeline, nodeStates, currentNodeId, variant = 'sidebar', }) { const containerRef = useRef(null); const [view, setView] = useState({ x: 0, y: 0, scale: 1 }); const [dragging, setDragging] = useState(null); const [selectedId, setSelectedId] = useState(currentNodeId); const [nodeOverrides, setNodeOverrides] = useState({}); const isCenter = variant === 'center'; const layout = useMemo( () => buildGraphLayout(pipeline, nodeStates, { vertical: true }), [pipeline, nodeStates] ); const { nodeW, nodeH } = layout; useEffect(() => { setSelectedId(currentNodeId); }, [currentNodeId]); useEffect(() => { const el = containerRef.current; if (!el || !layout.width) return; const cw = el.clientWidth || 200; const ch = el.clientHeight || 160; const scale = Math.min(1, (cw - 16) / layout.width, (ch - 16) / layout.height); setView({ x: (cw - layout.width * scale) / 2, y: Math.max(8, (ch - layout.height * scale) / 2), scale: Math.max(0.45, scale), }); }, [layout.width, layout.height, pipeline, variant]); const nodeMap = useMemo( () => Object.fromEntries(layout.nodes.map((n) => [n.id, n])), [layout.nodes] ); const handleWheel = useCallback((e) => { e.preventDefault(); const delta = e.deltaY > 0 ? 0.92 : 1.08; setView((v) => ({ ...v, scale: Math.min(2.5, Math.max(0.35, v.scale * delta)), })); }, []); const handleBgMouseDown = useCallback( (e) => { if (e.button !== 0) return; e.preventDefault(); setDragging({ type: 'pan', startX: e.clientX, startY: e.clientY, origin: { ...view } }); }, [view] ); const handleNodeMouseDown = useCallback( (e, nodeId) => { if (e.button !== 0) return; e.stopPropagation(); setSelectedId(nodeId); const node = nodeMap[nodeId]; if (!node) return; setDragging({ type: 'node', nodeId, startX: e.clientX, startY: e.clientY, origin: { x: node.x, y: node.y }, }); }, [nodeMap] ); useEffect(() => { if (!dragging) return undefined; const onMove = (e) => { if (dragging.type === 'pan') { setView((v) => ({ ...v, x: dragging.origin.x + (e.clientX - dragging.startX), y: dragging.origin.y + (e.clientY - dragging.startY), })); return; } if (dragging.type === 'node') { const dx = (e.clientX - dragging.startX) / view.scale; const dy = (e.clientY - dragging.startY) / view.scale; setNodeOverrides((prev) => ({ ...prev, [dragging.nodeId]: { x: dragging.origin.x + dx, y: dragging.origin.y + dy, }, })); } }; const onUp = () => setDragging(null); window.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp); return () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); }; }, [dragging, view.scale]); useEffect(() => { setNodeOverrides({}); }, [pipeline, nodeStates]); const positionedNodes = layout.nodes.map((n) => ({ ...n, ...(nodeOverrides[n.id] || {}), })); const posMap = Object.fromEntries(positionedNodes.map((n) => [n.id, n])); const pathFn = layout.edgePath || defaultEdgePath; if (!layout.nodes.length) { return