ProcessViewer.vue 13 KB

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