ProcessDesigner.vue 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  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" :disabled = "simulationStatus">保存模型</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. valueWatch: true, // xml 字符串的 watch 状态
  119. processId: String, // 流程 key 标识
  120. processName: String, // 流程 name 名字
  121. formId: Number, // 流程 form 表单编号
  122. translations: Object, // 自定义的翻译文件
  123. additionalModel: [Object, Array], // 自定义model
  124. moddleExtension: Object, // 自定义moddle
  125. onlyCustomizeAddi: {
  126. type: Boolean,
  127. default: false
  128. },
  129. onlyCustomizeModdle: {
  130. type: Boolean,
  131. default: false
  132. },
  133. simulation: {
  134. type: Boolean,
  135. default: true
  136. },
  137. keyboard: {
  138. type: Boolean,
  139. default: true
  140. },
  141. prefix: {
  142. type: String,
  143. default: "camunda"
  144. },
  145. events: {
  146. type: Array,
  147. default: () => ["element.click"]
  148. },
  149. headerButtonSize: {
  150. type: String,
  151. default: "small",
  152. validator: value => ["default", "medium", "small", "mini"].indexOf(value) !== -1
  153. },
  154. headerButtonType: {
  155. type: String,
  156. default: "primary",
  157. validator: value => ["default", "primary", "success", "warning", "danger", "info"].indexOf(value) !== -1
  158. }
  159. },
  160. data() {
  161. return {
  162. defaultZoom: 1,
  163. previewModelVisible: false,
  164. simulationStatus: false,
  165. previewResult: "",
  166. previewType: "xml",
  167. recoverable: false,
  168. revocable: false
  169. };
  170. },
  171. computed: {
  172. additionalModules() {
  173. const Modules = [];
  174. // 仅保留用户自定义扩展模块
  175. if (this.onlyCustomizeAddi) {
  176. if (Object.prototype.toString.call(this.additionalModel) === "[object Array]") {
  177. return this.additionalModel || [];
  178. }
  179. return [this.additionalModel];
  180. }
  181. // 插入用户自定义扩展模块
  182. if (Object.prototype.toString.call(this.additionalModel) === "[object Array]") {
  183. Modules.push(...this.additionalModel);
  184. } else {
  185. this.additionalModel && Modules.push(this.additionalModel);
  186. }
  187. // 翻译模块
  188. const TranslateModule = {
  189. translate: ["value", customTranslate(this.translations || translationsCN)]
  190. };
  191. Modules.push(TranslateModule);
  192. // 模拟流转模块
  193. if (this.simulation) {
  194. Modules.push(tokenSimulation);
  195. }
  196. // 根据需要的流程类型设置扩展元素构建模块
  197. // if (this.prefix === "bpmn") {
  198. // Modules.push(bpmnModdleExtension);
  199. // }
  200. if (this.prefix === "camunda") {
  201. Modules.push(camundaModdleExtension);
  202. }
  203. if (this.prefix === "flowable") {
  204. Modules.push(flowableModdleExtension);
  205. }
  206. if (this.prefix === "activiti") {
  207. Modules.push(activitiModdleExtension);
  208. }
  209. return Modules;
  210. },
  211. moddleExtensions() {
  212. const Extensions = {};
  213. // 仅使用用户自定义模块
  214. if (this.onlyCustomizeModdle) {
  215. return this.moddleExtension || null;
  216. }
  217. // 插入用户自定义模块
  218. if (this.moddleExtension) {
  219. for (let key in this.moddleExtension) {
  220. Extensions[key] = this.moddleExtension[key];
  221. }
  222. }
  223. // 根据需要的 "流程类型" 设置 对应的解析文件
  224. if (this.prefix === "activiti") {
  225. Extensions.activiti = activitiModdleDescriptor;
  226. }
  227. if (this.prefix === "flowable") {
  228. Extensions.flowable = flowableModdleDescriptor;
  229. }
  230. if (this.prefix === "camunda") {
  231. Extensions.camunda = camundaModdleDescriptor;
  232. }
  233. return Extensions;
  234. }
  235. },
  236. mounted() {
  237. this.initBpmnModeler();
  238. this.createNewDiagram(this.value);
  239. this.$once("hook:beforeDestroy", () => {
  240. if (this.bpmnModeler) this.bpmnModeler.destroy();
  241. this.$emit("destroy", this.bpmnModeler);
  242. this.bpmnModeler = null;
  243. });
  244. },
  245. methods: {
  246. initBpmnModeler() {
  247. if (this.bpmnModeler) return;
  248. this.bpmnModeler = new BpmnModeler({
  249. container: this.$refs["bpmn-canvas"],
  250. keyboard: this.keyboard ? { bindTo: document } : null,
  251. additionalModules: this.additionalModules,
  252. moddleExtensions: this.moddleExtensions
  253. });
  254. this.$emit("init-finished", this.bpmnModeler);
  255. this.initModelListeners();
  256. },
  257. initModelListeners() {
  258. const EventBus = this.bpmnModeler.get("eventBus");
  259. const that = this;
  260. // 注册需要的监听事件, 将. 替换为 - , 避免解析异常
  261. this.events.forEach(event => {
  262. EventBus.on(event, function(eventObj) {
  263. let eventName = event.replace(/\./g, "-");
  264. let element = eventObj ? eventObj.element : null;
  265. that.$emit(eventName, element, eventObj);
  266. });
  267. });
  268. // 监听图形改变返回xml
  269. EventBus.on("commandStack.changed", async event => {
  270. try {
  271. this.recoverable = this.bpmnModeler.get("commandStack").canRedo();
  272. this.revocable = this.bpmnModeler.get("commandStack").canUndo();
  273. let { xml } = await this.bpmnModeler.saveXML({ format: true });
  274. this.$emit("commandStack-changed", event);
  275. this.$emit("input", xml);
  276. this.$emit("change", xml);
  277. } catch (e) {
  278. console.error(`[Process Designer Warn]: ${e.message || e}`);
  279. }
  280. });
  281. // 监听视图缩放变化
  282. this.bpmnModeler.on("canvas.viewbox.changed", ({ viewbox }) => {
  283. this.$emit("canvas-viewbox-changed", { viewbox });
  284. const { scale } = viewbox;
  285. this.defaultZoom = Math.floor(scale * 100) / 100;
  286. });
  287. },
  288. /* 创建新的流程图 */
  289. async createNewDiagram(xml) {
  290. // 将字符串转换成图显示出来
  291. let newId = this.processId || `Process_${new Date().getTime()}`;
  292. let newName = this.processName || `业务流程_${new Date().getTime()}`;
  293. let xmlString = xml || DefaultEmptyXML(newId, newName, this.prefix);
  294. try {
  295. // console.log(this.bpmnModeler.importXML);
  296. let { warnings } = await this.bpmnModeler.importXML(xmlString);
  297. if (warnings && warnings.length) {
  298. warnings.forEach(warn => console.warn(warn));
  299. }
  300. } catch (e) {
  301. console.error(`[Process Designer Warn]: ${e?.message || e}`);
  302. }
  303. },
  304. // 下载流程图到本地
  305. async downloadProcess(type, name) {
  306. try {
  307. const _this = this;
  308. // 按需要类型创建文件并下载
  309. if (type === "xml" || type === "bpmn") {
  310. const { err, xml } = await this.bpmnModeler.saveXML();
  311. // 读取异常时抛出异常
  312. if (err) {
  313. console.error(`[Process Designer Warn ]: ${err.message || err}`);
  314. }
  315. let { href, filename } = _this.setEncoded(type.toUpperCase(), name, xml);
  316. downloadFunc(href, filename);
  317. } else {
  318. const { err, svg } = await this.bpmnModeler.saveSVG();
  319. // 读取异常时抛出异常
  320. if (err) {
  321. return console.error(err);
  322. }
  323. let { href, filename } = _this.setEncoded("SVG", name, svg);
  324. downloadFunc(href, filename);
  325. }
  326. } catch (e) {
  327. console.error(`[Process Designer Warn ]: ${e.message || e}`);
  328. }
  329. // 文件下载方法
  330. function downloadFunc(href, filename) {
  331. if (href && filename) {
  332. let a = document.createElement("a");
  333. a.download = filename; //指定下载的文件名
  334. a.href = href; // URL对象
  335. a.click(); // 模拟点击
  336. URL.revokeObjectURL(a.href); // 释放URL 对象
  337. }
  338. }
  339. },
  340. // 根据所需类型进行转码并返回下载地址
  341. setEncoded(type, filename = "diagram", data) {
  342. const encodedData = encodeURIComponent(data);
  343. return {
  344. filename: `${filename}.${type}`,
  345. href: `data:application/${type === "svg" ? "text/xml" : "bpmn20-xml"};charset=UTF-8,${encodedData}`,
  346. data: data
  347. };
  348. },
  349. // 加载本地文件
  350. importLocalFile() {
  351. const that = this;
  352. const file = this.$refs.refFile.files[0];
  353. const reader = new FileReader();
  354. reader.readAsText(file);
  355. reader.onload = function() {
  356. let xmlStr = this.result;
  357. that.createNewDiagram(xmlStr);
  358. };
  359. },
  360. /* ------------------------------------------------ refs methods ------------------------------------------------------ */
  361. downloadProcessAsXml() {
  362. this.downloadProcess("xml");
  363. },
  364. downloadProcessAsBpmn() {
  365. this.downloadProcess("bpmn");
  366. },
  367. downloadProcessAsSvg() {
  368. this.downloadProcess("svg");
  369. },
  370. processSimulation() {
  371. this.simulationStatus = !this.simulationStatus;
  372. this.simulation && this.bpmnModeler.get("toggleMode").toggleMode();
  373. },
  374. processRedo() {
  375. this.bpmnModeler.get("commandStack").redo();
  376. },
  377. processUndo() {
  378. this.bpmnModeler.get("commandStack").undo();
  379. },
  380. processZoomIn(zoomStep = 0.1) {
  381. let newZoom = Math.floor(this.defaultZoom * 100 + zoomStep * 100) / 100;
  382. if (newZoom > 4) {
  383. throw new Error("[Process Designer Warn ]: The zoom ratio cannot be greater than 4");
  384. }
  385. this.defaultZoom = newZoom;
  386. this.bpmnModeler.get("canvas").zoom(this.defaultZoom);
  387. },
  388. processZoomOut(zoomStep = 0.1) {
  389. let newZoom = Math.floor(this.defaultZoom * 100 - zoomStep * 100) / 100;
  390. if (newZoom < 0.2) {
  391. throw new Error("[Process Designer Warn ]: The zoom ratio cannot be less than 0.2");
  392. }
  393. this.defaultZoom = newZoom;
  394. this.bpmnModeler.get("canvas").zoom(this.defaultZoom);
  395. },
  396. processZoomTo(newZoom = 1) {
  397. if (newZoom < 0.2) {
  398. throw new Error("[Process Designer Warn ]: The zoom ratio cannot be less than 0.2");
  399. }
  400. if (newZoom > 4) {
  401. throw new Error("[Process Designer Warn ]: The zoom ratio cannot be greater than 4");
  402. }
  403. this.defaultZoom = newZoom;
  404. this.bpmnModeler.get("canvas").zoom(newZoom);
  405. },
  406. processReZoom() {
  407. this.defaultZoom = 1;
  408. this.bpmnModeler.get("canvas").zoom("fit-viewport", "auto");
  409. },
  410. processRestart() {
  411. this.recoverable = false;
  412. this.revocable = false;
  413. this.createNewDiagram(null);
  414. },
  415. elementsAlign(align) {
  416. const Align = this.bpmnModeler.get("alignElements");
  417. const Selection = this.bpmnModeler.get("selection");
  418. const SelectedElements = Selection.get();
  419. if (!SelectedElements || SelectedElements.length <= 1) {
  420. this.$message.warning("请按住 Ctrl 键选择多个元素对齐");
  421. return;
  422. }
  423. this.$confirm("自动对齐可能造成图形变形,是否继续?", "警告", {
  424. confirmButtonText: "确定",
  425. cancelButtonText: "取消",
  426. type: "warning"
  427. }).then(() => Align.trigger(SelectedElements, align));
  428. },
  429. /*----------------------------- 方法结束 ---------------------------------*/
  430. previewProcessXML() {
  431. this.bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
  432. this.previewResult = xml;
  433. this.previewType = "xml";
  434. this.previewModelVisible = true;
  435. });
  436. },
  437. previewProcessJson() {
  438. this.bpmnModeler.saveXML({ format: true }).then(({ xml }) => {
  439. this.previewResult = convert.xml2json(xml, { spaces: 2 });
  440. this.previewType = "json";
  441. this.previewModelVisible = true;
  442. });
  443. },
  444. /* ------------------------------------------------ 芋道源码 methods ------------------------------------------------------ */
  445. async processSave() {
  446. const { err, xml } = await this.bpmnModeler.saveXML();
  447. // 读取异常时抛出异常
  448. if (err) {
  449. this.$modal.msgError('保存模型失败,请重试!')
  450. return
  451. }
  452. // 触发 save 事件
  453. this.$emit('save', xml)
  454. },
  455. /** 高亮显示 */
  456. highlightedCode(previewType, previewResult) {
  457. const result = hljs.highlight(previewType, previewResult || "", true);
  458. return result.value || '&nbsp;';
  459. },
  460. }
  461. };
  462. </script>