React Flow 边事件处理实战:鼠标事件、键盘操作及连接规则设置(附完整代码)

发布于:2025-05-19 ⋅ 阅读:(18) ⋅ 点赞:(0)

本文为《React Agent:从零开始构建 AI 智能体》专栏系列文章。 专栏地址:https://blog.csdn.net/suiyingy/category_12933485.html。项目地址:https://gitee.com/fgai/react-agent(含完整代码示​例与实战源)。完整介绍:https://blog.csdn.net/suiyingy/article/details/146983582。

        边可以响应多种用户操作,如点击、双击、鼠标悬停等。通过绑定相应的事件处理函数,实现边的交互功能。点击边弹出其详细信息窗口;鼠标悬停时显示工具提示,说明边所代表的关系含义。

1 鼠标悬停

        下面程序为鼠标悬停事件示例,显示边的信息、改变线条颜色、宽度和线型。

import React, { useCallback, useState } from 'react';
import {
  ReactFlow,
  useNodesState,
  useEdgesState,
  addEdge,
} from 'reactflow';
import 'reactflow/dist/style.css';

const initialNodes = [
  { id: '1', position: { x: 0, y: 0 }, data: { label: '1' } },
  { id: '2', position: { x: 0, y: 100 }, data: { label: '2' } },
];

const initialEdges = [
  { 
    id: 'e1-2', 
    source: '1', 
    target: '2',
    type: 'default' 
  }
];

export default function FlowComponent() {
  const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
  const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
  const [hoveredEdgeId, setHoveredEdgeId] = useState(null);

  const onConnect = useCallback(
    (params) => setEdges((eds) => addEdge(params, eds)),
    [setEdges]
  );

  // 动态获取边样式
  const getEdgeStyle = (edge) => {
    return hoveredEdgeId === edge.id 
      ? {
          stroke: 'red',
          strokeWidth: 3,
          strokeDasharray: '5 5',
        }
      : {};
  };

  return (
    <div style={{ height: '500px', width: '100%' }}>
      <ReactFlow
        nodes={nodes}
        edges={edges.map(edge => ({
          ...edge,
          style: getEdgeStyle(edge)
        }))}
        onNodesChange={onNodesChange}
        onEdgesChange={onEdgesChange}
        onConnect={onConnect}
        onEdgeMouseEnter={(event, edge) => setHoveredEdgeId(edge.id)}
        onEdgeMouseLeave={() => setHoveredEdgeId(null)}
        fitView
      />
    </div>
  );
}

        运行程序后结果如下图所示。

图1 边 - 鼠标悬停

2 鼠标单击

        下面程序为鼠标单击事件示例,显示边的信息。

export default function App() {
  const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
  const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);

  const onConnect = useCallback(
    (params) => setEdges((eds) => addEdge(params, eds)),
    [setEdges],
  );

  const onEdgeClick = useCallback((event, edge) => {
    console.log('Clicked edge:', edge);
    // 这里可以添加更多逻辑,比如显示模态框等
  }, []);

  return (
    <div style={{ height: '500px' }}>
      <ReactFlow
        nodes={nodes}
        edges={edges}
        onNodesChange={onNodesChange}
        onEdgesChange={onEdgesChange}
        onConnect={onConnect}
        onEdgeClick={onEdgeClick}
        fitView
      />
    </div>
  );
}

        运行程序后结果如下图所示。

图2 边 - 鼠标单击

3 鼠标双击

        下面程序为鼠标双击事件示例。

export default function App() {
  const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
  const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);

  const onConnect = useCallback(
    (params) => setEdges((eds) => addEdge(params, eds)),
    [setEdges],
  );

  const handleEdgeDoubleClick = (event, edge) => {
    alert(`双击了边:${edge.id}`);
    // 这里可以添加更多自定义逻辑,比如:
    // - 删除边
    // - 编辑边属性
    // - 高亮关联节点等
  };

  return (
    <div style={{ height: '500px' }}>
      <ReactFlow
        nodes={nodes}
        edges={edges}
        onNodesChange={onNodesChange}
        onEdgesChange={onEdgesChange}
        onConnect={onConnect}
        onEdgeDoubleClick={handleEdgeDoubleClick}
        fitView
      />
    </div>
  );
}

4 键盘事件

        同样地,边也支持键盘事件,示例如下:

export default function App() {
  const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
  const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);

  // 处理连接线
  const onConnect = useCallback(
    (params) => setEdges((eds) => addEdge(params, eds)),
    [setEdges],
  );

  // 键盘事件处理
  const handleKeyDown = useCallback((event) => {
    if (event.key === 'Delete') {
      alert('键盘Delete键被按下')
    }
  }, [setNodes, setEdges]);

  return (
    <div 
      style={{ height: '500px', outline: 'none' }} 
      tabIndex={0} 
      onKeyDown={handleKeyDown}
    >
      <ReactFlow
        nodes={nodes}
        edges={edges}
        onNodesChange={onNodesChange}
        onEdgesChange={onEdgesChange}
        onConnect={onConnect}
        fitView
      />
    </div>
  );
}

5 连接事件

        在 React Flow 中,边的连接和断开是常见操作。当用户尝试连接两个节点时,系统需要验证连接的合法性,如检查节点的输入输出端口是否匹配、是否存在循环连接等。断开连接时需要处理相关的数据更新和视觉效果变化。可以通过onConnect 和 onEdgesDelete 事件进行自定义逻辑处理。

        onConnect示例程序如下所示。

export default function App() {
  const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
  const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);

  const onConnect = useCallback(
    (params) => {
      // 查找源节点和目标节点
      const sourceNode = nodes.find(node => node.id === params.source);
      const targetNode = nodes.find(node => node.id === params.target);
      
      // 打印节点信息
      console.log('连接起始端点:', sourceNode);
      console.log('连接终止端点:', targetNode);
      
      // 添加连接边
      setEdges((eds) => addEdge(params, eds));
    },
    [setEdges, nodes] // 添加nodes依赖确保获取最新数据
  );

  return (
    <div style={{ height: '500px' }}>
      <ReactFlow
        nodes={nodes}
        edges={edges}
        onNodesChange={onNodesChange}
        onEdgesChange={onEdgesChange}
        onConnect={onConnect}
        fitView
      />
    </div>
  );
}

        运行程序后结果如下图所示。

图3 onConnect 连接

6 连接规则

        我们也可以设置连接规则,例如下面程序不允许自身内部进行连接。

import React, { useCallback } from 'react';
import { ReactFlow, Handle, useNodesState, useEdgesState, addEdge } from 'reactflow';
import 'reactflow/dist/style.css';
import { FiDatabase, FiCloud } from 'react-icons/fi';
import { toast, Toaster } from 'react-hot-toast'; // 添加Toast组件


// 自定义节点组件 npm install react-hot-toast
const CustomNode = ({ id, data, selected }) => {
  return (
    <div className={`custom-node ${selected ? 'selected' : ''}`}>
      <Handle
        type="target"
        position="top"
        className="!bg-teal-500"
        // isValidConnection={(connection) => 
        //   connection.source !== id  // 禁止自连接
        // }
      />
      
      <div className="node-header">
        <FiCloud className="node-icon" />
        <h3 className="node-title">{data.label}</h3>
      </div>
      <div className="node-body">
        <FiDatabase className="node-icon" />
        <span className="node-info">{data.content}</span>
      </div>

      <Handle
        type="source"
        position="bottom"
        className="!bg-purple-500"
      />
      
      <Handle
        type="source"
        position="right"
        id={`${id}-output-2`}
        className="!bg-pink-500"
        style={{ top: '30%' }}
      />
    </div>
  );
};

const initialNodes = [
  { 
    id: '1', 
    position: { x: 0, y: 0 }, 
    data: { 
      label: '开始节点',
      content: '输入数据源'
    },
    type: 'custom',
  },
  { 
    id: '2', 
    position: { x: 200, y: 150 }, 
    data: { 
      label: '处理节点',
      content: '数据处理流程'
    },
    type: 'custom',
  },
];

const initialEdges = [{ 
  id: 'e1-2', 
  source: '1', 
  target: '2',
  animated: true,
  style: { stroke: '#94a3b8' },
}];

const nodeTypes = {
  custom: CustomNode,
};

// 节点样式
const nodeStyle = `
  .custom-node {
    background: linear-gradient(145deg, #ffffff, #f1f5f9);
    border-radius: 8px;
    border: 2px solid #cbd5e1;
    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
    padding: 16px;
    min-width: 200px;
    transition: all 0.2s ease;
  }

  .custom-node.selected {
    border-color: #6366f1;
    box-shadow: 0 4px 15px rgba(99, 102, 241, 0.2);
  }

  .custom-node:hover {
    transform: translateY(-2px);
  }

  .node-header {
    display: flex;
    align-items: center;
    margin-bottom: 12px;
    border-bottom: 1px solid #e2e8f0;
    padding-bottom: 8px;
  }

  .node-title {
    margin: 0;
    font-size: 1.1rem;
    color: #1e293b;
    margin-left: 8px;
  }

  .node-body {
    display: flex;
    align-items: center;
    color: #64748b;
  }

  .node-icon {
    font-size: 1.2rem;
    margin-right: 8px;
    color: #6366f1;
  }

  .node-info {
    font-size: 0.9rem;
  }

  .react-flow__handle {
    width: 14px;
    height: 14px;
    border-radius: 3px;
    border: none;
  }
`;

export default function App() {
  // 使用useNodesState管理节点状态
  const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
  const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
  // 连接处理回调
  const onConnect = useCallback(
    (connection) => {
      // 根据不同的输出端口设置边样式
      const edgeStyle = connection.sourceHandle?.endsWith('-output-2') 
        ? { stroke: '#ec4899' } 
        : { stroke: '#94a3b8' };

      return setEdges((eds) =>
        addEdge({
          ...connection,
          animated: true,
          style: edgeStyle,
        }, eds)
      );
    },
    [setEdges]
  );

  // 连接验证逻辑
  const isValidConnection = useCallback(
    (connection) => {
      // 禁止自连接
      if (connection.source === connection.target) {
        toast.error('不能连接到自身');
        // alert('不能连接到自身');
        return false;
      }

      // 检查目标节点是否已有连接
      const targetConnections = edges.filter(
        (edge) => edge.target === connection.target
      );

      if (targetConnections.length > 0) {
        toast.error('目标节点已有连接');
        console.log(`连接被禁止:节点 ${connection.target} 已有输入连接`);
        return false;
      }

      return true;
    },
    [edges]
  );


  return (
    <div style={{ height: '100vh', background: '#f8fafc' }}>
      <style>{nodeStyle}</style>
      <Toaster position="top-right" /> {/* Toast消息容器 */}
      <ReactFlow 
        nodes={nodes}
        edges={edges}
        onNodesChange={onNodesChange}  // 添加状态变更处理器
        onEdgesChange={onEdgesChange}
        onConnect={onConnect}
        nodeTypes={nodeTypes}
        isValidConnection={isValidConnection}
        fitView
        style={{ background: '#f8fafc' }}
        connectionLineStyle={{ stroke: '#94a3b8', strokeWidth: 2 }}
        defaultEdgeOptions={{
          type: 'smoothstep',
          animated: true,
          style: { strokeWidth: 2 }
        }}
      />
    </div>
  );
}

7 断开事件

        onEdgesDelete 示例程序如下所示。

export default function App() {
  const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
  const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);

  // 连接处理
  const onConnect = useCallback(
    (params) => {
      const sourceNode = nodes.find(n => n.id === params.source);
      const targetNode = nodes.find(n => n.id === params.target);
      
      console.log('[连接建立] 起始节点:', sourceNode);
      console.log('[连接建立] 终止节点:', targetNode);
      
      setEdges((eds) => addEdge(params, eds));
    },
    [setEdges, nodes]
  );

  // 断开连接处理
  const onEdgesDeleted = useCallback(
    (deletedEdges) => {
      deletedEdges.forEach(edge => {
        const sourceNode = nodes.find(n => n.id === edge.source);
        const targetNode = nodes.find(n => n.id === edge.target);
        
        console.log('[连接断开] 起始节点:', sourceNode);
        console.log('[连接断开] 终止节点:', targetNode);
      });
    },
    [nodes]
  );

  return (
    <div style={{ height: '500px' }}>
      <ReactFlow
        nodes={nodes}
        edges={edges}
        onNodesChange={onNodesChange}
        onEdgesChange={onEdgesChange}
        onConnect={onConnect}
        onEdgesDelete={onEdgesDeleted}  // 添加断开连接处理器
        fitView
      />
    </div>
  );
}

        运行程序后结果如下图所示。

图4 onEdgesDelete 连接断开

立即关注获取最新动态

点击订阅《React Agent 开发专栏》,每周获取智能体开发深度教程。项目代码持续更新至React Agent 开源仓库,欢迎 Star 获取实时更新通知!FGAI 人工智能平台FGAI 人工智能平台


网站公告

今日签到

点亮在社区的每一天
去签到