您现在的位置是:首页 > 技术教程 正文

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

admin 阅读: 2024-04-01
后台-插件-广告管理-内容页头部广告(手机)

ANTV X6 流程图编辑器应用

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

唠唠叨叨

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

事发背景

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

在这里插入图片描述
在官方文档中给出了场景案例,恰好就有我所需要的流程图:
在这里插入图片描述

实现效果

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

实现思路

  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
  • 2
  • 3
  • 4
  • 5
  1. 初始化画布,在html中建立节点库和画布div容器:
<-- 节点库 --> <div class="app-stencil" ref="stencilContainer"></div> <-- 画布 --> <div class="app-content" id="flowContainer" ref="container"></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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  1. 初始化节点、边:
    初始化节点、边数据:
const nodeData = { // 节点 nodes: [ ], // 边 edges: [ ], };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

准备节点样式内容数据,其中节点的左上角图标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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  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', }, ], }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

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

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" );
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81

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, )
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

以及可以在初始化画布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 } },
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68

7.方法们

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

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); } });
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121

其他有趣的用法

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

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

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

总结

在vue项目中,使用AntV X6实现节点和画布、连线的配置,非常灵活,一旦会用后有相当多的方法可以使用,自由度和定制度高。还有许多用法可以实现,如点击执行指令显示流程图的动画运行流:
在这里插入图片描述
还有自定义的布局等
在这里插入图片描述
剩下的其他拓展方法等我下次整理了再发,今天这篇太长了,肝不动了TvT。

参考资料

  1. ANTV-X6 流程图
  2. ANTV-X6 快速上手
标签:
声明

1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。

在线投稿:投稿 站长QQ:1888636

后台-插件-广告管理-内容页尾部广告(手机)
关注我们

扫一扫关注我们,了解最新精彩内容

搜索
排行榜