用zrender实现工作流图形化设计(附范例代码)
公司研发的管理系统有工作流图形化设计和查看功能,这个功能的开发历史比较久远。在那个暗无天日的年月里,IE几乎一统江湖,所以顺理成章地采用了当时红极一时的VML技术。
后来的事情大家都知道了,IE开始走下坡路,VML这个技术现在早已灭绝,导致原来的工作流图形化功能完全不能使用,所以需要采用新技术来重写工作流图形化功能。
多方对比之后,决定采用zrender库来实现(关于zrender库的介绍,请看http://ecomfe.github.io/zrender/),花了一天的时间,终于做出了一个大致的效果模型,如下图所示:
流程图由两类部件组成:活动部件和连接弧部件,每一类部件包含多个性状不同的部件。
以活动部件为例,圆形的是开始活动,平行四边形是自动活动,长方形是人工活动,等等。
在代码实现上,定义了Unit(部件基类),所有的部件都继承自这个基类。通过Graph类来管理整个流程图,包括所有部件、上下文菜单等等都由Graph来统一管理和调度,代码如下:
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中侦听鼠标移动事件,就是为了实现连接线等相关图形的实时重绘。
每个部件都规划了八个连接点,默认情况下,连接弧不固定与某个连接点,而是根据活动部件的位置关系,自动找出最近的连接点,所以在拖动活动部件的时候,可以看到连接弧在活动部件上的连接点在不断变化。
上面只是以最简化的方式实现了工作流图形化设计的基本功能,完善的图形化设计应包含曲线、连接点的拖放等等,如下图所示:
上面是公司产品中的工作流图形化设计功能,功能相对于上面的范例要完善许多,但基本原理不变,无非就是细节处理更多一些。
特别是在画曲线的地方花了很多时间,中学的平面几何知识几乎都忘记了,所以做起来花了不少功夫,这部分准备以后专门写篇文章来详谈。
本文的结尾会给出前期建模测试阶段的完整代码下载,是前期代码,不是最终代码,原因你懂的,见谅。