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