【VUE3+AntV X6】 引入ANTV X6 的流程图编辑器应用(一)
admin 阅读: 2024-04-01
后台-插件-广告管理-内容页头部广告(手机) |
ANTV X6 流程图编辑器应用
- 唠唠叨叨
- 事发背景
- 实现效果
- 实现思路
- 总结
- 参考资料
唠唠叨叨
唉!最近感觉非常忙,要学的东西太多,要实现的方法变幻莫测,层出不穷,越学越觉得自己不会的实在太多。 项目本来中标了,后来又不知道发生了什么,临到签合同又黄了,整的我们开发的瑟瑟发抖,产品整个大改,恐怕要…… 只能说还是抓紧时间好好学习,知识和技术是永远不会背叛自己的。- 1
- 2
- 3
事发背景
俺滴组长(后端大佬)要我配合开发数据平台的任务流全局调度系统,想用流程图实现作业运行配置。 于是我在各种流程图的前端框架中选了ANTV X6来引入实现。官方文档天天在看,可这知识他就是不入脑子呀TvT。 终于有一天腌肉某人一拍脑门醍醐灌顶实现了功能,遂记录实现过程及困难解决方法。- 1
- 2
- 3
在官方文档中给出了场景案例,恰好就有我所需要的流程图:
实现效果
经过一系列拼拼凑凑,修修改改,还有样式设计和调整,调度作业画布页面如下图所示:
实现思路
- 首先我们要了解系统的业务功能需求,以及业务流程的线路是怎样的;
如图是窗口的拆解,其中①作业树使用的是element UI的el-tree组件,是用于储存画布内容和画布回显更新的,②是选中节点树中的某项流程作业进行对应的任务操作,③和④才是实际应用AntV X6的画布插件实现的可拖拽、增、删、改、配置的流程图编辑器。
通过vue的父子组件我们创建一个index.vue的父组件,然后将这四个部分拆分为三个子组件的.vue文件,分别为DataTree.vue、DataButton.vue、FiliationGraph.vue(包含③、④两部分)今天记录的是FiliationGraph.vue内节点配置画布的实现。
2. 安装AntV X6
通过 npm 或 yarn 命令安装 X6。
- 1
- 2
- 3
- 4
- 5
- 初始化画布,在html中建立节点库和画布div容器:
- 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
- 2
- 3
- 4
- 5
- 6
- 7
- 8
准备节点样式内容数据,其中节点的左上角图标image是自定义的svg文件,根据产品ui或自己设计来使用哟。
- 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
- 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。
参考资料
- ANTV-X6 流程图
- ANTV-X6 快速上手
声明
1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,请转载时务必注明文章作者和来源,不尊重原创的行为我们将追究责任;3.作者投稿可能会经我们编辑修改或补充。
在线投稿:投稿 站长QQ:1888636
后台-插件-广告管理-内容页尾部广告(手机) |