內部管理系統須要多種流程設計,方便在 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
.