工作流二次开发之新增表单实践(二)

工作流二次开发之新增表单实践(二)

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

官方文档及下载地址

UeI8iT.png

关于springboot结合使用的项目,有前辈已经写了帖子并且集成好了

附上博客地址:Activiti工作流学习之SpringBoot整合Activiti5.22.0实现在线设计器(二)

如果你看了官方文档,且也认真看了这篇博客,那么博客中的开源代码你拉下来后,我想基本的功能你已经实现了;表单设计器的页面,你已经能看到了;那么我们现在要做的是工作流的二次开发,即现有的节点表单不满足我们现有的业务需求,我们需要对其进行二次改造,增加我们自己的表单!

新增自定义表单

增加表单之前,我们还需要稍微了解一下angular.js的东西,因为本项目的前端数据绑定是angular模板语法来绑定的;废话不多说,我们来看下前台代码结构:
UeIzkV.png

UeoAmR.png

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

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

知道了大概模板结构和方法后,我们照猫画虎;也需要增加对应的三个模板页面和对应的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输入框,我们了解到对应的数据结构也是一个数组;我这里多个是以分号分隔,只是展示,真实到后台数据还是原有的数组结构(注意:以逗号隔开,数据保存时会被存为多条,所以这里用&符号隔开)
UeooH1.png

废话不多说,我们还是来看下实现,对应的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出了原博客没有注明的代码。

注:补充一点,多个角色或人员采用#分割,因为后面发现&符号,流程发布后在标签中会被转义成&amp;而在其他标签值中不受影响,为了统一解析采用#分割更好!

小结:关于工作流的二次开发,其实主要工作量还是前端页面的改造部分,因为后端对应的api已经有了,最多就是扩展重写一下;最后就是工作流结合业务场景怎么使用了,这个还是要看官方文档和api了,奥力给!