最近呢写了一个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,从而解决自己问题

Logo

前往低代码交流专区

更多推荐