供稿人:许庆洋
维护员工信息时,可以上传由配置 bropen.framework.osm.employee.signature.img.size 制定大小的手写签名图片,签名图片可以通过配置 p:opinion 标签的 signature 属性,显示在意见栏中,或者调用相应的API实现Word或WPS套打。
在下面的项目需求中,需要在表单的上方显示所有审批人的手写签名,并按照一定规则排序:
- 流程处于起草状态时,不显示签名;
- 如果流程被退回,则前面的签名都擦除;
- 按照分组显示意见;
- 如果任务列表中存在相同任务环节名,并且他们的任务实际办理人相同,且存在已完成的任务,则擦除已完成任务的环节签名;
- 签名显示分3类:审批环节的签名、审议环节的签名、总裁/董事长的签名:
集团领导的审批任务,放到总裁、董事长审批行;
关键任务列表,按时间顺序排列、根据审批人查重,放到审批行;
其他任务列表,按照级别(员工title)排序、根据审批人查重,放到审议行; - 每行最多显示8条签名,签名图片大小为100*40;
- 当为已办、或代理人打开待办、或者非任一人处理时,签名的标题处显示“代理”二字;
- 超时自动提交的任务签名处显示“系统提交”;
- 否决的任务签名处显示“否决”;
- 决定性意见为“不同意”的任务签名处显示“不同意”;
- 其他办理完毕的任务签名不存在,签名处则显示“已处理”。
签名栏显示效果如下图所示:

由于这是一个共性的需求,可以自定义一个显示手写签名栏的taglib标签,以下实现代码:
/**
* 手写签名栏,放在表单的<h1>标签前
* 例如:<boe:signature bean="${xxxxInstance}" />
* @attr bean REQUIRED
*/
def signature = {attrs->
if ( !attrs.bean?.id ) return;
ProcessInstance flowInst = attrs.bean.workflowInstance;
def proc = flowInst.processDefinition();
// 流程处于起草状态时,不显示签名
if ( flowInst.startNode() == flowInst.nodesName ) return;
//
List tasks = [], criticalNodes = [], otherNodes = [], leaderTasks, criticalTasks, otherTasks;
// 需要显示签名的任务列表:非起草环节的待办
Task lastDraftTask = flowInst.tasks[0]; // 计算最后一次起草环节的任务:签名从这里开始,一旦退回,签名擦除
for ( t in flowInst.tasks ) if (t!=lastDraftTask && t.type==Task.TYPE_TODO && t.node==lastDraftTask.node && t.status!='terminated') lastDraftTask = t;
for ( t in flowInst.tasks ) if (t.type==Task.TYPE_TODO && t.createTime>lastDraftTask.createTime && t.status!='terminated') tasks << t;
// 如果退回到环节A,则不显示原来在环节A的任务-被退回来的环节A的任务(不含)
// 例如路径 A-B-C-A,其中C-A是退回,则不显示前面三个任务(A-B-C)的签名
tasks = CollectionUtils.sort(tasks, 'asc', 'id');
for ( int i=0; i<tasks.size(); i++ ) {
if ( !tasks[i] ) continue;
String node = tasks[i].node;
for ( int j=i+1; j<tasks.size(); j++ ) {
if ( tasks[j].node==node && tasks[j].sendType==Task.SEND_TYPE_BACK ) {
List prevs = tasks[j].prevAllTasks();
for ( int k=i; k<j; k++ ) if ( prevs.contains(tasks[k]) ) tasks[k] = null;
break;
}
}
}
// 如果任务列表中存在相同任务环节名,并且他们的任务实际办理人相同,且存在已完成的任务,则擦除已完成任务的环节签名
for ( int i=0; i<tasks.size(); i++ ) {
if ( tasks[i] == null || tasks[i].completeTime ) continue;
for ( int j=0; j<i; j++ ) {
if ( tasks[j] && tasks[j].completeTime && tasks[j].node==tasks[i].node && tasks[j].actorId==tasks[i].actorId ) tasks[j] = null;
}
}
tasks -= null;
// 集团领导的审批任务,放到总裁、董事长审批行
Closure uniqueClosure = {it.node + "@" + it.actorId};
Closure sortSwapper = { List valList, Date completeTime, Task task->
return completeTime ?: task.createTime;
}
leaderTasks = CollectionUtils.findAll(tasks, "actorTitle", ["总裁","董事长"]); // 后面再查重,下面两段代码还用这个list变量
// 关键任务列表,按时间顺序排列、根据审批人查重,放到审批行
for ( n in proc.values() ) if (n instanceof Map && n.node && n.critical ) criticalNodes << n.node;
criticalTasks = CollectionUtils.sort(
CollectionUtils.sort(CollectionUtils.findAll(tasks, "node", criticalNodes)-leaderTasks,
"desc", "completeTime").unique(uniqueClosure), "asc", "completeTime", sortSwapper);
// 其他任务列表,按照级别(员工title)排序、根据审批人查重,放到审议行
for ( n in proc.values() ) if (n instanceof Map && n.node && !n.critical ) otherNodes << n.node;
otherTasks = CollectionUtils.sort(
CollectionUtils.findAll(tasks, "node", otherNodes)-leaderTasks, "desc",
"completeTime", sortSwapper).unique(uniqueClosure);
otherTasks = otherTasks.sort{-app.AppUtils.getLevelByTitle(it.actorTitle)};
leaderTasks = leaderTasks.unique(uniqueClosure); // 集团领导的审批任务查重
// 渲染签名表格
StringBuilder sb = new StringBuilder();
sb << '<table style="padding: 5px 0; border-collapse: collapse;">'
renderSignature("审批", criticalTasks, sb);
renderSignature("审议", otherTasks, sb);
renderSignature("总裁、董事长审批", leaderTasks, sb);
sb << '</table>';
out << sb
}
private renderSignature(String caption, List tasks, StringBuilder sb) {
if ( !tasks ) return;
sb << '<tr class="prop"><td class="nameX" style="white-space:normal; text-align:center; vertical-align:middle; width:100px" rowspan="'
sb << (Math.ceil(tasks.size()/8) * 2) << '">' << caption << '</td>'
// 每行最多显示8条签名,签名图片大小为100*40
int max = 8;
for ( int i=0; i<tasks.size(); i+=max ) {
if ( i!=0 ) sb << '<tr class="prop">';
renderSignature( tasks.subList(i,[i+max,tasks.size()].min()), sb );
sb << '</tr>'
}
}
private renderSignature(List tasks, StringBuilder sb) {
for ( t in tasks ) {
sb << '<td class="nameX" style="white-space:normal; text-align:center; vertical-align:middle; width:100px; padding:4px">'
if ( t.actorSubstituteId ) {
// 仅当为已办、或代理人打开待办、或者非任一人处理时,才显示“代理”二字
if ( t.completeTime || t.substituteType==Substitute.TYPE_MOVE || t.actorSubstituteId==session.employeeId ) {
sb << '(代理)<br/>';
}
}
//sb << DateUtils.formatDatetime(t.createTime) << "<br/>"
sb << bropen.framework.core.osm.Organization.splitName(t.organizationFullName,1) << '/' << (t.actorTitle?:"员工") << '<br/>' << t.actor << '</td>'
}
sb << '</tr><tr class="prop" style="height:40px">'
for ( t in tasks ) {
// 计算员工签名:超时自动提交的显示“系统提交”,签名不存在则显示“已处理”
String sigid = null;
if ( t.prevStatus == "timeout" ) {
sigid = "wf_timeout"
} else if ( t.decisiveOpinion == "不同意" ) {
sigid = "wf_disagree";
} else if ( t.transition == "否决" ) {
sigid = "wf_reject";
} else if ( t.actorSubstituteId ) {
sigid = t.actorSubstitute?.isSignatureExists() ? t.actorSubstitute.id : "wf_done";
} else {
sigid = t.actor?.isSignatureExists() ? t.actor.id : "wf_done";
}
// 渲染单元格
sb << '<td class="valueX" style="text-align:center; vertical-align:middle; padding:0px; width:100px">'
if ( t.status=="completed" ) {
sb << '<img width="100" height="40" src="' << createLink(controller:'app', action:'signatureImage', id:sigid) << '"/>'
} else {
sb << " "
}
sb << '</td>'
}
}
上述代码中,显示签名图片的是一个app控制器的 signatureImage 操作,代码如下:
/**
* 显示签名图片(不加水印),用于签名栏
*/
def signatureImage() {
String filename = Employee.signatureFilename(params.id);
File file = new File(filename);
if ( file.exists() && file.canRead() ) {
// 渲染图片,并设置1天的浏览器缓存
osmEmployeeService.renderSignature( response, filename, false,
[validFor:86400, shared:true, store:true, lastModified:file.lastModified()] );
} else render "";
}
此外,也可以直接使用产品中显示签名图片的控制器操作 osm/signatureImage,该操作将返回一个添加了干扰水印的签名图片。
定义完手写签名的taglib后,在form、edit页面上面添加foo:signature标签即可。代码如下:
<div class="body">
<foo:signature bean="${xxxInstance}" />
<h1>${entityName}</h1>
....
</div>
总之,根据需求的不同,我们可以很灵活的定义不同的规则,展示出不同显示效果的签名栏。