ProcessDesigner.vue 17 KB

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