vue中使用mxgraph杂谈记
mxgraph
最近呢写了一个vue项目,是一个正常的后台管理系统,除开权限管理/二级路由/增删改查 这些常规的功能外,值得再提的就是在这个vue项目里使用了mxgraph这个库。
简单介绍下这个库,mxgraph是一个外国的库,主要功能是用来画流程图,即图形化展示,是一个蛮大型的库,里面封装了前端后端的各种方法,提供的操作功能也是很强大丰富,但有一个门槛,那就是他的文档是全英文的,百度一圈儿会发现并没有什么较完整的中文文档可以参考,只有一些别人写的demo可以下载来看看,所以这篇帖子也是当作这次使用mxgraph的记录吧。
第一:使用mxgraph的需求
在配置界面点击每条记录的’流程图’按钮,可以跳转到其对应的流程图页面,当没有配置过流程图时,是一个新的流程图配置页面,当之前配置过且保存成功后,其对应的流程图页面是回显之前的图形化界面。
跳转代码:
刚开始用的是router做跳转并带上每个记录的id,后来发现这样做后有个问题,那就是做的按键‘删除’‘前进’‘撤销’功能必须要刷新一次界面才会生效,方法的监听/执行都是完全正常的,但为什么必须要刷新后才能生效,这个问题暂且没搞明白,可能是vue和mxgraph库里的封装有冲突吧;后来就用的window.open来新开一个窗口并带上所需参数,这样做后就不用刷新也能正常生效按键快捷键的功能。
第二:两种场景
1.没有配置过时
是一个全新的配置界面,这其实是初始化了一个新的mxgraph对象,我们可以从左边的操作栏拉出不同的节点,左下角是一个outline窗口,用于界面图形的缩略展示,窗口可以移动展示界面位置,且可以放大/缩小界面节点
节点和线条在双击时都会展示模态框,模态框是判断节点/线条是否配置过信息来展示不同信息;在创建线条时,会做判断是否有源节点和目标节点,当缺少一个时就创建失败,保存时,判断节点和线条的配置信息是否配置,提醒节点/线条id号
2.配置过的信息做回显
图形的回显是借助的mxgraph封装的xml解析方法,节点/线条也都能双击出模态框做展示或者再编辑的功能
第三:代码展示
import {
mxConstants as MxConstants
} from 'mxgraph/javascript/mxClient'
// const outputIcon = 'static/icon/output.png'
// const inputIcon = 'static/icon/input.png'
const start = 'static/icon/start.png'
const end = 'static/icon/end.png'
const call = 'static/icon/call.png'
const rule = 'static/icon/rule.png'
const tstart = 'static/icon/tstart.png'
const tend = 'static/icon/tend.png'
export const toolbarItems = [
{
icon: start,
title: '开始',
nodetype:'START',
width: 50,
height: 50,
style: {
fillColor: 'transparent',
strokeColor: 'transparent',
strokeWidth: '1',
shape: MxConstants.SHAPE_LABEL,
align: MxConstants.ALIGN_CENTER,
verticalAlign: MxConstants.ALIGN_BOTTOM,
imageAlign: MxConstants.ALIGN_CENTER,
imageVerticalAlign: MxConstants.ALIGN_TOP,
image: start
}
},
{
icon: end,
title: '结束',
nodetype:'END',
width: 50,
height: 50,
style: {
fillColor: 'transparent', // 填充色
strokeColor: 'transparent', // 线条颜色
strokeWidth: '1', // 线条粗细
shape: MxConstants.SHAPE_LABEL, // 形状
align: MxConstants.ALIGN_CENTER, // 水平方向对其方式
verticalAlign: MxConstants.ALIGN_BOTTOM, // 垂直方向对其方式
imageAlign: MxConstants.ALIGN_CENTER, // 图形水平方向对其方式
imageVerticalAlign: MxConstants.ALIGN_TOP, // 图形方向对其方式
image: end // 图形
}
},
{
icon: rule,
title: '规则',
nodetype:'RULE',
width: 50,
height: 50,
style: {
fillColor: 'transparent', // 填充色
strokeColor: 'transparent', // 线条颜色
strokeWidth: '1', // 线条粗细
shape: MxConstants.SHAPE_LABEL, // 形状
align: MxConstants.ALIGN_CENTER, // 水平方向对其方式
verticalAlign: MxConstants.ALIGN_BOTTOM, // 垂直方向对其方式
imageAlign: MxConstants.ALIGN_CENTER, // 图形水平方向对其方式
imageVerticalAlign: MxConstants.ALIGN_TOP, // 图形方向对其方式
image: rule // 图形
}
},
{
icon: call,
title: '调用',
nodetype:'CALL',
width: 50,
height: 50,
style: {
fillColor: 'transparent', // 填充色
strokeColor: 'transparent', // 线条颜色
strokeWidth: '1', // 线条粗细
shape: MxConstants.SHAPE_LABEL, // 形状
align: MxConstants.ALIGN_CENTER, // 水平方向对其方式
verticalAlign: MxConstants.ALIGN_BOTTOM, // 垂直方向对其方式
imageAlign: MxConstants.ALIGN_CENTER, // 图形水平方向对其方式
imageVerticalAlign: MxConstants.ALIGN_TOP, // 图形方向对其方式
image: call // 图形
}
},
{
icon: tstart,
title: '多线程开始',
nodetype:'TSTART',
width: 50,
height: 50,
style: {
fillColor: 'transparent', // 填充色
strokeColor: 'transparent', // 线条颜色
strokeWidth: '1', // 线条粗细
shape: MxConstants.SHAPE_LABEL, // 形状
align: MxConstants.ALIGN_CENTER, // 水平方向对其方式
verticalAlign: MxConstants.ALIGN_BOTTOM, // 垂直方向对其方式
imageAlign: MxConstants.ALIGN_CENTER, // 图形水平方向对其方式
imageVerticalAlign: MxConstants.ALIGN_TOP, // 图形方向对其方式
image: tstart // 图形
}
},
{
icon: tend,
title: '多线程结束',
nodetype:'TEND',
width: 50,
height: 50,
style: {
fillColor: 'transparent', // 填充色
strokeColor: 'transparent', // 线条颜色
strokeWidth: '1', // 线条粗细
shape: MxConstants.SHAPE_LABEL, // 形状
align: MxConstants.ALIGN_CENTER, // 水平方向对其方式
verticalAlign: MxConstants.ALIGN_BOTTOM, // 垂直方向对其方式
imageAlign: MxConstants.ALIGN_CENTER, // 图形水平方向对其方式
imageVerticalAlign: MxConstants.ALIGN_TOP, // 图形方向对其方式
image: tend // 图形
}
}
]
首先是npm这个库的依赖,其次,这个库的核心模块都是封装在mxClient.js这个文件里,所以用import方式引入,这里面定义的数组项就是工具栏里的各个各个类型节点,图标是放在静态文件static里面。
当没有使用webpack工具时,需要在项目的根目录下,加一个配置文件如图,指明这个文件配置了当遇到mxClient.js时所要使用的loder及解析的模块。
当使用了webpack工具时,应在webpack.base.conf.js里加上这一个配置,也是指明当遇到mxClient.js时要使用的loader及解析模块。
//创建图表
createGraph() {
let self = this ;
self.graph = new MxGraph(self.$refs.container);
let style = [];
style[MxConstants.STYLE_SHAPE] = MxConstants.SHAPE_CONNECTOR;
style[MxConstants.STYLE_STROKECOLOR] = '#8800FF';
style[MxConstants.STYLE_ALIGN] = MxConstants.ALIGN_CENTER;
style[MxConstants.STYLE_VERTICAL_ALIGN] = MxConstants.ALIGN_MIDDLE;
style[MxConstants.STYLE_ENDARROW] = MxConstants.ARROW_CLASSIC;
style[MxConstants.STYLE_FONTSIZE] = '12';
//设置背景色:
style[MxConstants.STYLE_LABEL_BACKGROUNDCOLOR] = '#FEFEFE';
self.graph.getStylesheet().putDefaultEdgeStyle( style );
},
//初始化图表
initGraph() {
let self = this ;
if (self.R.isNil(self.graph)) {
return
};
self.graph.setConnectable(true) ; // 允许连线
self.graph.setCellsEditable(false) ; // 不可修改
self.graph.convertValueToString = (cell) => { // 从value中获取显示的内容
// 当为节点时,根据不同节点名称定义不同节点类型
if( cell.edge != true ){
if ( cell.title == '开始' ){
self.nodeMap[cell.id].nodetype = 'START';
}else if( cell.title == '结束' ){
self.nodeMap[cell.id].nodetype = 'END';
}else if( cell.title == '调用' ){
self.nodeMap[cell.id].nodetype = 'CALL';
}else if( cell.title == '多线程开始' ){
self.nodeMap[cell.id].nodetype = 'TSTART';
}else if( cell.title == '多线程结束' ){
self.nodeMap[cell.id].nodetype = 'TEND';
}else if( cell.title == '规则' ){
self.nodeMap[cell.id].nodetype = 'RULE';
};
}
// 定义显示的lable
if( cell.title ){
return '{id:' + cell.id + '}' + cell.title;
}else{
return '{id:' + cell.id + '}';
};
};
// 添加outline
let outline = new mxOutline(self.graph, document.getElementById('graphOutline'));
// 监听双击时的编辑
self.graph.addListener(MxEvent.DOUBLE_CLICK, (graph, evt) => {
const cell = self.R.pathOr([], ['properties', 'cell'], evt);
console.info(cell,typeof cell) // 在控制台输出双击的cell
// 编辑时反映射节点/线条信息
let cellId = cell.id;
// 判断是否点击空白区域
if( cell instanceof Array ){
return;
}else{
// 判断是节点还是线条
if( cell.edge == true ){
let edgeObj = self.edgeMap[cellId];
self.edgeFormObj = deepCopyDataObj( edgeObj );
self.dialogStatus_edge = 'edit';
self.handleOpenEdgeDialog();
}else{
let nodeObj = self.nodeMap[cellId];
// Object.assign(); 此方法只会深拷贝第一层数据,比如nodeObj里的第二层paramList属于第二层数组,那么不会被深拷贝,依然会以nodeMap为数据源进行联动改变
self.nodeFormObj = deepCopyDataObj( nodeObj );
self.dialogStatus_node = 'edit';
self.handleOpenNodeDialog();
};
}
}); // end of DOUBLE_CLICK
// 创建新节点/线条
self.graph.addListener( MxEvent.CELLS_ADDED , (sender, evt) => {
const cell = evt.properties.cells[0];
// 判断是节点还是线条,做不同配置
if ( cell.edge ) { // 当为线条时,判断线条的源节点、目标节点是否存在
// 设置线条title背景色
// let style = [];
// style[MxConstants.STYLE_LABEL_BACKGROUNDCOLOR] = '#FEFEFE';
// self.graph.getStylesheet().putDefaultEdgeStyle(style);
if( cell.source == null || cell.target == null ){
//删除刚刚创建的cell
self.graph.removeCells( [cell] , true );
self.$message.info( '连线必须选择2个不同的节点!' );
return;
}else{
// 配置新线条
let edgeObj = {}; // 创建临时对象
edgeObj.edgeid = cell.id;
edgeObj.edgename = cell.title;
edgeObj.edgesourceid = cell.source.id;
edgeObj.edgetargetid = cell.target.id;
edgeObj.paramList = [];
self.edgeMap[edgeObj.edgeid] = edgeObj; // 将临时对象映射进中转对象
self.postSaveData.edgeList.push( edgeObj ); // 将临时对象存进提交的nodeList数组
};
}else{
// 当为节点时,根据不同节点名称定义不同节点类型
// if ( cell.title == '开始' ){ // 这儿取不出title
// cell.nodetype = 'START';
// }else if( cell.title == '结束' ){
// cell.nodetype = 'END';
// }else if( cell.title == '调用' ){
// cell.nodetype = 'CALL';
// }else if( cell.title == '多线程开始' ){
// cell.nodetype = 'TSTART';
// }else if( cell.title == '多线程结束' ){
// cell.nodetype = 'TEND';
// }else if( cell.title == '规则' ){
// cell.nodetype = 'RULE';
// };
// 配置新节点
let nodeObj = {}; // 创建临时对象
nodeObj.nodeid = cell.id;
nodeObj.nodename = cell.title;
nodeObj.nodetype = cell.nodetype;
nodeObj.failpass = '2';
nodeObj.paramList = [];
self.nodeMap[nodeObj.nodeid] = nodeObj; // 将临时对象映射进中转对象
self.postSaveData.nodeList.push( nodeObj ); // 将临时对象存进提交的nodeList数组
};
}); // end of CELLS_ADDED
//初始化右键菜单
MxEvent.disableContextMenu( self.$refs.container)
self.initContentMenu() // 初始化上下文菜单
self.initUndoManager() // 初始化undo/redo
self.initKeyHandler() // 初始化键盘事件
}, // end of initGraph
//添加元件
addCell(toolItem, x, y) {
let self = this ;
const {width, height} = toolItem
const styleObj = toolItem['style']
const style = Object.keys(styleObj).map((attr) => `${attr}=${styleObj[attr]}`).join(';')
const parent = self.graph.getDefaultParent()
self.graph.getModel().beginUpdate()
try {
let vertex = self.graph.insertVertex(parent, null, null, x, y, width, height, style)
//TODO: 图表中,显示title和编码
vertex.title = toolItem['title']
} finally {
self.graph.getModel().endUpdate()
}
},
//初始化工具栏
initToolbar() {
let self = this ;
const domArray = self.$refs.toolItem
if (!(domArray instanceof Array) || domArray.length <= 0) {
return
}
domArray.forEach((dom, domIndex) => {
const toolItem = self.toolbarItems[domIndex]
const {width, height} = toolItem
//拖放事件
const dropHandler = (graph, evt, cell, x, y) => {
self.addCell(toolItem, x, y)
}
const createDragPreview = () => {
const elt = document.createElement('div')
elt.style.border = '2px dotted black'
elt.style.width = `${width}px`
elt.style.height = `${height}px`
return elt
}
MxUtils.makeDraggable(dom, self.graph, dropHandler, createDragPreview(), 0, 0, false, true)
})
},
//初始化上下文菜单
initContentMenu() {
let self = this ;
self.graph.popupMenuHandler.autoExpand = true;
self.graph.popupMenuHandler.factoryMethod = (menu/*, cell*/) => {
menu.addItem('删除', null, () => {
self.deleteCells({cells: self.graph.getSelectionCells(), includeEdges: true})
})
menu.addSeparator()
menu.addItem('redo', null, () => {
self.redo()
})
menu.addItem('undo', null, () => {
self.undo()
})
}
},
//初始化'撤销管理器'
initUndoManager() {
let self = this ;
self.undoMng = new MxUndoManager()
let listen = (sender, evt) => {
self.undoMng.undoableEditHappened(evt.getProperty('edit'))
}
self.graph.getModel().addListener(MxEvent.UNDO, listen)
self.graph.getView().addListener(MxEvent.UNDO, listen)
},
//初始化键盘事件响应
initKeyHandler() {
let self = this ;
if (!self.graph) {
throw new Error('graph 没有初始化')
}
self.keyHandler = new MxKeyHandler(self.graph);
self.keyHandler.bindControlKey(90, () => {
// 撤销
self.undo()
})
self.keyHandler.bindControlKey(89, () => {
// 前进
self.redo()
})
self.keyHandler.bindKey(46, () => {
// 删除选中的cell
let test = self.graph.getSelectionCells();
self.deleteCells({cells: self.graph.getSelectionCells(), includeEdges: true});
})
},
//连接两个组件
connectCell({parent = null, id = null, source, target, value = '', style = ''}) {
let self = this ;
if (!source || !target) {
throw new Error('source 和 target 不得为空')
}
return self.graph.insertEdge(parent ? parent : self.graph.getDefaultParent(), id, value, source, target, style)
},
//删除组件
deleteCells({cells = [], includeEdges = false, multilevel = true}) {
let self = this ;
if (!cells || !(cells instanceof Array)) {
throw new Error('cells 必须是一个数组')
}
let tmpSet = new Set(cells)
if (multilevel) {
cells.forEach((cell) => {
self.findDeleteCell(cell, tmpSet)
})
}
self.graph.removeCells(Array.from(tmpSet), includeEdges)
},
//查找要删除的组件(级联删除)
findDeleteCell(cell, deleteSet) {
let self = this ;
deleteSet.add(cell)
if (cell.edges) {
cell.edges.forEach((tmpEdge) => {
if (tmpEdge.target !== cell) {
deleteSet.add(tmpEdge.target)
self.findDeleteCell(tmpEdge.target, deleteSet)
}
})
}
},
//获取需要撤销操作、重复操作的组件
getUndoRedoCell() {
let self = this ;
let cells = []
if (self.undoMng) {
let undoIndex = self.undoMng.indexOfNextAdd - 1
if (self.undoMng.history[undoIndex]) {
cells = self.undoMng.history[undoIndex].changes.map((change) => {
if (change.child) {
return change.child
} else {
return change.cell
}
})
}
}
return cells
},
//撤销操作
undo() {
let self = this ;
if (!self.undoMng) {
throw new Error('mxUndoManager 没有初始化')
}
console.info('后退的Cells', self.getUndoRedoCell())
self.undoMng.undo()
},
//重复操作
redo() {
let self = this ;
if (!self.undoMng) {
throw new Error('mxUndoManager 没有初始化')
}
self.undoMng.redo()
console.info('前进的Cells', self.getUndoRedoCell())
},
//保存图表的内容到xml
handleSaveGraphToXml( ){
let self = this ;
var encoder = new MxCodec();
var node = encoder.encode( self.graph.getModel());
// console.log( '图表对应的xml文件内容:' , MxUtils.getXml( node ) );
// 过滤掉已经删除的节点/线条
let cellsObj = self.graph.model.cells;
let newNodeList = [];
let newEdgeList = [];
for( let key in cellsObj ){
if( cellsObj[key].hasOwnProperty('edge') ){
self.postSaveData.edgeList.forEach( item => {
if( item.edgeid == cellsObj[key].id ){
newEdgeList.push(item);
};
});
}else{
self.postSaveData.nodeList.forEach( item => {
if( item.nodeid == cellsObj[key].id ){
newNodeList.push(item);
};
});
};
};
self.postSaveData.nodeList = newNodeList;
self.postSaveData.edgeList = newEdgeList;
self.postSaveData.serviceid = self.flowchartId;
self.postSaveData.processxml = MxUtils.getXml( node );
let formData = deepCopyDataObj( self.postSaveData );
console.log('formData',formData) // 提交对象
// 提交校验
delete cellsObj[0];
delete cellsObj[1];
let unEdgesNodeArr = []; // 用于存储未连线的节点id
for( let key in cellsObj ){
if( !cellsObj[key].edge ){
if( !cellsObj[key].edges || cellsObj[key].edges.length == 0 ){
unEdgesNodeArr.push( cellsObj[key].id );
};
}
};
// 判断是否有未连线节点
if( unEdgesNodeArr.length == 0 ){
for( let key in cellsObj ){
if( cellsObj[key].value == null ){
self.$message({
message: `id为【${cellsObj[key].id}】的配置信息未配置,终止保存!`,
type: 'warning'
});
return;
};
};
// 发送服务器
postSaveFlowchart( formData )
.then(response => {
if(response.data.code == 200){
self.$message({
message: `保存成功`,
type: 'success'
});
}else{
self.$message({
message: response.data.msg,
type: 'danger'
});
}
})
.catch( (err) =>{
});
}else{
self.$message({
message: `id为【${unEdgesNodeArr.join(',')}】的节点未连线,终止保存!`,
type: 'warning'
});
};
},
//从xml解析出图表
handleParseGraphFromXml( graphXml ){
let self = this ;
window['mxGraphModel'] = MxGraphModel
window['mxGeometry'] = MxGeometry
const xmlDocument = MxUtils.parseXml(graphXml);
const decoder = new MxCodec(xmlDocument);
const node = xmlDocument.documentElement;
decoder.decode( node, self.graph.getModel() );
self.$message({
message: `解析成功`,
type: 'success'
});
},
这个是实例化mxgraph对象/初始化/使用各个模块的方法,代码里对功能都做了详细的注释。
html里给一个div添加ref来制定mxgraph实例的容器。
通过import的方式在这个页面引入要使用到的模块。
在vue的mounted的周期里调用创建/实例化mxgraph的方法。
这个文件是用来引入outline模块的,因为mxgraph对outline已经做了封装,所以只需要借用即可,只是这儿需要挂载到window上。
总结:这个项目所使用到的mxgraph库里的模块,其实是比较少的,只用到了基础的几个模块,若有需要其它更多的参考功能,这儿贴几个链接慢慢发掘。
mxgraph官方案例
mxgraph入门实例教程
mxgraph官方文档
可参考的别人写的demo
mxgraph自定义节点属性
最后,通过这个mxgraph库的摸索,有两点感悟,一是需要把原生js好好的系统再学习,即使现在大部分时间都在使用各种框架,但是我们所使用的库都是原生js来封装的,只有把原生js掌握好了,在拿到一个新的库时,就更加容易理解和看懂实现的原理;二是要多多善用github这个平台,在遇到不会的问题时,多去找找别人实现的项目demo,从而解决自己问题。
更多推荐
所有评论(0)