栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > Web开发 > JavaScript

基于G6的流程编辑器

JavaScript 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

基于G6的流程编辑器

人人都会做系列之流程编辑器 前言

很长一段时间以来,一些诸如 BPM 啊,或者所谓的 xxx 自动化的产品,在介绍亮点的时候,其中都会有一条是可视化编辑流程图。
正好最近有个契机,就实现了一下基础功能。
预览地址
code

技术栈
  1. React + Hook + Typescript,这个是未来至少一两年最主流的(之一)

  2. AntV 的G6(3.8.0)

  3. 基于Cra脚手架初始化的项目,基于customize-cra对webpack做了一些基础配置变更,比如支持less module,支持alias这些就不赘述了。

  4. react-dnd实现从工具栏拖拽至画布

项目地址
  • 代码: github
  • 演示: preview
G6 简介

G6是一个图可视化引擎,简单的说就是用来展示关系的。既然是关系,那数据中必不可少的就是nodes和edges了,其中nodes用来描述节点,edges用来描述边,最基础的如下所示:

const graphData = {
  nodes: [
    {
      id: 'node-1',
      label: 'node1'
    },
    {
      id: 'node-2',
      label: 'node2'
    }
  ],
  edges: [
    {
      source: 'node-1',
      target: 'node-2',
      label: 'edge1'
    }
  ]
}

有了以上格式的数据,G6就会自动生成关系图(以“图”的形式,展示主体与关系)。

思路

流程图本质上来说,其实也就是“关系图”,每一个过程就是一个节点,过程之间的关系就是边。
有了这个认知,再基于现在的数据驱动的思路,如何基于G6生成流程图就很简单了。

  • 从工具栏拖动工具到画布上,触发增加node的事件,为图数据增加一个node
  • 从node的anchorPointer开始拖拽时,触发增加edge的事件,为图数据增加一条edge
  • 当鼠标拖拽着node移动时,触发更新edge坐标事件,实时修改edge的坐标(x 和 y 的值)
  • 当鼠标松开时,判断当前鼠标位置。如果在某个 node 上,增将当前edge的target指定为当前node,否则删除当前edge
  • 选中某个node或者edge时,获取其属性(label),当修改label值并按下保存后,将新的label的值更新至图数据
  • 在node或者edge上使用鼠标右键点击时,呼出contextMenu,点击删除后,删除node或者edge。需要注意的是,如果删除的是edge,直接删除即可。如果是node的话,则需要同时将起点(source)和终点(target)为该node的edge也同时删除
具体实现 工具栏拖拽

React中拖拽组件有很多,最终选择了react-dnd,具体原因不赘述了。
引入react-dnd后,创建两个容器组件:drag-item和drag-container,顾名思义,一个是用来包裹可拖拽对象的,另一个用来包裹接受被拖拽对象的容器。
关键就是在拖拽对象拖拽结束时,向外派发当前对象以及坐标。

// drag-item.tsx
const DragItem: FC = ({ name, children, onDragEnd }) => {
  const [{ isDragging }, dragRef] = useDrag({
    item: { name, type: 'DragItem' },
    collect: (monitor) => ({
      isDragging: monitor.isDragging()
    }),
    end: (item, monitor: DragSourceMonitor) => {
      const dropResult = monitor.getDropResult();
      if (item && dropResult) {
 onDragEnd && onDragEnd(item, dropResult.position);
      }
    }
  });
  const opacity = isDragging ? 0.4 : 1;
  return (
    
  • {children}
  • ); };
    拖拽完成后添加节点

    G6是有一套完善的坐标体系的:G6 坐标系深度解析
    提供了将浏览器坐标转换为画布坐标的 API,但是后期在实现将内容居中显示时遇到了问题,本来以为直接使用浏览器坐标可以解决,就又用回了浏览器坐标,结果发现还是有问题。(在初始化画布时,如果使用了自动居中,画布的坐标原点会发生变化。)
    代码很简单,就是判断如果拖拽元素落点处于画布中,添加一个对应的node,坐标就是落点,这样画布中就会在这个位置出现这个node
    值得一提的是,可以为node设置anchorPoint来指定node的哪些位置可以作为连接点:节点的连接点 anchorPoint
    另外还有一个属性叫linkPoint

    // 根据不同工具类型,添加不同样式node
    const getNodeStyle = (name: string) => {
      if (name === 'common') {
        return {
          type: 'circle',
          size: 80,
          style: {
     stroke: 'blue',
     fill: '#FFF'
          }
        };
      } else if (name === 'start') {
        return {
          type: 'rect',
          size: [80, 40],
          style: {
     fill: '#FFF',
     stroke: 'red'
          }
        };
      } else if (name === 'juge') {
        return {
          type: 'diamond',
          size: 80,
          style: {
     fill: '#FFF',
     stroke: 'yellow'
          }
        };
      }
    };
    
    const onDragEnd = (item: { name: string }, position: { x: number, y: number }) => {
      // const point = editor.current?.getPointByClient(position.x, position.y);
      // console.log(point);
      if (position && position.x > 160 && position.y > 50) {
        // 完全进入画布,则生成一个节点
        let key = `id-${id++}`;
        const style = getNodeStyle(item.name);
        const newNode = {
          ...style,
          id: key,
          x: position.x - (160 - NODE_WIDTH / 2),
          y: position.y - (50 - NODE_HEIGHT / 2),
          anchorPoints: [
     [0.5, 0],
     [1, 0.5],
     [0.5, 1],
     [0, 0.5]
          ],
          label: item.name
        };
        editor.current?.addItem('node', newNode);
      }
    };
    
    模拟拖拽实现生成连线
    • 使用G6自带的Behavior来为两个node生成edge。
      官网示例
      缺点是只能通过click事件来触发。

    • 自己通过模拟拖拽的方式实现
      因为node本身也是可以拖拽的,这样就和拖拽连线产生来冲突。
      因此拖拽连线的起点,就需要做特殊出来,判断在linkPoint上才触发创建edge的事件。
      本来没有什么头绪,后来发现官网又一个类似的示例
      大致思路是当鼠标按下时,判断如果当前位置处于linkPoint上,则创建一个source和target均为当前node的edge,然后当mousemove的时候,去更新edge的位置(即x和y的值更新为当前鼠标的坐标),当鼠标松开时,则判断是否处于node范围,如果处于某个node范围中,则将edge的target更新为该node,否则删除该edge。

      比较好的实现方式是和示例一样,以registerBehavior的方式将相关事件都注册在一起。

    G6.registerBehavior('drag-point-add-edge', {
        getEvents() {
          return {
     click: 'onMouseClick',
     mousedown: 'onMouseDown',
     mousemove: 'onMouseMove',
     mouseup: 'onMouseUp',
     'node:click': 'onNodeClick',
     'edge:click': 'onEdgeClick'
          };
        },
        onMouseDown(ev: any) {
          ev.preventDefault();
          const self = this;
          const node = ev.item;
          if (node && ev.target.get('className').startsWith('link-point')) {
     const graph = self.graph as Graph;
     const model = node.getModel();
    
     if (!self.addingEdge && !self.edge) {
       self.edge = graph.addItem('edge', {
         source: model.id,
         target: model.id
       });
       self.addingEdge = true;
     }
          }
        },
        onMouseMove(ev: any) {
          ev.preventDefault();
          const self = this;
          const point = { x: ev.x, y: ev.y };
          if (self.addingEdge && self.edge) {
     (self.graph as Graph).updateItem(self.edge as IEdge, {
       target: point
     });
          }
        },
        onMouseUp(ev: any) {
          ev.preventDefault();
          const self = this;
          const node = ev.item;
          const graph = self.graph as Graph;
          // 这里会走两次,第二次destroyed为true
          // 因此增加判断
          if (node && !node.destroyed && node.getType() === 'node') {
     const model = node.getModel();
     console.log(model);
     if (self.addingEdge && self.edge) {
       graph.updateItem(self.edge as IEdge, {
         target: model.id
       });
       self.edge = null;
       self.addingEdge = false;
     }
          } else {
     if (self.addingEdge && self.edge) {
       graph.removeItem(self.edge as IEdge);
       self.edge = null;
       self.addingEdge = false;
     }
          }
        }
      });
    
    选中Node或者Edge后编辑

    G6提供了状态,以及状态样式:State
    因此可以很方便实现选中后变更状态

    右键菜单、底部 grid、minimap

    这些都是G6提供的组件,只需要在初始化时传入plugins即可。

    • Minimap
    • ContextMenu
    小结

    预览地址
    至此,一个满足基础功能的流程编辑器就完成了。
    (其实还有很多可以优化的点,比如拖拽时显示辅助线,对齐到网格时如果设置为居中会有坐标起点不在原点的问题等等。)

    转载请注明:文章转载自 www.mshxw.com
    我们一直用心在做
    关于我们 文章归档 网站地图 联系我们

    版权所有 (c)2021-2022 MSHXW.COM

    ICP备案号:晋ICP备2021003244-6号