流程图绘制在项目中实际是一个复杂的应用,但是因为有很多算法都是和项目中业务相关不一定符合其他小伙伴的实际应用情况并且项目存在保密机制不方便全部分享出来,所以本文章仅抽取最基础的部分简单介绍流程图创建、节点链接、节点删除、菜单操作、重新渲染、本地存储的交互。仅供有类似需求的小伙伴参考,效果图如下:


具体实现的业务效果有如下几点

  1. 节点拖拽功能,节点任意链接;
  2. 默认节点锚点(入点、出点)动态设置;
  3. 节点、边线菜单操作,快捷键操作;
  4. 节点状态切换,画布一键清除;
  5. 根据业务场景加入动态参数配置;
  6. 节点事件监听根据业务需求动态添加逻辑;
  7. 流程图数据再渲染;
  8. 不同模式切换以及支持的操作;

一、安装相关依赖组件

  1. 流程图绘制是基于antv/g6的基础 :
    安装命令:npm install @antv/g6 -S
    官方文档:https://www.yuque.com/antv/g6/api-g6
  2. 拖拽功能使用的是jquery里的拖拽插件:
    安装命令:npm install jquery -S
    npm install jquery-ui -S

二、思路梳理组件拆分


从上面简单的思路图可以看出,新增、编辑、展示、提交数据这种都属于业务部分,流程图这块属于他们的公用部分,所以把流程图这块单独拆分为一个公用组件,并支持不同业务的能力。

  1. 绘制流程图公用组件;
  2. 新增/编辑可以拆分为同一个业务组件;
  3. 详情展示一个业务组件;

三、流程图公用组件部分

  1. 理解基本概念:画布、图形、 节点(形状、文字)、锚点、边线具体理解可以参考G6官方文档;
  2. 流程图公用组件这部分,是基于antv/g6的绘制能力结合vuex状态管理的存储的一个综合运用;
    a. 因业务独特的交互需求,运用了G6的自定义能力;
    b. 自定义节点,包括节点形状、文本、锚点设计、状态切换样式等设置;
    c. 自定义边线,包括边线形状、箭头、动画效果等设置;
    d. 自定义右键菜单事件,菜单动态位置、展示等设置;
    e. 自定义添加边线的事件;
    f. 自定义键盘快捷键事件;
    g. 自定义节点交互事件;
    h. 自定义边线交互事件;
    i. 自定义节点、边线交互状态样式切换事件;
(1) AppFlows.vue公用组件代码如下:
<template><div class="sys-flows" ref="sysflows" :id="graphContainer"><!-- 节点右键菜单 --><ul class="sys-flows-contentMenu" :style="contentMenuStyle"><!-- <li><i class="el-icon-document-copy"></i> 编辑</li> --><li @click="graph_Remove"><i class="el-icon-delete-solid"></i> 删除</li></ul></div>
</template>
<script>
/*** @description 全局公用流程图组件* @author      sunsy*/
import G6 from '@antv/g6';
import customNode from '@/util/G6/customNode.js';
import customEdge from '@/util/G6/customEdge.js';
import addMenu from '@/util/G6/behavor_addMenu.js';
import addEdge from '@/util/G6/behavor_addEdge.js';
import keyboard from '@/util/G6/behavor_keyboard.js';
import hoverNode from '@/util/G6/behavor_hoverNode.js';
import hoverEdge from '@/util/G6/behavor_hoverEdge.js';
import selectNode from '@/util/G6/behavor_selectNode.js';
export default {name:'AppFlows',props:{// 需要渲染的画布的数据initData: {type: Object,default: function() {return {nodes: [],edges: [],}}},// 需要画布支持的模式(上面有提到业务场景中有新增、编辑、渲染)几种场景下需要支持的操作不同,用模式来区分initMode: {type: String,default: function() {return 'default';}}},data(){return {graph: null,graphData: this.initData,graphContainer: 'dragger-target',contentMenuStyle: '',}},computed: {// 监听计算业务中右键菜单的变化contentMenuShow(){return this.$store.state.graphs.contentMenuShow;},// 监听计算业务中当前画布操作的事件events(){return this.$store.state.graphs.events;}},watch: {// 监听右键菜单样式来控制位置变化contentMenuShow(nval,oval) {this.contentMenuStyle = this.$store.state.graphs.contentMenuStyle;},// 监听画布操作事件变化后回执到业务回调事件具体业务具体处理events(nval,oval) {if(nval!==oval) {this.$emit('getEvents',nval);}},// 监听渲染数据变化重新渲染画布initData(nval,oval) {if(nval!==oval) {this.graphData = nval;this.graph_Read();}},//监听多标签打开路由变化,更新store的缓存的当前实例$route(){let graphCache     = this.$store.state.graphs.graphCache;let graphCacheKeys = Object.keys(graphCache);let graphCacheKey  = this.$route.path;if(graphCacheKeys.includes(graphCacheKey)) {this.$store.dispatch('graphs/updateGraph');}},},created() {this.init();},mounted(){this.$nextTick(() => {this.graph_Init();});},methods:{init() {//注册自定义节点customNode.init();//注册自定义边线customEdge.init(); //注册自定义行为到G6const behavors = {'hover-node': hoverNode,'add-edge': addEdge,'select-node': selectNode,'hover-edge': hoverEdge,'keyboard':keyboard,'add-menu':addMenu};for (let key in behavors) {G6.registerBehavior(key, behavors[key])}},//初始化G6graph_Init() {const _this  = this;const width  = this.$refs.sysflows.clientWidth;const height = this.$refs.sysflows.clientHeight;const modes  = this.graph_Mode();const graph  = new G6.Graph({container: _this.graphContainer,width: width,height: height,fitView: false,  //是否开启画布自适应。开启后图自动适配画布大小minZoom: 0.5,maxZoom: 2,// plugins: [grid],// groupByTypes: true,modes: modes,defaultNode:  {shape: 'customNode',size: [ 120, 50 ],},defaultEdge: {shape: 'customEdge',style: {radius: 20,offset: 45,endArrow: true,lineWidth: 1,stroke: '#1A9CFF'}},edgeStateStyles: {selected: {stroke: '#1A9CFF'}}});_this.$store.dispatch('graphs/init',graph);_this.graph = _this.$store.state.graphs.graph;_this.graph_Read();},//渲染G6画布graph_Read() {const _this     = this;const graphData = this.graphData;if(graphData.nodes && graphData.nodes.length>0) {_this.graph.read(graphData);_this.$store.dispatch('graphs/refresh');}},//删除节点graph_Remove() {this.$store.dispatch('graphs/remove');this.$store.dispatch('graphs/cotentmenu',{ show:false, style: ''});},//读取G6需要的功能graph_Mode() {let modes;let mode   = this.initMode;if(mode =='editor') {//开启编辑模式的时候开放编辑相关能力modes = { default: ['drag-canvas', 'zoom-canvas', 'drag-node','select-node','keyboard','hover-node','hover-edge','add-menu',],addEdge: ['add-edge'],}} else {//默认为显示模式modes = { default: ['drag-canvas', 'zoom-canvas', 'select-node','hover-node','hover-edge'],}}return modes;}}
}
</script>
<style lang="scss" scoped>
.sys-flows{position: relative;width: 100%;height: 100%;background-color: rgb(242, 242, 242);background-size:10px 10px;background-image:linear-gradient(transparent 9px,#dedede 9px,#dedede 10px),linear-gradient(90deg,transparent 9px,#dedede 9px,#dedede 10px);cursor: default;&-contentMenu{position: absolute;width:80px;height:auto;z-index:2; // left: 10px;// top: 10px;display: none; cursor: pointer; background:#fff;color: #333;border-radius:3px; box-shadow: 0px 2px 8px rgba(0, 0, 0,0.15);overflow: hidden;li{text-align: center;line-height: 30px;&:hover{background: #E9F9FE;}}}.g6-tooltip {border: 1px solid #e2e2e2;border-radius: 4px;font-size: 12px;color: #545454;background-color: rgba(255, 255, 255, 0.9);padding: 10px 8px;box-shadow: rgb(174, 174, 174) 0px 0px 10px;}
}
</style>

公用组件中使用了G6自定义能力,自定义注册行为有多个js文件,以下我会选几个重要行为展示,不都全部展示出来,有需要的下载示例代码自己查看即可。

(2)customNode.js自定义节点代码如下:
/*** @description 全局公用流程图组件-自定义节点* @author      sunsy*/
import G6 from '@antv/g6';
import store from '@/store';
export default {init() { //********************************注册自定义节点**********************************const customNode = {draw(cfg, group) {console.log(G6)let  size = cfg.size;if(!size){size = [120 , 50];}const shapeId = 'rect' + G6.Util.uniqueId();const width  = parseInt(size[0]);const height = parseInt(size[1]);const offsetX  = width / 2;const offsetY  = height / 2;const fillColor  = '#E8F8FE';const stokeColor = '#ACDAF8';// 创建节点const rect = group.addShape('rect', {attrs: {id: shapeId,x: -offsetX,y: -offsetY,width: width,height: height,radius: 5,stroke: stokeColor,fill: fillColor,lineWidth: 1,},});// 父节点中创建文本形状if (cfg.label) {let text = cfg.label;if(text.length >8) {text = text.substr(0,8) + '\n' + text.substr(8);}group.addShape('text', {attrs: {text: text,x: 0,y: 0,fill: '#6A6C6D',fontSize: 14,textAlign: 'center',textBaseline: 'middle',fontWeight: 'bold',parent: shapeId,}});}// 判断Graph实例处于编辑模式绘制锚点const graph   = store.state.graphs.graph;const modes   = graph._cfg.modes;if(modes.addEdge) {const linkPoints = [[-offsetX,0], [offsetX,0], [0,-offsetY], [0,offsetY]];for (let i = 0; i < linkPoints.length; i++) {let inId  = 'circle-in-' + G6.Util.uniqueId();let outId = 'circle-out-' + G6.Util.uniqueId();group.addShape('circle', {attrs: {id: outId,parent: inId,x: linkPoints[i][0],y: linkPoints[i][1],r: 10,fill: '#1A9CFF',opacity: 0,isOutPointOut: cfg.isOutPoint,isInPointOut: cfg.isInPoint,},});group.addShape('circle', {attrs: {id: inId,x: linkPoints[i][0],y: linkPoints[i][1],r: 4,fill: '#fff',stroke: '#1A9CFF',opacity: 0,isOutPoint: cfg.isOutPoint,isInPoint: cfg.isInPoint,},});}}return rect;},// 切换节点状态样式setState(name, value, item) {const group = item.getContainer();const shape = group.get("children")[0]; const children = group.findAll(g => {return g._attrs.parent === shape._attrs.id});const circles = group.findAll(circle => {return circle._attrs.isInPoint || circle._attrs.isOutPoint;});const defaultStyles = () => {shape.attr({'fill':'#E8F8FE', 'stroke':'#ACDAF8'});circles.forEach(circle => {circle.attr('opacity', 0)});};const selectedStyles = () => {shape.attr({'fill':'#94D6FC', 'stroke':'#1A9CFF','cursor':'move'});children.forEach(child => {child.attr('cursor', 'move');});circles.forEach(circle => {circle.attr('opacity', 1)});};const successStyles = () => {shape.attr({'fill':'#90B44B', 'stroke':'#ACDAF8'});} const warningStyle = () => {shape.attr({'fill':'#FAD689', 'stroke':'#D19826'});}switch (name) {case "success":if(value) {successStyles();} else {defaultStyles();}break;case "warning":if(value) {warningStyle();} else {defaultStyles();}break;case "hover":case "selected":if (value) {selectedStyles()} else {defaultStyles()}break;}},};// 注册到g6G6.registerNode('customNode', customNode);}
};

自定义节点文件里,可以根据ui图和业务交互自行定义需要的形状,不同交互状态样式,文字溢出等设置。

(3)customEdge.js 自定义边线代码如下:
/*** @description 全局公用流程图组件-自定义边线* @author      sunsy*/
import G6 from '@antv/g6';
export default {init() {const dashArray = [[0, 1],[0, 2],[1, 2],[0, 1, 1, 2],[0, 2, 1, 2],[1, 2, 1, 2],[2, 2, 1, 2],[3, 2, 1, 2],[4, 2, 1, 2]];const lineDash = [4,2,1,2];const interval = 9;//***************************************注册自定义边***************************************const customEdge = {draw(cfg, group) {let sourceNode, targetNode, start, end;if (typeof (cfg.source) === 'string') {cfg.source = cfg.sourceNode}if(!cfg.start){cfg.start={x: 0,y: 17}}if(!cfg.end){cfg.end={x: 0,y: -17}}if (!cfg.source.x) {sourceNode = cfg.source.getModel()start = { x: sourceNode.x + cfg.start.x, y: sourceNode.y + cfg.start.y };} else {start = cfg.source}if (typeof (cfg.target) === 'string') {cfg.target = cfg.targetNode;}if (!cfg.target.x) {targetNode = cfg.target.getModel();end = { x: targetNode.x + cfg.end.x, y: targetNode.y +  cfg.end.y };} else {end = cfg.target}let path = [];let hgap = Math.abs(end.x - start.x);if (end.x > start.x) {path = [['M', start.x, start.y],['C',start.x,start.y + hgap / (hgap / 50),end.x,end.y - hgap / (hgap / 50),end.x,end.y - 4],['L',end.x,end.y]];} else {path = [['M', start.x, start.y],['C',start.x,start.y + hgap / (hgap / 50),end.x,end.y - hgap / (hgap / 50),end.x,end.y - 4],['L',end.x,end.y]];}let lineWidth = 1;let MIN_ARROW_SIZE = 3;lineWidth = lineWidth > MIN_ARROW_SIZE ? lineWidth : MIN_ARROW_SIZE;const width = lineWidth * 10 / 3;const halfHeight = lineWidth * 4 / 3;const radius = lineWidth * 4;const endArrowPath = [['M', -width, halfHeight],['L', 0, 0],['L', -width, -halfHeight],['A', radius, radius, 0, 0, 1, -width, halfHeight],['Z']];const keyShape = group.addShape('path', {attrs: {id: 'edge' + G6.Util.uniqueId(),path: path,stroke: '#b8c3ce',//边的点击宽度(值越大越容易点击线)lineAppendWidth: 20,//结束箭头路径endArrow: {path: endArrowPath,d: 1}}});return keyShape},afterDraw(cfg, group) {if (cfg.source.getModel().isOutPoint && cfg.target.getModel().isInPoint) {//添加虚线轨迹动画const shape = group.get('children')[0];const length = shape.getTotalLength(); // G 增加了 totalLength 的接口let totalArray = [];for (var i = 0; i < length; i += interval) {totalArray = totalArray.concat(lineDash);}let index = 0;shape.animate({onFrame() {const cfg = {lineDash: dashArray[index].concat(totalArray)};index = (index + 1) % interval;return cfg;},repeat: true}, 3000);}},setState(name, value, item) {const group = item.getContainer();const shape = group.get("children")[0];const selectStyles = () => {shape.attr("stroke", "#6ab7ff");};const unSelectStyles = () => {shape.attr("stroke", "#b8c3ce");};switch (name) {case "selected":case "hover":if (value) {selectStyles();} else {unSelectStyles(); }break;}}};G6.registerEdge('customEdge', customEdge);//************************************注册自定义边线虚线***************************************const linkEdge = {draw(cfg, group) {let sourceNode, targetNode, start, end;if (!cfg.source.x) {sourceNode = cfg.source.getModel()start = { x: sourceNode.x + cfg.start.x, y: sourceNode.y + cfg.start.y };} else {start = cfg.source}if (!cfg.target.x) {targetNode = cfg.target.getModel()end = { x: targetNode.x + cfg.end.x, y: targetNode.y + cfg.end.y };} else {end = cfg.target;}let path = [];path = [['M', start.x, start.y],['L', end.x, end.y]];const keyShape = group.addShape('path', {attrs: {id: 'edge' + G6.Util.uniqueId(),path: path,stroke: '#1890FF',strokeOpacity: 0.9,lineDash: [5, 5]}});return keyShape},};G6.registerEdge('link-edge', linkEdge);}
};

自定义边线文件中,可以设置边线的头和尾形状、边线的形状、动画效果等。

(4)behavor_addEdge.js自定义节点、边线绘制事件代码如下:
/*** @description 全局公用流程图组件-注册画线行为* @author      sunsy*/
import G6 from '@antv/g6';
import store from '@/store';
let startPoint = null;
let startItem  = null;
let endPoint   = {};
let activeItem = null;
let curInPoint = null;
export default {getEvents() {return {'mousemove': 'onMousemove','mouseup': 'onMouseup','node:mouseover': 'onMouseover','node:mouseleave': 'onMouseleave'};},onMouseup(e) {// 判断当前对象是节点const item = e.itemif (item && item.getType() === 'node') {const group = item.getContainer()// 判断当前对象是否是入点,是则循环节点中子节点中存在入点外圈元素并设置为目标锚点if (e.target._attrs.isInPoint) {const children = group._cfg.children;children.map(child => {if (child._attrs.isInPointOut && child._attrs.parent === e.target._attrs.id) {activeItem = child;}});curInPoint = e.target;// 判断当前对象是否有出点,是则循环节点中子节点中存在出点外圈元素并设置为目标锚点} else if (e.target._attrs.isInPointOut) {activeItem = e.target;const children = group._cfg.children;children.map(child => {if (child._attrs.isInPoint && child._attrs.id === e.target._attrs.parent) {curInPoint = child}});}// 判断如果存在目标锚点,创建一条两个节点间的边线基本数据if (activeItem) {const endX = parseInt(curInPoint._attrs.x);const endY = parseInt(curInPoint._attrs.y);endPoint = { x: endX, y: endY };if (this.edge) {this.graph.removeItem(this.edge);const model = {id: 'edge-' + G6.Util.uniqueId(),source: startItem,sourceId: startItem._cfg.id,target: item,targetId: item._cfg.id,start: startPoint,end: endPoint,shape: 'customEdge',type: 'edge'}store.dispatch('graphs/add',{type:model.type, model:model});}} else {// 删除未找到目标节点的边线 if (this.edge) {this.graph.removeItem(this.edge);}}} else {if (this.edge) {this.graph.removeItem(this.edge);}  }// 上面执行完成重新切换画布中所有节点的锚点展示状态样式this.graph.find("node", node => {const group = node.get('group');const children = group._cfg.children;children.map(child => {if (child._attrs.isInPointOut) {child.attr('opacity', 0);}if (child._attrs.isInPoint) {child.attr('opacity', 0);}if (child._attrs.isOutPoint) {child.attr({'opacity': 0, 'fill': '#fff'});}})})// 切换开始节点的状态样式if (startItem) {this.graph.setItemState(startItem, 'hover', false);}this.graph.paint();startPoint = null;startItem = null;endPoint = {};activeItem = null;curInPoint = null;this.graph.setMode('default');},// 鼠标移动过程中,不断计算和切换边线开始和结束点的坐标位置并修改边线存储数据onMousemove(e) {const item = e.item;if (!startPoint) {this.graph.find("node", node => {const group = node.get('group');const children = group._cfg.children;children.map(child => {if (child._attrs.isInPointOut) {child.attr('opacity', 0.3);}if (child._attrs.isInPoint) {child.attr('opacity', 1);}})});const startX = parseInt(e.target._attrs.x);const startY = parseInt(e.target._attrs.y);startPoint = { x: startX, y: startY };startItem = itemthis.edge = this.graph.addItem('edge', {source: item,target: item,start: startPoint,end: startPoint,shape: 'link-edge'});} else {const point = { x: e.x, y: e.y };if (this.edge) {// 增加边的过程中,移动时边跟着移动this.graph.updateItem(this.edge, {//  start: startPoint,target: point});}}},// 鼠标经过锚点设置锚点的高亮效果onMouseover(e) {const item = e.item;if (item && item.getType() === 'node') {if (e.target._attrs.isInPointOut && !this.hasTran) {this.hasTran = true;//添加translate平移和scale缩放效果e.target.transform([['t', 0, 3],['s', 1.2, 1.2],]);}this.graph.paint();}},// 鼠标离开锚点重置锚点的高亮效果onMouseleave() {this.graph.find("node", node => {const group = node.get('group');const children = group._cfg.children;children.map(child => {if (child._attrs.isInPointOut) {child.resetMatrix();}})})this.hasTran = false;this.graph.paint();}
};

自定义行为中监听鼠标行为,动态切换锚点交互、边线和节点的样式、锚点样式等业务逻辑。
其他的例如右键菜单、节点选中、节点点击、键盘事件监听等行为就不详细说明和代码展示,具体代码都会在下载包里大家可以自己研究参考。

(5)利用Vuex管理流程图数据、状态、事件等行为

提示:流程图最重要G6实例、(节点、边线、状态)数据、行为存储部分主要靠vuex实现。

  1. 因为我的项目是多标签打开并可以保持的项目框架,所以需要考虑多个G6实例同时存在的情况,切换标签页面不刷新的情况又能保持渲染对应的g6实例,所以需要考虑多G6实例对象存储;
  2. 因此Vuex定义的状态树中:graphCache、graphCacheKey、graphCacheName是比较特殊的变量;
  3. 以上三个特殊变量主要是做多实例存储、切换实例使用,其他剩余变量都是针对单个g6实例数据存储的,graphCache存储的其实是排除以上三个变量数据的缓存集合。
  4. 当切换标签页面的时候,实际是根据状态树中的graphCacheKey,从graphCache集合中拿到对应的实例然后在分别赋值给当前实例的相关状态;

store/modules/graphs/index.js 代码如下

import router from '@/router'
const state = {//Graph实例对象缓存列表graphCache: {},//当前访问的Graph实例对象键名graphCacheKey: null,//添加到Graph实例对象列表需要排除的属性graphCacheName: ['graphCache', 'graphCacheKey','graphCacheName','events'],//Graph实例对象graph: {},//Graph实例数据data:{nodes: [],edges: [],},//Graph实例当前事件events: {},//Graph实例当前选中对象selectedItem: null,//Graph实例当前选中节点selectedNode: null,//Graph实例当前选中边线selectedEdge: null,//Graph实例右键菜单显示/隐藏状态contentMenuShow: false,//Graph实例右键样式contentMenuStyle: '',
}const mutations = {setGraphCacheKey:  (state, data) => {state.graphCacheKey = data;},setGraphCache: (state, data) => {state.graphCache[data.key] = data.val;},setUpdateGraph:  (state) => {const cur      = state.graphCacheKey;const curGraph = state.graphCache[cur];for(let key in state) {if(!state.graphCacheName.includes(key)) {state[key] = curGraph[key];}}},setGraph: (state, data) => {state.graph = data;},setData: (state, data) => {state.data = {nodes: data.nodes,edges: data.edges};},setSelectedItem: (state, data) =>{if(data.select) {let model = data.target.getModel();let type  = data.target.getType();if(type === 'node') {state.selectedNode = model;} else if(type === 'edge') {state.selectedEdge = model;}state.selectedItem = data.target;} else {state.selectedItem = null;state.selectedNode = null;state.selectedEdge = null;}},setCotentMenu: (state, data) =>{state.contentMenuShow = data.show;state.contentMenuStyle = data.style;},setEvents: (state, data) =>{state.events  = data;}
}const getters = {//读取节点/边线getById:  (state)=> (data)=>{const node = state.graph.findById(data.id);return node;},
}const actions = {//设置当前激活的graph实例,便于存储多graph实例setGraphCache: ({ commit, state }) =>  {const route  = router.currentRoute;const path   = route.path;commit('setGraphCacheKey',path); },//更新Graph实例列表缓存updateGraphCache: ({ commit, state }) =>  {let key = state.graphCacheKey;let obj = {}; for(let key in state) {if(!state.graphCacheName.includes(key)) {obj[key] = state[key];}}commit('setGraphCache',{key: key ,val: obj });},//读取缓存更新graph实例updateGraph : ({ commit, dispatch }) =>  {dispatch('setGraphCache').then(()=>{commit('setUpdateGraph');});},//存储初始化graph实例init: ({ commit, dispatch }, data) =>  {commit('setGraph', data);dispatch('setGraphCache').then(()=>{dispatch('updateGraphCache');});},//新增节点/边线add: ({ dispatch, state }, data) =>  {let type  = data.type;let model = data.model;if(type === 'edge') {//重复边线处理/有向无环处理let edges = state.data.edges;let ishas = edges.some(item=> {return (item.source == model.sourceId  && item.target == model.targetId) || (item.source == model.targetId && item.target == model.sourceId) || (model.sourceId == model.targetId);});if(ishas) {return;}}state.graph.add(type, model);dispatch('refresh').then(()=>{dispatch('selectedstate',{select: true, target: model});});},//修改节点/边线update: ({ dispatch, state }, data) =>  {const node = state.graph.findById(data.id);state.graph.update(node, data);dispatch('refresh');},//删除节点/边线remove: ({ dispatch, state }, data) =>  {let selected = state.selectedItem;if(!data && !selected) {return false;}if(!data) {data     = selected.getModel();}const node = state.graph.findById(data.id)state.graph.remove(node);dispatch('refresh');},//更新graph数据集合refresh: ({ commit, dispatch, state }, data) =>  {if(!data) {let data =  state.graph.save();commit('setData', data);} else {commit('setData', data);}dispatch('updateGraphCache');},//修改选中节点/边线selectedstate: ({ state, dispatch }, data) =>  {let graph = state.graph;let node  = graph.findById(data.target.id);// graph.trigger('node:click');if(node._cfg.type === 'node') {const selected = graph.findAllByState('node', 'selected');selected.forEach(node => {if (node !== item) {graph.setItemState(node, 'selected', false);}});graph.setItemState(node, 'selected', data.select);let item  = { select: data.select, target: node };dispatch('selected',item);}},//更新选中节点/边线存储数据selected: ({ commit, dispatch }, data) =>  {commit('setSelectedItem', data);dispatch('updateGraphCache');},//更新graph显示/隐藏右键菜单cotentmenu:  ({ commit, dispatch }, data) =>  {commit('setCotentMenu', data)dispatch('updateGraphCache');},//更新graph实例事件,便于公用组件监听回执,不同页面回调执行自己的业务逻辑events: ({ commit, dispatch }, data) =>  {commit('setEvents', data);dispatch('updateGraphCache');},//清除graph实例数据并刷新clear: ({ commit, dispatch, state }, data) =>  {commit('setData', data);commit('setEvents', {});commit('setSelectedItem', { target: null, select: false});state.graph.clear();state.graph.refresh();dispatch('updateGraphCache');}
}export default {namespaced: true,state,mutations,actions,getters,
}

以上部分基本是公用流程图组件的相关部分了,剩下的就是根据具体业务场景的使用。

四、新增/编辑模型业务(拖拽+流程图应用)

算子数据准备
  1. 因业务需求我们的算子都是读取后端已经设置好的基本算子数据,不允许动态生成算子,参数配置和交互这一块也是相当负责和不同,为了方便理解Demo展示里我做成了简单参数配置交互形式。具体数据格式参考下图:

  2. 当然固定算子这部分业务逻辑换成动态生成也很方便,只要稍微调整下前面这块的逻辑即可,便可以生成简单动态流程图绘制工具,这块留着大家自行发挥吧,不做演示。

  3. 根据业务需求自定义的节点中我设置了四个固定的锚点,会根据以上初始化节点数据中isInPointisOutPoint 来动态变化节点是否开启入点和出点。

  4. 具体的切换场景如下几种详细演示和说明:


算子和节点的关系理解

因业务不同可能大家不理解本业务中的算子和节点的关系,其实大家可以理解业务中的算子就是预置好的节点基本信息,设置好节点的名称、出点、入点等。

新增和编辑业务结合为一个组件
<template><div class="sys-page"><div class="models-flows"><!-- 左侧算子区域 --><div class="page-panel page-panel-primary models-flows-left"><div class="panel-head"><h5>模型算子</h5></div><div class="panel-body"><ul ref="dragger-source" id="draggerTarget"><li v-for="(item,index) in butnList" :key="index" class="dragger-button" :data-id="item.label">{{item.name}}</li></ul></div></div><!-- 中间绘制区域 --><div class="page-panel page-panel-primary models-flows-middle">      <el-row :gutter="10" class="panel-head"><el-col :lg="12"><h5>新增模型</h5></el-col><el-col :lg="12" class="textR"><el-button type="primary" size="mini" @click="resetModelFlow"><i class="el-icon-delete"></i> 重置画板</el-button><el-button type="success" size="mini" @click="saveModelFlow" :disabled="graphData.nodes.length==0 ? true: false"><i class="el-icon-document-checked"></i> 保存为模型</el-button></el-col></el-row><div class="panel-body"><AppFlows initMode="editor" :initData="detailInfo" @getEvents="getGraphEvents"></AppFlows></div></div><!-- 右侧模型区域 --><div class="page-panel page-panel-primary models-flows-right"><el-row :gutter="10" class="panel-head"><el-col :lg="12"><h5>参数配置</h5></el-col><el-col :lg="12" class="textR"><el-button type="primary" size="mini" @click="setCurrentNode"><i class="el-icon-document-checked"></i> 保存</el-button></el-col></el-row><div class="panel-body"><el-form ref="form" :model="modelInfo" size="small" label-width="100px"><el-form-item label="名称:" prop="name">{{modelInfo.name}}</el-form-item><el-form-item label="开始日期:" prop="startDate"><el-date-picker placeholder="选择日期" v-model="modelInfo.startDate" value-format="yyyy-MM-dd" style="width:100%"></el-date-picker></el-form-item><el-form-item label="结束日期:" prop="endDate"><el-date-picker placeholder="选择日期" v-model="modelInfo.endDate" value-format="yyyy-MM-dd" style="width:100%"></el-date-picker></el-form-item><el-form-item label="备注:" prop="desc"><el-input type="textarea" v-model="modelInfo.desc" placeholder="输入备注"></el-input></el-form-item></el-form></div></div></div></div>
</template>
<script>
/**5. @description 模型新增/编辑6. @author      sunsy*/
import AppFlows from '../appflows'
import $ from 'jquery';
import 'jquery-ui/ui/widgets/draggable';
import 'jquery-ui/ui/widgets/droppable';
import { mapState,mapGetters,mapActions } from 'vuex'
export default {components:{AppFlows,},data() {return {//用户信息userInfo: JSON.parse(localStorage.getItem('userInfo')),//拖拽目标容器draggerTarget: 'draggerTarget',//选中节点对象modelInfo: {},//编辑模型详情对象detailInfo: {}};},computed: {...mapState({// 分析任务列表butnList: state=> state.basic.taskType,// 绘制数据graphData: state=> state.graphs.data,// 选中节点selectedNode: state=> state.graphs.selectedNode,}),},created() {this.getDetail();},mounted() {this.initLeftFunc();},methods: {// 读取模型详情getDetail() {//读取接口数据赋值初始化画布数据this.detailInfo = {nodes: [],edges: []}},// 初始化左侧按钮initLeftFunc(){const _this = this;$( ".dragger-button").draggable({appendTo: '#' + _this.draggerTarget,cursor: 'move',helper: 'clone',opacity: 0.35,revert: 'invaild',start:function(ev,ui){},drag: function(ev, ui){},stop: function(ev){if(ev.toElement.nodeName === 'CANVAS') {let id       = ev.target.dataset.id;let butnInfo = _this.$store.getters['basic/getTaskType'](id);const nodeId = _this.graphData.nodes.length;const node = {id: id + '-' + nodeId,label: butnInfo.name, x: ev.offsetX,y: ev.offsetY,type: butnInfo.label,isInPoint: butnInfo.isInPoint,isOutPoint: butnInfo.isOutPoint,}_this.$store.dispatch('graphs/add',{type:'node',model:node}).then(()=>{                            _this.modelInfo  = {name: butnInfo.name,desc: null,startDate: null,endDate: null}});}},});},//获取画布当前事件getGraphEvents(data) {if(data.type ==='node:click') {if(this.selectedNode.params) {this.modelInfo = this.selectedNode.params;} else {this.modelInfo  = {name: this.selectedNode.label,desc: null,startDate: null,endDate: null}}}},//修改节点参数setCurrentNode(){let nodeList = this.graphData.nodes;for(let i = 0; i< nodeList.length; i++) {if(nodeList[i].id == this.selectedNode.id) {nodeList[i].params = this.modelInfo;this.$store.dispatch('graphs/update',nodeList[i]);this.$message({ message: "参数保存成功!", type: "success" });break;}}},//保存为模型弹窗saveModelFlow(){let nodesList = this.graphData.nodes;let edgesList = this.graphData.edges;for(let key in nodesList) {if(!nodesList[key].params) {this.$store.dispatch('graphs/selectedstate',{ select: true,  target: nodesList[key]});this.$message({ message: "请补全选中的节点参数并保存!", type: "warning" });break;}}// if(edgesList.length ==0) {//     this.$message({ //         message: "请确保节点之间只有一个逻辑关系的连接线!", //         type: "warning" //     });// }//其他业务处理然后数据保存到服务器           },//重置画板resetModelFlow() {const data = {nodes:[],edges:[],};this.$store.dispatch('graphs/clear', data);this.modelInfo  = {};this.detailInfo = {};},},
};
</script>
<style lang="scss" scoped>
.models-flows{display: flex;justify-content: space-between;height: calc(100vh - 150px);margin-top: 20px;overflow: hidden;&-left{width: 13%;ul{padding:20px;li{padding:10px;text-align: center; list-style: none; background: #3BC0B3;color: #fff;border:solid 1px #3BC0B3;border-radius:5px;margin-top: 15px;cursor: pointer;&:hover,&:nth-child(n+6){border-color: #3BC0B3;color: #fff;background-color: #26766E;}}}}&-middle{width: 69.7%;.panel-body{padding: 0;height: 100%;}}&-right{width: 17%;position: relative;.panel-body{height: 92%;overflow-y: auto;color: #fff;/deep/ .el-form-item__label {color: #fff;}}}
}
</style>
  1. 新增根据拖入到画布的算子的基本信息和位置,画布中创建对应的节点数据;
  2. 编辑的时候读取画布需要的数据格式,然后根据公用组件的intData参数传入到公用组件中渲染;
  3. 编辑状态其实就是多出了数据初始化部分,其余部分和新增状态操作基本一致。

五、读取详情渲染流程图

  1. 详情状态一般只是做展示简单的交互即可,不像编辑和新增状态需要那么多交互,所以通过参数控制定义一个简单模式即可。initMode参数可以不传,因为公用组件里默认为展示简单交互模式。
  2. 如需展示详细参数,利用getEvents回执事件,监听单击、双击等事件处理即可。
  3. 详情组件完整排版和应用不作介绍比较简单,文章内容有点长了。大家理解即可,流程图应用部分示例代码:
  <AppFlows :initData="detailInfo" @getEvents="getGraphEvents"></AppFlows>

六、Demo代码下载


终于写完了,这个坑填的有点久抱歉,最近事情比较多一直都是陆陆续续的,而且项目比较久了具体业务相当复杂,为了单独摘出可演示Demo又自己回顾了一次以前的自己写代码逻辑。

不得不吐槽当时我做这个功能的时候简直要秃顶爆肝,项目组单独给我预留了调研和开发的时间,当时的g6官方文档不是现在的版本,不得不吐槽当时的官方文档简直没法看,我自认为理解能力是比较强的人,但看他们的文档简直痛苦到极点。

不过现在官方改版后的版本发现好太多了,不管怎么样还是要感谢有了g6的基础和他们咨询群小伙伴的答疑才能根据自己的业务需求实现自己复杂的业务功能。

完整demo下载地址:https://download.csdn.net/download/sunshouyan/12820045

希望本文章对有需求的有参考价值,具体演示代码稍后会上传资源库,大家可以根据需求下载参考。

七、懒人先立flag挖坑占位…


下一篇文章更新利用node/express模块搭建前端数据库,模拟后端创建业务接口。

Vuex和antv/g6实现在线绘制分析流程图功能相关推荐

  1. vue office在线编辑_VUE和Antv G6实现在线拓扑图编辑

    我使用的是G6 2.0,也可以使用 G6 3.0,3.0的拓扑图单独作为一个编辑器使用,使用更加方便.不过2.0的比较简单,容易上手. 1.首先在Antv官网上找到蚂蚁Antv G6插件,引入插件. ...

  2. 数据流程图模板分享 怎样绘制数据流程图

    数据流程图是一种全面的描述系统数据流程的主要工具,用一组流程图符号来展示信息,可以更具数据流程图判断信息的流程,处理以及存储.这么好用的流程图要怎样绘制呢?下面是分享的几款数据流程图模板以及在线绘制数 ...

  3. vue中使用antv/g6 绘制关系图、结构图

    使用antv/g6绘制关系图 效果图 代码实现 npm install @antv/g6 --save <template><div id="app">&l ...

  4. ANTV/G6 绘制网络拓扑图

    最近看其他项目绘制网络拓扑图用了vue-super-flow 绘制的不是太理想,所以自己研究了一下,尝试用antv/g6绘制了一下. 参看了官方api https://g6.antv.vision/z ...

  5. 在线绘制富集分析多组气泡图和单细胞分析marker基因矩阵气泡图

    常规的GO或者KEGG通路富集分析结果通常以气泡图的形式展示,然而这个气泡图仅仅是一个比较的结果,如果想在一张图上展示多个比较的结果,就需要用到多组气泡图(图1,左侧). 单细胞RNA-seq分析结果 ...

  6. antv G6绘制流程图

    先贴上实现的效果图 G6官网,关于G6的使用,可以自行进入官网查看. 按照官网可以绘制出简单的流程 但是当实际绘制流程图时,简直是要被连接线的折点给搞崩溃. 节点配置项 anchorPoints:指定 ...

  7. antv g6 禁止移动_十 AntV

    ← Highcharts AntV 是蚂蚁金服全新一代数据可视化解决方案,致力于提供一套简单方便.专业可靠.无限可能的数据可视化最佳实践. AntV 包括以下解决方案: G2:可视化引擎 G2Plot ...

  8. AntV G6 的坑之——从卡掉渣到满帧需要几步

    AntV G6 是一款图可视化与分析开源引擎.<AntV G6 的坑之--XXX>系列文章持续更新中,总结常见问题及神坑解决方案.任何问题可在 GitHub Issue 中提问,求 Git ...

  9. JGG | 这么漂亮的Venn网络竟然可以一步在线绘制?

    2021年8月2日,JGG在线发表了中国中医科学院黄璐琦院士团队和中国科学院遗传与发育生物学研究所刘永鑫高级工程师合作题为"EVenn: Easy to create repeatable ...

最新文章

  1. nagios 监控NFS
  2. C++ primer - - 第一部分
  3. 20分钟构建属于自己的 Linux 发行版
  4. 如何在pycharm添加扩张工具
  5. 信息学奥赛一本通1349-最优布线问题
  6. Spring MVC:表单处理卷。 2 –复选框处理
  7. Ubuntu 18的中文界面切换《图解教程》亲测成功
  8. 为什么linux远程一进入sudo su就卡住_Linux 实战(上)
  9. Win7(包括32和64位)使用GitHub
  10. ASP.NET自定义控件示例:ASP.NET Custom control with designer integration
  11. ThreadLocal源码学习
  12. 企业信息系统网络安全整改方案
  13. 计算机 睡眠 无法打印,打印机脱机无法打印解决教程
  14. [渝粤教育] 岭南师范学院 文学创意写作 参考 资料
  15. lr mysql 增删改查_ssh增删改查流程
  16. jdk12switch表达式
  17. 《预训练周刊》第52期:屏蔽视觉预训练、目标导向对话
  18. Bind9源代码分析
  19. 谷歌浏览器突然不能翻译了怎么解决?无法翻译此网页的解决方法
  20. 消防应急通讯平台设计

热门文章

  1. 【网络】IPV4数据报头部格式
  2. Spinnaker 高可用安装构思与实践
  3. Layui 弹出层模块
  4. 如何更改一个特定提交的提交作者?
  5. git 得到某次提交之前的代码
  6. Vim的 IDE:SpaceVim
  7. RichView TRVStyle ParaStyles
  8. oracle增量备份新建数据库,Linux ORCLE数据库增量备份脚本
  9. 【Python刷题篇】——Python数据分析 01 查看数据(一)
  10. 微信小程序搜索框防抖