如何在ReactFlow中实现类似Draw.io的拖拽调整连线形状功能?
在ReactFlow中实现类似Draw.io的可拆分/合并线条功能
完全可以在ReactFlow中实现类似Draw.io的线条拆分与合并功能。ReactFlow的自定义Edge机制提供了足够的灵活性,通过扩展Edge组件、管理控制点状态即可实现需求。
你提供的代码仅实现了基础的自定义Edge和中间控制点渲染,但缺少核心的拖拽拆分、新增控制点以及合并逻辑,需要进一步扩展。
核心实现思路
- 数据结构扩展:为每条Edge添加
controlPoints字段,存储拆分后的控制点坐标,以此记录线条的分段结构。 - 自定义Edge渲染:根据
controlPoints渲染多段连续线条,每段线条的中点渲染可拖拽控制点。 - 拖拽交互处理:监听控制点的拖拽事件,实时更新坐标;当拖拽控制点回到原线段中点时,自动合并线段并移除该控制点。
- 状态管理:通过React状态维护
elements数组,实时更新Edge的配置。
完整实现代码
import React, { useState, useCallback } from 'react'; import { ReactFlowProvider, Flow, Controls, getBezierPath } from 'react-flow-renderer'; // 自定义控制点组件 const ControlPoint = ({ position, onDragStart, onDrag, onDragEnd, isMergable }) => { return ( <div style={{ position: 'absolute', width: 12, height: 12, borderRadius: '50%', backgroundColor: isMergable ? '#888' : '#ff4081', cursor: 'move', transform: `translate(${position.x - 6}px, ${position.y - 6}px)`, zIndex: 10, }} onMouseDown={onDragStart} /> ); }; // 自定义可拆分Edge组件 const SplittableEdge = ({ id, sourceX, sourceY, targetX, targetY, data, onEdgeUpdate }) => { const [draggingPoint, setDraggingPoint] = useState(null); const controlPoints = data.controlPoints || []; // 构建完整的点序列:起点 → 所有控制点 → 终点 const points = [{ x: sourceX, y: sourceY }, ...controlPoints, { x: targetX, y: targetY }]; // 处理拖拽开始 const handleDragStart = useCallback((index, e) => { e.stopPropagation(); setDraggingPoint(index); }, []); // 处理拖拽中实时更新控制点坐标 const handleDrag = useCallback( (e) => { if (draggingPoint === null) return; const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; const newControlPoints = [...controlPoints]; newControlPoints[draggingPoint] = { x, y }; onEdgeUpdate(id, { controlPoints: newControlPoints }); }, [draggingPoint, controlPoints, id, onEdgeUpdate] ); // 处理拖拽结束,判断是否需要合并线段 const handleDragEnd = useCallback(() => { if (draggingPoint === null) return; const point = controlPoints[draggingPoint]; const prevPoint = points[draggingPoint]; const nextPoint = points[draggingPoint + 2]; // 计算原线段中点,判断当前控制点是否接近中点 const midX = (prevPoint.x + nextPoint.x) / 2; const midY = (prevPoint.y + nextPoint.y) / 2; const distance = Math.sqrt((point.x - midX) ** 2 + (point.y - midY) ** 2); // 距离小于8px时合并线段,移除该控制点 if (distance < 8) { const newControlPoints = controlPoints.filter((_, idx) => idx !== draggingPoint); onEdgeUpdate(id, { controlPoints: newControlPoints }); } setDraggingPoint(null); }, [draggingPoint, controlPoints, points, id, onEdgeUpdate]); // 渲染所有线段及可拆分中点 const renderSegments = () => { return points.map((point, idx) => { if (idx === points.length - 1) return null; const nextPoint = points[idx + 1]; const midPoint = { x: (point.x + nextPoint.x) / 2, y: (point.y + nextPoint.y) / 2, }; // 判断当前线段是否已被拆分,避免重复添加控制点 const isSegmentSplit = controlPoints.some( (cp) => Math.abs(cp.x - midPoint.x) < 5 && Math.abs(cp.y - midPoint.y) < 5 ); return ( <g key={`segment-${id}-${idx}`}> {/* 渲染线段 */} <line x1={point.x} y1={point.y} x2={nextPoint.x} y2={nextPoint.y} stroke="#555" strokeWidth={2} fill="none" /> {/* 渲染可拆分的中点控制点(仅未拆分线段显示) */} {!isSegmentSplit && ( <ControlPoint position={midPoint} onDragStart={() => { const newControlPoints = [...controlPoints, midPoint]; onEdgeUpdate(id, { controlPoints: newControlPoints }); }} onDrag={() => {}} onDragEnd={() => {}} isMergable={false} /> )} </g> ); }); }; // 渲染已拆分的可拖拽控制点 const renderControlPoints = () => { return controlPoints.map((point, idx) => ( <ControlPoint key={`cp-${id}-${idx}`} position={point} onDragStart={(e) => handleDragStart(idx, e)} onDrag={handleDrag} onDragEnd={handleDragEnd} isMergable={true} /> )); }; return ( <g> {renderSegments()} {renderControlPoints()} {/* 渲染箭头 */} <marker id={`arrowhead-${id}`} markerWidth="10" markerHeight="10" refX="9" refY="3" orient="auto" > <polygon points="0 0, 10 3, 0 6" fill="#555" /> </marker> <line x1={targetX} y1={targetY} x2={points[points.length - 2].x} y2={points[points.length - 2].y} stroke="#555" strokeWidth={2} markerEnd={`url(#arrowhead-${id})`} /> </g> ); }; const FlowComponent = () => { const [elements, setElements] = useState([ { id: '1', type: 'input', data: { label: '节点1' }, position: { x: 100, y: 100 } }, { id: '2', type: 'output', data: { label: '节点2' }, position: { x: 400, y: 200 } }, { id: 'e1-2', source: '1', target: '2', type: 'splittableEdge', data: { controlPoints: [] }, }, ]); // 更新Edge数据的回调 const onEdgeUpdate = useCallback((edgeId, newData) => { setElements((prev) => prev.map((el) => { if (el.id === edgeId) { return { ...el, data: { ...el.data, ...newData } }; } return el; }) ); }, []); const edgeTypes = { splittableEdge: SplittableEdge, }; return ( <ReactFlowProvider> <div style={{ height: '600px', border: '1px solid #eee' }}> <Flow elements={elements} edgeTypes={edgeTypes} onEdgeUpdate={onEdgeUpdate} fitView /> <Controls /> </div> </ReactFlowProvider> ); }; export default FlowComponent;
功能说明
- 线条拆分:点击线段中点的粉色控制点,会自动在该位置添加可拖拽的灰色控制点,将原线段拆分为两段,同时每段新线段的中点会生成新的粉色拆分控制点。
- 线条合并:拖拽灰色控制点回到原线段中点附近(距离阈值8px),该控制点会自动消失,两段线段合并为一条。
- 流畅交互:拖拽过程中实时更新线条路径,合并逻辑自动触发,保持操作的流畅性。
内容的提问来源于stack exchange,提问作者Alok Abhijeet
相关产品推荐
相关产品推荐

