ProcessDesigner.vue 18 KB

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