From f9633e23b1b4a0080d5f1e6e9387c0f018b8379b Mon Sep 17 00:00:00 2001 From: dengqichen Date: Fri, 14 Nov 2025 18:27:42 +0800 Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84=E6=B6=88=E6=81=AF=E9=80=9A?= =?UTF-8?q?=E7=9F=A5=E5=BC=B9=E7=AA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Workflow/Design/components/CustomEdge.tsx | 240 ++++-------------- .../Workflow/Design/components/FlowCanvas.tsx | 4 +- .../Workflow/Design/components/SmartEdge.tsx | 88 ------- 3 files changed, 45 insertions(+), 287 deletions(-) delete mode 100644 frontend/src/pages/Workflow/Design/components/SmartEdge.tsx diff --git a/frontend/src/pages/Workflow/Design/components/CustomEdge.tsx b/frontend/src/pages/Workflow/Design/components/CustomEdge.tsx index cfd003c4..6f546a1c 100644 --- a/frontend/src/pages/Workflow/Design/components/CustomEdge.tsx +++ b/frontend/src/pages/Workflow/Design/components/CustomEdge.tsx @@ -1,29 +1,24 @@ -import React, { useState, useMemo, useCallback } from 'react'; -import { BaseEdge, EdgeLabelRenderer, EdgeProps, getBezierPath, useReactFlow } from '@xyflow/react'; -import type { FlowNode, ControlPoint } from '../types'; +import React, { useState, useMemo } from 'react'; +import { useReactFlow } from '@xyflow/react'; +import { SmartStepEdge as BaseSmartEdge } from '@tisoap/react-flow-smart-edge'; +import type { FlowNode } from '../types'; import { convertToDisplayName } from '@/utils/workflow/variableConversion'; /** - * 自定义边组件 - * 使用简单贝塞尔曲线 + 单控制点,支持 hover 高亮和拖动调整路径 + * 自定义边组件 - 智能避障 + * 使用 SmartStepEdge 实现智能路由避障,采用直角路径 */ -const CustomEdge: React.FC = ({ - id, - sourceX, - sourceY, - targetX, - targetY, - sourcePosition, - targetPosition, - style = {}, - markerEnd, - label, - selected, - data, -}) => { +const CustomEdge: React.FC = (props) => { + const { + style = {}, + markerEnd, + label, + selected, + ...restProps + } = props; + const [isHovered, setIsHovered] = useState(false); - const [isDragging, setIsDragging] = useState(false); - const { getNodes, setEdges, screenToFlowPosition } = useReactFlow(); + const { getNodes } = useReactFlow(); // 将 label 中的 UUID 转换为节点名 const displayLabel = useMemo(() => { @@ -35,111 +30,6 @@ const CustomEdge: React.FC = ({ return convertToDisplayName(label, allNodes); }, [label, getNodes]); - // 计算单个控制点:优先使用保存的 controlPoint,否则使用智能默认位置 - const controlPoint: ControlPoint = useMemo(() => { - const savedCP = (data as any)?.controlPoint as ControlPoint | undefined; - if (savedCP) return savedCP; - - // 智能默认位置:根据源和目标位置关系计算 - const dx = targetX - sourceX; - const dy = targetY - sourceY; - const distance = Math.sqrt(dx * dx + dy * dy); - - // 控制点在中点位置,垂直偏移距离的 20%,方向取决于节点位置关系 - const midX = (sourceX + targetX) / 2; - const midY = (sourceY + targetY) / 2; - - // 计算垂直方向的偏移(顺时针 90 度) - const offsetRatio = 0.2; - const offsetX = -dy / distance * distance * offsetRatio; - const offsetY = dx / distance * distance * offsetRatio; - - return { - x: midX + offsetX, - y: midY + offsetY, - }; - }, [data, sourceX, sourceY, targetX, targetY]); - - // 若数据中缺少 controlPoint,则在首次渲染时写回 - React.useEffect(() => { - if (!(data as any)?.controlPoint) { - setEdges((eds) => eds.map(ed => - ed.id === id ? { ...ed, data: { ...ed.data, controlPoint } } : ed - )); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - // 使用 Catmull-Rom 样条曲线,确保曲线严格经过控制点且平滑 - const [edgePath, labelX, labelY] = useMemo(() => { - // Catmull-Rom 样条曲线会经过所有关键点 - // 转换为三次贝塞尔曲线:需要计算切线 - - // 点序列:起点 -> 控制点 -> 终点 - const p0 = { x: sourceX, y: sourceY }; - const p1 = controlPoint; - const p2 = { x: targetX, y: targetY }; - - // 为了使用 Catmull-Rom,需要虚拟的前后点(用于计算切线) - // 虚拟前点:起点的镜像延伸 - const p_1 = { - x: p0.x - (p1.x - p0.x) * 0.3, - y: p0.y - (p1.y - p0.y) * 0.3, - }; - - // 虚拟后点:终点的镜像延伸 - const p3 = { - x: p2.x + (p2.x - p1.x) * 0.3, - y: p2.y + (p2.y - p1.y) * 0.3, - }; - - // Catmull-Rom 转贝塞尔:第一段(p0 -> p1) - // 切线: t0 = (p1 - p_1) / 2, t1 = (p2 - p0) / 2 - 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; - - // Catmull-Rom 转贝塞尔:第二段(p1 -> p2) - // 切线: t0 = (p2 - p0) / 2, t1 = (p3 - p1) / 2 - const c3x = p1.x + (p2.x - p0.x) / 6; - const c3y = p1.y + (p2.y - p0.y) / 6; - const c4x = p2.x - (p3.x - p1.x) / 6; - const c4y = p2.y - (p3.y - p1.y) / 6; - - // 拼接两段三次贝塞尔曲线 - const path = `M ${p0.x},${p0.y} C ${c1x},${c1y} ${c2x},${c2y} ${p1.x},${p1.y} C ${c3x},${c3y} ${c4x},${c4y} ${p2.x},${p2.y}`; - - // 标签显示在控制点位置 - return [path, controlPoint.x, controlPoint.y] as [string, number, number]; - }, [controlPoint, sourceX, sourceY, targetX, targetY]); - - // 拖动控制点处理 - const handleControlPointMouseDown = useCallback((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 => - ed.id === id - ? { ...ed, data: { ...ed.data, controlPoint: { x: flowPosition.x, y: flowPosition.y } } } - : ed - )); - }; - - const handleUp = () => { - setIsDragging(false); - window.removeEventListener('mousemove', handleMove); - window.removeEventListener('mouseup', handleUp); - }; - - window.addEventListener('mousemove', handleMove); - window.addEventListener('mouseup', handleUp); - }, [id, setEdges, screenToFlowPosition]); - - // 根据状态确定样式 const edgeStyle = { ...style, @@ -156,81 +46,37 @@ const CustomEdge: React.FC = ({ return markerEnd; })(); + // 自定义标签样式 + const labelStyle = { + fill: selected ? '#3b82f6' : '#64748b', + fontWeight: 500, + fontSize: 12, + }; + + const labelBgStyle = { + fill: 'white', + fillOpacity: 1, + }; + + const labelBgPadding = [4, 8] as [number, number]; + const labelBgBorderRadius = 6; + return ( - <> - setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + - - {/* 增加点击区域 */} - setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - /> - - {/* 单个可拖动控制点 */} - {(selected || isHovered) && ( - - - - - )} - - {/* 边标签 */} - {displayLabel && ( - -
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - {displayLabel} -
-
- )} - + ); }; diff --git a/frontend/src/pages/Workflow/Design/components/FlowCanvas.tsx b/frontend/src/pages/Workflow/Design/components/FlowCanvas.tsx index ad68e336..deab4121 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 SmartEdge from './SmartEdge'; +import CustomEdge from './CustomEdge'; import { generateEdgeId } from '../utils/idGenerator'; interface FlowCanvasProps { @@ -179,7 +179,7 @@ const FlowCanvas: React.FC = ({ onDragOver={handleDragOver} onMove={onViewportChange} nodeTypes={nodeTypes} - edgeTypes={{ smoothstep: SmartEdge }} + edgeTypes={{ smoothstep: CustomEdge }} isValidConnection={isValidConnection} defaultEdgeOptions={{ type: 'smoothstep', diff --git a/frontend/src/pages/Workflow/Design/components/SmartEdge.tsx b/frontend/src/pages/Workflow/Design/components/SmartEdge.tsx deleted file mode 100644 index ee972bc0..00000000 --- a/frontend/src/pages/Workflow/Design/components/SmartEdge.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import { EdgeLabelRenderer, EdgeProps, useReactFlow } from '@xyflow/react'; -import { SmartStepEdge as BaseSmartEdge } from '@tisoap/react-flow-smart-edge'; -import type { FlowNode } from '../types'; -import { convertToDisplayName } from '@/utils/workflow/variableConversion'; - -/** - * 智能边组件 - 自动避开节点(基于 SmartStepEdge) - * 使用 react-flow-smart-edge 实现智能路由避障,采用直角路径 - */ -const SmartEdge: React.FC = (props) => { - const { - id, - label, - selected, - style = {}, - markerEnd, - } = props; - - const [isHovered, setIsHovered] = useState(false); - const { getNodes } = useReactFlow(); - - // 将 label 中的 UUID 转换为节点名 - const displayLabel = useMemo(() => { - if (!label || typeof label !== 'string') { - return label; - } - - const allNodes = getNodes() as FlowNode[]; - return convertToDisplayName(label, allNodes); - }, [label, getNodes]); - - // 增强样式 - const enhancedStyle = { - ...style, - stroke: selected ? '#3b82f6' : isHovered ? '#64748b' : '#94a3b8', - strokeWidth: selected ? 3 : isHovered ? 2.5 : 2, - transition: 'all 0.2s ease', - }; - - const enhancedMarkerEnd = (() => { - 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 ( - <> - {/* 使用 SmartStepEdge 作为基础(直角路径) */} - - - {/* 自定义标签(如果有) */} - {displayLabel && ( - -
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} - > - {displayLabel} -
-
- )} - - ); -}; - -export default SmartEdge;