import React, { useState, useMemo, useCallback } from 'react'; import { BaseEdge, EdgeLabelRenderer, EdgeProps, getSmoothStepPath, useReactFlow } from '@xyflow/react'; import type { FlowNode, ControlPoint } from '../types'; import { convertToDisplayName } from '@/utils/workflow/variableConversion'; /** * 自定义边组件 * 支持hover高亮、样式优化和拖动改变路径 */ const CustomEdge: React.FC = ({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, style = {}, markerEnd, label, selected, data, }) => { const [isHovered, setIsHovered] = useState(false); const [isDragging, setIsDragging] = useState(false); const { getNodes, setEdges, screenToFlowPosition } = useReactFlow(); // 从 data 中获取自定义的控制点 const customControlPoint = data?.controlPoint as ControlPoint | undefined; // 将 label 中的 UUID 转换为节点名 const displayLabel = useMemo(() => { if (!label || typeof label !== 'string') { return label; } const allNodes = getNodes() as FlowNode[]; return convertToDisplayName(label, allNodes); }, [label, getNodes]); // 计算三个拐点(vertices):优先用数据中的 vertices;否则从旧的 controlPoint 推导;最后用默认1/4、1/2、3/4 const vertices: ControlPoint[] = useMemo(() => { const dataVertices = (data as any)?.vertices as ControlPoint[] | undefined; if (Array.isArray(dataVertices) && dataVertices.length === 3) return dataVertices; const cp = customControlPoint; if (cp) { return [ { x: (sourceX + cp.x) / 2, y: (sourceY + cp.y) / 2 }, { x: cp.x, y: cp.y }, { x: (cp.x + targetX) / 2, y: (cp.y + targetY) / 2 }, ]; } return [ { x: sourceX + (targetX - sourceX) * 0.25, y: sourceY + (targetY - sourceY) * 0.25 }, { x: (sourceX + targetX) / 2, y: (sourceY + targetY) / 2 }, { x: sourceX + (targetX - sourceX) * 0.75, y: sourceY + (targetY - sourceY) * 0.75 }, ]; }, [data, customControlPoint, sourceX, sourceY, targetX, targetY]); // 若数据中缺少 vertices,则在首次渲染时写回,确保后续保存能持久化 React.useEffect(() => { const hasDataVertices = Array.isArray((data as any)?.vertices) && (data as any).vertices.length === 3; if (!hasDataVertices) { setEdges((eds) => eds.map(ed => ed.id === id ? { ...ed, data: { ...ed.data, vertices } } : ed)); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // 生成路径:使用 Catmull-Rom -> Cubic Bezier 插值,曲线严格经过拐点 const [edgePath, labelX, labelY] = useMemo(() => { // 拼接点序列:起点、3个拐点、终点 const pts = [ { x: sourceX, y: sourceY }, vertices[0], vertices[1], vertices[2], { x: targetX, y: targetY }, ]; // Catmull-Rom 转三次贝塞尔:C1 = P0 + (P1 - P_{-1})/6, C2 = P1 - (P2 - P0)/6 const segs: string[] = []; for (let i = 0; i < pts.length - 1; i++) { const p_1 = i === 0 ? pts[i] : pts[i - 1]; const p0 = pts[i]; const p1 = pts[i + 1]; const p2 = i + 2 < pts.length ? pts[i + 2] : p1; const c1x = p0.x + (p1.x - p_1.x) / 6; const c1y = p0.y + (p1.y - p_1.y) / 6; const c2x = p1.x - (p2.x - p0.x) / 6; const c2y = p1.y - (p2.y - p0.y) / 6; segs.push(`C ${c1x},${c1y} ${c2x},${c2y} ${p1.x},${p1.y}`); } const path = `M ${pts[0].x},${pts[0].y} ${segs.join(' ')}`; // 标签显示在第二个拐点 return [path, vertices[1].x, vertices[1].y] as [string, number, number]; }, [vertices, sourceX, sourceY, targetX, targetY]); // 拖动处理(支持第 idx 个拐点) const handleVertexMouseDown = useCallback((idx: number, e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); setIsDragging(true); const handleMove = (moveEvent: MouseEvent) => { const flowPosition = screenToFlowPosition({ x: moveEvent.clientX, y: moveEvent.clientY }); setEdges((eds) => eds.map(ed => { if (ed.id !== id) return ed; const old = (ed as any).data?.vertices as ControlPoint[] | undefined; const base = Array.isArray(old) && old.length === 3 ? [...old] : vertices.slice(); base[idx] = { x: flowPosition.x, y: flowPosition.y }; return { ...ed, data: { ...ed.data, vertices: base } } as any; })); }; const handleUp = () => { setIsDragging(false); window.removeEventListener('mousemove', handleMove); window.removeEventListener('mouseup', handleUp); }; window.addEventListener('mousemove', handleMove); window.addEventListener('mouseup', handleUp); }, [id, setEdges, screenToFlowPosition, vertices]); // 根据状态确定样式 const edgeStyle = { ...style, stroke: selected ? '#3b82f6' : isHovered ? '#64748b' : '#94a3b8', strokeWidth: selected ? 3 : isHovered ? 2.5 : 2, transition: 'all 0.2s ease', }; // ✅ 修复:确保 markerEnd 是对象后再 spread const markerEndStyle = (() => { if (!markerEnd || typeof markerEnd !== 'object') return markerEnd; const markerObj = markerEnd as Record; if (selected) return { ...markerObj, color: '#3b82f6' }; if (isHovered) return { ...markerObj, color: '#64748b' }; return markerEnd; })(); return ( <> {/* 增加点击区域 */} setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} /> {/* 控制点(三个拐点,直接在SVG中渲染,可拖动) */} {(selected || isHovered) && ( {vertices.map((pt, idx) => ( handleVertexMouseDown(idx, e)} /> handleVertexMouseDown(idx, e)} /> ))} )} {/* 边标签 */} {displayLabel && (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > {displayLabel}
)} ); }; export default CustomEdge;