wyg
2024-06-14 a57dc2fae73d6e0dd315a120ca43ee685a6c7b7c
提交 | 用户 | 时间
a57dc2 1 <template>
W 2   <div class="my-process-designer">
3     <div class="my-process-designer__header">
4       <slot name="control-header"></slot>
5       <template v-if="!$slots['control-header']">
6         <el-button-group key="file-control">
7           <el-button :size="headerButtonSize" :type="headerButtonType" icon="el-icon-edit-outline" @click="onSave">保存流程</el-button>
8           <el-button :size="headerButtonSize" :type="headerButtonType" icon="el-icon-folder-opened" @click="$refs.refFile.click()">打开文件</el-button>
9           <el-tooltip effect="light">
10             <div slot="content">
11               <el-button :size="headerButtonSize" type="text" @click="downloadProcessAsXml()">下载为XML文件</el-button>
12               <br />
13               <el-button :size="headerButtonSize" type="text" @click="downloadProcessAsSvg()">下载为SVG文件</el-button>
14               <br />
15               <el-button :size="headerButtonSize" type="text" @click="downloadProcessAsBpmn()">下载为BPMN文件</el-button>
16             </div>
17             <el-button :size="headerButtonSize" :type="headerButtonType" icon="el-icon-download">下载文件</el-button>
18           </el-tooltip>
19           <el-tooltip effect="light">
20             <div slot="content">
21               <el-button :size="headerButtonSize" type="text" @click="previewProcessXML">预览XML</el-button>
22               <br />
23               <el-button :size="headerButtonSize" type="text" @click="previewProcessJson">预览JSON</el-button>
24             </div>
25             <el-button :size="headerButtonSize" :type="headerButtonType" icon="el-icon-view">预览</el-button>
26           </el-tooltip>
27           <el-tooltip v-if="simulation" effect="light" :content="this.simulationStatus ? '退出模拟' : '开启模拟'">
28             <el-button :size="headerButtonSize" :type="headerButtonType" icon="el-icon-cpu" @click="processSimulation">
29               模拟
30             </el-button>
31           </el-tooltip>
32         </el-button-group>
33         <el-button-group key="align-control">
34           <el-tooltip effect="light" content="向左对齐">
35             <el-button :size="headerButtonSize" class="align align-left" icon="el-icon-s-data" @click="elementsAlign('left')" />
36           </el-tooltip>
37           <el-tooltip effect="light" content="向右对齐">
38             <el-button :size="headerButtonSize" class="align align-right" icon="el-icon-s-data" @click="elementsAlign('right')" />
39           </el-tooltip>
40           <el-tooltip effect="light" content="向上对齐">
41             <el-button :size="headerButtonSize" class="align align-top" icon="el-icon-s-data" @click="elementsAlign('top')" />
42           </el-tooltip>
43           <el-tooltip effect="light" content="向下对齐">
44             <el-button :size="headerButtonSize" class="align align-bottom" icon="el-icon-s-data" @click="elementsAlign('bottom')" />
45           </el-tooltip>
46           <el-tooltip effect="light" content="水平居中">
47             <el-button :size="headerButtonSize" class="align align-center" icon="el-icon-s-data" @click="elementsAlign('center')" />
48           </el-tooltip>
49           <el-tooltip effect="light" content="垂直居中">
50             <el-button :size="headerButtonSize" class="align align-middle" icon="el-icon-s-data" @click="elementsAlign('middle')" />
51           </el-tooltip>
52         </el-button-group>
53         <el-button-group key="scale-control">
54           <el-tooltip effect="light" content="缩小视图">
55             <el-button :size="headerButtonSize" :disabled="defaultZoom <= 0.3" icon="el-icon-zoom-out" @click="processZoomOut()" />
56           </el-tooltip>
57           <el-button :size="headerButtonSize">{{ Math.floor(this.defaultZoom * 10 * 10) + "%" }}</el-button>
58           <el-tooltip effect="light" content="放大视图">
59             <el-button :size="headerButtonSize" :disabled="defaultZoom >= 3.9" icon="el-icon-zoom-in" @click="processZoomIn()" />
60           </el-tooltip>
61           <el-tooltip effect="light" content="重置视图并居中">
62             <el-button :size="headerButtonSize" icon="el-icon-c-scale-to-original" @click="processReZoom()" />
63           </el-tooltip>
64         </el-button-group>
65         <el-button-group key="stack-control">
66           <el-tooltip effect="light" content="撤销">
67             <el-button :size="headerButtonSize" :disabled="!revocable" icon="el-icon-refresh-left" @click="processUndo()" />
68           </el-tooltip>
69           <el-tooltip effect="light" content="恢复">
70             <el-button :size="headerButtonSize" :disabled="!recoverable" icon="el-icon-refresh-right" @click="processRedo()" />
71           </el-tooltip>
72           <el-tooltip effect="light" content="重新绘制">
73             <el-button :size="headerButtonSize" icon="el-icon-refresh" @click="processRestart" />
74           </el-tooltip>
75         </el-button-group>
76       </template>
77       <!-- 用于打开本地文件-->
78       <input type="file" id="files" ref="refFile" style="display: none" accept=".xml, .bpmn" @change="importLocalFile" />
79     </div>
80     <div class="my-process-designer__container">
81       <div class="my-process-designer__canvas" ref="bpmn-canvas"></div>
82     </div>
83     <el-dialog title="预览" width="60%" :visible.sync="previewModelVisible" append-to-body destroy-on-close>
84       <highlightjs :language="previewType" :code="previewResult" style="height: 60vh" />
85     </el-dialog>
86   </div>
87 </template>
88
89 <script>
90 // 生产环境时优化
91 // const BpmnModeler = window.BpmnJS;
92 import BpmnModeler from "bpmn-js/lib/Modeler";
93 import DefaultEmptyXML from "./plugins/defaultEmpty";
94 // 翻译方法
95 import customTranslate from "./plugins/translate/customTranslate";
96 import translationsCN from "./plugins/translate/zh";
97 // 模拟流转流程
98 import tokenSimulation from "bpmn-js-token-simulation";
99 // 标签解析构建器
100 // import bpmnPropertiesProvider from "bpmn-js-properties-panel/lib/provider/bpmn";
101 // 标签解析 Moddle
102 import camundaModdleDescriptor from "./plugins/descriptor/camundaDescriptor.json";
103 import activitiModdleDescriptor from "./plugins/descriptor/activitiDescriptor.json";
104 import flowableModdleDescriptor from "./plugins/descriptor/flowableDescriptor.json";
105 // 标签解析 Extension
106 import camundaModdleExtension from "./plugins/extension-moddle/camunda";
107 import activitiModdleExtension from "./plugins/extension-moddle/activiti";
108 import flowableModdleExtension from "./plugins/extension-moddle/flowable";
109 // 引入json转换与高亮
110 import convert from "xml-js";
111
112 export default {
113   name: "BpmnProcessDesigner",
114   componentName: "BpmnProcessDesigner",
115   props: {
116     value: String, // xml 字符串
117     processId: String,
118     processName: String,
119     translations: Object, // 自定义的翻译文件
120     additionalModel: [Object, Array], // 自定义model
121     moddleExtension: Object, // 自定义moddle
122     onlyCustomizeAddi: {
123       type: Boolean,
124       default: false
125     },
126     onlyCustomizeModdle: {
127       type: Boolean,
128       default: false
129     },
130     simulation: {
131       type: Boolean,
132       default: true
133     },
134     keyboard: {
135       type: Boolean,
136       default: true
137     },
138     prefix: {
139       type: String,
140       default: "flowable"
141     },
142     events: {
143       type: Array,
144       default: () => ["element.click"]
145     },
146     headerButtonSize: {
147       type: String,
148       default: "small",
149       validator: value => ["default", "medium", "small", "mini"].indexOf(value) !== -1
150     },
151     headerButtonType: {
152       type: String,
153       default: "primary",
154       validator: value => ["default", "primary", "success", "warning", "danger", "info"].indexOf(value) !== -1
155     }
156   },
157   data() {
158     return {
159       defaultZoom: 1,
160       previewModelVisible: false,
161       simulationStatus: false,
162       previewResult: "",
163       previewType: "xml",
164       recoverable: false,
165       revocable: false
166     };
167   },
168   computed: {
169     additionalModules() {
170       const Modules = [];
171       // 仅保留用户自定义扩展模块
172       if (this.onlyCustomizeAddi) {
173         if (Object.prototype.toString.call(this.additionalModel) === "[object Array]") {
174           return this.additionalModel || [];
175         }
176         return [this.additionalModel];
177       }
178
179       // 插入用户自定义扩展模块
180       if (Object.prototype.toString.call(this.additionalModel) === "[object Array]") {
181         Modules.push(...this.additionalModel);
182       } else {
183         this.additionalModel && Modules.push(this.additionalModel);
184       }
185
186       // 翻译模块
187       const TranslateModule = {
188         translate: ["value", customTranslate(this.translations || translationsCN)]
189       };
190       Modules.push(TranslateModule);
191
192       // 模拟流转模块
193       if (this.simulation) {
194         Modules.push(tokenSimulation);
195       }
196
197       // 根据需要的流程类型设置扩展元素构建模块
198       // if (this.prefix === "bpmn") {
199       //   Modules.push(bpmnModdleExtension);
200       // }
201       if (this.prefix === "camunda") {
202         Modules.push(camundaModdleExtension);
203       }
204       if (this.prefix === "flowable") {
205         Modules.push(flowableModdleExtension);
206       }
207       if (this.prefix === "activiti") {
208         Modules.push(activitiModdleExtension);
209       }
210
211       return Modules;
212     },
213     moddleExtensions() {
214       const Extensions = {};
215       // 仅使用用户自定义模块
216       if (this.onlyCustomizeModdle) {
217         return this.moddleExtension || null;
218       }
219
220       // 插入用户自定义模块
221       if (this.moddleExtension) {
222         for (let key in this.moddleExtension) {
223           Extensions[key] = this.moddleExtension[key];
224         }
225       }
226
227       // 根据需要的 "流程类型" 设置 对应的解析文件
228       if (this.prefix === "activiti") {
229         Extensions.activiti = activitiModdleDescriptor;
230       }
231       if (this.prefix === "flowable") {
232         Extensions.flowable = flowableModdleDescriptor;
233       }
234       if (this.prefix === "camunda") {
235         Extensions.camunda = camundaModdleDescriptor;
236       }
237
238       return Extensions;
239     }
240   },
241   mounted() {
242     this.initBpmnModeler();
243     this.createNewDiagram(this.value);
244     this.$once("hook:beforeDestroy", () => {
245       if (this.bpmnModeler) this.bpmnModeler.destroy();
246       this.$emit("destroy", this.bpmnModeler);
247       this.bpmnModeler = null;
248     });
249   },
250   methods: {
251     onSave () {
252       return new Promise((resolve, reject) => {
253         if (this.bpmnModeler == null) {
254           reject();
255         }
256         this.bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
257           this.$emit('save', xml);
258           resolve(xml);
259         });
260       })
261     },
262     initBpmnModeler() {
263       if (this.bpmnModeler) return;
264       this.bpmnModeler = new BpmnModeler({
265         container: this.$refs["bpmn-canvas"],
266         keyboard: this.keyboard ? { bindTo: document } : null,
267         additionalModules: this.additionalModules,
268         moddleExtensions: this.moddleExtensions
269       });
270       this.$emit("init-finished", this.bpmnModeler);
271       this.initModelListeners();
272     },
273     initModelListeners() {
274       const EventBus = this.bpmnModeler.get("eventBus");
275       const that = this;
276       // 注册需要的监听事件, 将. 替换为 - , 避免解析异常
277       this.events.forEach(event => {
278         EventBus.on(event, function(eventObj) {
279           let eventName = event.replace(/\./g, "-");
280           let element = eventObj ? eventObj.element : null;
281           that.$emit(eventName, element, eventObj);
282           that.$emit('event', eventName, element, eventObj);
283         });
284       });
285       // 监听图形改变返回xml
286       EventBus.on("commandStack.changed", async event => {
287         try {
288           this.recoverable = this.bpmnModeler.get("commandStack").canRedo();
289           this.revocable = this.bpmnModeler.get("commandStack").canUndo();
290           let { xml } = await this.bpmnModeler.saveXML({ format: true });
291           this.$emit("commandStack-changed", event);
292           this.$emit("input", xml);
293           this.$emit("change", xml);
294         } catch (e) {
295           console.error(`[Process Designer Warn]: ${e.message || e}`);
296         }
297       });
298       // 监听视图缩放变化
299       this.bpmnModeler.on("canvas.viewbox.changed", ({ viewbox }) => {
300         this.$emit("canvas-viewbox-changed", { viewbox });
301         const { scale } = viewbox;
302         this.defaultZoom = Math.floor(scale * 100) / 100;
303       });
304     },
305     /* 创建新的流程图 */
306     async createNewDiagram(xml) {
307       // 将字符串转换成图显示出来
308       let newId = this.processId || `Process_${new Date().getTime()}`;
309       let newName = this.processName || `业务流程_${new Date().getTime()}`;
310       let xmlString = xml || DefaultEmptyXML(newId, newName, this.prefix);
311       try {
312         let { warnings } = await this.bpmnModeler.importXML(xmlString);
313         if (warnings && warnings.length) {
314           warnings.forEach(warn => console.warn(warn));
315         }
316       } catch (e) {
317         console.error(`[Process Designer Warn]: ${e.message || e}`);
318       }
319     },
320
321     // 下载流程图到本地
322     async downloadProcess(type, name) {
323       try {
324         const _this = this;
325         // 按需要类型创建文件并下载
326         if (type === "xml" || type === "bpmn") {
327           const { err, xml } = await this.bpmnModeler.saveXML();
328           // 读取异常时抛出异常
329           if (err) {
330             console.error(`[Process Designer Warn ]: ${err.message || err}`);
331           }
332           let { href, filename } = _this.setEncoded(type.toUpperCase(), name, xml);
333           downloadFunc(href, filename);
334         } else {
335           const { err, svg } = await this.bpmnModeler.saveSVG();
336           // 读取异常时抛出异常
337           if (err) {
338             return console.error(err);
339           }
340           let { href, filename } = _this.setEncoded("SVG", name, svg);
341           downloadFunc(href, filename);
342         }
343       } catch (e) {
344         console.error(`[Process Designer Warn ]: ${e.message || e}`);
345       }
346       // 文件下载方法
347       function downloadFunc(href, filename) {
348         if (href && filename) {
349           let a = document.createElement("a");
350           a.download = filename; //指定下载的文件名
351           a.href = href; //  URL对象
352           a.click(); // 模拟点击
353           URL.revokeObjectURL(a.href); // 释放URL 对象
354         }
355       }
356     },
357
358     // 根据所需类型进行转码并返回下载地址
359     setEncoded(type, filename = "diagram", data) {
360       const encodedData = encodeURIComponent(data);
361       return {
362         filename: `${filename}.${type}`,
363         href: `data:application/${type === "svg" ? "text/xml" : "bpmn20-xml"};charset=UTF-8,${encodedData}`,
364         data: data
365       };
366     },
367
368     // 加载本地文件
369     importLocalFile() {
370       const that = this;
371       const file = this.$refs.refFile.files[0];
372       const reader = new FileReader();
373       reader.readAsText(file);
374       reader.onload = function() {
375         let xmlStr = this.result;
376         that.createNewDiagram(xmlStr);
377       };
378     },
379     /* ------------------------------------------------ refs methods ------------------------------------------------------ */
380     downloadProcessAsXml() {
381       this.downloadProcess("xml");
382     },
383     downloadProcessAsBpmn() {
384       this.downloadProcess("bpmn");
385     },
386     downloadProcessAsSvg() {
387       this.downloadProcess("svg");
388     },
389     processSimulation() {
390       this.simulationStatus = !this.simulationStatus;
391       this.simulation && this.bpmnModeler.get("toggleMode").toggleMode();
392     },
393     processRedo() {
394       this.bpmnModeler.get("commandStack").redo();
395     },
396     processUndo() {
397       this.bpmnModeler.get("commandStack").undo();
398     },
399     processZoomIn(zoomStep = 0.1) {
400       let newZoom = Math.floor(this.defaultZoom * 100 + zoomStep * 100) / 100;
401       if (newZoom > 4) {
402         throw new Error("[Process Designer Warn ]: The zoom ratio cannot be greater than 4");
403       }
404       this.defaultZoom = newZoom;
405       this.bpmnModeler.get("canvas").zoom(this.defaultZoom);
406     },
407     processZoomOut(zoomStep = 0.1) {
408       let newZoom = Math.floor(this.defaultZoom * 100 - zoomStep * 100) / 100;
409       if (newZoom < 0.2) {
410         throw new Error("[Process Designer Warn ]: The zoom ratio cannot be less than 0.2");
411       }
412       this.defaultZoom = newZoom;
413       this.bpmnModeler.get("canvas").zoom(this.defaultZoom);
414     },
415     processZoomTo(newZoom = 1) {
416       if (newZoom < 0.2) {
417         throw new Error("[Process Designer Warn ]: The zoom ratio cannot be less than 0.2");
418       }
419       if (newZoom > 4) {
420         throw new Error("[Process Designer Warn ]: The zoom ratio cannot be greater than 4");
421       }
422       this.defaultZoom = newZoom;
423       this.bpmnModeler.get("canvas").zoom(newZoom);
424     },
425     processReZoom() {
426       this.defaultZoom = 1;
427       this.bpmnModeler.get("canvas").zoom("fit-viewport", "auto");
428     },
429     processRestart() {
430       this.recoverable = false;
431       this.revocable = false;
432       this.createNewDiagram(null).then(() => this.bpmnModeler.get("canvas").zoom(1, "auto"));
433     },
434     elementsAlign(align) {
435       const Align = this.bpmnModeler.get("alignElements");
436       const Selection = this.bpmnModeler.get("selection");
437       const SelectedElements = Selection.get();
438       if (!SelectedElements || SelectedElements.length <= 1) {
439         this.$message.warning("请按住 Ctrl 键选择多个元素对齐");
440         return;
441       }
442       this.$confirm("自动对齐可能造成图形变形,是否继续?", "警告", {
443         confirmButtonText: "确定",
444         cancelButtonText: "取消",
445         type: "warning"
446       }).then(() => Align.trigger(SelectedElements, align));
447     },
448     /*-----------------------------    方法结束     ---------------------------------*/
449     previewProcessXML() {
450       this.bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
451         this.previewResult = xml;
452         this.previewType = "xml";
453         this.previewModelVisible = true;
454       });
455     },
456     previewProcessJson() {
457       this.bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
458         this.previewResult = convert.xml2json(xml, { spaces: 2 });
459         this.previewType = "json";
460         this.previewModelVisible = true;
461       });
462     }
463   }
464 };
465 </script>