深入React Flow Renderer(二):构建拖动操作栏

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

引言

操作栏是工作流界面的一部分,通常位于界面的一侧或顶部。它包含了用户可以从中拖拽节点到画布上的组件列表。在我们的示例中,操作栏将位于界面的左侧。

创建操作栏组件

首先,让我们看一下如何创建操作栏组件。在我们的示例中,我们使用了React组件,名为Slider。这个组件接收一个名为components的属性,该属性包含了可用的组件列表。

ts 复制代码
// 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, any>(
  [
    [
      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<any>(['input']);
  const [allowedNodes, setAllowedNodes] = React.useState<any>([]);

  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 (
    <div className="sider">
      {allowType.map((type: any) =>
      //自定义手风琴,项目中使用的是mui,你可以使用其他组件库,这里就不贴出手风琴的代码了,请你根据你的项目,使用对应的组件。如果不需要手风琴组件。可以拥<div>来代替
        <CustomAccordion
          title={
            ruleNodeTypeDescriptors.get(type as any)?.name as string}
          key={type}
        >
          <div className="nodes">
            {allowedNodes
              .filter((node: any) => node.type === type)
              .map((x: any, i: number) =>
                <div
                  key={`${x.type}-${i}`}
                  className={`sider-node ${
                    classMap.get(x.type) || 'default-node'
                  }`}
                  onDragStart={(e) => onDragStart(e, x)}
                  draggable
                >
                  <div>{x.name}</div>
                  {/* 黑色遮罩层 */}
                  <div className="overlay"></div>
                </div>
              )}
          </div>
        </CustomAccordion>
      )}
    </div>
  );
}

在上述代码中,我们定义了一个Slider组件,它将组件列表映射到可展开的自定义组件中,并为每个组件添加了拖拽支持。

拖拽事件处理

拖拽操作栏的核心功能在于如何处理拖拽事件。在我们的示例中,我们使用了onDragStart函数来处理节点拖拽开始事件。该函数会设置被拖拽的节点的类型和名称,并记录被拖拽节点的完整信息。

ts 复制代码
/**
 * 处理节点拖拽开始事件的回调函数
  * @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';
};

这个函数会在用户拖拽节点时被触发,并且会设置相关的数据以便后续在画布上放置节点时使用。

总结

通过创建一个可拖动的操作栏,用户可以方便地将节点拖放到工作流画布上。在本文中,我们了解了如何创建操作栏组件,处理拖拽事件,并将组件列表展示给用户。下一篇博客中,我们将继续深入研究工作流界面的其他方面,包括画布的交互性和节点的定制。敬请期待!

相关推荐
我要洋人死8 分钟前
导航栏及下拉菜单的实现
前端·css·css3
科技探秘人20 分钟前
Chrome与火狐哪个浏览器的隐私追踪功能更好
前端·chrome
科技探秘人20 分钟前
Chrome与傲游浏览器性能与功能的深度对比
前端·chrome
JerryXZR26 分钟前
前端开发中ES6的技术细节二
前端·javascript·es6
七星静香27 分钟前
laravel chunkById 分块查询 使用时的问题
java·前端·laravel
q24985969330 分钟前
前端预览word、excel、ppt
前端·word·excel
小华同学ai36 分钟前
wflow-web:开源啦 ,高仿钉钉、飞书、企业微信的审批流程设计器,轻松打造属于你的工作流设计器
前端·钉钉·飞书
Gavin_91544 分钟前
【JavaScript】模块化开发
前端·javascript·vue.js
懒大王爱吃狼2 小时前
Python教程:python枚举类定义和使用
开发语言·前端·javascript·python·python基础·python编程·python书籍
小牛itbull3 小时前
ReactPress:重塑内容管理的未来
react.js·github·reactpress