潘志宝
3 天以前 221918bba28d2384d03c596a68256d7832e4a0e0
提交 | 用户 | 时间
820397 1 <template>
H 2   <div class="my-process-designer">
3     <div class="my-process-designer__header" style="z-index: 999; display: table-row-group">
4       <slot name="control-header"></slot>
5       <template v-if="!$slots['control-header']">
6         <ElButtonGroup key="file-control">
7           <XButton preIcon="ep:folder-opened" title="打开文件" @click="refFile.click()" />
8           <el-tooltip effect="light" placement="bottom">
9             <template #content>
10               <div style="color: #409eff">
11                 <!-- <el-button link @click="downloadProcessAsXml()">下载为XML文件</el-button> -->
12                 <XTextButton title="下载为XML文件" @click="downloadProcessAsXml()" />
13                 <br />
14
15                 <!-- <el-button link @click="downloadProcessAsSvg()">下载为SVG文件</el-button> -->
16                 <XTextButton title="下载为SVG文件" @click="downloadProcessAsSvg()" />
17                 <br />
18
19                 <!-- <el-button link @click="downloadProcessAsBpmn()">下载为BPMN文件</el-button> -->
20                 <XTextButton title="下载为BPMN文件" @click="downloadProcessAsBpmn()" />
21               </div>
22             </template>
23             <XButton title="下载文件" preIcon="ep:download" />
24           </el-tooltip>
25           <el-tooltip effect="light">
26             <XButton preIcon="ep:view" title="浏览" />
27             <template #content>
28               <!-- <el-button link @click="previewProcessXML">预览XML</el-button> -->
29               <XTextButton title="预览XML" @click="previewProcessXML" />
30               <br />
31               <!-- <el-button link @click="previewProcessJson">预览JSON</el-button> -->
32               <XTextButton title="预览JSON" @click="previewProcessJson" />
33             </template>
34           </el-tooltip>
35           <el-tooltip
36             v-if="props.simulation"
37             effect="light"
38             :content="simulationStatus ? '退出模拟' : '开启模拟'"
39           >
40             <XButton preIcon="ep:cpu" title="模拟" @click="processSimulation" />
41           </el-tooltip>
42         </ElButtonGroup>
43         <ElButtonGroup key="align-control">
44           <el-tooltip effect="light" content="向左对齐">
45             <!-- <el-button
46               class="align align-left"
47               icon="el-icon-s-data"
48               @click="elementsAlign('left')"
49             /> -->
50             <XButton
51               preIcon="fa:align-left"
52               class="align align-bottom"
53               @click="elementsAlign('left')"
54             />
55           </el-tooltip>
56           <el-tooltip effect="light" content="向右对齐">
57             <!-- <el-button
58               class="align align-right"
59               icon="el-icon-s-data"
60               @click="elementsAlign('right')"
61             /> -->
62             <XButton
63               preIcon="fa:align-left"
64               class="align align-top"
65               @click="elementsAlign('right')"
66             />
67           </el-tooltip>
68           <el-tooltip effect="light" content="向上对齐">
69             <!-- <el-button
70               class="align align-top"
71               icon="el-icon-s-data"
72               @click="elementsAlign('top')"
73             /> -->
74             <XButton
75               preIcon="fa:align-left"
76               class="align align-left"
77               @click="elementsAlign('top')"
78             />
79           </el-tooltip>
80           <el-tooltip effect="light" content="向下对齐">
81             <!-- <el-button
82               class="align align-bottom"
83               icon="el-icon-s-data"
84               @click="elementsAlign('bottom')"
85             /> -->
86             <XButton
87               preIcon="fa:align-left"
88               class="align align-right"
89               @click="elementsAlign('bottom')"
90             />
91           </el-tooltip>
92           <el-tooltip effect="light" content="水平居中">
93             <!-- <el-button
94               class="align align-center"
95               icon="el-icon-s-data"
96               @click="elementsAlign('center')"
97             /> -->
98             <!-- class="align align-center" -->
99             <XButton
100               preIcon="fa:align-left"
101               class="align align-center"
102               @click="elementsAlign('center')"
103             />
104           </el-tooltip>
105           <el-tooltip effect="light" content="垂直居中">
106             <!-- <el-button
107               class="align align-middle"
108               icon="el-icon-s-data"
109               @click="elementsAlign('middle')"
110             /> -->
111             <XButton
112               preIcon="fa:align-left"
113               class="align align-middle"
114               @click="elementsAlign('middle')"
115             />
116           </el-tooltip>
117         </ElButtonGroup>
118         <ElButtonGroup key="scale-control">
119           <el-tooltip effect="light" content="缩小视图">
120             <!-- <el-button
121               :disabled="defaultZoom < 0.2"
122               icon="el-icon-zoom-out"
123               @click="processZoomOut()"
124             /> -->
125             <XButton
126               preIcon="ep:zoom-out"
127               @click="processZoomOut()"
128               :disabled="defaultZoom < 0.2"
129             />
130           </el-tooltip>
131           <el-button>{{ Math.floor(defaultZoom * 10 * 10) + '%' }}</el-button>
132           <el-tooltip effect="light" content="放大视图">
133             <!-- <el-button
134               :disabled="defaultZoom > 4"
135               icon="el-icon-zoom-in"
136               @click="processZoomIn()"
137             /> -->
138             <XButton preIcon="ep:zoom-in" @click="processZoomIn()" :disabled="defaultZoom > 4" />
139           </el-tooltip>
140           <el-tooltip effect="light" content="重置视图并居中">
141             <!-- <el-button icon="el-icon-c-scale-to-original" @click="processReZoom()" /> -->
142             <XButton preIcon="ep:scale-to-original" @click="processReZoom()" />
143           </el-tooltip>
144         </ElButtonGroup>
145         <ElButtonGroup key="stack-control">
146           <el-tooltip effect="light" content="撤销">
147             <!-- <el-button :disabled="!revocable" icon="el-icon-refresh-left" @click="processUndo()" /> -->
148             <XButton preIcon="ep:refresh-left" @click="processUndo()" :disabled="!revocable" />
149           </el-tooltip>
150           <el-tooltip effect="light" content="恢复">
151             <!-- <el-button
152               :disabled="!recoverable"
153               icon="el-icon-refresh-right"
154               @click="processRedo()"
155             /> -->
156             <XButton preIcon="ep:refresh-right" @click="processRedo()" :disabled="!recoverable" />
157           </el-tooltip>
158           <el-tooltip effect="light" content="重新绘制">
159             <!-- <el-button icon="el-icon-refresh" @click="processRestart" /> -->
160             <XButton preIcon="ep:refresh" @click="processRestart()" />
161           </el-tooltip>
162         </ElButtonGroup>
163       </template>
164       <!-- 用于打开本地文件-->
165       <input
166         type="file"
167         id="files"
168         ref="refFile"
169         style="display: none"
170         accept=".xml, .bpmn"
171         @change="importLocalFile"
172       />
173     </div>
174     <div class="my-process-designer__container">
175       <div
176         class="my-process-designer__canvas"
177         ref="bpmnCanvas"
178         id="bpmnCanvas"
179         style="width: 1680px; height: 800px"
180       ></div>
181       <!-- <div id="js-properties-panel" class="panel"></div> -->
182       <!-- <div class="my-process-designer__canvas" ref="bpmn-canvas"></div> -->
183     </div>
184     <Dialog
185       title="预览"
186       v-model="previewModelVisible"
187       width="80%"
188       :scroll="true"
189       max-height="600px"
190     >
191       <!-- append-to-body -->
192       <div v-highlight>
193         <code class="hljs">
194           <!-- 高亮代码块 -->
195           {{ previewResult }}
196         </code>
197       </div>
198     </Dialog>
199   </div>
200 </template>
201
202 <script lang="ts" setup>
203 // import 'bpmn-js/dist/assets/diagram-js.css' // 左边工具栏以及编辑节点的样式
204 // import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css'
205 // import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css'
206 // import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css'
207 // import 'bpmn-js-properties-panel/dist/assets/bpmn-js-properties-panel.css' // 右侧框样式
208 import { ElMessage, ElMessageBox } from 'element-plus'
209 import BpmnModeler from 'bpmn-js/lib/Modeler'
210 import DefaultEmptyXML from './plugins/defaultEmpty'
211 // 翻译方法
212 import customTranslate from './plugins/translate/customTranslate'
213 import translationsCN from './plugins/translate/zh'
214 // 模拟流转流程
215 import tokenSimulation from 'bpmn-js-token-simulation'
216 // 标签解析构建器
217 // import bpmnPropertiesProvider from "bpmn-js-properties-panel/lib/provider/bpmn";
218 // import propertiesPanelModule from 'bpmn-js-properties-panel'
219 // import propertiesProviderModule from 'bpmn-js-properties-panel/lib/provider/camunda'
220 // 标签解析 Moddle
221 import camundaModdleDescriptor from './plugins/descriptor/camundaDescriptor.json'
222 import activitiModdleDescriptor from './plugins/descriptor/activitiDescriptor.json'
223 import flowableModdleDescriptor from './plugins/descriptor/flowableDescriptor.json'
224 // 标签解析 Extension
225 import camundaModdleExtension from './plugins/extension-moddle/camunda'
226 import activitiModdleExtension from './plugins/extension-moddle/activiti'
227 import flowableModdleExtension from './plugins/extension-moddle/flowable'
228 // 引入json转换与高亮
229 // import xml2js from 'xml-js'
230 // import xml2js from 'fast-xml-parser'
231 import { XmlNode, XmlNodeType, parseXmlString } from 'steady-xml'
232 // 代码高亮插件
233 // import hljs from 'highlight.js/lib/highlight'
234 // import 'highlight.js/styles/github-gist.css'
235 // hljs.registerLanguage('xml', 'highlight.js/lib/languages/xml')
236 // hljs.registerLanguage('json', 'highlight.js/lib/languages/json')
237 // const eventName = reactive({
238 //   name: ''
239 // })
240
241 defineOptions({ name: 'MyProcessDesigner' })
242
243 const bpmnCanvas = ref()
244 const refFile = ref()
245 const emit = defineEmits([
246   'destroy',
247   'init-finished',
248   'save',
249   'commandStack-changed',
250   'input',
251   'change',
252   'canvas-viewbox-changed',
253   // eventName.name
254   'element-click'
255 ])
256
257 const props = defineProps({
258   value: String, // xml 字符串
259   // valueWatch: true, // xml 字符串的 watch 状态
260   processId: String, // 流程 key 标识
261   processName: String, // 流程 name 名字
262   formId: Number, // 流程 form 表单编号
263   translations: {
264     // 自定义的翻译文件
265     type: Object,
266     default: () => {}
267   },
268   additionalModel: [Object, Array], // 自定义model
269   moddleExtension: {
270     // 自定义moddle
271     type: Object,
272     default: () => {}
273   },
274   onlyCustomizeAddi: {
275     type: Boolean,
276     default: false
277   },
278   onlyCustomizeModdle: {
279     type: Boolean,
280     default: false
281   },
282   simulation: {
283     type: Boolean,
284     default: true
285   },
286   keyboard: {
287     type: Boolean,
288     default: true
289   },
290   prefix: {
291     type: String,
292     default: 'camunda'
293   },
294   events: {
295     type: Array,
296     default: () => ['element.click']
297   },
298   headerButtonSize: {
299     type: String,
300     default: 'small',
301     validator: (value: string) => ['default', 'medium', 'small', 'mini'].indexOf(value) !== -1
302   },
303   headerButtonType: {
304     type: String,
305     default: 'primary',
306     validator: (value: string) =>
307       ['default', 'primary', 'success', 'warning', 'danger', 'info'].indexOf(value) !== -1
308   }
309 })
c9a6f7 310
H 311 // 监听value变化,重新加载流程图
312 watch(
313   () => props.value,
314   (newValue) => {
315     if (newValue && bpmnModeler) {
316       createNewDiagram(newValue)
317     }
318   },
319   { immediate: true }
320 )
321
322 // 监听processId和processName变化
323 watch(
324   [() => props.processId, () => props.processName],
325   ([newId, newName]) => {
326     if (newId && newName && !props.value) {
327       createNewDiagram(null)
328     }
329   },
330   { immediate: true }
331 )
820397 332
H 333 provide('configGlobal', props)
334 let bpmnModeler: any = null
335 const defaultZoom = ref(1)
336 const previewModelVisible = ref(false)
337 const simulationStatus = ref(false)
338 const previewResult = ref('')
339 const previewType = ref('xml')
340 const recoverable = ref(false)
341 const revocable = ref(false)
342 const additionalModules = computed(() => {
343   console.log(props.additionalModel, 'additionalModel')
344   const Modules: any[] = []
345   // 仅保留用户自定义扩展模块
346   if (props.onlyCustomizeAddi) {
347     if (Object.prototype.toString.call(props.additionalModel) == '[object Array]') {
348       return props.additionalModel || []
349     }
350     return [props.additionalModel]
351   }
352
353   // 插入用户自定义扩展模块
354   if (Object.prototype.toString.call(props.additionalModel) == '[object Array]') {
355     Modules.push(...(props.additionalModel as any[]))
356   } else {
357     props.additionalModel && Modules.push(props.additionalModel)
358   }
359
360   // 翻译模块
361   const TranslateModule = {
362     translate: ['value', customTranslate(props.translations || translationsCN)]
363   }
364   Modules.push(TranslateModule)
365
366   // 模拟流转模块
367   if (props.simulation) {
368     Modules.push(tokenSimulation)
369   }
370
371   // 根据需要的流程类型设置扩展元素构建模块
372   // if (this.prefix === "bpmn") {
373   //   Modules.push(bpmnModdleExtension);
374   // }
375   console.log(props.prefix, 'props.prefix ')
376   if (props.prefix === 'camunda') {
377     Modules.push(camundaModdleExtension)
378   }
379   if (props.prefix === 'flowable') {
380     Modules.push(flowableModdleExtension)
381   }
382   if (props.prefix === 'activiti') {
383     Modules.push(activitiModdleExtension)
384   }
385
386   return Modules
387 })
388 const moddleExtensions = computed(() => {
389   console.log(props.onlyCustomizeModdle, 'props.onlyCustomizeModdle')
390   console.log(props.moddleExtension, 'props.moddleExtension')
391   console.log(props.prefix, 'props.prefix')
392   const Extensions: any = {}
393   // 仅使用用户自定义模块
394   if (props.onlyCustomizeModdle) {
395     return props.moddleExtension || null
396   }
397
398   // 插入用户自定义模块
399   if (props.moddleExtension) {
400     for (let key in props.moddleExtension) {
401       Extensions[key] = props.moddleExtension[key]
402     }
403   }
404
405   // 根据需要的 "流程类型" 设置 对应的解析文件
406   if (props.prefix === 'activiti') {
407     Extensions.activiti = activitiModdleDescriptor
408   }
409   if (props.prefix === 'flowable') {
410     Extensions.flowable = flowableModdleDescriptor
411   }
412   if (props.prefix === 'camunda') {
413     Extensions.camunda = camundaModdleDescriptor
414   }
415   return Extensions
416 })
417 console.log(additionalModules, 'additionalModules()')
418 console.log(moddleExtensions, 'moddleExtensions()')
419 const initBpmnModeler = () => {
420   if (bpmnModeler) return
421   let data = document.getElementById('bpmnCanvas')
422   console.log(data, 'data')
423   console.log(props.keyboard, 'props.keyboard')
424   console.log(additionalModules, 'additionalModules()')
425   console.log(moddleExtensions, 'moddleExtensions()')
426
427   bpmnModeler = new BpmnModeler({
428     // container: this.$refs['bpmn-canvas'],
429     // container: getCurrentInstance(),
430     // container: needClass,
431     // container: bpmnCanvas.value,
432     container: data,
433     // width: '100%',
434     // 添加控制板
435     // propertiesPanel: {
436     // parent: '#js-properties-panel'
437     // },
438     keyboard: props.keyboard ? { bindTo: document } : null,
439     // additionalModules: additionalModules.value,
440     additionalModules: additionalModules.value,
441     moddleExtensions: moddleExtensions.value
442
443     // additionalModules: [
444     // additionalModules.value
445     // propertiesPanelModule,
446     // propertiesProviderModule
447     // propertiesProviderModule
448     // ],
449     // moddleExtensions: { camunda: moddleExtensions.value }
450   })
451
452   // bpmnModeler.createDiagram()
453
454   // console.log(bpmnModeler, 'bpmnModeler111111')
455   emit('init-finished', bpmnModeler)
456   initModelListeners()
457 }
458
459 const initModelListeners = () => {
460   const EventBus = bpmnModeler.get('eventBus')
461   console.log(EventBus, 'EventBus')
462   // 注册需要的监听事件, 将. 替换为 - , 避免解析异常
463   props.events.forEach((event: any) => {
464     EventBus.on(event, function (eventObj) {
465       let eventName = event.replace(/\./g, '-')
466       // eventName.name = eventName
467       let element = eventObj ? eventObj.element : null
468       console.log(eventName, 'eventName')
469       console.log(element, 'element')
470       emit('element-click', element, eventObj)
471       // emit(eventName, element, eventObj)
472     })
473   })
474   // 监听图形改变返回xml
475   EventBus.on('commandStack.changed', async (event) => {
476     try {
477       recoverable.value = bpmnModeler.get('commandStack').canRedo()
478       revocable.value = bpmnModeler.get('commandStack').canUndo()
479       let { xml } = await bpmnModeler.saveXML({ format: true })
480       emit('commandStack-changed', event)
481       emit('input', xml)
482       emit('change', xml)
483     } catch (e: any) {
484       console.error(`[Process Designer Warn]: ${e.message || e}`)
485     }
486   })
487   // 监听视图缩放变化
488   bpmnModeler.on('canvas.viewbox.changed', ({ viewbox }) => {
489     emit('canvas-viewbox-changed', { viewbox })
490     const { scale } = viewbox
491     defaultZoom.value = Math.floor(scale * 100) / 100
492   })
493 }
494 /* 创建新的流程图 */
495 const createNewDiagram = async (xml) => {
496   console.log(xml, 'xml')
497   // 将字符串转换成图显示出来
498   let newId = props.processId || `Process_${new Date().getTime()}`
499   let newName = props.processName || `业务流程_${new Date().getTime()}`
500   let xmlString = xml || DefaultEmptyXML(newId, newName, props.prefix)
501   try {
502     // console.log(xmlString, 'xmlString')
503     // console.log(this.bpmnModeler.importXML);
504     let { warnings } = await bpmnModeler.importXML(xmlString)
505     console.log(warnings, 'warnings')
506     if (warnings && warnings.length) {
507       warnings.forEach((warn) => console.warn(warn))
508     }
509   } catch (e: any) {
510     console.error(`[Process Designer Warn]: ${e.message || e}`)
511   }
512 }
513
514 // 下载流程图到本地
515 const downloadProcess = async (type) => {
516   try {
517     // 按需要类型创建文件并下载
518     if (type === 'xml' || type === 'bpmn') {
519       const { err, xml } = await bpmnModeler.saveXML()
520       // 读取异常时抛出异常
521       if (err) {
522         console.error(`[Process Designer Warn ]: ${err.message || err}`)
523       }
524       let { href, filename } = setEncoded(type.toUpperCase(), xml)
525       downloadFunc(href, filename)
526     } else {
527       const { err, svg } = await bpmnModeler.saveSVG()
528       // 读取异常时抛出异常
529       if (err) {
530         return console.error(err)
531       }
532       let { href, filename } = setEncoded('SVG', svg)
533       downloadFunc(href, filename)
534     }
535   } catch (e: any) {
536     console.error(`[Process Designer Warn ]: ${e.message || e}`)
537   }
538   // 文件下载方法
539   function downloadFunc(href, filename) {
540     if (href && filename) {
541       let a = document.createElement('a')
542       a.download = filename //指定下载的文件名
543       a.href = href //  URL对象
544       a.click() // 模拟点击
545       URL.revokeObjectURL(a.href) // 释放URL 对象
546     }
547   }
548 }
549
550 // 根据所需类型进行转码并返回下载地址
551 const setEncoded = (type, data) => {
552   const filename = 'diagram'
553   const encodedData = encodeURIComponent(data)
554   return {
555     filename: `${filename}.${type}`,
556     href: `data:application/${
557       type === 'svg' ? 'text/xml' : 'bpmn20-xml'
558     };charset=UTF-8,${encodedData}`,
559     data: data
560   }
561 }
562
563 // 加载本地文件
564 const importLocalFile = () => {
565   const file = refFile.value.files[0]
566   const reader = new FileReader()
567   reader.readAsText(file)
568   reader.onload = function () {
569     let xmlStr = this.result
570     createNewDiagram(xmlStr)
571   }
572 }
573 /* ------------------------------------------------ refs methods ------------------------------------------------------ */
574 const downloadProcessAsXml = () => {
575   downloadProcess('xml')
576 }
577 const downloadProcessAsBpmn = () => {
578   downloadProcess('bpmn')
579 }
580 const downloadProcessAsSvg = () => {
581   downloadProcess('svg')
582 }
583 const processSimulation = () => {
584   simulationStatus.value = !simulationStatus.value
585   console.log(bpmnModeler.get('toggleMode', 'strict'), "bpmnModeler.get('toggleMode')")
586   props.simulation && bpmnModeler.get('toggleMode', 'strict').toggleMode()
587 }
588 const processRedo = () => {
589   bpmnModeler.get('commandStack').redo()
590 }
591 const processUndo = () => {
592   bpmnModeler.get('commandStack').undo()
593 }
594 const processZoomIn = (zoomStep = 0.1) => {
595   let newZoom = Math.floor(defaultZoom.value * 100 + zoomStep * 100) / 100
596   if (newZoom > 4) {
597     throw new Error('[Process Designer Warn ]: The zoom ratio cannot be greater than 4')
598   }
599   defaultZoom.value = newZoom
600   bpmnModeler.get('canvas').zoom(defaultZoom.value)
601 }
602 const processZoomOut = (zoomStep = 0.1) => {
603   let newZoom = Math.floor(defaultZoom.value * 100 - zoomStep * 100) / 100
604   if (newZoom < 0.2) {
605     throw new Error('[Process Designer Warn ]: The zoom ratio cannot be less than 0.2')
606   }
607   defaultZoom.value = newZoom
608   bpmnModeler.get('canvas').zoom(defaultZoom.value)
609 }
610 const processReZoom = () => {
611   defaultZoom.value = 1
612   bpmnModeler.get('canvas').zoom('fit-viewport', 'auto')
613 }
614 const processRestart = () => {
615   recoverable.value = false
616   revocable.value = false
617   createNewDiagram(null)
618 }
619 const elementsAlign = (align) => {
620   const Align = bpmnModeler.get('alignElements')
621   const Selection = bpmnModeler.get('selection')
622   const SelectedElements = Selection.get()
623   if (!SelectedElements || SelectedElements.length <= 1) {
624     ElMessage.warning('请按住 Shift 键选择多个元素对齐')
625     // alert('请按住 Ctrl 键选择多个元素对齐
626     return
627   }
628   ElMessageBox.confirm('自动对齐可能造成图形变形,是否继续?', '警告', {
629     confirmButtonText: '确定',
630     cancelButtonText: '取消',
631     type: 'warning'
632   }).then(() => {
633     Align.trigger(SelectedElements, align)
634   })
635 }
636 /*-----------------------------    方法结束     ---------------------------------*/
637 const previewProcessXML = () => {
638   console.log(bpmnModeler.saveXML, 'bpmnModeler')
639   bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
640     // console.log(xml, 'xml111111')
641     previewResult.value = xml
642     previewType.value = 'xml'
643     previewModelVisible.value = true
644   })
645 }
646 const previewProcessJson = () => {
647   bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
648     const rootNodes = new XmlNode(XmlNodeType.Root, parseXmlString(xml))
649     previewResult.value = rootNodes.parent?.toJSON() as unknown as string
650     previewType.value = 'json'
651     previewModelVisible.value = true
652   })
653 }
c9a6f7 654
3e359e 655 /* ------------------------------------------------ 芋道源码 methods ------------------------------------------------------ */
820397 656 onMounted(() => {
H 657   initBpmnModeler()
658   createNewDiagram(props.value)
659 })
660 onBeforeUnmount(() => {
661   if (bpmnModeler) bpmnModeler.destroy()
662   emit('destroy', bpmnModeler)
663   bpmnModeler = null
664 })
665 </script>