【VUE3+AntV X6】 引入ANTV X6 的流程图编辑器应用(一)

2024-03-10 2599阅读

ANTV X6 流程图编辑器应用

  • 唠唠叨叨
    • 事发背景
    • 实现效果
    • 实现思路
    • 总结
    • 参考资

      唠唠叨叨

      		唉!最近感觉非常忙,要学的东西太多,要实现的方法变幻莫测,层出不穷,越学越觉得自己不会的实在太多。
      	项目本来中标了,后来又不知道发生了什么,临到签合同又黄了,整的我们开发的瑟瑟发抖,产品整个大改,恐怕要……
      	只能说还是抓紧时间好好学习,知识和技术是永远不会背叛自己的。
      

      事发背景

      		俺滴组长(后端大佬)要我配合开发数据平台的任务流全局调度系统,想用流程图实现作业运行配置。
      	于是我在各种流程图的前端框架中选了ANTV X6来引入实现。官方文档天天在看,可这知识他就是不入脑子呀TvT。
      	终于有一天腌肉某人一拍脑门醍醐灌顶实现了功能,遂记录实现过程及困难解决方法。
      

      【VUE3+AntV X6】 引入ANTV X6 的流程图编辑器应用(一) 第1张

      在官方文档中给出了场景案例,恰好就有我所需要的流程图:

      【VUE3+AntV X6】 引入ANTV X6 的流程图编辑器应用(一) 第2张

      实现效果

      经过一系列拼拼凑凑,修修改改,还有样式设计和调整,调度作业画布页面如下图所示:

      【VUE3+AntV X6】 引入ANTV X6 的流程图编辑器应用(一) 第3张

      实现思路

      1. 首先我们要了解系统的业务功能需求,以及业务流程的线路是怎样的;

        如图是窗口的拆解,其中①作业树使用的是element UI的el-tree组件,是用于储存画布内容和画布回显更新的,②是选中节点树中的某项流程作业进行对应的任务操作,③和④才是实际应用AntV X6的画布插件实现的可拖拽、增、删、改、配置的流程图编辑器。

        【VUE3+AntV X6】 引入ANTV X6 的流程图编辑器应用(一) 第4张

      通过vue的父子组件我们创建一个index.vue的父组件,然后将这四个部分拆分为三个子组件的.vue文件,分别为DataTree.vue、DataButton.vue、FiliationGraph.vue(包含③、④两部分)今天记录的是FiliationGraph.vue内节点配置画布的实现。

      2. 安装AntV X6

      通过 npm 或 yarn 命令安装 X6。

      # npm
      $ npm install @antv/x6 --save
      # yarn
      $ yarn add @antv/x6
      
      1. 初始化画布,在html中建立节点库和画布div容器:
      // 引入 import { Graph, Shape, Addon, FunctionExt } from "@antv/x6"; const { Stencil } = Addon; //节点库 const {graph} = Graph; //画布 const stencil = new Stencil({ //新建节点库 title: "数据集成", target: graph, search: false, // 搜索 collapsable: true, stencilGraphWidth: 300, //容器宽度 stencilGraphHeight: 600, //容器长度 groups: [ //分组 { name: "processLibrary", title: "dataSource", }, ], layoutOptions: { dx: 30, dy: 20, columns: 1, //列数(行内节点数) columnWidth: 130, //列宽 rowHeight: 100, //行高 }, }); proxy.$refs.stencilContainer.appendChild(stencil.container) //注册到div中 const graph = new Graph({ // 新建画布 container: document.getElementById('flowContainer'), width: 800, height: 600, background: { color: "#F2F7FA", }, });
      1. 初始化节点、边:

        初始化节点、边数据:

      const nodeData = {
       // 节点
        nodes: [
        ],
        // 边
        edges: [
        ],
      };
      

      准备节点样式内容数据,其中节点的左上角图标image是自定义的svg文件,根据产品ui或自己设计来使用哟。

      【VUE3+AntV X6】 引入ANTV X6 的流程图编辑器应用(一) 第5张

      const imageShapes = [
       {
          body: {
            fill: "rgba(102, 153, 204, 0.05)",
            stroke: "rgb(102, 153, 204)",
          },
          label: {
            text: state.collectLabel,
            fill: 'rgb(102, 153, 204)',
          },
          image: require('/src/assets/Scheduler/DataCollect.svg'),
        },
        {
          body: {
            fill: "rgba(185, 147, 214, 0.05)",
            stroke: "rgb(185, 147, 214)",
          },
          label: {
            text: state.flinkLabel,
            fill: 'rgb(185, 147, 214)',
            },
          image: require('/src/assets/Scheduler/DataFlink.svg'),
        },
        {
          body: {
            fill: "rgba(154, 184, 122, 0.05)",
            stroke: "rgb(154, 184, 122)",
          },
          label: {
            text: state.controlLabel,
            fill: 'rgb(154, 184, 122)',
            },
          image: require('/src/assets/Scheduler/DataQc.svg'),
        },
        {
          body: {
            fill: "rgba(247, 178, 103, 0.05)",
            stroke: "rgb(247, 178, 103)",
          },
          label: {
            text: state.monitorLabel,
            fill: 'rgb(247, 178, 103)',
            },
          image: require('/src/assets/Scheduler/DataWatch.svg'),
        },
        {
          body: {
            fill: "rgba(219, 127, 142, 0.05)",
            stroke: "rgb(219, 127, 142)",
          },
          label: {
            text: state.standerLabel,
            fill: 'rgb(219, 127, 142)',
            },
          image: require('/src/assets/Scheduler/DataStandred.svg'),
        },
      ]
      
      1. 左侧模型栏节点样式:

        初始化链接桩

      const ports = {
       groups: {
         in: {
           position: 'top',
           attrs: {
             circle: {
               r: 4,
               magnet: true,
               stroke: '#108ee9',
               strokeWidth: 2,
               fill: '#fff',
               style: {
                 visibility: "hidden",
               },
             }
           }
         },
         out: {
           position: 'bottom',
           attrs: {
             circle: {
               r: 4,
               magnet: true,
               stroke: '#31d0c6',
               strokeWidth: 2,
               fill: '#fff',
               style: {
                 visibility: "hidden",
               },
             }
           }
         }
       },
       items: [
         {
           id: state.currentCode+ '_in',
           group: 'in',
         },
         {
           id: state.currentCode + '_out',
           group: 'out',
         },
       ],
      }
      

      根据样式数据,设计画布左侧节点样式

      Graph.registerNode( //注册节点
        'custom-node',
        {
          inherit: 'rect', //基础图形
          width: 140,
          height: 76,
          attrs: {  //自定义样式
            body: {  //节点主体
              strokeWidth: 1,
              rx: 30,
              ry: 30,
            },
            image: {  //图片
              width: 20,
              height: 20,
              x: 12,
              y: 3,
            },
            text: {  //主题文本
              refX: 40,
              refY: 15,
              fontSize: 15,
              'text-anchor': 'start',
            },
            label: {  //标签名
              text: 'Please nominate this node',
              id: 0,  //自定义传给接口的数据
              data: {},  //这里是我加入的标签内自定义储存的数据内容
              refX: 10,
              refY: 30,
              fontSize: 12,
              fill: 'rgba(0,0,0,0.6)',
              'text-anchor': 'start',
              textWrap: {  //如果节点命名标签过长可以使用这个属性来定义文本内容
                width: -10,      // 宽度减少 10px
                height: '70%',   // 高度为参照元素高度的一半
                ellipsis: true,  // 文本超出显示范围时,自动添加省略号
                breakWord: true, // 是否截断单词
              }
            },
          },
          markup: [ //组合
            {
              tagName: 'rect',
              selector: 'body',
            },
            {
              tagName: 'image',
              selector: 'image',
            },
            {
              tagName: 'text',
              selector: 'text',
            },
            {
              tagName: 'text',
              selector: 'label',
            },
          ],
          data: {},
          relation: {},
          ports: { ...ports }, //链接桩定义
        },
        true,
      )
      const imageNodes = imageShapes.map((item) => //呈现画布节点数据的样式
        graph.createNode({
          shape: 'custom-node',
          attrs: {
            image: {
              'xlink:href': item.image,
            },
            body: item.body,
            text: item.label,
          },
        }),
      )
      stencil.load( //载入左侧节点模型库
        imageNodes,
        "processLibrary"
      );
      

      6.定义边的样式(节点连线)

      Graph.registerConnector(
        'algo-edge',
        (source, target) => {
          const offset = 4
          const control = 80
          const v1 = { x: source.x, y: source.y + offset + control }
          const v2 = { x: target.x, y: target.y - offset - control }
          return `M ${source.x} ${source.y}
                  L ${source.x} ${source.y + offset}
                  C ${v1.x} ${v1.y} ${v2.x} ${v2.y} ${target.x} ${target.y - offset}
                  L ${target.x} ${target.y}`
        },
        true,
      )
      

      以及可以在初始化画布graph = new graph({})方法中,定义连线规则

      // 连线规则
      connecting: {
        snap: true,  // 当 snap 设置为 true 时连线的过程中距离节点或者连接桩 50px 时会触发自动吸附
        allowBlank: false,  // 是否允许连接到画布空白位置的点,默认为 true
        allowLoop: false, // 是否允许创建循环连线,即边的起始节点和终止节点为同一节点,默认为 true
        allowMulti: false, // 当设置为 false 时,在起始和终止节点之间只允许创建一条边
        highlight: true,  // 拖动边时,是否高亮显示所有可用的连接桩或节点,默认值为 false。
        sourceAnchor: {  // 当连接到节点时,通过 sourceAnchor 来指定源节点的锚点。
          name: 'bottom',
          args: {
            dx: 0,
          },
        },
        targetAnchor: {  // 当连接到节点时,通过 targetAnchor 来指定目标节点的锚点。
          name: 'top',
          args: {
            dx: 0,
          },
        },
        connectionPoint: 'anchor',  // 指定连接点,默认值为 boundary。
        connector: 'algo-edge',  // 连接器将起点、路由返回的点、终点加工为 元素的 d 属性,决定了边渲染到画布后的样式,默认值为 normal。
        createEdge() {
          return graph.createEdge({
            attrs: {
              line: {
                strokeDasharray: '5 5',
                stroke: '#808080',
                strokeWidth: 1,
                targetMarker: {
                  name: 'block',
                  args: {
                    size: '6',
                  },
                },
              },
            },
          })
        },
        validateMagnet({ magnet }) {
          return magnet.getAttribute('port-group') !== 'in'
        },
        validateConnection({
          sourceView,
          targetView,
          sourceMagnet,
          targetMagnet
        }) {
          if (sourceView === targetView) {
            return false;
          }
          if (!sourceMagnet) {
            return false;
          }
          // 只能连接到输入链接桩
          if (
            !targetMagnet ||
            targetMagnet.getAttribute("port-group") !== "in"
          ) {
            return false;
          }
          return true;
        },
        // 当停止拖动边的时候根据 validateEdge 返回值来判断边是否生效,如果返回 false, 该边会被清除。
        validateEdge({ edge }) {
          const { source, target } = edge
          return true
        }
      },
      

      7.方法们

      graph.toJSON()  //  数据导出
      graph.fromJSON()  //  数据渲染
      graph.isPannable()  //  画布是否可以平移
      graph.enablePanning()  //  启用画布平移
      graph.centerContent();  //  中心对称
      

      8.绑定事件

      graph.on('node:added', ({ node, cell }) => {}) // 节点移入画布事件
      graph.on("cell:dblclick", ({ node, cell }) => {});  //  节点双击事件
      graph.on("node:mouseenter", ({ node }) => {  //  节点删除操作
        // 鼠标 Hover 节点时添加删除按钮
        node.addTools({
          name: "button-remove",
          args: {
            x: 0,
            y: 0,
            offset: { x: 10, y: 10 },
            markup: [  //自定义的删除按钮样式
              {
                tagName: 'circle',
                selector: 'button',
                attrs: {
                  r: 8,
                  stroke: '#F25C54',
                  strokeWidth: 1,
                  fill: 'rgba(214, 40, 40, 0.25)',
                  cursor: 'pointer',
                },
              },
              {
                tagName: 'text',
                textContent: '✕',
                selector: 'icon',
                attrs: {
                  fill: '#F25C54',
                  fontSize: 7,
                  textAnchor: 'middle',
                  pointerEvents: 'none',
                  y: '0.3em',
                },
              },
            ],
          },
        });
      });
      graph.on("node:removed", ({ node, options }) => {  //  删除节点事件
        if (!options.ui) {
          return;
        }
      });
      graph.on("node:mouseleave", ({ node }) => {
        // 鼠标移开节点时删除删除按钮
        node.removeTools();
      });
      // 线删除操作
      graph.on("edge:mouseenter", ({ edge }) => {
        // 鼠标 Hover 边时添加删除按钮
        edge.addTools([
          "target-arrowhead",
          {
            name: "button-remove",
            args: {
              distance: -30,
            },
          },
        ]);
      });
      graph.on("edge:removed", ({ edge, options }) => {  //  移除连线事件
        if (!options.ui) {
          return;
        }
        const cellId = edge.getTargetCellId()
        const target = graph.getCellById(cellId)、
        if (target) {
          const id = target.ports.items[0].id
          target && target.setPortProp(id, 'connected', false)
        }else{
          target && target.setPortProp(cellId+'_in', 'connected', false)
        }
      });
      graph.on("edge:mouseleave", ({ edge }) => {
        // 鼠标移开边时删除删除按钮
        edge.removeTools();
      });
      graph.on('node:change:data', ({ node }) => {  //  修改节点数据的实时响应事件
        node.data = eachNodeData
      })
      graph.on("node:contextmenu", ({ cell, view }) => {  //  节点文本
        const oldText = cell.attr("text/textWrap/text");
        const elem = view.container.querySelector(".x6-edit-text");
        if (elem == null) {
          return;
        }
        cell.attr("text/style/display", "none");
        if (elem) {
          elem.style.display = "";
          elem.contentEditable = "true";
          elem.innerText = oldText;
          elem.focus();
        }
        const onBlur = () => {
          cell.attr("text/textWrap/text", elem.innerText);
          cell.attr("text/style/display", "");
          elem.style.display = "none";
          elem.contentEditable = "false";
        };
        elem.addEventListener("blur", () => {
          onBlur();
          elem.removeEventListener("blur", onBlur);
        });
      });
      const container = document.getElementById("flowContainer");
      graph.on("node:mouseenter",FunctionExt.debounce(() => {  //  节点链接桩显隐
          const ports = container.querySelectorAll(".x6-port-body");
          showPorts(ports, true);
        }),
        500
      );
      graph.on("node:mouseleave", () => {  //  节点链接桩显隐
        const ports = container.querySelectorAll(".x6-port-body");
        showPorts(ports, false);
      });
      graph.bindKey("backspace", () => {  //  点按空格后跳出带有节点的画布区域
        const cells = graph.getSelectedCells();
        if (cells.length) {
          graph.removeCells(cells);
        }
      });
      

      其他有趣的用法

      //设置指定路径上的属性值cell.attr('text/text', value)  其中label/text为自定义标题
      cell.attr('label/text', state.flinkLabel)
      cell.attr('label/data', j)
      

      9.根据接口数据回显画布流程图

      //就是通过这个方法来实现的
      graph.fromJSON(nodeData)  //  数据渲染,括号内填入回显的数组数据变量
      //而nodeData在前面2.提出的数据准备中初始化定义了,我们在接口返回的数据里填充数据和样式就搞定回显了
      

      总结

      在vue项目中,使用AntV X6实现节点和画布、连线的配置,非常灵活,一旦会用后有相当多的方法可以使用,自由度和定制度高。还有许多用法可以实现,如点击执行指令显示流程图的动画运行流:

      【VUE3+AntV X6】 引入ANTV X6 的流程图编辑器应用(一) 第6张

      还有自定义的布局等

      【VUE3+AntV X6】 引入ANTV X6 的流程图编辑器应用(一) 第7张

      剩下的其他拓展方法等我下次整理了再发,今天这篇太长了,肝不动了TvT。

      参考资料

      1. ANTV-X6 流程图
      2. ANTV-X6 快速上手

    免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们,邮箱:ciyunidc@ciyunshuju.com。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

    目录[+]