内部管理系统须要多种流程设计,方便在 Web PC
手动设计业务流程,保证单个业务能够设计多个流程,而且能够进行流程跟踪的综合要求。css
后端有一套本身的流程引擎,解析相应的 xml
,而后部署业务流程。
起初后端是想直接用 activiti-designeer 作流程设计,该方法基本知足业务需求,但后期发现这样作太麻烦,因而推荐前端使用 BPMN插件,放在管理系统中使用,生成 xml
及 svg
字符串传给接口,保存该业务流程。官方实例html
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:') } });
使用了 ant-design-pro 最初搭建好的后台项目(非 ts 版本) 搭建的项目:
BPMN React 例子,更多关注 思否前端
流程设计的界面按照图片上的布局能够分红四部分:左(工具面板)、中(画布)、右(表单面板)、悬浮(附加操做)react
2.画布 --> 绘制流程图,点击网关/节点,可操做对应网关/节点,调整节点间的关系git
在项目中引入 bpmn-js/lib/Modeler
获取 BPMN 建模器,而后建立一个建摸器github
import BpmnModeler from 'bpmn-js/lib/Modeler'; this.bpmnModeler = new BpmnModeler({ container: '#canvas', });
能够在当前 Modeler
的基础上添加一些额外的功能。好比,工具面板,调色板等等canvas
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.表单面板 --> 可对流程网关/节点添加字段标记及条件segmentfault
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);
this.bpmnModeler.saveSVG({format: true}, (err, data) => {});
this.bpmnModeler.saveXML({format: true}, (err, data) => {});
源码中 ```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...)antd
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', ];
svg
能够关于 viewBox preserveAspectRatio
viewBox="x, y, width, height"
更形象的解释就是:SVG
就像是咱们的显示器屏幕,viewBox
就是截屏工具选中的那个框框,最终的呈现就是把框框中的截屏内容再次在显示器中全屏显示!
preserveAspectRatio="xMinYMin meet"
preserveAspectRatio
属性的值为空格分隔的两个值组合而成。例如,上面的 xMidYMid
和 meet
.