ProcessDesigner.vue 18 KB

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