用zrender实现工作流图形化设计(附范例代码)
公司研发的管理系统有工作流图形化设计和查看功能,这个功能的开发历史比较久远。在那个暗无天日的年月里,IE几乎一统江湖,所以顺理成章地采用了当时红极一时的VML技术。
后来的事情大家都知道了,IE开始走下坡路,VML这个技术现在早已灭绝,导致原来的工作流图形化功能完全不能使用,所以需要采用新技术来重写工作流图形化功能。
多方对比之后,决定采用zrender库来实现(关于zrender库的介绍,请看http://ecomfe.github.io/zrender/),花了一天的时间,终于做出了一个大致的效果模型,如下图所示:
流程图由两类部件组成:活动部件和连接弧部件,每一类部件包含多个性状不同的部件。
以活动部件为例,圆形的是开始活动,平行四边形是自动活动,长方形是人工活动,等等。
在代码实现上,定义了Unit(部件基类),所有的部件都继承自这个基类。通过Graph类来管理整个流程图,包括所有部件、上下文菜单等等都由Graph来统一管理和调度,代码如下:
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 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 | var Libra = {}; Libra.Workflow = {}; Libra.Workflow.Graph = function (type, options){ var graph = this , activities = {}, transitions = {}; var zrenderInstance, contextMenuContainer; this .type = type; this .addActivity = function (activity){ activity.graph = graph; activities[activity.id] = {object:activity}; }; this .getActivity = function (id){ return activities[id].object; }; this .addTransition = function (transition){ transition.graph = graph; transitions[transition.id] = {object:transition}; }; function modElements(shapes){ shapes.each( function (shape){ zrenderInstance.modElement(shape); }); return shapes; } // 当前正在拖放的节点 var dragingActivity = null ; // 活动节点拖放开始 this .onActivityDragStart = function (activity){ dragingActivity = activity; }; // 活动节点拖放结束 this .onActivityDragEnd = function (){ if (dragingActivity) refreshActivityTransitions(dragingActivity); dragingActivity = null ; }; // 拖动过程处理 function zrenderInstanceOnMouseMove(){ if (dragingActivity != null ) refreshActivityTransitions(dragingActivity); } // 刷新活动相关的所有连接弧 function refreshActivityTransitions(activity){ var activityId = activity.id; for ( var key in transitions){ var transition = transitions[key].object; if (transition.from === activityId || transition.to == activityId){ zrenderInstance.refreshShapes(modElements(transition.refresh(graph))); } } } // 当前选中的部件 var selectedUnit = null ; this .onUnitSelect = function (unit){ if (selectedUnit) zrenderInstance.refreshShapes(modElements(selectedUnit.unselect(graph))); zrenderInstance.refreshShapes(modElements(unit.select(graph))); selectedUnit = unit; }; // 记录当前鼠标在哪个部件上,可以用来生成上下文相关菜单 var currentUnit = null ; this .onUnitMouseOver = function (unit){ currentUnit = unit; }; this .onUnitMouseOut = function (unit){ if (currentUnit === unit) currentUnit = null ; }; // 上下文菜单事件响应 function onContextMenu(event){ Event.stop(event); if (currentUnit) currentUnit.showContextMenu(event, contextMenuContainer, graph); } this .addShape = function (shape){ zrenderInstance.addShape(shape); }; // 初始化 this .init = function (){ var canvasElement = options.canvas.element; canvasElement.empty(); canvasElement.setStyle({height: document.viewport.getHeight() + 'px' }); zrenderInstance = graph.type.zrender.init(document.getElementById(canvasElement.identify())); for ( var key in activities){ activities[key].object.addTo(graph); } for ( var key in transitions){ transitions[key].object.addTo(graph); } // 创建上下文菜单容器 contextMenuContainer = new Element( 'div' , { 'class' : 'context-menu' }); contextMenuContainer.hide(); document.body.appendChild(contextMenuContainer); Event.observe(contextMenuContainer, 'mouseout' , function (event){ // 关闭时,应判断鼠标是否已经移出菜单容器 if (!Position.within(contextMenuContainer, event.clientX, event.clientY)){ contextMenuContainer.hide(); } }); // 侦听拖动过程 zrenderInstance.on( 'mousemove' , zrenderInstanceOnMouseMove); // 上下文菜单 Event.observe(document, 'contextmenu' , onContextMenu); }; // 呈现或刷新呈现 this .render = function (){ var canvasElement = options.canvas.element; canvasElement.setStyle({height: document.viewport.getHeight() + 'px' }); zrenderInstance.render(); }; }; /* * 部件(包括活动和连接弧) */ Libra.Workflow.Unit = Class.create({ id: null , title: null , graph: null , // 当前是否被选中 selected: false , // 上下文菜单项集合 contextMenuItems: [], initialize: function (options){ var _this = this ; _this.id = options.id; _this.title = options.title; }, createShapeOptions: function (){ var _this = this ; return { hoverable : true , clickable : true , onclick: function (params){ // 选中并高亮 _this.graph.onUnitSelect(_this); }, onmouseover: function (params){ _this.graph.onUnitMouseOver(_this); }, onmouseout: function (params){ _this.graph.onUnitMouseOut(_this); } }; }, addTo: function (graph){}, // 刷新显示 refresh: function (graph){ return []; }, // 选中 select: function (graph){ this .selected = true ; return this .refresh(graph); }, // 取消选中 unselect: function (graph){ this .selected = false ; return this .refresh(graph); }, // 显示上下文菜单 showContextMenu: function (event, container, graph){ container.hide(); container.innerHTML = '' ; var ul = new Element( 'ul' ); container.appendChild(ul); this .buildContextMenuItems(ul, graph); // 加偏移,让鼠标位于菜单内 var offset = -5; var rightEdge = document.body.clientWidth - event.clientX; var bottomEdge = document.body.clientHeight - event.clientY; if (rightEdge < container.offsetWidth) container.style.left = document.body.scrollLeft + event.clientX - container.offsetWidth + offset; else container.style.left = document.body.scrollLeft + event.clientX + offset; if (bottomEdge < container.offsetHeight) container.style.top = document.body.scrollTop + event.clientY - container.offsetHeight + offset; else container.style.top = document.body.scrollTop + event.clientY + offset; container.show(); }, // 创建上下文菜单项 buildContextMenuItems: function (container, graph){ var unit = this ; unit.contextMenuItems.each( function (item){ item.addTo(container); }); } }); |
zrender默认已经支持了对图形的拖动,所以活动部件的拖动只需要设置dragable属性为真即可。不过虽然活动部件可以拖动,但活动部件上的连接线不会跟着一起动,这需要侦听拖动开始事件、拖动结束事件以及拖动过程中的鼠标移动事件,来实现连接线的实时重绘。在Graph中侦听鼠标移动事件,就是为了实现连接线等相关图形的实时重绘。
每个部件都规划了八个连接点,默认情况下,连接弧不固定与某个连接点,而是根据活动部件的位置关系,自动找出最近的连接点,所以在拖动活动部件的时候,可以看到连接弧在活动部件上的连接点在不断变化。
上面只是以最简化的方式实现了工作流图形化设计的基本功能,完善的图形化设计应包含曲线、连接点的拖放等等,如下图所示:
上面是公司产品中的工作流图形化设计功能,功能相对于上面的范例要完善许多,但基本原理不变,无非就是细节处理更多一些。
特别是在画曲线的地方花了很多时间,中学的平面几何知识几乎都忘记了,所以做起来花了不少功夫,这部分准备以后专门写篇文章来详谈。
本文的结尾会给出前期建模测试阶段的完整代码下载,是前期代码,不是最终代码,原因你懂的,见谅。
posted @ 2016-12-05 22:35 良村 阅读(10321) 评论(9) 推荐(5) 编辑