2016年12月5日

用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中侦听鼠标移动事件,就是为了实现连接线等相关图形的实时重绘。

每个部件都规划了八个连接点,默认情况下,连接弧不固定与某个连接点,而是根据活动部件的位置关系,自动找出最近的连接点,所以在拖动活动部件的时候,可以看到连接弧在活动部件上的连接点在不断变化。

上面只是以最简化的方式实现了工作流图形化设计的基本功能,完善的图形化设计应包含曲线、连接点的拖放等等,如下图所示:

上面是公司产品中的工作流图形化设计功能,功能相对于上面的范例要完善许多,但基本原理不变,无非就是细节处理更多一些。

特别是在画曲线的地方花了很多时间,中学的平面几何知识几乎都忘记了,所以做起来花了不少功夫,这部分准备以后专门写篇文章来详谈。

本文的结尾会给出前期建模测试阶段的完整代码下载,是前期代码,不是最终代码,原因你懂的,见谅。

 https://files.cnblogs.com/files/rrooyy/WorkflowGraphic.zip

posted @ 2016-12-05 22:35 良村 阅读(10350) 评论(9) 推荐(5)

2016年4月3日

特殊字符\u2028导致的Javascript脚本异常

摘要: 这原本是个小错误,但排查花了不少时间,因此写下来和大家分析一下。通过Ajax动态从后台读取文章内容,并显示在页面上,加载到某篇文章的时候,报javascript语法错误,无法显示文章内容。编码为2028的字符为行分隔符,会被浏览器理解为换行,而在Javascript的字符串表达式中是不允许换行的,从而导致错误。 阅读全文

posted @ 2016-04-03 15:51 良村 阅读(15252) 评论(1) 推荐(6)

2010年12月9日

不同寻常的浏览器请求无响应错误

摘要: 公司的服务器托管在上海漕宝机房,服务器上除了公司网站外,还安装了几套公司自有产品的试用系统(B/S架构的),一直以来运行正常。最近发现有些页面在提交时无反应,等了很久之后显示“Internet Explorer 无法显示该网页”错误;类似的还有一些链接点击之后无反应,等了很久之后显示“Internet Explorer 无法显示该网页”错误。这个错误比较诡异,花了不少时间才排除,特此记录下来供大家参考。 阅读全文

posted @ 2010-12-09 15:04 良村 阅读(3850) 评论(6) 推荐(10)

2010年5月17日

进销存管理中对红冲处理的误区

摘要: 红冲常常被用在进销存管理中,不仅用于修正单据错误,还可用于处理货到发票未到时的暂估入库,所以说,红冲是进销存管理中不可缺少的重要功能。不过需要谨慎的是红冲绝对不能滥用,虽然说红冲是进销存管理中的重要功能,但绝不能把红冲当成是万能良药到处使用。红冲其实是一剂的猛药,虽然见效快,但副作用也很大,这个副作用就是会造成成本异常。 阅读全文

posted @ 2010-05-17 18:34 良村 阅读(3480) 评论(38) 推荐(6)

2010年5月11日

进销存管理中负库存产生的原因以及对应措施

摘要: 负库存是进销存管理中经常出现的一种异常现象,虽说是异常现象,但许多负库存并不是错误。由于产生负库存的原因复杂,且往往会造成库存成本异常,使得负库存成了进销存管理中一个无法回避难题。下面将从着重分析负库存产生的原因,并探讨进销存软件在处理负库存问题上的应对措施。 阅读全文

posted @ 2010-05-11 23:18 良村 阅读(3306) 评论(3) 推荐(0)

2008年12月14日

打造自己的Html文本编辑控件

摘要: 目前有很多开源的Html编辑器可以直接用,这些编辑器功能丰富,开发使用也很方便。但实际使用过程中只需要其中一小部分功能,或者需要扩展一些特殊功能,所以就下决心研究这些开源编辑器代码以便进行扩展。研究过程并不顺利,主要是本人的Javascript水平有限,虽说是开源但注释并不详细,介绍这方面原理的中文资料也很少,好不容易看懂之后觉得有必要自己写一个来加深理解,另外自己写的在版权方面也可以减少一些不必要的麻烦。 阅读全文

posted @ 2008-12-14 01:25 良村 阅读(8450) 评论(29) 推荐(0)

2006年8月19日

探讨对Web控件的异常处理

摘要: 在实际开发中,常常有这样的需求,即页面是由多个相对独立的控件组成,其中一个控件的错误不能影响到其它控件的正常显示。这就需要在控件内部捕捉错误,并自行处理错误,然而控件基类并没有提供这样的错误捕捉功能。如何用简单有效方法来实现呢? 阅读全文

posted @ 2006-08-19 18:45 良村 阅读(2064) 评论(2) 推荐(0)

2005年10月3日

看了一篇不错的文章 - 使用 UTF-8 对 XML 文档进行编码

摘要: 以前就一直很疑惑,为什么XML文档大都采用UTF-8编码?还有.NET中的字符串又是UTF-16编码?看了这篇文章之后就会明白了。使用 UTF-8 对 XML 文档进行编码 阅读全文

posted @ 2005-10-03 11:27 良村 阅读(877) 评论(1) 推荐(0)

2005年8月21日

Ajax学习笔记(2) - 一定要用XML吗?

摘要: Ajax一定要用XML吗?我觉得不一定用。不用XML的理由:1. javascript脚本解析Xml比较慢;2. 对于一些简单数据,用Xml有点大炮轰蚊子的感觉;3. XmlHttp提供了responseText,就是给了大家不用Xml的方便;不用Xml用什么?可以采用技术很多,对于简单数据你可以返回自定义的数据格式,比如,第1位是状态位,第2位之后是数据。对于复杂数据,你甚至可以直接返回java... 阅读全文

posted @ 2005-08-21 12:38 良村 阅读(2954) 评论(11) 推荐(0)

Ajax 学习笔记(1)

摘要: 技术的核心是采用XmlHttp来请求和接收回应。XmlHttp只是一种技术规范,具体的实现上IE和Firefox有所不同,IE是用ActiveX方式,而Firefox是内置实现的。IE下创建XmlHttp对象:A=new ActiveXObject('Msxml2.XMLHTTP');Firefox下创建XmlHttp对象:A=new XMLHttpRequest();还可以用下面的写法,可以比较... 阅读全文

posted @ 2005-08-21 01:21 良村 阅读(3644) 评论(9) 推荐(0)

导航

< 2025年6月 >
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 1 2 3 4 5
6 7 8 9 10 11 12
点击右上角即可分享
微信分享提示