内部管理系统需要多种流程设计,方便在 Web PC
手动设计业务流程,保证单个业务可以设计多个流程,并且可以进行流程跟踪的综合要求。
后端有一套自己的流程引擎,解析相应的 xml
,然后部署业务流程。
起初后端是想直接用 activiti-designeer 做流程设计,该方法基本满足业务需求,但后期发现这样做太麻烦,于是推荐前端使用 BPMN插件,放在管理系统中使用,生成 xml
及 svg
字符串传给接口,保存该业务流程。官方实例
基本用法
- 安装依赖包
yarn add bpmn-js
yarn add bpmn-js-properties-panel // 属性面板
yarn add camunda-bpmn-moddle // BPMN读写,与流程引擎相关联
- 初始化
let xml; // BPMN 2.0 xml
let viewer = new BpmnJS({
container: '#canvas',
height: 400,
});
viewer.importXML(xml, function(err) {
if (err) {
console.log('error rendering: ', err)
} else {
console.log('rendered:')
}
});
React 中用法
使用了 ant-design-pro 最初搭建好的后台项目(非 ts 版本) 搭建的项目:
BPMN React 例子,更多关注 思否
流程设计的界面按照图片上的布局可以分成四部分:左(工具面板)、中(画布)、右(表单面板)、悬浮(附加操作)
- 1.工具面板 --> 添加流程网关、工具跟相应的节点,起到流程设计过程中的辅助作用
-
2.画布 --> 绘制流程图,点击网关/节点,可操作对应网关/节点,调整节点间的关系
在项目中引入
bpmn-js/lib/Modeler
获取 BPMN 建模器,然后创建一个建摸器import BpmnModeler from 'bpmn-js/lib/Modeler'; this.bpmnModeler = new BpmnModeler({ container: '#canvas', });
可以在当前
Modeler
的基础上添加一些额外的功能。比如,工具面板,调色板等等import Modeler from 'bpmn-js/lib/Modeler'; import {assign, isArray} from 'min-dash'; import inherits from 'inherits'; import CustomTranslate from './customTranslate'; import CustomPalette from './customPalette'; import ColorPickerModule from './customColor'; export default function CustomModeler(options) { Modeler.call(this, options); this.customElements = []; } inherits(CustomModeler, Modeler); CustomModeler.prototype._modules = [].concat(CustomModeler.prototype._modules, [ CustomTranslate, CustomPalette, ColorPickerModule, ]);
-
3.表单面板 --> 可对流程网关/节点添加字段标记及条件
import propertiesPanelModule from 'bpmn-js-properties-panel'; // bpmn中自带的控件 import propertiesProviderModule from './PanelToolbar'; // 自定义表单
// PanelToolbar/index.js import inherits from 'inherits'; import PropertiesActivator from 'bpmn-js-properties-panel/lib/PropertiesActivator'; import baseInfo from './parts/BaseInfoProps'; // 创建基础信息看板 function createBaseInfoTab(element, bpmnFactory, elementRegistry, translate) { const generalGroup = { id: 'baseInfo', label: '', entries: [], }; baseInfo(generalGroup, element, bpmnFactory, translate); return [generalGroup]; } function MagicPropertiesProvider(eventBus, bpmnFactory, elementRegistry, translate) { PropertiesActivator.call(this, eventBus); this.getTabs = function(element) { const baseInfoTab = { id: 'baseInfo', label: '基本信息', groups: createBaseInfoTab(element, bpmnFactory, elementRegistry, translate), }; return [baseInfoTab]; }; } inherits(MagicPropertiesProvider, PropertiesActivator); MagicPropertiesProvider.$inject = ['eventBus', 'bpmnFactory', 'elementRegistry', 'translate']; export default { __init__: ['propertiesProvider'], propertiesProvider: ['type', MagicPropertiesProvider], };
-
4.附加操作 --> 可以提供 打开bpmn文件、回退、缩放、下载、预览、保存等功能(按照自己需要,调用 BPMN 对象中提供的方法)
import React, {Component} from 'react'; import {notification} from 'antd'; class EditTools extends Component { state = { scale: 1, // 流程图比例 } // 打开文件 handleOpen = () => { this.file.click(); }; // 下载xml/svg download = (type, data, name) => { let dataTrack = ''; const a = document.createElement('a'); switch (type) { case 'xml': dataTrack = 'bpmn'; break; case 'svg': dataTrack = 'svg'; break; default: break; } name = name || `diagram.${dataTrack}`; a.setAttribute( 'href', `data:application/bpmn20-xml;charset=UTF-8,${encodeURIComponent(data)}` ); a.setAttribute('target', '_blank'); a.setAttribute('dataTrack', `diagram:download-${dataTrack}`); a.setAttribute('download', name); document.body.appendChild(a); a.click(); document.body.removeChild(a); }; // 导入 xml 文件 handleOpenFile = e => { const that = this; const file = e.target.files[0]; const reader = new FileReader(); let data = ''; reader.readAsText(file); reader.onload = function(event) { data = event.target.result; that.renderDiagram(data, 'open'); }; }; // 保存 handleSave = () => { this.bpmnModeler.saveXML({format: true}, (err, xml) => { console.log(xml); }); this.bpmnModeler.saveSVG({format: true}, (err, data) => { console.log(data); }); }; // 前进 handleRedo = () => { this.bpmnModeler.get('commandStack').redo(); }; // 后退 handleUndo = () => { this.bpmnModeler.get('commandStack').undo(); }; // 下载 SVG 格式 handleDownloadSvg = () => { this.bpmnModeler.saveSVG({format: true}, (err, data) => { this.download('svg', data); }); }; // 下载 XML 格式 handleDownloadXml = () => { this.bpmnModeler.saveXML({format: true}, (err, data) => { this.download('xml', data); }); }; // 流程图放大缩小 handleZoom = radio => { const newScale = !radio ? 1.0 // 不输入radio则还原 : this.state.scale + radio <= 0.2 // 最小缩小倍数 ? 0.2 : this.state.scale + radio; this.bpmnModeler.get('canvas').zoom(newScale); this.setState({ scale: newScale, }); }; // 渲染 xml 格式 renderDiagram = xml => { this.bpmnModeler.importXML(xml, err => { if (err) { notification.error({ message: '提示', description: '导入失败', }); } }); }; render () { return (<ul className={styles.controlList}> <li className={`${styles.control} ${styles.line}`}> <input ref={file => { this.file = file; }} className={styles.openFile} type="file" onChange={this.onOpenFIle} /> <button type="button" title="打开BPMN文件" onClick={this.handleOpen}> <i className={styles.open} /> </button> </li> <li className={styles.control}> <button type="button" title="撤销" onClick={this.handleUndo}> <i className={styles.undo} /> </button> </li> <li className={`${styles.control} ${styles.line}`}> <button type="button" title="恢复" onClick={this.handleRedo}> <i className={styles.redo} /> </button> </li> <li className={styles.control}> <button type="button" title="重置大小" onClick={this.handleZoom}> <i className={styles.zoom} /> </button> </li> <li className={styles.control}> <button type="button" title="放大" onClick={() => this.handleZoom(0.1)}> <i className={styles.zoomIn} /> </button> </li> <li className={`${styles.control} ${styles.line}`}> <button type="button" title="缩小" onClick={() => this.handleZoom(-0.1)}> <i className={styles.zoomOut} /> </button> </li> <li className={styles.control}> <button type="button" title="下载BPMN文件" onClick={this.handleDownloadXml}> <i className={styles.download} /> </button> </li> <li className={styles.control}> <button type="button" title="下载流程图片" onClick={this.handleDownloadSvg}> <i className={styles.image} /> </button> </li> </ul>) } } export default EditTools;
- 导入文件
this.bpmnModeler.importXML(xml, err => {});
- 前进
this.bpmnModeler.get('commandStack').redo();
- 后退
this.bpmnModeler.get('commandStack').undo();
- 放大/缩小/重置
this.bpmnModeler.get('canvas').zoom(newScale);
- 下载 svg
this.bpmnModeler.saveSVG({format: true}, (err, data) => {});
- 下载 xml
this.bpmnModeler.saveXML({format: true}, (err, data) => {});
- 点击 xml,获取节点 id
- 导入文件
源码中
```js
/**
* Register an event listener
*
* Remove a previously added listener via {@link #off(event, callback)}.
*
* @param {String} event
* @param {Number} [priority]
* @param {Function} callback
* @param {Object} [that]
*/
Viewer.prototype.on = function(event, priority, callback, target) {
return this.get('eventBus').on(event, priority, callback, target);
};
```
元素添加相应事件。比如,点击、悬浮等等
```js
import React, {Component, Fragment} from 'react';
import BpmnViewer from 'bpmn-js';
import {diagramXML} from './xml';
import './Bpmn.css';
class Bpmn extends Component {
componentDidMount() {
const {callback} = this.props;
let viewer = new BpmnViewer({
container: '#canvas',
// height: 400
});
viewer.importXML(diagramXML, function(err) {
if (err) {
console.error('failed to load diagram');
console.error(err);
return console.log('failed to load diagram', err);
}
let eventBus = viewer.get('eventBus');
let events = [
'element.click',
// 'element.dblclick',
// 'element.hover',
// 'element.out',
// 'element.mousedown',
// 'element.mouseup'
];
events.forEach(function(event) {
eventBus.on(event, function(e) {
console.log(event, 'on', e.element.id);
callback(e.element.id); // 流程图点击回调
});
});
// 删除 bpmn logo
const bjsIoLogo = document.querySelector('.bjs-powered-by');
while (bjsIoLogo.firstChild) {
bjsIoLogo.removeChild(bjsIoLogo.firstChild);
}
});
}
render() {
const {data} = this.props;
return (<Fragment>
<div id="canvas" style={{height: '100%'}} />
<div>{data.id}</div>
</Fragment>);
}
}
export default Bpmn;
```
遇到的问题
- 自定义节点信息表单
官网提供了一些 BPMN 实例,可以自定义单个表单(inout、select、checkbox...)
import entryFactory from 'bpmn-js-properties-panel/lib/factory/EntryFactory';
import script from 'bpmn-js-properties-panel/lib/provider/camunda/parts/implementation/Script';
import {query} from 'min-dom';
// 编号
const BaseInfoProps = (group, element, bpmnFactory, translate) => {
group.entries.push(
entryFactory.textField({
id: 'id',
label: translate('编号'),
modelProperty: 'id',
})
);
group.entries.push(
entryFactory.textField({
id: 'name',
label: translate('名称'),
modelProperty: 'name',
validate: function(element, values) {
let validationResult = {};
if (!values.name) {
validationResult.name = '请输入节点名称';
}
if (values.name && values.name.length > 30) {
validationResult.name = '名称最多30个字';
}
return validationResult;
},
})
);
group.entries.push({
id: 'condition',
label: translate('Condition'),
html: `
<div class="bpp-row">
<label for="cam-condition">${translate('Expression')}</label>
<div class="bpp-field-wrapper">
<input id="cam-condition" type="text" name="condition" placeholder="请输入" />
<button class="clear" data-action="clear" data-show="canClear">
<span>X</span>
</button>
</div>
</div>
`,
get: function(element) {
let values = {};
// ...
return values;
},
set: function(element, values) {
let commands = [];
// ...
return commands;
},
validate: function(element, values) {
let validationResult = {};
if (!values.condition) {
validationResult.condition = '请输入表达式${表达式}';
}
return validationResult;
},
clear: function(element, inputNode) {
query('input[name=condition]', inputNode).value = '';
return true;
},
canClear: function(element, inputNode) {
let input = query('input[name=condition]', inputNode);
return input.value !== '';
},
script: script,
cssClasses: ['bpp-textfield'],
});
}
export default BaseInfoProps;
No provider for "e"!
在本地联调部署都没有问题,打包到正式环境的时候,进入初始化截断,开始报以下错误:
Error: No provider for "e"! (Resolving: colorPicker -> e)
起初以为 colorPicker
中的代码不够完善,反正这个也不用,就删了吧,上线要紧,结果错误总是惊人的相似,又出现以下错误:
Error: No provider for "e"! (Resolving: propertiesPanel -> propertiesProvider -> e)
No provider for "e"! (Resolving: colorPicker -> e)
于是找到了这个网站 BPMN问题网站,里面有一些解释,意思就是:定义的函数需要使用 $inject
来注释服务 annotate your service
.
export default function ColorPicker(eventBus, contextPad, commandStack) {
// ...
}
ColorPicker.$inject = [
'eventBus',
'contextPad',
'commandStack',
];
- 生成的 xml 位置不能进行调节,但是
svg
可以
关于 viewBox preserveAspectRatio
viewBox="x, y, width, height"
更形象的解释就是:SVG
就像是我们的显示器屏幕,viewBox
就是截屏工具选中的那个框框,最终的呈现就是把框框中的截屏内容再次在显示器中全屏显示!
preserveAspectRatio="xMinYMin meet"
preserveAspectRatio
属性的值为空格分隔的两个值组合而成。例如,上面的 xMidYMid
和 meet
.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。