再接上篇,目前基本已完成工作流表单属性的自增和页面调整工作;现将步骤和关键代码总结如下:
官方文档及下载地址

关于springboot结合使用的项目,有前辈已经写了帖子并且集成好了
附上博客地址:Activiti工作流学习之SpringBoot整合Activiti5.22.0实现在线设计器(二)
如果你看了官方文档,且也认真看了这篇博客,那么博客中的开源代码你拉下来后,我想基本的功能你已经实现了;表单设计器的页面,你已经能看到了;那么我们现在要做的是工作流的二次开发,即现有的节点表单不满足我们现有的业务需求,我们需要对其进行二次改造,增加我们自己的表单!
新增自定义表单
增加表单之前,我们还需要稍微了解一下angular.js的东西,因为本项目的前端数据绑定是angular模板语法来绑定的;废话不多说,我们来看下前台代码结构:


解释下:properties文件夹下就是表单页面,write-template对应的是编辑页面,popup对应的是弹窗,display对应的是绑定字段属性的一个展示页面;且根据名字,你可以知道xxx-aaa-controller.js代表aaa相关页面的操作js;好,了解这些后你需要知道表单是如何增加的,以上这些只是表单属性操作的页面,我们来看下resources下的stencilset.json;

因为我们是需要到对应的任务节点添加字段,即在用户活动中增加自定义的属性,那么我们找到对应的用户活动在propertypackages中添加我们自定义的package即可:

知道了大概模板结构和方法后,我们照猫画虎;也需要增加对应的三个模板页面和对应的controller.js;如果是弹窗,简单的页面我们直接写参照描述表单的写法,如果是比较复杂的表格筛选这样的,建议使用layui;具体页面可以参考我之前的博客关于layui表格的实践,在modal-body中引入自己写的页面即可,我这里做了个隐藏输入框是为了表单选中后的一个回显!
1 2 3 4
| <div class="modal-body"> <iframe id="role_frame" src="../../../associateDeptDialog.html" frameborder="0" name="myframe" scrolling="yes" width="100%" height="400px"></iframe> <input style="display:none" type="text" ng-model="property.value" id="roles_id"> </div>
|
关于数据传值,特别说明下由于采用iframe标签,涉及到父子页面的传值问题,我们需要先在自定义的页面将值组装好,然后父页面调用子页面的方法拿到返回值;代码如下,在对应的controller中:
1 2 3 4 5 6
| $scope.save = function() { var selectUsers = myframe.window.canSave(); $scope.property.value = selectUsers; $scope.updatePropertyInModel($scope.property); $scope.close(); };
|
改造原有表单逻辑
那么我们要对原有的表单进行改造呢?首先还是要清楚,数据怎么来的怎么绑定的,绑定的数据格式是怎么样的!
特说明一点,此次工作流的二次开发过程中,发现节点代理有候选人候选组的属性,在节点流转的时候我们根据表act_ru_identitylink
即可了解当前处理人;那么我们需要根据我们自己的用户角色来进行选择,则需要改造原有的表单逻辑。
如下,我们修改代理人表单,以实现勾选的方式填充进关联用户和关联角色;需要注意的是,关联用户即候选人和候选组都是多个,改之前是多个input输入框,我们了解到对应的数据结构也是一个数组;我这里多个是以分号分隔,只是展示,真实到后台数据还是原有的数组结构(注意:以逗号隔开,数据保存时会被存为多条,所以这里用&符号隔开)

废话不多说,我们还是来看下实现,对应的popu弹窗页面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <div class="row row-no-gutter"> <div class="form-group"> <label>关联用户(候选人)</label> <button ng-click="chooseUser()" class="btn btn-primary" style="margin-left: 10px;">选择用户</button> <input type="text" class="form-control" ng-model="property.selectUsers" id="users_id" readonly="readonly"> </div> <div class="form-group"> <label>关联角色(候选组)</label> <button ng-click="chooseRole()" class="btn btn-primary" style="margin-left: 10px;">选择角色</button> <input type="text" class="form-control" ng-model="property.selectRoles" id="roles_id" readonly="readonly"> </div> </div>
|
部分controller.js页面,因为两个选择按钮的弹窗一致,只po出一个代码即可,其余不重要的js可以屏蔽掉:
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
| if ($scope.assignment.candidateUsers == undefined || $scope.assignment.candidateUsers.length == 0){ $scope.assignment.candidateUsers = [{value: ''}]; $scope.property.selectUsers = ""; } else { var candidateUsers = $scope.assignment.candidateUsers; var selectUsers = ""; $scope.property.selectUsers = ""; for(var i = 0;i< candidateUsers.length;i++){ if(i < candidateUsers.length - 1){ selectUsers += candidateUsers[i].value + ";"; }else if(i == candidateUsers.length - 1){ selectUsers += candidateUsers[i].value; } } $scope.property.selectUsers = selectUsers; } $scope.chooseUser = function () { layui.use(['layer'], function(){ var layer = layui.layer; layer.open({ type: 2, title: '关联用户选择', shadeClose: true, shade: 0, area: ['75%', '80%'], maxmin: true, content: ['../../../associateUserDialog.html', 'yes'], btn: ['确定', '取消'], yes: function(index, layer0){ var iframeWin0 = window[layer0.find('iframe')[0]['name']]; var selectUsers = iframeWin0.canSave(); console.log(selectUsers); $scope.assignment.candidateUsers = []; var candidateUsers = []; if(selectUsers){ var users_list = selectUsers.split(";") for(var i =0;i< users_list.length;i++){ var map = {}; var user = users_list[i]; map.value = user; candidateUsers.push(map); } } $scope.assignment.candidateUsers = candidateUsers; $scope.property.selectUsers = selectUsers; layer.close(index); }, btn2: function(index, layero){ } }); }); }
|
因为此次修改涉及到二次数据回显:第一次点击代理数据回显到对应的第一层弹窗中,第二次回显到我们自己写的弹窗中,所以associateUserDialog.html页面中canSave方法需要修改一下:
1 2 3 4 5 6 7 8 9 10 11 12
| function canSave(){ userStr = ''; for(var i in selectUser){ if(i == selectUser.length-1){ userStr += selectUser[i].id+"&"+selectUser[i].roleName+"&"+selectUser[i].roleCode; }else{ userStr += selectUser[i].id+"&"+selectUser[i].roleName+"&"+selectUser[i].roleCode+";"; } } window.parent.document.getElementById("roles_id").value = userStr; return userStr; }
|
好,自此我们完成了自定义表单和原有表单的改造;接下来我们来看下,自定义字段后台该怎么处理吧;因为原有表单的改造,我们只是改变了表单填充的方式并没有改变数据结构,所以不需要处理;而我们自定义的表单属性,原有的节点实体是无法识别的,我们需要自己处理下;
后台自定义属性处理
通过了解,我们知道我们自己增加的属性是以对应的json保存在表act_ge_bytearry中的bytes字段中的,那么我们还需要做的就是流程发布的时候,将对应的表单属性解析为对应的xml;我们来看下发布接口:deployment
通过断点调试及源码了解,我们不难发现json 转xml的方法convertJsonToElement
;那么关键就是重新它,和在转换时使用我们自定义的转换器;DefinedBpmnJsonConverter
继承 UserTaskJsonConverter
重新方法如下:
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
| @Override protected FlowElement convertJsonToElement(JsonNode elementNode, JsonNode modelNode, Map<String, JsonNode> shapeMap) { FlowElement flowElement = super.convertJsonToElement(elementNode,modelNode,shapeMap); UserTask userTask = (UserTask)flowElement;
CustomProperty customProperty1= new CustomProperty(); customProperty1.setName("associaterolestype"); customProperty1.setSimpleValue(this.getPropertyValueAsString("associaterolestype",elementNode));
CustomProperty customProperty2 = new CustomProperty(); customProperty2.setName("associatedepts"); customProperty2.setSimpleValue(this.getPropertyValueAsString("associatedepts",elementNode));
userTask.getCustomProperties().add(customProperty1); userTask.getCustomProperties().add(customProperty2);
return userTask; }
public static void definedBpmConverter(){ fillTypes(ChildBpmnJsonConverter.getConvertersToBpmnMap(),ChildBpmnJsonConverter.getConvertersToJsonMap()); }
public static void fillTypes(Map<String, Class<? extends BaseBpmnJsonConverter>> convertersToBpmnMap, Map<Class<? extends BaseElement>, Class<? extends BaseBpmnJsonConverter>> convertersToJsonMap) { fillJsonTypes(convertersToBpmnMap); fillBpmnTypes(convertersToJsonMap); } public static void fillJsonTypes(Map<String, Class<? extends BaseBpmnJsonConverter>> convertersToBpmnMap) { convertersToBpmnMap.put("UserTask", DefinedBpmnJsonConverter.class); } public static void fillBpmnTypes(Map<Class<? extends BaseElement>, Class<? extends BaseBpmnJsonConverter>> convertersToJsonMap) { convertersToJsonMap.put(UserTask.class, DefinedBpmnJsonConverter.class); }
|
ChildBpmnJsonConverter
代码如下:
1 2 3 4 5 6 7 8
| public class ChildBpmnJsonConverter extends BpmnJsonConverter { public static Map<String, Class<? extends BaseBpmnJsonConverter>> getConvertersToBpmnMap(){ return convertersToBpmnMap; }; public static Map<Class<? extends BaseElement>, Class<? extends BaseBpmnJsonConverter>> getConvertersToJsonMap(){ return convertersToJsonMap; }; }
|
在deployment
接口中,我们需要json转换前调用自定义的转换代码如下:
1 2 3 4
| BpmnJsonConverter jsonConverter = new BpmnJsonConverter(); DefinedBpmnJsonConverter.definedBpmConverter(); BpmnModel model = jsonConverter.convertToBpmnModel(modelNode);
|
关于后端自定义属性的解析亦可参考博文:activiti modeler 任务节点自定义属性扩展 ,此处只是po出了原博客没有的代码。
注:补充一点,多个角色或人员采用#
分割,因为后面发现&
符号,流程发布后在标签中会被转义成&
而在其他标签值中不受影响,为了统一解析采用#
分割更好!
小结:关于工作流的二次开发,其实主要工作量还是前端页面的改造部分,因为后端对应的api已经有了,最多就是扩展重写一下。至于工作流结合业务场景怎么使用,这个还需看官方文档和api和具体需求了,奥力给!