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