「AntV」使用AntV X6实现流程编排设计器

通过对BPMN的深入学习,以及对业界成熟的流程编排设计器的调研,了解到要研发一个流程编排设计器,需要实现如下几个主要的功能:

  1. 支持创建各种流程图元素,包括任务(活动)、网关、事件等。
  2. 支持绘制各种连接线,包括普通连接线、条件连接线、消息连接线等。
  3. 支持流程图的编辑,包括元素的选择、移动、删除、复制、粘贴等。
  4. 支持流程图的保存,包括保存流程图数据、导入/导出流程图数据等。
  5. 具备良好的可扩展性,可以通过插件等方式进行功能扩展和定制化。
  6. 具备良好的用户体验,包括界面美观、交互友好等。

除了设计器本身的能力,还需要物料区域,工具栏操作区域,属性设置区域等额外的功能。

技术方案

基于阿里低代码引擎(Low-Code Engine)强大的定制扩展能力,自定义开发了缩放、组件面板、AntV X6画布面板、设置器面板等功能,构建出遵循BPMN规范的流程编排设计器。

  • 组件面板基于标准的物料协议完成物料元素渲染;
  • 画布面板基于 AntV X6 实现,借助自身Addon拖拽能力完成物料到画布的编排功能;
  • 设置器面板基于 Formily 表单协议完成各类配置表单渲染;
  • 协议转换基于fast-xml-parser,完成Graph JSON 和 XML 之间的协议转换。

在这里插入图片描述

实现步骤

以下内容将重点介绍一下基于AntV X6实现流程编排画布的过程,以及介绍在开发过程中重点用到的配置项和使用方法。当然设计器的内容除了画布,还有物料区、设置区、协议转换等内容,在这里不做过多阐述。

初始化画布

在页面中创建一个画布容器x6-container,用于绘制BPMN流程图。期间需要构造画布配置参数GraphOptions,初始化画布对象。

import React from "react";

export default props => {
  const containerRef = useRef(null);

  useLayoutEffect(() => {
    // 初始化图形
    registerShape();

    // 初始化画布
    let options = getDefaultGraphOptions(containerRef.current);
    const _graph = new Graph(options);

    // 初始化撤销重做、快捷键、图形变换等插件
    initPlugins(_graph);

    // 初始化删除,双击等事件
    initEvents(_graph);
  }, []);
  
  return (
    <div className="lc-designer lowcode-plugin-designer">
      <div className="lc-project">
        <div className="lc-simulator-canvas lc-simulator-device-default">
          <div id="x6-container" ref={containerRef} />
        </div>
      </div>
    </div>
  );
};
连线(connecting)

通过配置 connecting 可以实现丰富的连线交互。

router

采用了曼哈顿算法,注意需要使用excludeShapes排除对于Group节点的计算

{
  router: {
    name: "manhattan",
      args: {
        excludeShapes: [ElementType.Group],
        padding: 25
    }
  },
}
createEdge

自定义新建边的样式,在动态拖拽生成线条时使用。

{
  createEdge() {
    return new Shape.Edge({
      shape: ElementType.SequenceFlow,
      router: {
        name: "manhattan",
        args: {
          excludeShapes: [ElementType.Group],
          padding: 25
        }
      },
    });
  },
}
组合(embedding)

通过 embedding 可以将一个节点拖动到另一个节点中,使其成为另一节点的子节点,默认禁用。本项目中用于支持向Group节点中拖动子节点达到分组展示的效果。

{
  embedding: {
    enabled: true,  // 是否允许节点之间嵌套
    findParent({ node }) {
      const bbox = node.getBBox();
      return this.getNodes().filter(item => {
        const data = item.getData<any>();
        if (data && data.parent) {
          item.toBack(); // 修改Group节点zIndex,解决拖拽覆盖问题
          const targetBBox = item.getBBox();
          return bbox.isIntersectWithRect(targetBBox);
        }
        return false;
      });
    },
	},
}
限制(interacting)

限制节点和边的交互行为,实际案例中用到了edgeLabelMovable,支持边的标签可移动。

  • nodeMovable 节点是否可以被移动。
  • edgeMovable 边是否可以被移动。
  • edgeLabelMovable 边的标签是否可以被移动。

初始化元素

BPMN规范中,存在事件、网关、活动等基础元素,因此定义了如下几种元素类型,并分别注册了节点和边。每种图形都有自身的属性配置。

export enum ElementType {
  /** 开始事件 */
  StartEvent = "startEvent",
  /** 结束事件 */
  EndEvent = "endEvent",
  /** 错误结束事件 */
  ErrorEndEvent = "errorEndEvent",
  /** 连接线 */
  SequenceFlow = "sequenceFlow",
  /** 排他网关 */
  ExclusiveGateway = "exclusiveGateway",
  /** 并行网关 */
  ParallelGateway = "parallelGateway",
  /** 功能节点 */
  FunctionTask = "functionTask",
  /** 分组 */
  Group = "group",
}

配置节点各类属性信息,并注册节点。这里以Group元素为例,代码如下:

// 分组元素配置信息
const defaultGroupConfig = {
  inherit: "rect",
  width: 240,
  height: 160,
  zIndex: 0,
  markup: [
    {
      tagName: 'rect',
      selector: 'body',
    },
    {
      tagName: 'text',
      selector: 'text',
    },
  ],
  attrs: {
    text: {
      refX: 0.5,
      refY: 10,
      textAnchor: 'middle',
      textVerticalAnchor: 'top',
      // 文本换行:https://antv-x6.gitee.io/zh/docs/api/registry/attr/#textwrap
      textWrap: {
        width: 200,
        height: 30,
        ellipsis: true,
      },
    },
    body: {
      rx: 6,
      ry: 6,
      strokeWidth: 1,
      strokeDasharray: "3,3",
      stroke: "#bfbfbf",
      fill: "rgba(238,238,238,.3)",
    },
  },
  data: {
    parent: true,
  },
}

Graph.registerNode(EElementType.Group, defaultGroupConfig, true);
markup

markup 指定了渲染节点时使用的 SVG片段,使用 JSON 格式描述。如上代码则表示节点内部包含<rect><text>两个SVG元素,渲染到页面之后,节点对应的元素如下:

<g data-cell-id="d873ae84-1655-4973-9ba8-bd65b6613b80" data-shape="group" class="x6-cell x6-node" transform="translate(-590,20)">
  <rect fill="rgba(238,238,238,.3)" stroke="#bfbfbf" stroke-width="1" rx="6" ry="6" stroke-dasharray="3,3" width="240" height="160"></rect>
  <text font-size="14" xml:space="preserve" fill="#000000" text-anchor="middle" font-family="Arial, helvetica, sans-serif" text="分组" transform="matrix(1,0,0,1,120,10)">
    <tspan dy="0.8em" class="v-line">分组</tspan>
  </text>
</g>
tagName

指定需要创建哪种 SVG/HTML 元素

selector

该元素的唯一选择器,通过选择器为该元素指定属性样式

attrs

属性选项 attrs 是一个复杂对象,该对象的 Key 是节点 Markup 定义中元素的选择器(selector),对应的值是应用到该 SVG 元素的 SVG 属性值(如 fillstroke),如果你对 SVG 属性还不熟悉,可以参考 MDN 提供的填充和边框入门教程。

效果图

在这里插入图片描述

初始化连接线

Graph.registerEdge(
    ElementType.SequenceFlow,
    {
      inherit: "edge",
      attrs: {
        line: {
          strokeWidth: Size.LineStrokeWith,
          stroke: Colors.Line
        }
      },
      router: {
        name: "manhattan",
        args: {
          excludeShapes: [ElementType.Group], // 解决Group节点下manhattan算法失效问题,导致线条显示异常
          padding: 25
        }
      }
    },
    true,
);

初始化插件

快捷键

使用@antv/x6-plugin-keyboard,为画布绑定快捷键,例如复制,粘贴,删除等

import { Graph } from "@antv/x6";
import { Keyboard } from "@antv/x6-plugin-keyboard";

export const initPlugins = (graph: Graph) => {
  graph.use(
    new Keyboard({
      enabled: true,
    })
  );
};

对齐线

使用@antv/x6-plugin-snapline,在移动节点时辅助排版

import { Graph } from "@antv/x6";
import { Snapline } from "@antv/x6-plugin-snapline";

export const initPlugins = (graph: Graph) => {
  graph.use(
    new Snapline({
      enabled: true,
    }),
  );

撤销重做

使用@antv/x6-plugin-history,实现元素操作的撤销和重做

import { Graph } from "@antv/x6";
import { History } from "@antv/x6-plugin-history";

export const initPlugins = (graph: Graph) => {
  graph.use(
    new History({
      enabled: true,
      beforeAddCommand(event, args: any) {
        // console.log(event, args);
        if (args.key === "tools") {
          return false;
        }
        return true;
      },
    }),
  );

框选插件

使用@antv/x6-plugin-selection,可以实现点击元素选中,启用多选能力,按住Ctrl/Command后点击元素可以多选

import { Graph } from "@antv/x6";
import { Selection } from "@antv/x6-plugin-selection";

export const initPlugins = (graph: Graph) => {
  graph.use(
    // 支持节点选中样式
    new Selection({
      enabled: true,
    }),
  );
};

图形变换

使用@antv/x6-plugin-transform,实现节点大小的调整,节点渲染角度的调整,例如本项目中的Group节点就需要进行大小调整。

import { Graph } from "@antv/x6";
import { Transform } from "@antv/x6-plugin-transform";

export const initPlugins = (graph: Graph) => {
  graph.use(
    new Transform({
      resizing: {
        enabled: node => {
          // 支持分组节点调整大小
          if (node.shape === "group") {
            return true;
          }
          return false;
        },
      },
    }),
  );
};

拖拽

使用@antv/x6-plugin-dnd,通过拖拽交互往画布中添加节点,本项目中需要从流程图组件库中拖拽组件到画布中。

import { Dnd } from "@antv/x6-plugin-dnd";

const ComponentPanel = props => {
    const dndRef = useRef<any>();
    const dndContainerRef = useRef(null);
    useLayoutEffect(() => {
      const _dnd = new Dnd({
        target: graph, //来源全局Graph实例,此处省略
        scaled: false,
        dndContainer: dndContainerRef.current as any,
      });
      dndRef.current = _dnd; 
    }

    const startDrag = useCallback((e: React.MouseEvent<HTMLDivElement, MouseEvent>, data: any) => {
        const target = e.currentTarget;
        const type = target.getAttribute("data-type");
        const nodeConfig: any = {
          shape: type,
          label: data.title,
          data: {
            ...data
          }
        };

        // 此处省略Graph实例获取
        const node = graph.createNode(nodeConfig);

        dndRef.current.start(node, e.nativeEvent as any);
    }, []);

    return (
        <div className="x6-component-panel" ref={dndContainerRef}>
          <CustomComponent startDrag={startDrag} />
        </div>
    );

}

初始化事件

选中/取消选中

监听边的选中和取消选中事件,对应修改线条样式内容。

export const initEvents = (graph: Graph) => {
  graph.on("edge:selected", ({ edge }) => {
    edge.toFront();
    edge.attr({
      line: {
        stroke: Colors.LineActived,
        strokeWidth: 2
      },
    })
  });
  graph.on("edge:unselected", ({ edge }) => {
    edge.attr({
      line: {
        stroke: Colors.Line,
        strokeWidth: Size.LineStrokeWith
      },
    })
  });
}
鼠标移入移出

监听节点的鼠标移入、移出事件,控制链接桩的显示和隐藏

export const initEvents = (graph: Graph) => {

    const showPorts = (ports: NodeListOf<SVGElement>, show: boolean) => {
      for (let i = 0, len = ports.length; i < len; i += 1) {
        ports[i].style.visibility = show ? 'visible' : 'hidden'
      }
    }
    graph.on('node:mouseenter', () => {
      const container = document.getElementById('graph-container')!
      const ports = container.querySelectorAll(
        '.x6-port-body',
      ) as NodeListOf<SVGElement>
      showPorts(ports, true)
    })
    graph.on('node:mouseleave', () => {
      const container = document.getElementById('graph-container')!
      const ports = container.querySelectorAll(
        '.x6-port-body',
      ) as NodeListOf<SVGElement>
      showPorts(ports, false)
    })
}
节点双击

监听节点的双击事件,添加小工具编辑节点名称

export const initEvents = (graph: Graph) => {
  graph.on('node:dblclick', ({ cell, e }) => {
    const name = 'node-editor';
    cell.removeTool(name)
    cell.addTools({
      name,
      args: {
        event: e,
        attrs: {
          backgroundColor: '#fff',
        },
      },
    })
  })
}

接入画布

使用低代码引擎提供的插件API,移除低代码引擎默认的画布,添加自定义开发的画布组件(AntV X6),方式如下:

const PluginX6Designer = (ctx: ILowCodePluginContext) => {
  return {
    init() {
      const { skeleton, project } = ctx;
      skeleton.remove({
        name: 'designer',
        area: 'mainArea',
        type: 'Widget'
      });
      skeleton.add({
        area: 'mainArea',
        name: 'designer',
        type: 'Widget',
        content: X6Designer,
        contentProps: {
          ctx,
        }
      });
    }
  }
}

PluginX6Designer.pluginName = 'plugin-x6-designer';
export default PluginX6Designer;

// 注册X6画布
await plugins.register(PluginX6Designer);

最终效果

涉及业务敏感信息已做模糊处理
在这里插入图片描述

写在最后

基于AntV X6实现流程编排设计器,有如下几个方面的优点:

  1. 低成本定制能力:AntV X6是基于HTML和SVG的图表编辑引擎,提供了丰富的API和自定义事件,可以通过编写JavaScript代码,快速实现自定义的图表样式、布局和交互方式,满足具体业务需求。
  2. 高度可扩展性:AntV X6提供了众多内置扩展,包括DAG图、ER图、流程图等应用,可以帮助你根据自己的需要,自由扩展和定制功能和组件,实现更加灵活的业务需求。
  3. 强大的交互功能:AntV X6提供了丰富的图表组件和交互功能,支持拖拽、连线、缩放等多种交互方式,还可以集成自定义的图表组件和逻辑处理,实现更加丰富的功能需求。

开发过程还是漫长和曲折的,需要不断熟悉查阅官网API,做各种类型和效果的尝试。需要说明的是,在本项目开发时,低代码引擎和X6相结合还没有开源的解决方案,因此在熟悉了两者的文档后,终于探索出一条结合的道路。因此在实现拖拽的交互逻辑中则是采用了AntV X6DND插件。同样也可以采用低代码引擎本身的拖拽机制,有兴趣可以官网了解开源的方案。

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
要在Vue3中使用AntV X6实现右键菜单功能,你可以按照以下步骤操作: 1. 安装依赖:在Vue项目中安装AntV X6ant-design-vue库。 ``` npm install @antv/x6 ant-design-vue --save ``` 2. 创建右键菜单组件:创建一个Vue组件,作为X6图形编辑右键菜单的容。 ```vue <template> <a-dropdown :trigger="['contextmenu']" @visible-change="onVisibleChange"> <a-menu slot="overlay" :style="{ width: '120px' }"> <a-menu-item @click="deleteNode">删除节点</a-menu-item> </a-menu> <div class="x6-contextmenu" ref="container"></div> </a-dropdown> </template> <script> import { defineComponent } from 'vue'; import { Dropdown, Menu, message } from 'ant-design-vue'; export default defineComponent({ name: 'X6ContextMenu', components: { Dropdown, Menu, MenuItem: Menu.Item, }, emits: ['deleteNode'], mounted() { this.menu = this.$refs.container; this.menu.addEventListener('contextmenu', (e) => { e.preventDefault(); e.stopPropagation(); }); }, methods: { onVisibleChange(visible) { if (visible) { this.$emit('contextmenu', this.menu); } }, deleteNode() { this.$emit('deleteNode'); }, }, }); </script> ``` 3. 在X6图形编辑中添加右键菜单:在X6图形编辑中添加右键菜单功能。 ```vue <template> <div class="x6-editor"> <x6-contextmenu @contextmenu="onContextMenu" @deleteNode="deleteNode"></x6-contextmenu> <div class="x6-graph" ref="container"></div> </div> </template> <script> import { defineComponent } from 'vue'; import { Graph, Node } from '@antv/x6'; import X6ContextMenu from './X6ContextMenu.vue'; export default defineComponent({ name: 'X6Editor', components: { X6ContextMenu, }, data() { return { graph: null, }; }, mounted() { this.graph = new Graph({ container: this.$refs.container, grid: true, // 其他配置 }); // 添加节点 const node = this.graph.addNode({ // 节点配置 }); // 右键菜单事件 this.graph.on('contextmenu', ({ x, y }) => { this.$refs.contextmenu.show(x, y); }); }, methods: { onContextMenu(menu) { // 清空菜单 menu.innerHTML = ''; // 添加菜单项 const deleteMenuItem = document.createElement('a-menu-item'); deleteMenuItem.innerHTML = '删除节点'; deleteMenuItem.addEventListener('click', () => { this.deleteNode(); }); menu.appendChild(deleteMenuItem); }, deleteNode() { // 删除节点 this.graph.removeNode(node); }, }, }); </script> ``` 这样就可以在Vue3中使用AntV X6实现右键菜单功能了。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

青菜小王子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值