在上一篇博客中,我们介绍了如何启动React Flow Renderer并创建一个基本的工作流界面。本文将进一步深入,着重讨论如何构建一个可拖动的操作栏,它是用户与工作流交互的入口之一。

(图片来源网络,侵删)
引言
操作栏是工作流界面的一部分,通常位于界面的一侧或顶部。它包含了用户可以从中拖拽节点到画布上的组件列表。在我们的示例中,操作栏将位于界面的左侧。
创建操作栏组件
首先,让我们看一下如何创建操作栏组件。在我们的示例中,我们使用了React组件,名为Slider。这个组件接收一个名为components的属性,该属性包含了可用的组件列表。

(图片来源网络,侵删)
// Slider/index.jsx import React from 'react'; //项目中自定义的手风琴组件,请你使用自己项目中的组件 import { CustomAccordion } from '@/components/CustomeAccordion'; // 模拟节点 const mockComponent = [ { 'name': 'clear alarm', 'type': 'ACTION', 'clazz': 'action.TbClearAlarmNode' }, { 'name': 'create alarm', 'type': 'ACTION', 'clazz': 'action.TbCreateAlarmNode' }, { 'name': 'device profile', 'type': 'ACTION', 'clazz': 'profile.TbDeviceProfileNode' }, { 'name': 'log', 'type': 'ACTION', 'clazz': 'action.TbLogNode' }, { 'name': 'message type switch', 'type': 'FILTER', 'clazz': 'filter.TbMsgTypeSwitchNode' }, { 'name': 'rpc call request', 'type': 'ACTION', 'clazz': 'rpc.TbSendRPCRequestNode' }, { 'name': 'rule chain', 'type': 'FLOW', 'clazz': 'flow.TbRuleChainInputNode' }, { 'name': 'save attributes', 'type': 'ACTION', 'clazz': 'telemetry.TbMsgAttributesNode' }, { 'name': 'save timeseries', 'type': 'ACTION', 'clazz': 'telemetry.TbMsgTimeseriesNode' }, { 'name': 'script', 'type': 'TRANSFORMATION', 'clazz': 'transform.TbTransformMsgNode' } ]; export enum RuleNodeType { FILTER = 'FILTER', ENRICHMENT = 'ENRICHMENT', TRANSFORMATION = 'TRANSFORMATION', ACTION = 'ACTION', EXTERNAL = 'EXTERNAL', FLOW = 'FLOW', UNKNOWN = 'UNKNOWN', INPUT = 'INPUT', } export const ruleNodeTypeDescriptors = new Map( [ [ RuleNodeType.FILTER, { value: RuleNodeType.FILTER, name: 'rulenode.type-filter', details: 'rulenode.type-filter-details', nodeClass: 'tb-filter-type', icon: 'filter_list' } ], [ RuleNodeType.ENRICHMENT, { value: RuleNodeType.ENRICHMENT, name: 'rulenode.type-enrichment', details: 'rulenode.type-enrichment-details', nodeClass: 'tb-enrichment-type', icon: 'playlist_add' } ], [ RuleNodeType.TRANSFORMATION, { value: RuleNodeType.TRANSFORMATION, name: 'rulenode.type-transformation', details: 'rulenode.type-transformation-details', nodeClass: 'tb-transformation-type', icon: 'transform' } ], [ RuleNodeType.ACTION, { value: RuleNodeType.ACTION, name: 'rulenode.type-action', details: 'rulenode.type-action-details', nodeClass: 'tb-action-type', icon: 'flash_on' } ], [ RuleNodeType.EXTERNAL, { value: RuleNodeType.EXTERNAL, name: 'rulenode.type-external', details: 'rulenode.type-external-details', nodeClass: 'tb-external-type', icon: 'cloud_upload' } ], [ RuleNodeType.FLOW, { value: RuleNodeType.FLOW, name: 'rulenode.type-flow', details: 'rulenode.type-flow-details', nodeClass: 'tb-flow-type', icon: 'settings_ethernet' } ], [ RuleNodeType.INPUT, { value: RuleNodeType.INPUT, name: 'rulenode.type-input', details: 'rulenode.type-input-details', nodeClass: 'tb-input-type', icon: 'input', special: true } ], [ RuleNodeType.UNKNOWN, { value: RuleNodeType.UNKNOWN, name: 'rulenode.type-unknown', details: 'rulenode.type-unknown-details', nodeClass: 'tb-unknown-type', icon: 'help_outline' } ] ] ); const classMap = new Map([ ['ACTION', 'relation-node'], ['input', 'input-node'], ['FILTER', 'filter-node'], ['ENRICHMENT', 'enrichment-node'], ['TRANSFORMATION', 'transformation-node'], ['EXTERNAL', 'external-node'], ['FLOW', 'flow-node'] ]); // const allowType = ruleNodeTypeComponentTypes; const allowNodesClazz = [ 'telemetry.TbMsgAttributesNode', 'filter.TbMsgTypeSwitchNode', 'action.TbLogNode', 'rpc.TbSendRPCRequestNode', 'profile.TbDeviceProfileNode', 'telemetry.TbMsgTimeseriesNode', 'action.TbCreateAlarmNode', 'action.TbClearAlarmNode', 'flow.TbRuleChainInputNode', 'transform.TbTransformMsgNode' ]; export default function Slider() { const [allowType, setAllowType] = React.useState(['input']); const [allowedNodes, setAllowedNodes] = React.useState([]); React.useEffect(() => { // 将组件按名称进行排序 const sortedComponents = mockComponent?.sort((a: any, b: any) => a.name?.localeCompare(b.name) ); // 过滤出符合条件的组件并拼接到allowedNodes数组中 const filteredComponents = sortedComponents?.filter((component: any) => allowNodesClazz.includes(component.clazz) ) || []; const updatedAllowedNodes = [...filteredComponents]; // 获取所有组件的类型,并和allowType数组进行合并 const updatedTypes = updatedAllowedNodes.map((component) => component.type); // 去除重复的节点并更新allowedNodes状态 setAllowedNodes(Array.from(new Set(updatedAllowedNodes))); // 去除重复的类型并更新allowType状态(如果为空数组,则设置为默认值) setAllowType(Array.from(new Set(updatedTypes)) || []); }, []); return ({allowType.map((type: any) => //自定义手风琴,项目中使用的是mui,你可以使用其他组件库,这里就不贴出手风琴的代码了,请你根据你的项目,使用对应的组件。如果不需要手风琴组件。可以拥来代替 ruleNodeTypeDescriptors.get(type as any)?.name as string} key={type})} ); }{allowedNodes .filter((node: any) => node.type === type) .map((x: any, i: number) => `${x.type}-${i}`} className={`sider-node ${ classMap.get(x.type) || 'default-node' }`} onDragStart={(e) = onDragStart(e, x)} draggable >)}{x.name}{/* 黑色遮罩层 */}
在上述代码中,我们定义了一个Slider组件,它将组件列表映射到可展开的自定义组件中,并为每个组件添加了拖拽支持。
拖拽事件处理
拖拽操作栏的核心功能在于如何处理拖拽事件。在我们的示例中,我们使用了onDragStart函数来处理节点拖拽开始事件。该函数会设置被拖拽的节点的类型和名称,并记录被拖拽节点的完整信息。
/** * 处理节点拖拽开始事件的回调函数 * @param {Event} evt - 拖拽事件对象 * @param {Object} node - 被拖拽的节点对象 */ const onDragStart = (evt: any, node: any) => { // 记录被拖拽的节点类型和名称 evt.dataTransfer.setData( 'application/reactflow', node.type + ',' + node.name ); // 记录被拖拽的节点的完整信息 evt.dataTransfer.setData('application/reactflownode', JSON.stringify(node)); // 设置拖拽效果为移动 evt.dataTransfer.effectAllowed = 'move'; };
这个函数会在用户拖拽节点时被触发,并且会设置相关的数据以便后续在画布上放置节点时使用。
总结
通过创建一个可拖动的操作栏,用户可以方便地将节点拖放到工作流画布上。在本文中,我们了解了如何创建操作栏组件,处理拖拽事件,并将组件列表展示给用户。下一篇博客中,我们将继续深入研究工作流界面的其他方面,包括画布的交互性和节点的定制。敬请期待!