当前位置:   article > 正文

ruoyi-nbcio-plus基于vue3的flowable流程设计器主界面升级修改_flowable vue3流程设计器

flowable vue3流程设计器

更多ruoyi-nbcio功能请看演示系统

gitee源代码地址

前后端代码: https://gitee.com/nbacheng/ruoyi-nbcio

演示地址:RuoYi-Nbcio后台管理系统 http://122.227.135.243:9666/

更多nbcio-boot功能请看演示系统 

gitee源代码地址

后端代码: https://gitee.com/nbacheng/nbcio-boot

前端代码:https://gitee.com/nbacheng/nbcio-vue.git

在线演示(包括H5) : http://122.227.135.243:9888

1、原有ProcessDesigner.vue的vue2代码如下:

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

2、修改成vue3后的代码如下:

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

3、效果图如下:

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/418040
推荐阅读
相关标签
  

闽ICP备14008679号