本文为《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 人工智能平台