首页 > 社会热点 > >正文

boot-admin整合flowable官方editor-app进行BPMN2.0建模

来源:博客园2023-04-21 01:56:21

正所谓百家争鸣、见仁见智、众说纷纭、各有千秋!在工作流bpmn2.0可视化建模工具实现的细分领域,网上扑面而来的是 bpmn.js这个渲染工具包和web建模器,而笔者却认为使用flowable官方开源 editor-app才是王道。

Flowable 开源版本中的 web 版流程设计器editor-app,展示风格和功能基本跟 activiti-modeler 一样,集成简单,开发工作量小,界面美观大方,功能强大,用户体验友好。


(相关资料图)

通过以下两张Gif动图来个PK,您的直观感受如何呢?bpmn.js运行效果图(gif动图取自互联网)

Flowable editor-app运行效果:

boot-admin是一款采用前后端分离模式、基于SpringCloud微服务架构的SaaS后台管理框架。系统内置基础管理、权限管理、运行管理、定义管理、代码生成器和办公管理6个功能模块,集成分布式事务Seata、工作流引擎Flowable、业务规则引擎Drools、后台作业调度框架Quartz等,技术栈包括Mybatis-plus、Redis、Nacos、Seata、Flowable、Drools、Quartz、SpringCloud、Springboot Admin Gateway、Liquibase、jwt、Openfeign、I18n等。gitee源码地址github源码地址

下面介绍 boot-admin对flowable官方bpmn2.0可视化建模工具 editor-app的集成改造步骤:

获取前端源码下载官方数据包flowable-6.4.1.zip从压缩包中解压出flowable-6.4.1\wars下面的flowable-modeler.war从flowable-modeler.war中解压出 WEB-INF\classes\static\editor-app 文件夹将数据包中 editor-app 文件夹复制到 boot-admin项目 前端工程的 public 文件夹下面在 boot-admin项目 前端工程 public 文件夹下面创建 modeler.html 作为编辑器入口

modeler.html内容:

             Activiti Editor                                    
{{alerts.current.message}}
{{alerts.queue.length + 1}}
<script src="/editor-app/libs/jquery_1.11.0/jquery.min.js"></script><script src="/editor-app/libs/jquery-ui-1.10.3.custom.min.js"></script><script src="/editor-app/libs/angular_1.2.13/angular.min.js"></script><script src="/editor-app/libs/angular_1.2.13/angular-animate.min.js"></script><script src="/editor-app/libs/bootstrap_3.1.1/js/bootstrap.min.js"></script><script src="/editor-app/libs/angular-resource_1.2.13/angular-resource.min.js"></script><script src="/editor-app/libs/angular-cookies_1.2.13/angular-cookies.min.js"></script><script src="/editor-app/libs/angular-sanitize_1.2.13/angular-sanitize.min.js"></script><script src="/editor-app/libs/angular-route_1.2.13/angular-route.min.js"></script><script src="/editor-app/libs/angular-translate_2.4.2/angular-translate.min.js"></script><script src="/editor-app/libs/angular-translate-storage-cookie/angular-translate-storage-cookie.js"></script><script src="/editor-app/libs/angular-translate-loader-static-files/angular-translate-loader-static-files.js"></script><script src="/editor-app/libs/angular-strap_2.0.5/angular-strap.min.js"></script><script src="/editor-app/libs/angular-strap_2.0.5/angular-strap.tpl.min.js"></script><script src="/editor-app/libs/momentjs_2.5.1/momentjs.min.js"></script><script src="/editor-app/libs/ui-utils.min-0.0.4.js" type="text/javascript"></script><script src="/editor-app/libs/ng-grid-2.0.7-min.js" type="text/javascript"></script><script src="/editor-app/libs/angular-dragdrop.min-1.0.3.js" type="text/javascript"></script><script src="/editor-app/libs/mousetrap-1.4.5.min.js" type="text/javascript"></script><script src="/editor-app/libs/jquery.autogrow-textarea.js" type="text/javascript"></script><script src="/editor-app/libs/prototype-1.5.1.js" type="text/javascript"></script><script src="/editor-app/libs/path_parser.js" type="text/javascript"></script><script src="/editor-app/libs/angular-scroll_0.5.7/angular-scroll.min.js" type="text/javascript"></script><script src="/editor-app/app-cfg.js?v=1"></script><script src="/editor-app/editor-config.js" type="text/javascript"></script><script src="/editor-app/configuration/url-config.js" type="text/javascript"></script><script src="/editor-app/editor/i18n/translation_en_us.js" type="text/javascript"></script><script src="/editor-app/editor/i18n/translation_signavio_en_us.js" type="text/javascript"></script><script src="/editor-app/editor/oryx.debug.js" type="text/javascript"></script><script src="/editor-app/app.js"></script><script src="/editor-app/eventbus.js" type="text/javascript"></script><script src="/editor-app/editor-controller.js" type="text/javascript"></script><script src="/editor-app/stencil-controller.js" type="text/javascript"></script><script src="/editor-app/toolbar-controller.js" type="text/javascript"></script><script src="/editor-app/header-controller.js" type="text/javascript"></script><script src="/editor-app/select-shape-controller.js" type="text/javascript"></script><script src="/editor-app/editor-utils.js" type="text/javascript"></script><script src="/editor-app/configuration/toolbar-default-actions.js" type="text/javascript"></script><script src="/editor-app/configuration/properties-default-controllers.js" type="text/javascript"></script><script src="/editor-app/configuration/properties-execution-listeners-controller.js" type="text/javascript"></script><script src="/editor-app/configuration/properties-event-listeners-controller.js" type="text/javascript"></script><script src="/editor-app/configuration/properties-assignment-controller.js" type="text/javascript"></script><script src="/editor-app/configuration/properties-fields-controller.js" type="text/javascript"></script><script src="/editor-app/configuration/properties-form-properties-controller.js" type="text/javascript"></script><script src="/editor-app/configuration/properties-in-parameters-controller.js" type="text/javascript"></script><script src="/editor-app/configuration/properties-multiinstance-controller.js" type="text/javascript"></script><script src="/editor-app/configuration/properties-out-parameters-controller.js" type="text/javascript"></script><script src="/editor-app/configuration/properties-task-listeners-controller.js" type="text/javascript"></script><script src="/editor-app/configuration/properties-sequenceflow-order-controller.js" type="text/javascript"></script><script src="/editor-app/configuration/properties-condition-expression-controller.js" type="text/javascript"></script><script src="/editor-app/configuration/properties-signal-definitions-controller.js" type="text/javascript"></script><script src="/editor-app/configuration/properties-signal-scope-controller.js" type="text/javascript"></script><script src="/editor-app/configuration/properties-message-definitions-controller.js" type="text/javascript"></script><script src="/editor-app/configuration/properties-message-scope-controller.js" type="text/javascript"></script><script src="/editor-app/configuration/toolbar.js" type="text/javascript"></script><script src="/editor-app/configuration/toolbar-custom-actions.js" type="text/javascript"></script><script src="/editor-app/configuration/properties.js" type="text/javascript"></script><script src="/editor-app/configuration/properties-custom-controllers.js" type="text/javascript"></script>
整合改造前端源码修改 ACTIVITI.CONFIG ,设置网关 URL
var ACTIVITI = ACTIVITI || {};ACTIVITI.CONFIG = {"contextRoot" : "http://网关IP:网关端口号/api/workflow/auth/activiti",};
修改 configuration\url-config.js,设置各具体访问点URL
var KISBPM = KISBPM || {};KISBPM.URL = {  //通过modelId,获取已保存模型的json数据  getModel: function(modelId) {    return ACTIVITI.CONFIG.contextRoot + "/model/json?modelId=" + modelId;  },  //获取汉化资源json数据  getStencilSet: function() {    return ACTIVITI.CONFIG.contextRoot + "/editor/stencilset?version=" + Date.now();  },  //保存模型数据  putModel: function(modelId) {    return ACTIVITI.CONFIG.contextRoot + "/model/save?modelId=" + modelId;  },  //从cookie中读取令牌  getToken: function() {    var cookies = document.cookie;    var list = cookies.split("; "); // 解析出名/值对列表    for (var i = 0; i < list.length; i++) {      var arr = list[i].split("="); // 解析出名和值      if (arr[0] == "Admin-Token") {        var cookieVal = decodeURIComponent(arr[1]); // 对cookie值解码        break;      }    }    return "Bearer" + cookieVal;  }};
修改 /public/editor-app/stencil-controller.js 中获取汉化包的方法,由源码中自由访问修改为携带令牌访问后台资源
$http({method: "GET",            headers: {                  "X-Token": KISBPM.URL.getToken()              },            url: KISBPM.URL.getStencilSet()})            .success(function (data, status, headers, config) {            var quickMenuDefinition = ["UserTask", "EndNoneEvent", "ExclusiveGateway",                                       "CatchTimerEvent", "ThrowNoneEvent", "TextAnnotation",                                       "SequenceFlow", "Association"];            var ignoreForPaletteDefinition = ["SequenceFlow", "MessageFlow", "Association", "DataAssociation", "DataStore", "SendTask"];            var quickMenuItems = [];            var morphRoles = [];                for (var i = 0; i < data.rules.morphingRules.length; i++)                {                    var role = data.rules.morphingRules[i].role;                    var roleItem = {"role": role, "morphOptions": []};                    morphRoles.push(roleItem);                }                // Check all received items                for (var stencilIndex = 0; stencilIndex < data.stencils.length; stencilIndex++)                {                // Check if the root group is the "diagram" group. If so, this item should not be shown.                    var currentGroupName = data.stencils[stencilIndex].groups[0];                    if (currentGroupName === "Diagram" || currentGroupName === "Form") {                        continue;  // go to next item                    }                    var removed = false;                    if (data.stencils[stencilIndex].removed) {                        removed = true;                    }                    var currentGroup = undefined;                    if (!removed) {                        // Check if this group already exists. If not, we create a new one                        if (currentGroupName !== null && currentGroupName !== undefined && currentGroupName.length > 0) {                            currentGroup = findGroup(currentGroupName, stencilItemGroups); // Find group in root groups array                            if (currentGroup === null) {                                currentGroup = addGroup(currentGroupName, stencilItemGroups);                            }                            // Add all child groups (if any)                            for (var groupIndex = 1; groupIndex < data.stencils[stencilIndex].groups.length; groupIndex++) {                                var childGroupName = data.stencils[stencilIndex].groups[groupIndex];                                var childGroup = findGroup(childGroupName, currentGroup.groups);                                if (childGroup === null) {                                    childGroup = addGroup(childGroupName, currentGroup.groups);                                }                                // The current group variable holds the parent of the next group (if any),                                // and is basically the last element in the array of groups defined in the stencil item                                currentGroup = childGroup;                            }                        }                    }                    // Construct the stencil item                    var stencilItem = {"id": data.stencils[stencilIndex].id,                        "name": data.stencils[stencilIndex].title,                        "description": data.stencils[stencilIndex].description,                        "icon": data.stencils[stencilIndex].icon,                        "type": data.stencils[stencilIndex].type,                        "roles": data.stencils[stencilIndex].roles,                        "removed": removed,                        "customIcon": false,                        "canConnect": false,                        "canConnectTo": false,                        "canConnectAssociation": false};                    if (data.stencils[stencilIndex].customIconId && data.stencils[stencilIndex].customIconId > 0) {                        stencilItem.customIcon = true;                        stencilItem.icon = data.stencils[stencilIndex].customIconId;                    }                    if (!removed) {                        if (quickMenuDefinition.indexOf(stencilItem.id) >= 0) {                        quickMenuItems[quickMenuDefinition.indexOf(stencilItem.id)] = stencilItem;                        }                    }                    if (stencilItem.id === "TextAnnotation" || stencilItem.id === "BoundaryCompensationEvent") {                    stencilItem.canConnectAssociation = true;                    }                    for (var i = 0; i < data.stencils[stencilIndex].roles.length; i++) {                    var stencilRole = data.stencils[stencilIndex].roles[i];                    if (stencilRole === "sequence_start") {                    stencilItem.canConnect = true;                    } else if (stencilRole === "sequence_end") {                    stencilItem.canConnectTo = true;                    }                    for (var j = 0; j < morphRoles.length; j++) {                    if (stencilRole === morphRoles[j].role) {                        if (!removed) {                         morphRoles[j].morphOptions.push(stencilItem);                    }                    stencilItem.morphRole = morphRoles[j].role;                    break;                    }                    }                    }                    if (currentGroup) {                    // Add the stencil item to the correct group                    currentGroup.items.push(stencilItem);                    if (ignoreForPaletteDefinition.indexOf(stencilItem.id) < 0) {                    currentGroup.paletteItems.push(stencilItem);                    }                    } else {                        // It"s a root stencil element                        if (!removed) {                            stencilItemGroups.push(stencilItem);                        }                    }                }                for (var i = 0; i < stencilItemGroups.length; i++)                {                if (stencilItemGroups[i].paletteItems && stencilItemGroups[i].paletteItems.length == 0)                {                stencilItemGroups[i].visible = false;                }                }                $scope.stencilItemGroups = stencilItemGroups;                var containmentRules = [];                for (var i = 0; i < data.rules.containmentRules.length; i++)                {                    var rule = data.rules.containmentRules[i];                    containmentRules.push(rule);                }                $scope.containmentRules = containmentRules;                // remove quick menu items which are not available anymore due to custom pallette                var availableQuickMenuItems = [];                for (var i = 0; i < quickMenuItems.length; i++)                {                    if (quickMenuItems[i]) {                        availableQuickMenuItems[availableQuickMenuItems.length] = quickMenuItems[i];                    }                }                $scope.quickMenuItems = availableQuickMenuItems;                $scope.morphRoles = morphRoles;            }).            error(function (data, status, headers, config) {                console.log("Something went wrong when fetching stencil items:" + JSON.stringify(data));            });
修改 /public/editor-app/app.js 中获取模型数据的方法,由源码中自由访问修改为携带令牌访问后台资源
function fetchModel(modelId) {                var modelUrl = KISBPM.URL.getModel(modelId);                $http({method: "GET",                headers: {"X-Token": KISBPM.URL.getToken()},                url: modelUrl}).                    success(function (data, status, headers, config) {                        $rootScope.editor = new ORYX.Editor(data);                        $rootScope.modelData = angular.fromJson(data);                        $rootScope.editorFactory.resolve();                    }).                    error(function (data, status, headers, config) {                      console.log("Error loading model with id " + modelId + " " + data);                    });            }
修改 /public/editor-app/configuration/toolbar-default-actions.js 中保存模型的方法,由源码中自由访问修改为携带令牌访问后台资源
$http({    method: "PUT",            data: params,            ignoreErrors: true,            headers: {"Accept": "application/json",                      "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",                      "X-Token": KISBPM.URL.getToken()},            transformRequest: function (obj) {                var str = [];                for (var p in obj) {                    str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));                }                return str.join("&");            },            url: KISBPM.URL.putModel(modelMetaData.modelId)})            .success(function (data, status, headers, config) {                $scope.editor.handleEvents({                    type: ORYX.CONFIG.EVENT_SAVED                });                $scope.modelData.name = $scope.saveDialog.name;                $scope.modelData.lastUpdated = data.lastUpdated;                $scope.status.loading = false;                $scope.$hide();                // Fire event to all who is listening                var saveEvent = {                    type: KISBPM.eventBus.EVENT_TYPE_MODEL_SAVED,                    model: params,                    modelId: modelMetaData.modelId,            eventType: "update-model"                };                KISBPM.eventBus.dispatch(KISBPM.eventBus.EVENT_TYPE_MODEL_SAVED, saveEvent);                // Reset state                $scope.error = undefined;                $scope.status.loading = false;                // Execute any callback                if (successCallback) {                    successCallback();                }            })            .error(function (data, status, headers, config) {                $scope.error = {};                console.log("Something went wrong when updating the process model:" + JSON.stringify(data));                $scope.status.loading = false;            });
创建 Modeler.vue 组件,以 iframe 形式将 editor-app 嵌入 vue-element-ui的弹窗 el-dialog 中
<script>  export default {    name: "Modeler",    data() {      return {        dialogVisible: false,        contents: "/modeler.html?modelId=0"      }    },    mounted() {    },    methods: {      setSrc(src){        this.contents="/modeler.html?modelId="+src      },      showDialog() {        this.dialogVisible = true      },      closeDialog(){        this.$emit("refreshTable",true)      }    }  }</script>
模型管理VUE文件
<script>  import Modeler from "./components/Modeler"  import {    fetchModelPage,    saveNewModel,    delModel,    deployModel,    fetchXml  } from "@/api/workflow-model"  import {    getDictionaryOptionsByItemType,    lazyFetchDictionaryNode  } from "@/api/dictionary"  export default {    name: "model",    computed: {},    components: {      Modeler    },    data() {      const that = this;      return {        loading: true,        mainTableData: [],        mainDataForm: {          editingRecord: {            key: "",            name: "",            version: "",            enabled: "1",            deleted: "1",            description: "无",          },          mainDataFormDialogVisible: false,          mainDataFormDialogTitle: "连续新增"        },        sourceCodeForm: {          editingRecord: {            sourceCode: ""          },          dialogVisible: false,        },        filterDrawer: {          dialogVisible: false,          formLabelWidth: "100px",          formData: {            id: "",            key: "",            name: "",            version: null,            createTime: null,            lastUpdateTime: null,            datestamp: null,            enabled: "",            deleted: "",            description: "",            currentPage: 1,            pageSize: 10,            total: 0,          },        },        optionMap: new Map(),        //本页需要加载的option数据类型罗列在下面的数组中        optionKey: [          this.$commonDicType.ENABLED(),          this.$commonDicType.DELETED(),        ],        cascaderValue: {},        rules: {          id: [{            required: true,            message: "请输入主键",            trigger: "blur"          }],          key: [{            required: true,            message: "请输入模型标识",            trigger: "blur"          }],          name: [{            required: true,            message: "请输入模型名称",            trigger: "blur"          }],          version: [{            required: true,            message: "请输入版本号",            trigger: "blur"          }],          createTime: [{            required: true,            message: "请输入记录创建时间",            trigger: "blur"          }],          lastUpdateTime: [{            required: true,            message: "请输入记录最后修改时间",            trigger: "blur"          }],        }      }    },    created() {},    mounted() {      this.loadAllOptions()      this.getMainTableData()    },    watch: {},    inject: ["reload"],    methods: {      refresh() {        this.reload()      },      loadAllOptions() {        for (var i = 0; i < this.optionKey.length; i++) {          this.loadDictionaryOptions(this.optionKey[i], false)        }      },      colFormatter(row, column, cellValue, key) {        return this.$commonUtils.optoinValue2Lable(this.optionMap.get(key), cellValue)      },      dateTimeColFormatter(row, column, cellValue) {        return this.$commonUtils.dateTimeFormat(cellValue)      },      dateColFormatter(row, column, cellValue) {        return this.$commonUtils.dateFormat(cellValue)      },      async loadDictionaryOptions(itemType, includeAllOptions) {        this.listLoading = true        const response = await getDictionaryOptionsByItemType(itemType, includeAllOptions)        this.listLoading = false        if (response.code !== 100) {          this.$message({            message: response.message,            type: "warning"          })          return        }        const {          data        } = response        this.optionMap.set(itemType, data)      },      handlePageSizeChange(val) {        if (val != this.filterDrawer.formData.pageSize) {          this.filterDrawer.formData.pageSize = val;          this.getMainTableData()        }      },      handlePageCurrentChange(val) {        if (val != this.filterDrawer.formData.currentPage) {          this.filterDrawer.formData.currentPage = val;          this.getMainTableData()        }      },      indexMethod(index) {        return this.filterDrawer.formData.pageSize * (this.filterDrawer.formData.currentPage - 1) + index + 1;      },      resetForm(formName) {        this.$refs[formName].resetFields();      },      showDrawer() {        this.filterDrawer.dialogVisible = true      },      hideDrawer() {        this.filterDrawer.dialogVisible = false      },      handleQueryButton() {        this.filterDrawer.formData.currentPage = 1        this.getMainTableData()      },      async getMainTableData() {        this.loading = false        const response = await fetchModelPage(this.filterDrawer.formData)        this.loading = false        if (100 !== response.code) {          this.$message({            message: response.message,            type: "warning"          })          return        }        const {          data        } = response        this.mainTableData = data.records        this.filterDrawer.formData.total = data.total      },      handleEditRow(row) {        this.$nextTick(() => {          this.$refs.modelerComponent.setSrc(row.id)          this.$refs.modelerComponent.showDialog()        })      },      handleDeleteRow(row) {        this.$confirm("此操作将删除选中的数据, 是否继续?", "提示", {          confirmButtonText: "确定",          cancelButtonText: "取消",          type: "warning"        }).then(() => {          this.awaitDelModel(row.id)        }).catch(() => {          this.$message({            type: "info",            message: "已取消删除"          });        });      },      handleDeployModel(row) {        this.$confirm("此操作将部署选中的模型, 是否继续?", "提示", {          confirmButtonText: "确定",          cancelButtonText: "取消",          type: "warning"        }).then(() => {          this.awaitDeployModel(row.id)        }).catch(() => {          this.$message({            type: "info",            message: "已取消部署"          });        });      },      async handleFetchXml(row){        const guidVO = {          guid: row.id        }        const result = await fetchXml(guidVO)        if (this.$commonResultCode.SUCCESS() == result.code) {          this.sourceCodeForm.editingRecord.sourceCode = result.data          this.sourceCodeForm.dialogVisible = true        }        this.$message({          message: result.message,          type: "warning"        })      },      async awaitDelModel(guid) {        const guidVO = {          guid        }        const result = await delModel(guidVO)        if (this.$commonResultCode.SUCCESS() == result.code) {          this.getMainTableData()        }        this.$message({          message: result.message,          type: "warning"        })      },      async awaitDeployModel(guid) {        const guidVO = {          guid        }        const result = await deployModel(guidVO)        this.$message({          message: result.message,          type: "warning"        })      },      handleClickAddButton() {        this.mainDataForm.mainDataFormDialogTitle = "创建新的模型"        this.initmainDataForm()        this.mainDataForm.mainDataFormDialogVisible = true      },      initmainDataForm() {        this.mainDataForm.editingRecord.id = ""        this.mainDataForm.editingRecord.key = ""        this.mainDataForm.editingRecord.name = ""        this.mainDataForm.editingRecord.description = ""      },      handleSubmitMainDataForm() {        this.$refs["mainEditForm"].validate((valid) => {          if (valid) {            this.submitMainDataForm()          } else {            console.log("未通过表单校验!!");            return false;          }        });      },      async submitMainDataForm() {        const response = await saveNewModel(this.mainDataForm.editingRecord)        if (response.code !== 100) {          this.$message({            message: response.message,            type: "warning"          })          return        }        const {          data        } = response        this.mainDataForm.mainDataFormDialogVisible = false        this.$nextTick(() => {          this.$refs.modelerComponent.setSrc(data)          this.$refs.modelerComponent.showDialog()        })      },      handleCloseMainDataFormDialog() {        this.getMainTableData()        this.mainDataForm.mainDataFormDialogVisible = false      },      async loadLazyCodeNode(dicType, code, resolve) {        this.listLoading = true        const response = await lazyFetchDictionaryNode(dicType, code)        this.listLoading = false        if (response.code !== 100) {          this.$message({            message: response.message,            type: "warning"          })          return        }        const {          data        } = response        // 通过调用resolve将子节点数据返回,通知组件数据加载完成        resolve(data);      },      handleCloseSourceCodeDialog(){        this.sourceCodeForm.dialogVisible = false      }    }  }</script>

workflow-model.js

import request from "@/utils/request"//分页获取模型数据export function fetchModelPage(data) {  return request({    url: "/api/workflow/auth/activiti/model/page",    method: "post",    data  })}//保存模型export function saveNewModel(data) {  return request({    url: "/api/workflow/auth/activiti/model/add",    method: "post",    data  })}//删除模型数据export function delModel(data) {  return request({    url: "/api/workflow/auth/activiti/model/del",    method: "post",    data  })}//部署模型export function deployModel(data) {  return request({    url: "/api/workflow/auth/activiti/model/deploy",    method: "post",    data  })}//获取模型XMLexport function fetchXml(data) {  return request({    url: "/api/workflow/auth/activiti/model/xml",    method: "post",    data  })}
后端功能实现

对应前端需求,后端主要实现使用flowable引擎,获取汉化资源、读取模型数据、保存模型数据三个功能。

具体内容参见下一篇博文

项目源码仓库github项目源码仓库gitee

标签:

下一篇:
上一篇: