You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 04:54:51