小工具知识网

当前位置: 首页 > 行业知识 > 【VUE3+AntV X6】 引入ANTV X6 的流程图编辑器应用(一)

行业知识

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

2026-11-13 13:12:09 来源:互联网转载

ANTV X6 流程图编辑器应用

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

      唠唠叨叨

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

      事发背景

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

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

      实现效果

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

      实现思路

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

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

      通过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中建立节点库和画布p容器:
      // 引入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) //注册到p中const graph = new Graph({ // 新建画布  container: document.getElementById('flowContainer'),  width: 800,  height: 600,  background: {    color: "#F2F7FA",  },});
      1. 初始化节点、边:

        初始化节点、边数据:

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

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

      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实现节点和画布、连线的配置,非常灵活,一旦会用后有相当多的方法可以使用,自由度和定制度高。还有许多用法可以实现,如点击执行指令显示流程图的动画运行流:

      还有自定义的布局等

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

      参考资料

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

上一篇:服务器时间错误怎么办?

下一篇:【live2D看板娘】为你的网站添加萌萌的二次元板娘,这都拿不下你?