流程图绘制在项目中实际是一个复杂的应用,但是因为有很多算法都是和项目中业务相关不一定符合其他小伙伴的实际应用情况并且项目存在保密机制不方便全部分享出来,所以本文章仅抽取最基础的部分简单介绍流程图创建、节点链接、节点删除、菜单操作、重新渲染、本地存储的交互。仅供有类似需求的小伙伴参考,效果图如下:
具体实现的业务效果有如下几点:
从上面简单的思路图可以看出,新增、编辑、展示、提交数据这种都属于业务部分,流程图这块属于他们的公用部分,所以把流程图这块单独拆分为一个公用组件,并支持不同业务的能力。
<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();
//注册自定义行为到G6
const 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])
}
},
//初始化G6
graph_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文件,以下我会选几个重要行为展示,不都全部展示出来,有需要的下载示例代码自己查看即可。
/**
* @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;
}
},
};
// 注册到g6
G6.registerNode('customNode', customNode);
}
};
自定义节点文件里,可以根据ui图和业务交互自行定义需要的形状,不同交互状态样式,文字溢出等设置。
/**
* @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);
}
};
自定义边线文件中,可以设置边线的头和尾形状、边线的形状、动画效果等。
/**
* @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.item
if (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 = item
this.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();
}
};
自定义行为中监听鼠标行为,动态切换锚点交互、边线和节点的样式、锚点样式等业务逻辑。
其他的例如右键菜单、节点选中、节点点击、键盘事件监听等行为就不详细说明和代码展示,具体代码都会在下载包里大家可以自己研究参考。
提示:流程图最重要G6实例、(节点、边线、状态)数据、行为存储部分主要靠vuex实现。
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,
}
以上部分基本是公用流程图组件的相关部分了,剩下的就是根据具体业务场景的使用。
因业务需求我们的算子都是读取后端已经设置好的基本算子数据,不允许动态生成算子,参数配置和交互这一块也是相当负责和不同,为了方便理解Demo展示里我做成了简单参数配置交互形式。具体数据格式参考下图:
当然固定算子这部分业务逻辑换成动态生成也很方便,只要稍微调整下前面这块的逻辑即可,便可以生成简单动态流程图绘制工具,这块留着大家自行发挥吧,不做演示。
根据业务需求自定义的节点中我设置了四个固定的锚点,会根据以上初始化节点数据中isInPoint和isOutPoint 来动态变化节点是否开启入点和出点。
因业务不同可能大家不理解本业务中的算子和节点的关系,其实大家可以理解业务中的算子就是预置好的节点基本信息,设置好节点的名称、出点、入点等。
<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>
<AppFlows :initData="detailInfo" @getEvents="getGraphEvents"></AppFlows>
终于写完了,这个坑填的有点久抱歉,最近事情比较多一直都是陆陆续续的,而且项目比较久了具体业务相当复杂,为了单独摘出可演示Demo又自己回顾了一次以前的自己写代码逻辑。
不得不吐槽当时我做这个功能的时候简直要秃顶爆肝,项目组单独给我预留了调研和开发的时间,当时的g6官方文档不是现在的版本,不得不吐槽当时的官方文档简直没法看,我自认为理解能力是比较强的人,但看他们的文档简直痛苦到极点。
不过现在官方改版后的版本发现好太多了,不管怎么样还是要感谢有了g6的基础和他们咨询群小伙伴的答疑才能根据自己的业务需求实现自己复杂的业务功能。
完整demo下载地址:https://download.csdn.net/download/sunshouyan/12820045
希望本文章对有需求的有参考价值,具体演示代码稍后会上传资源库,大家可以根据需求下载参考。