ProcessViewer.vue 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. <template>
  2. <div class="my-process-designer">
  3. <div class="my-process-designer__container">
  4. <div class="my-process-designer__canvas" ref="bpmn-canvas"></div>
  5. </div>
  6. </div>
  7. </template>
  8. <script>
  9. import BpmnViewer from "bpmn-js/lib/Viewer";
  10. import DefaultEmptyXML from "./plugins/defaultEmpty";
  11. export default {
  12. name: "MyProcessViewer",
  13. componentName: "MyProcessViewer",
  14. props: {
  15. value: String, // xml 字符串
  16. prefix: {
  17. type: String,
  18. default: "camunda"
  19. },
  20. taskData: {
  21. type: Array,
  22. default: () => []
  23. }
  24. },
  25. data() {
  26. return {
  27. xml: '',
  28. tasks: [],
  29. };
  30. },
  31. mounted() {
  32. this.xml = this.value;
  33. this.tasks = this.taskData;
  34. // 初始化
  35. this.initBpmnModeler();
  36. this.createNewDiagram(this.xml);
  37. this.$once("hook:beforeDestroy", () => {
  38. if (this.bpmnModeler) this.bpmnModeler.destroy();
  39. this.$emit("destroy", this.bpmnModeler);
  40. this.bpmnModeler = null;
  41. });
  42. // 初始模型的监听器
  43. this.initModelListeners();
  44. },
  45. watch: {
  46. value: function (newValue) { // 在 xmlString 发生变化时,重新创建,从而绘制流程图
  47. this.xml = newValue;
  48. this.createNewDiagram(this.xml);
  49. },
  50. taskData: function (newTaskData) {
  51. this.tasks = newTaskData;
  52. this.createNewDiagram(this.xml);
  53. }
  54. },
  55. methods: {
  56. initBpmnModeler() {
  57. if (this.bpmnModeler) return;
  58. this.bpmnModeler = new BpmnViewer({
  59. container: this.$refs["bpmn-canvas"],
  60. bpmnRenderer: {
  61. }
  62. })
  63. },
  64. /* 创建新的流程图 */
  65. async createNewDiagram(xml) {
  66. // 将字符串转换成图显示出来
  67. let newId = `Process_${new Date().getTime()}`;
  68. let newName = `业务流程_${new Date().getTime()}`;
  69. let xmlString = xml || DefaultEmptyXML(newId, newName, this.prefix);
  70. try {
  71. // console.log(this.bpmnModeler.importXML);
  72. let { warnings } = await this.bpmnModeler.importXML(xmlString);
  73. if (warnings && warnings.length) {
  74. warnings.forEach(warn => console.warn(warn));
  75. }
  76. // 高亮流程图
  77. await this.highlightDiagram();
  78. } catch (e) {
  79. console.error(e);
  80. // console.error(`[Process Designer Warn]: ${e?.message || e}`);
  81. }
  82. },
  83. /* 高亮流程图 */
  84. async highlightDiagram() {
  85. let activityList = this.tasks;
  86. if (activityList.length === 0) {
  87. return;
  88. }
  89. // 参考自 https://gitee.com/tony2y/RuoYi-flowable/blob/master/ruoyi-ui/src/components/Process/index.vue#L222 实现
  90. // 再次基础上,增加不同审批结果的颜色等等
  91. let canvas = this.bpmnModeler.get('canvas');
  92. let todoActivity = activityList.find(m => !m.endTime) // 找到待办的任务
  93. let endActivity = activityList[activityList.length - 1] // 找到结束任务
  94. this.bpmnModeler.getDefinitions().rootElements[0].flowElements?.forEach(n => {
  95. let activity = activityList.find(m => m.key === n.id) // 找到对应的活动
  96. if (n.$type === 'bpmn:UserTask') { // 用户任务
  97. if (!activity) {
  98. return;
  99. }
  100. if (activity.task) {
  101. const result = activity.task.result;
  102. if (result === 1) {
  103. canvas.addMarker(n.id, 'highlight-todo');
  104. } else if (result === 2) {
  105. canvas.addMarker(n.id, 'highlight');
  106. } else if (result === 3) {
  107. canvas.addMarker(n.id, 'highlight-reject');
  108. } else if (result === 4) {
  109. canvas.addMarker(n.id, 'highlight-cancel');
  110. }
  111. }
  112. n.outgoing?.forEach(nn => {
  113. let targetTask = activityList.find(m => m.key === nn.targetRef.id)
  114. if (targetTask) {
  115. canvas.addMarker(nn.id, targetTask.endTime ? 'highlight' : 'highlight-todo');
  116. } else if (nn.targetRef.$type === 'bpmn:ExclusiveGateway') {
  117. // canvas.addMarker(nn.id, 'highlight');
  118. canvas.addMarker(nn.id, activity.endTime ? 'highlight' : 'highlight-todo');
  119. canvas.addMarker(nn.targetRef.id, activity.endTime ? 'highlight' : 'highlight-todo');
  120. } else if (nn.targetRef.$type === 'bpmn:EndEvent') {
  121. if (!todoActivity && endActivity.key === n.id) {
  122. canvas.addMarker(nn.id, 'highlight');
  123. canvas.addMarker(nn.targetRef.id, 'highlight');
  124. }
  125. if (!activity.endTime) {
  126. canvas.addMarker(nn.id, 'highlight-todo');
  127. canvas.addMarker(nn.targetRef.id, 'highlight-todo');
  128. }
  129. }
  130. });
  131. } else if (n.$type === 'bpmn:ExclusiveGateway') { // 排它网关
  132. n.outgoing?.forEach(nn => {
  133. let targetTask = activityList.find(m => m.key === nn.targetRef.id)
  134. if (targetTask) {
  135. canvas.addMarker(nn.id, targetTask.endTime ? 'highlight' : 'highlight-todo');
  136. }
  137. })
  138. } else if (n.$type === 'bpmn:ParallelGateway') { // 并行网关
  139. if (activity) {
  140. canvas.addMarker(n.id, activity.endTime ? 'highlight' : 'highlight-todo')
  141. n.outgoing?.forEach(nn => {
  142. const targetTask = activityList.find(m => m.key === nn.targetRef.id)
  143. if (targetTask) {
  144. canvas.addMarker(nn.id, targetTask.endTime ? 'highlight' : 'highlight-todo')
  145. canvas.addMarker(nn.targetRef.id, targetTask.endTime ? 'highlight' : 'highlight-todo')
  146. }
  147. })
  148. }
  149. } else if (n.$type === 'bpmn:StartEvent') { // 开始节点
  150. n.outgoing?.forEach(nn => { // outgoing 例如说【bpmn:SequenceFlow】连线
  151. let fromTask = activityList.find(m => m.key === nn.targetRef.id)
  152. if (fromTask) {
  153. canvas.addMarker(nn.id, 'highlight');
  154. canvas.addMarker(n.id, 'highlight');
  155. }
  156. });
  157. } else if (n.$type === 'bpmn:EndEvent') { // 结束节点
  158. if (endActivity.key !== n.id) { // 保证 endActivity 就是 EndEvent
  159. return;
  160. }
  161. // 在并行网关后,跟着多个任务,如果其中一个任务完成,endActivity 的 endTime 就会存在值
  162. // 所以,通过 todoActivity 在做一次判断
  163. if (endActivity.endTime && !todoActivity) {
  164. canvas.addMarker(n.id, 'highlight');
  165. }
  166. }
  167. })
  168. },
  169. initModelListeners() {
  170. const EventBus = this.bpmnModeler.get("eventBus");
  171. const that = this;
  172. // 注册需要的监听事件, 将. 替换为 - , 避免解析异常
  173. EventBus.on('element.hover', function(eventObj) {
  174. let element = eventObj ? eventObj.element : null;
  175. that.elementHover(element);
  176. });
  177. EventBus.on('element.out', function(eventObj) {
  178. let element = eventObj ? eventObj.element : null;
  179. that.elementOut(element);
  180. });
  181. },
  182. // 流程图的元素被 hover
  183. elementHover(element) {
  184. this.element = element;
  185. !this.elementOverlayIds && (this.elementOverlayIds = {});
  186. !this.overlays && (this.overlays = this.bpmnModeler.get("overlays"));
  187. // 展示信息
  188. if (!this.elementOverlayIds[element.id] && element.type !== "bpmn:Process") {
  189. this.elementOverlayIds[element.id] = this.overlays.add(element, {
  190. position: { left: 0, bottom: 0 },
  191. html: `<div class="element-overlays">
  192. <p>Elemet id: ${element.id}</p>
  193. <p>Elemet type: ${element.type}</p>
  194. </div>`
  195. });
  196. }
  197. },
  198. // 流程图的元素被 out
  199. elementOut(element) {
  200. this.overlays.remove({ element });
  201. this.elementOverlayIds[element.id] = null;
  202. },
  203. }
  204. };
  205. </script>
  206. <style>
  207. /** 通过 */
  208. .highlight.djs-shape .djs-visual > :nth-child(1) {
  209. fill: green !important;
  210. stroke: green !important;
  211. fill-opacity: 0.2 !important;
  212. }
  213. .highlight.djs-shape .djs-visual > :nth-child(2) {
  214. fill: green !important;
  215. }
  216. .highlight.djs-shape .djs-visual > path {
  217. fill: green !important;
  218. fill-opacity: 0.2 !important;
  219. stroke: green !important;
  220. }
  221. .highlight.djs-connection > .djs-visual > path {
  222. stroke: green !important;
  223. }
  224. .highlight:not(.djs-connection) .djs-visual > :nth-child(1) {
  225. fill: green !important; /* color elements as green */
  226. }
  227. /deep/.highlight.djs-shape .djs-visual > :nth-child(1) {
  228. fill: green !important;
  229. stroke: green !important;
  230. fill-opacity: 0.2 !important;
  231. }
  232. /deep/.highlight.djs-shape .djs-visual > :nth-child(2) {
  233. fill: green !important;
  234. }
  235. /deep/.highlight.djs-shape .djs-visual > path {
  236. fill: green !important;
  237. fill-opacity: 0.2 !important;
  238. stroke: green !important;
  239. }
  240. /deep/.highlight.djs-connection > .djs-visual > path {
  241. stroke: green !important;
  242. }
  243. /** 处理中 */
  244. .highlight-todo.djs-connection > .djs-visual > path {
  245. stroke: orange !important;
  246. stroke-dasharray: 4px !important;
  247. fill-opacity: 0.2 !important;
  248. }
  249. .highlight-todo.djs-shape .djs-visual > :nth-child(1) {
  250. fill: orange !important;
  251. stroke: orange !important;
  252. stroke-dasharray: 4px !important;
  253. fill-opacity: 0.2 !important;
  254. }
  255. /deep/.highlight-todo.djs-connection > .djs-visual > path {
  256. stroke: orange !important;
  257. stroke-dasharray: 4px !important;
  258. fill-opacity: 0.2 !important;
  259. marker-end: url(#sequenceflow-end-_E7DFDF-_E7DFDF-803g1kf6zwzmcig1y2ulm5egr);
  260. }
  261. /deep/.highlight-todo.djs-shape .djs-visual > :nth-child(1) {
  262. fill: orange !important;
  263. stroke: orange !important;
  264. stroke-dasharray: 4px !important;
  265. fill-opacity: 0.2 !important;
  266. }
  267. /** 不通过 */
  268. .highlight-reject.djs-shape .djs-visual > :nth-child(1) {
  269. fill: red !important;
  270. stroke: red !important;
  271. fill-opacity: 0.2 !important;
  272. }
  273. .highlight-reject.djs-shape .djs-visual > :nth-child(2) {
  274. fill: red !important;
  275. }
  276. .highlight-reject.djs-shape .djs-visual > path {
  277. fill: red !important;
  278. fill-opacity: 0.2 !important;
  279. stroke: red !important;
  280. }
  281. .highlight-reject.djs-connection > .djs-visual > path {
  282. stroke: red !important;
  283. }
  284. .highlight-reject:not(.djs-connection) .djs-visual > :nth-child(1) {
  285. fill: red !important; /* color elements as green */
  286. }
  287. /deep/.highlight-reject.djs-shape .djs-visual > :nth-child(1) {
  288. fill: red !important;
  289. stroke: red !important;
  290. fill-opacity: 0.2 !important;
  291. }
  292. /deep/.highlight-reject.djs-shape .djs-visual > :nth-child(2) {
  293. fill: red !important;
  294. }
  295. /deep/.highlight-reject.djs-shape .djs-visual > path {
  296. fill: red !important;
  297. fill-opacity: 0.2 !important;
  298. stroke: red !important;
  299. }
  300. /deep/.highlight-reject.djs-connection > .djs-visual > path {
  301. stroke: red !important;
  302. }
  303. /** 已取消 */
  304. .highlight-cancel.djs-shape .djs-visual > :nth-child(1) {
  305. fill: grey !important;
  306. stroke: grey !important;
  307. fill-opacity: 0.2 !important;
  308. }
  309. .highlight-cancel.djs-shape .djs-visual > :nth-child(2) {
  310. fill: grey !important;
  311. }
  312. .highlight-cancel.djs-shape .djs-visual > path {
  313. fill: grey !important;
  314. fill-opacity: 0.2 !important;
  315. stroke: grey !important;
  316. }
  317. .highlight-cancel.djs-connection > .djs-visual > path {
  318. stroke: grey !important;
  319. }
  320. .highlight-cancel:not(.djs-connection) .djs-visual > :nth-child(1) {
  321. fill: grey !important; /* color elements as green */
  322. }
  323. /deep/.highlight-cancel.djs-shape .djs-visual > :nth-child(1) {
  324. fill: grey !important;
  325. stroke: grey !important;
  326. fill-opacity: 0.2 !important;
  327. }
  328. /deep/.highlight-cancel.djs-shape .djs-visual > :nth-child(2) {
  329. fill: grey !important;
  330. }
  331. /deep/.highlight-cancel.djs-shape .djs-visual > path {
  332. fill: grey !important;
  333. fill-opacity: 0.2 !important;
  334. stroke: grey !important;
  335. }
  336. /deep/.highlight-cancel.djs-connection > .djs-visual > path {
  337. stroke: grey !important;
  338. }
  339. </style>