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

2023-05-19,,

正所谓百家争鸣、见仁见智、众说纷纭、各有千秋!在工作流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内容:

<!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',
};
    修改 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 中
<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引擎,获取汉化资源、读取模型数据、保存模型数据三个功能。

具体内容参见下一篇博文

项目源码仓库github

项目源码仓库gitee

boot-admin整合flowable官方editor-app进行BPMN2.0建模的相关教程结束。

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

下载本文的Word格式文档,以方便收藏与打印。