再接上篇,目前基本已完成工作流表单属性的自增和页面调整工作;现将步骤和关键代码总结如下:
官方文档及下载地址
关于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中引入自己写的页面即可,我这里做了个隐藏输入框是为了表单选中后的一个回显!
<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中:
$scope.save = function() {
var selectUsers = myframe.window.canSave();//调用子页面的canSave方法拿到对应的值
$scope.property.value = selectUsers;
$scope.updatePropertyInModel($scope.property);
$scope.close();
};
改造原有表单逻辑
那么我们要对原有的表单进行改造呢?首先还是要清楚,数据怎么来的怎么绑定的,绑定的数据格式是怎么样的!
特说明一点,此次工作流的二次开发过程中,发现节点代理有候选人候选组的属性,在节点流转的时候我们根据表act_ru_identitylink
即可了解当前处理人;那么我们需要根据我们自己的用户角色来进行选择,则需要改造原有的表单逻辑。
如下,我们修改代理人表单,以实现勾选的方式填充进关联用户和关联角色;需要注意的是,关联用户即候选人和候选组都是多个,改之前是多个input输入框,我们了解到对应的数据结构也是一个数组;我这里多个是以分号分隔,只是展示,真实到后台数据还是原有的数组结构(注意:以逗号隔开,数据保存时会被存为多条,所以这里用&符号隔开)
废话不多说,我们还是来看下实现,对应的popu弹窗页面:
<div class="row row-no-gutter">
<div class="form-group">
<!--<label for="userField">{{'PROPERTY.ASSIGNMENT.CANDIDATE_USERS' | translate}}</label>-->
<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 ng-repeat="candidateUser in assignment.candidateUsers">-->
<!--input id="userField" class="form-control" type="text" ng-model="candidateUser.value" />-->
<!--<i class="glyphicon glyphicon-minus clickable-property" ng-click="removeCandidateUserValue($index)"></i>-->
<!--<i ng-if="$index == (assignment.candidateUsers.length - 1)" class="glyphicon glyphicon-plus clickable-property" ng-click="addCandidateUserValue($index)"></i>-->
<!--</div>-->
</div>
<div class="form-group">
<!--<label for="groupField">{{'PROPERTY.ASSIGNMENT.CANDIDATE_GROUPS' | translate}}</label>-->
<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 ng-repeat="candidateGroup in assignment.candidateGroups">-->
<!--<input id="groupField" class="form-control" type="text" ng-model="candidateGroup.value" />-->
<!--<i class="glyphicon glyphicon-minus clickable-property" ng-click="removeCandidateGroupValue($index)"></i>-->
<!--<i ng-if="$index == (assignment.candidateGroups.length - 1)" class="glyphicon glyphicon-plus clickable-property" ng-click="addCandidateGroupValue($index)"></i>-->
<!-- </div>-->
</div>
</div>
部分controller.js页面,因为两个选择按钮的弹窗一致,只po出一个代码即可,其余不重要的js可以屏蔽掉:
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'],//iframe的url
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方法需要修改一下:
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
重写方法如下:
@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));
//将自定义属性设置在CustomProperties中,这是很多博客没有写明的!
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
代码如下:
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转换前调用自定义的转换代码如下:
// 解析转换
BpmnJsonConverter jsonConverter = new BpmnJsonConverter();
DefinedBpmnJsonConverter.definedBpmConverter();
BpmnModel model = jsonConverter.convertToBpmnModel(modelNode);
关于后端自定义属性的解析亦可参考博文:activiti modeler 任务节点自定义属性扩展,此处只是po出了原博客没有注明的代码。
注意:补充一点,多个角色或人员采用#
分割,因为后面发现&
符号,流程发布后在标签中会被转义成&
而在其他标签值中不受影响,为了统一解析采用#
分割更好!
小结:关于工作流的二次开发,其实主要工作量还是前端页面的改造部分,因为后端对应的api已经有了,最多就是扩展重写一下;最后就是工作流结合业务场景怎么使用了,这个还是要看官方文档和api了,奥力给!