正所谓百家争鸣、见仁见智、众说纷纭、各有千秋!在工作流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.1wars下面的flowable-modeler.war
- 从flowable-modeler.war中解压出 WEB-INFclassesstaticeditor-app 文件夹
- 将数据包中 editor-app 文件夹复制到 boot-admin项目 前端工程的 public 文件夹下面
- 在 boot-admin项目 前端工程 public 文件夹下面创建 modeler.html 作为编辑器入口
modeler.html内容:
<!doctype html> <!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]--> <!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]--> <!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]--> <!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]--> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>Activiti Editor</title> <meta name="description" content=""> <meta name="viewport" content="initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, width=device-width"> <!-- Place favicon.ico and apple-touch-icon.png in the root directory --> <link rel="Stylesheet" media="screen" href="/editor-app/libs/ng-grid-2.0.7.min.css" type="text/css"/> <link rel="stylesheet" href="/editor-app/libs/bootstrap_3.1.1/css/bootstrap.min.css"/> <link rel="Stylesheet" media="screen" href="/editor-app/editor/css/editor.css" type="text/css"/> <link rel="stylesheet" href="/editor-app/css/style.css" type="text/css"/> <link rel="stylesheet" href="/editor-app/css/style-common.css"> <link rel="stylesheet" href="/editor-app/css/style-editor.css"> </head> <body> <!-- 不显示flowable logo条 --> <!-- <div class="navbar navbar-fixed-top navbar-inverse" role="navigation" id="main-header"> <div class="navbar-header"> <a href="" ng-click="backToLanding()" class="navbar-brand" title="{{'GENERAL.MAIN-TITLE' | translate}}"><span class="sr-only">{{'GENERAL.MAIN-TITLE' | translate}}</span></a> </div> </div> --> <!--[if lt IE 9]> <div class="unsupported-browser"> <p class="alert error">You are using an unsupported browser. Please upgrade your browser in order to use the editor.</p> </div> <![endif]--> <div class="alert-wrapper" ng-cloak> <div class="alert fadein {{alerts.current.type}}" ng-show="alerts.current" ng-click="dismissAlert()"> <i class="glyphicon" ng-class="{'glyphicon-ok': alerts.current.type == 'info', 'glyphicon-remove': alerts.current.type == 'error'}"></i> <span>{{alerts.current.message}}</span> <div class="pull-right" ng-show="alerts.queue.length > 0"> <span class="badge">{{alerts.queue.length + 1}}</span> </div> </div> </div> <div id="main" class="wrapper full clearfix" ng-style="{height: window.height + 'px'}" ng-app="activitiModeler" ng-include="'/editor-app/editor.html'"> </div> <!--[if lt IE 9]> <script src="/editor-app/libs/es5-shim-15.3.4.5/es5-shim.js"></script> <script src="/editor-app/libs/json3_3.2.6/lib/json3.min.js"></script> <![endif]--> <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> <!-- Configuration --> <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> </body> </html>
整合改造前端源码
- 修改 ACTIVITI.CONFIG ,设置网关 URL
var ACTIVITI = ACTIVITI || {}; ACTIVITI.CONFIG = { 'contextRoot' : 'http://网关IP:网关端口号/api/workflow/auth/activiti', };
- 修改 configurationurl-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 中
<template> <div class="app-container" style="background-color: #FFFFFF;"> <el-dialog :visible.sync="dialogVisible" :close-on-click-modal="false" width="80%" height="100%" title="模型编辑器" @close="closeDialog"> <div> <iframe ref="Modeler" id="map" scrolling="auto" v-bind:src="contents" frameborder="0" style="top:0px;left: 0px;right:0px;bottom:0px;width: 100%;height: 600px;"></iframe> </div> </el-dialog> </div> </template> <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文件
<!-- 本文件自动生成,再次生成时易被覆盖 --> <!-- @author 虚领顶劲气沉丹田 --> <!-- @since 2023-2-27 17:02:05 --> <template> <div class="app-container background-white"> <!-- 查询抽屉开始 --> <el-drawer :visible.sync="filterDrawer.dialogVisible" direction="rtl" title="请输入查询条件" :with-header="false" size="30%"> <div class="demo-drawer__content"> <el-form class="demo-form-inline" style="margin-top: 25px;margin-right: 20px;" ref="drawerForm" :model="filterDrawer.formData"> <el-form-item label="主键" :label-width="filterDrawer.formLabelWidth" prop="id"> <el-input placeholder="请输入主键" size="mini" prefix-icon="el-icon-search" v-model="filterDrawer.formData.id"> </el-input> </el-form-item> <el-form-item label="模型标识" :label-width="filterDrawer.formLabelWidth" prop="key"> <el-input placeholder="请输入模型标识" size="mini" prefix-icon="el-icon-search" v-model="filterDrawer.formData.key"> </el-input> </el-form-item> <el-form-item label="模型名称" :label-width="filterDrawer.formLabelWidth" prop="name"> <el-input placeholder="请输入模型名称" size="mini" prefix-icon="el-icon-search" v-model="filterDrawer.formData.name"> </el-input> </el-form-item> <el-form-item label="版本号" :label-width="filterDrawer.formLabelWidth" prop="version"> <el-input placeholder="请输入版本号" size="mini" prefix-icon="el-icon-search" v-model="filterDrawer.formData.version"> </el-input> </el-form-item> <el-form-item label="记录创建时间" prop="createTime" :label-width="filterDrawer.formLabelWidth"> <el-date-picker v-model="filterDrawer.formData.createTime" type="date" placeholder="选择日期"> </el-date-picker> </el-form-item> <el-form-item label="记录最后修改时间" prop="lastUpdateTime" :label-width="filterDrawer.formLabelWidth"> <el-date-picker v-model="filterDrawer.formData.lastUpdateTime" type="date" placeholder="选择日期"> </el-date-picker> </el-form-item> <el-form-item :label-width="filterDrawer.formLabelWidth"> <el-button v-on:click="handleQueryButton()" size="mini" type="success" icon="el-icon-search">查询</el-button> <el-button v-on:click="resetForm('drawerForm')" size="mini" type="primary" icon="el-icon-refresh">重置 </el-button> <el-button v-on:click="hideDrawer()" size="mini" icon="el-icon-close">关闭</el-button> </el-form-item> </el-form> </div> </el-drawer> <!-- 查询抽屉结束 --> <!-- 按钮区域开始 --> <div ref="buttonContainer" class="background-gray" style="margin-top: 0px;margin-bottom: 0px;padding-top: 0px;"> <div class="btn-container" style="padding-top: 5px;padding-bottom: 5px;margin-top: 0px;"> <el-button size="mini" class="btn-item" type="success" icon="el-icon-refresh" @click="refresh()"> 刷新 </el-button> <el-button size="mini" class="btn-item" type="primary" icon="el-icon-edit" @click="handleClickAddButton()"> 新建 </el-button> <el-button size="mini" class="btn-item" type="success" icon="el-icon-search" @click="showDrawer()"> 查询 </el-button> </div> </div> <!-- 按钮区域接收 --> <!-- 数据列表区域开始 --> <div class="table-container" style="padding: 0;margin: 0px 0px 0px 0px;"> <el-table v-loading="loading" :data="mainTableData" border fit style="width: 100%" max-height="500"> <el-table-column type="expand"> <template slot-scope="props"> <el-form label-position="left" class="demo-table-expand"> <el-form-item label="主键"> <span>{{ props.row.id }}</span> </el-form-item> <el-form-item label="模型标识"> <span>{{ props.row.key }}</span> </el-form-item> <el-form-item label="模型名称"> <span>{{ props.row.name }}</span> </el-form-item> <el-form-item label="版本号"> <span>{{ props.row.version }}</span> </el-form-item> <el-form-item label="记录创建时间"> <span>{{ $commonUtils.dateTimeFormat(props.row.createTime) }}</span> </el-form-item> <el-form-item label="记录最后修改时间"> <span>{{ $commonUtils.dateTimeFormat(props.row.lastUpdateTime) }}</span> </el-form-item> </el-form> </template> </el-table-column> <el-table-column type="index" label="序号" :index="indexMethod" width="70"> </el-table-column> <!-- <el-table-column prop="id" label="主键" show-overflow-tooltip sortable width="120"></el-table-column> --> <el-table-column prop="key" label="模型标识" show-overflow-tooltip sortable></el-table-column> <el-table-column prop="name" label="模型名称" show-overflow-tooltip sortable></el-table-column> <el-table-column prop="category" label="类别" show-overflow-tooltip sortable></el-table-column> <el-table-column prop="version" label="版本" show-overflow-tooltip sortable width="50"></el-table-column> <el-table-column prop="createTime" label="记录创建时间" show-overflow-tooltip sortable :formatter="(row,column,cellValue) => dateTimeColFormatter(row,column,cellValue)"></el-table-column> <el-table-column prop="lastUpdateTime" label="记录最后修改时间" show-overflow-tooltip sortable :formatter="(row,column,cellValue) => dateTimeColFormatter(row,column,cellValue)"></el-table-column> <el-table-column align="center" label="操作" show-overflow-tooltip min-width="120"> <template slot-scope="scope"> <el-button size="least" type="primary" @click="handleEditRow(scope.row)">修改</el-button> <el-button size="least" type="danger" @click="handleDeleteRow(scope.row)">删除</el-button> <el-button size="least" type="warning" @click="handleDeployModel(scope.row)">部署</el-button> <el-button size="least" type="success" @click="handleFetchXml(scope.row)">XML</el-button> </template> </el-table-column> </el-table> </div> <!-- 数据列表区域结束 --> <!-- 分页组件开始 --> <div ref="paginationContainer" style="text-align: center;"> <el-pagination v-on:size-change="handlePageSizeChange" v-on:current-change="handlePageCurrentChange" :current-page="filterDrawer.formData.currentPage" :page-sizes="[5,10,20,50,100,500]" :page-size="filterDrawer.formData.pageSize" layout="total, sizes, prev, pager, next, jumper" :total="filterDrawer.formData.total"> </el-pagination> </div> <!-- 分页组件结束 --> <!-- 表数据编辑对话框区开始 --> <el-dialog :visible.sync="mainDataForm.mainDataFormDialogVisible" width="80%" :close-on-click-modal="false" :title="mainDataForm.mainDataFormDialogTitle"> <el-form ref="mainEditForm" :model="mainDataForm.editingRecord" :rules="rules" size="medium" label-width="150px"> <el-form-item label="模型标识" prop="key"> <el-input v-model="mainDataForm.editingRecord.key" placeholder="请输入模型标识" clearable :style="{width: '100%'}" /> </el-form-item> <el-form-item label="模型名称" prop="name"> <el-input v-model="mainDataForm.editingRecord.name" placeholder="请输入模型名称" clearable :style="{width: '100%'}" /> </el-form-item> <el-form-item label="模型说明" prop="name"> <el-input v-model="mainDataForm.editingRecord.description" placeholder="请输入模型说明" clearable :style="{width: '100%'}" /> </el-form-item> </el-form> <div slot="footer" class="dialog-footer"> <el-button @click="handleCloseMainDataFormDialog()"> 关闭 </el-button> <el-button type="primary" @click="handleSubmitMainDataForm()"> 创建 </el-button> <el-button type="primary" @click="resetForm('mainEditForm')"> 重置 </el-button> </div> </el-dialog> <el-dialog :visible.sync="sourceCodeForm.dialogVisible" width="80%" :close-on-click-modal="false" title="XML"> <el-input type="textarea" v-model="sourceCodeForm.editingRecord.sourceCode" :rows="20" readonly></el-input> <div slot="footer" class="dialog-footer"> <el-button @click="handleCloseSourceCodeDialog()"> 关闭 </el-button> <el-button type="primary" @click="handleSaveFileButton()"> 生成文件 </el-button> </div> </el-dialog> <!-- 表数据编辑对话框区结束 --> <!-- 模型编辑 --> <Modeler ref="modelerComponent" @refreshTable="getMainTableData" /> </div> </template> <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> <style> .demo-table-expand { font-size: 0; } .demo-table-expand label { width: 190px; color: #99a9bf; } .demo-table-expand .el-form-item { text-align: left; margin-right: 0; margin-bottom: 0; width: 100%; } /*1.显示滚动条:当内容超出容器的时候,可以拖动:*/ .el-drawer__body { overflow: auto; /* overflow-x: auto; */ } /*2.隐藏滚动条,太丑了*/ .el-drawer__container ::-webkit-scrollbar { display: none; } </style>
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 }) } //获取模型XML export function fetchXml(data) { return request({ url: '/api/workflow/auth/activiti/model/xml', method: 'post', data }) }
后端功能实现
对应前端需求,后端主要实现使用flowable引擎,获取汉化资源、读取模型数据、保存模型数据三个功能。
具体内容参见下一篇博文