ProcessViewer.vue 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  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. import dictMixins from '@/mixins/dictMixins';
  12. export default {
  13. name: 'MyProcessViewer',
  14. componentName: 'MyProcessViewer',
  15. mixins: [dictMixins],
  16. props: {
  17. value: {
  18. // BPMN XML 字符串
  19. type: String
  20. },
  21. prefix: {
  22. // 使用哪个引擎
  23. type: String,
  24. default: 'camunda'
  25. },
  26. activityData: {
  27. // 活动的数据。传递时,可高亮流程
  28. type: Array,
  29. default: () => []
  30. },
  31. processInstanceData: {
  32. // 流程实例的数据。传递时,可展示流程发起人等信息
  33. type: Object
  34. },
  35. taskData: {
  36. // 任务实例的数据。传递时,可展示 UserTask 审核相关的信息
  37. type: Array,
  38. default: () => []
  39. }
  40. },
  41. data() {
  42. return {
  43. xml: '',
  44. activityList: [],
  45. processInstance: undefined,
  46. taskList: []
  47. };
  48. },
  49. created() {
  50. this.requestDict('流程实例的结果');
  51. },
  52. mounted() {
  53. this.xml = this.value;
  54. this.activityList = this.activityData;
  55. // 初始化
  56. this.initBpmnModeler();
  57. this.createNewDiagram(this.xml);
  58. this.$once('hook:beforeDestroy', () => {
  59. if (this.bpmnModeler) this.bpmnModeler.destroy();
  60. this.$emit('destroy', this.bpmnModeler);
  61. this.bpmnModeler = null;
  62. });
  63. // 初始模型的监听器
  64. this.initModelListeners();
  65. },
  66. watch: {
  67. value: function (newValue) {
  68. // 在 xmlString 发生变化时,重新创建,从而绘制流程图
  69. this.xml = newValue;
  70. this.createNewDiagram(this.xml);
  71. },
  72. activityData: function (newActivityData) {
  73. this.activityList = newActivityData;
  74. this.createNewDiagram(this.xml);
  75. },
  76. processInstanceData: function (newProcessInstanceData) {
  77. this.processInstance = newProcessInstanceData;
  78. this.createNewDiagram(this.xml);
  79. },
  80. taskData: function (newTaskListData) {
  81. this.taskList = newTaskListData;
  82. this.createNewDiagram(this.xml);
  83. }
  84. },
  85. methods: {
  86. initBpmnModeler() {
  87. if (this.bpmnModeler) return;
  88. this.bpmnModeler = new BpmnViewer({
  89. container: this.$refs['bpmn-canvas'],
  90. bpmnRenderer: {}
  91. });
  92. },
  93. /* 创建新的流程图 */
  94. async createNewDiagram(xml) {
  95. // 将字符串转换成图显示出来
  96. let newId = `Process_${new Date().getTime()}`;
  97. let newName = `业务流程_${new Date().getTime()}`;
  98. let xmlString = xml || DefaultEmptyXML(newId, newName, this.prefix);
  99. try {
  100. // console.log(this.bpmnModeler.importXML);
  101. let { warnings } = await this.bpmnModeler.importXML(xmlString);
  102. if (warnings && warnings.length) {
  103. warnings.forEach((warn) => console.warn(warn));
  104. }
  105. // 高亮流程图
  106. await this.highlightDiagram();
  107. const canvas = this.bpmnModeler.get('canvas');
  108. canvas.zoom('fit-viewport', 'auto');
  109. } catch (e) {
  110. console.error(e);
  111. // console.error(`[Process Designer Warn]: ${e?.message || e}`);
  112. }
  113. },
  114. /* 高亮流程图 */
  115. // TODO 芋艿:如果多个 endActivity 的话,目前的逻辑可能有一定的问题。https://www.jdon.com/workflow/multi-events.html
  116. async highlightDiagram() {
  117. const activityList = this.activityList;
  118. if (activityList.length === 0) {
  119. return;
  120. }
  121. // 参考自 https://gitee.com/tony2y/RuoYi-flowable/blob/master/ruoyi-ui/src/components/Process/index.vue#L222 实现
  122. // 再次基础上,增加不同审批结果的颜色等等
  123. let canvas = this.bpmnModeler.get('canvas');
  124. let todoActivity = activityList.find((m) => !m.endTime); // 找到待办的任务
  125. let endActivity = activityList[activityList.length - 1]; // 获得最后一个任务
  126. // debugger
  127. // console.log(this.bpmnModeler.getDefinitions().rootElements[0].flowElements);
  128. this.bpmnModeler
  129. .getDefinitions()
  130. .rootElements[0].flowElements?.forEach((n) => {
  131. let activity = activityList.find((m) => m.key === n.id); // 找到对应的活动
  132. if (!activity) {
  133. return;
  134. }
  135. if (n.$type === 'bpmn:UserTask') {
  136. // 用户任务
  137. // 处理用户任务的高亮
  138. const task = this.taskList.find((m) => m.id === activity.taskId); // 找到活动对应的 taskId
  139. if (!task) {
  140. return;
  141. }
  142. // 高亮任务
  143. canvas.addMarker(n.id, this.getResultCss(task.result));
  144. // 如果非通过,就不走后面的线条了
  145. if (task.result !== 2) {
  146. return;
  147. }
  148. // 处理 outgoing 出线
  149. const outgoing = this.getActivityOutgoing(activity);
  150. outgoing?.forEach((nn) => {
  151. // debugger
  152. let targetActivity = activityList.find(
  153. (m) => m.key === nn.targetRef.id
  154. );
  155. // 如果目标活动存在,则根据该活动是否结束,进行【bpmn:SequenceFlow】连线的高亮设置
  156. if (targetActivity) {
  157. canvas.addMarker(
  158. nn.id,
  159. targetActivity.endTime ? 'highlight' : 'highlight-todo'
  160. );
  161. } else if (nn.targetRef.$type === 'bpmn:ExclusiveGateway') {
  162. // TODO 芋艿:这个流程,暂时没走到过
  163. canvas.addMarker(
  164. nn.id,
  165. activity.endTime ? 'highlight' : 'highlight-todo'
  166. );
  167. canvas.addMarker(
  168. nn.targetRef.id,
  169. activity.endTime ? 'highlight' : 'highlight-todo'
  170. );
  171. } else if (nn.targetRef.$type === 'bpmn:EndEvent') {
  172. // TODO 芋艿:这个流程,暂时没走到过
  173. if (!todoActivity && endActivity.key === n.id) {
  174. canvas.addMarker(nn.id, 'highlight');
  175. canvas.addMarker(nn.targetRef.id, 'highlight');
  176. }
  177. if (!activity.endTime) {
  178. canvas.addMarker(nn.id, 'highlight-todo');
  179. canvas.addMarker(nn.targetRef.id, 'highlight-todo');
  180. }
  181. }
  182. });
  183. } else if (n.$type === 'bpmn:ExclusiveGateway') {
  184. // 排它网关
  185. // 设置【bpmn:ExclusiveGateway】排它网关的高亮
  186. canvas.addMarker(n.id, this.getActivityHighlightCss(activity));
  187. // 查找需要高亮的连线
  188. let matchNN = undefined;
  189. let matchActivity = undefined;
  190. n.outgoing?.forEach((nn) => {
  191. let targetActivity = activityList.find(
  192. (m) => m.key === nn.targetRef.id
  193. );
  194. if (!targetActivity) {
  195. return;
  196. }
  197. // 特殊判断 endEvent 类型的原因,ExclusiveGateway 可能后续连有 2 个路径:
  198. // 1. 一个是 UserTask => EndEvent
  199. // 2. 一个是 EndEvent
  200. // 在选择路径 1 时,其实 EndEvent 可能也存在,导致 1 和 2 都高亮,显然是不正确的。
  201. // 所以,在 matchActivity 为 EndEvent 时,需要进行覆盖~~
  202. if (!matchActivity || matchActivity.type === 'endEvent') {
  203. matchNN = nn;
  204. matchActivity = targetActivity;
  205. }
  206. });
  207. if (matchNN && matchActivity) {
  208. canvas.addMarker(
  209. matchNN.id,
  210. this.getActivityHighlightCss(matchActivity)
  211. );
  212. }
  213. } else if (n.$type === 'bpmn:ParallelGateway') {
  214. // 并行网关
  215. // 设置【bpmn:ParallelGateway】并行网关的高亮
  216. canvas.addMarker(n.id, this.getActivityHighlightCss(activity));
  217. n.outgoing?.forEach((nn) => {
  218. // 获得连线是否有指向目标。如果有,则进行高亮
  219. const targetActivity = activityList.find(
  220. (m) => m.key === nn.targetRef.id
  221. );
  222. if (targetActivity) {
  223. canvas.addMarker(
  224. nn.id,
  225. this.getActivityHighlightCss(targetActivity)
  226. ); // 高亮【bpmn:SequenceFlow】连线
  227. // 高亮【...】目标。其中 ... 可以是 bpm:UserTask、也可以是其它的。当然,如果是 bpm:UserTask 的话,其实不做高亮也没问题,因为上面有逻辑做了这块。
  228. canvas.addMarker(
  229. nn.targetRef.id,
  230. this.getActivityHighlightCss(targetActivity)
  231. );
  232. }
  233. });
  234. } else if (n.$type === 'bpmn:StartEvent') {
  235. // 开始节点
  236. n.outgoing?.forEach((nn) => {
  237. // outgoing 例如说【bpmn:SequenceFlow】连线
  238. // 获得连线是否有指向目标。如果有,则进行高亮
  239. let targetActivity = activityList.find(
  240. (m) => m.key === nn.targetRef.id
  241. );
  242. if (targetActivity) {
  243. canvas.addMarker(nn.id, 'highlight'); // 高亮【bpmn:SequenceFlow】连线
  244. canvas.addMarker(n.id, 'highlight'); // 高亮【bpmn:StartEvent】开始节点(自己)
  245. }
  246. });
  247. } else if (n.$type === 'bpmn:EndEvent') {
  248. // 结束节点
  249. if (!this.processInstance || this.processInstance.result === 1) {
  250. return;
  251. }
  252. canvas.addMarker(
  253. n.id,
  254. this.getResultCss(this.processInstance.result)
  255. );
  256. } else if (n.$type === 'bpmn:ServiceTask') {
  257. //服务任务
  258. if (activity.startTime > 0 && activity.endTime === 0) {
  259. //进入执行,标识进行色
  260. canvas.addMarker(n.id, this.getResultCss(1));
  261. }
  262. if (activity.endTime > 0) {
  263. // 执行完成,节点标识完成色, 所有outgoing标识完成色。
  264. canvas.addMarker(n.id, this.getResultCss(2));
  265. const outgoing = this.getActivityOutgoing(activity);
  266. outgoing?.forEach((out) => {
  267. canvas.addMarker(out.id, this.getResultCss(2));
  268. });
  269. }
  270. }
  271. });
  272. },
  273. getActivityHighlightCss(activity) {
  274. return activity.endTime ? 'highlight' : 'highlight-todo';
  275. },
  276. getResultCss(result) {
  277. if (result === 1) {
  278. // 审批中
  279. return 'highlight-todo';
  280. } else if (result === 2) {
  281. // 已通过
  282. return 'highlight';
  283. } else if (result === 3) {
  284. // 不通过
  285. return 'highlight-reject';
  286. } else if (result === 4) {
  287. // 已取消
  288. return 'highlight-cancel';
  289. } else if (result === 5) {
  290. // 已退回
  291. return 'highlight-back';
  292. } else if (result === 6) {
  293. // 已委派
  294. return 'highlight-todo';
  295. }
  296. return '';
  297. },
  298. getActivityOutgoing(activity) {
  299. // 如果有 outgoing,则直接使用它
  300. if (activity.outgoing && activity.outgoing.length > 0) {
  301. return activity.outgoing;
  302. }
  303. // 如果没有,则遍历获得起点为它的【bpmn:SequenceFlow】节点们。原因是:bpmn-js 的 UserTask 拿不到 outgoing
  304. const flowElements =
  305. this.bpmnModeler.getDefinitions().rootElements[0].flowElements;
  306. const outgoing = [];
  307. flowElements.forEach((item) => {
  308. if (item.$type !== 'bpmn:SequenceFlow') {
  309. return;
  310. }
  311. if (item.sourceRef.id === activity.key) {
  312. outgoing.push(item);
  313. }
  314. });
  315. return outgoing;
  316. },
  317. initModelListeners() {
  318. const EventBus = this.bpmnModeler.get('eventBus');
  319. const that = this;
  320. // 注册需要的监听事件
  321. EventBus.on('element.hover', function (eventObj) {
  322. let element = eventObj ? eventObj.element : null;
  323. that.elementHover(element);
  324. });
  325. EventBus.on('element.out', function (eventObj) {
  326. let element = eventObj ? eventObj.element : null;
  327. that.elementOut(element);
  328. });
  329. },
  330. // 流程图的元素被 hover
  331. elementHover(element) {
  332. this.element = element;
  333. !this.elementOverlayIds && (this.elementOverlayIds = {});
  334. !this.overlays && (this.overlays = this.bpmnModeler.get('overlays'));
  335. // 展示信息
  336. const activity = this.activityList.find((m) => m.key === element.id);
  337. if (!activity) {
  338. return;
  339. }
  340. if (
  341. !this.elementOverlayIds[element.id] &&
  342. element.type !== 'bpmn:Process'
  343. ) {
  344. let html = `<div class="element-overlays">
  345. <p>Elemet id: ${element.id}</p>
  346. <p>Elemet type: ${element.type}</p>
  347. </div>`; // 默认值
  348. if (element.type === 'bpmn:StartEvent' && this.processInstance) {
  349. html = `<p>发起人:${this.processInstance.startUser.nickname}</p>
  350. <p>部门:${this.processInstance.startUser.deptName}</p>
  351. <p>创建时间:${this.processInstance.createTime}`;
  352. } else if (element.type === 'bpmn:UserTask') {
  353. // debugger
  354. let task = this.taskList.find((m) => m.id === activity.taskId); // 找到活动对应的 taskId
  355. if (!task) {
  356. return;
  357. }
  358. html = `<p>审批人:${task.assigneeUser.nickname}</p>
  359. <p>部门:${task.assigneeUser.deptName}</p>
  360. <p>结果:${this.getDictValue(
  361. '流程实例的结果',
  362. task.result
  363. )}</p>
  364. <p>创建时间:${task.createTime}</p>`;
  365. if (task.endTime) {
  366. html += `<p>结束时间:${task.endTime}</p>`;
  367. }
  368. if (task.reason) {
  369. html += `<p>审批建议:${task.reason}</p>`;
  370. }
  371. } else if (
  372. element.type === 'bpmn:ServiceTask' &&
  373. this.processInstance
  374. ) {
  375. if (activity.startTime > 0) {
  376. html = `<p>创建时间:${activity.startTime}</p>`;
  377. }
  378. if (activity.endTime > 0) {
  379. html += `<p>结束时间:${activity.endTime}</p>`;
  380. }
  381. console.log(html);
  382. } else if (element.type === 'bpmn:EndEvent' && this.processInstance) {
  383. html = `<p>结果:${this.getDictValue(
  384. '流程实例的结果',
  385. this.processInstance.result
  386. )}</p>`;
  387. if (this.processInstance.endTime) {
  388. html += `<p>结束时间:${this.processInstance.endTime}</p>`;
  389. }
  390. }
  391. this.elementOverlayIds[element.id] = this.overlays.add(element, {
  392. position: { left: 0, bottom: 0 },
  393. html: `<div class="element-overlays">${html}</div>`
  394. });
  395. }
  396. },
  397. // 流程图的元素被 out
  398. elementOut(element) {
  399. this.overlays.remove({ element });
  400. this.elementOverlayIds[element.id] = null;
  401. }
  402. }
  403. };
  404. </script>
  405. <style>
  406. .bjs-powered-by {
  407. display: none;
  408. }
  409. /** 处理中 */
  410. .highlight-todo.djs-connection > .djs-visual > path {
  411. stroke: #1890ff !important;
  412. stroke-dasharray: 4px !important;
  413. fill-opacity: 0.2 !important;
  414. }
  415. .highlight-todo.djs-shape .djs-visual > :nth-child(1) {
  416. fill: #1890ff !important;
  417. stroke: #1890ff !important;
  418. stroke-dasharray: 4px !important;
  419. fill-opacity: 0.2 !important;
  420. }
  421. :deep(.highlight-todo.djs-connection > .djs-visual > path) {
  422. stroke: #1890ff !important;
  423. stroke-dasharray: 4px !important;
  424. fill-opacity: 0.2 !important;
  425. marker-end: url(#sequenceflow-end-_E7DFDF-_E7DFDF-803g1kf6zwzmcig1y2ulm5egr);
  426. }
  427. :deep(.highlight-todo.djs-shape .djs-visual > :nth-child(1)) {
  428. fill: #1890ff !important;
  429. stroke: #1890ff !important;
  430. stroke-dasharray: 4px !important;
  431. fill-opacity: 0.2 !important;
  432. }
  433. /** 通过 */
  434. .highlight.djs-shape .djs-visual > :nth-child(1) {
  435. fill: green !important;
  436. stroke: green !important;
  437. fill-opacity: 0.2 !important;
  438. }
  439. .highlight.djs-shape .djs-visual > :nth-child(2) {
  440. fill: green !important;
  441. }
  442. .highlight.djs-shape .djs-visual > path {
  443. fill: green !important;
  444. fill-opacity: 0.2 !important;
  445. stroke: green !important;
  446. }
  447. .highlight.djs-connection > .djs-visual > path {
  448. stroke: green !important;
  449. }
  450. .highlight:not(.djs-connection) .djs-visual > :nth-child(1) {
  451. fill: green !important; /* color elements as green */
  452. }
  453. :deep(.highlight.djs-shape .djs-visual > :nth-child(1)) {
  454. fill: green !important;
  455. stroke: green !important;
  456. fill-opacity: 0.2 !important;
  457. }
  458. :deep(.highlight.djs-shape .djs-visual > :nth-child(2)) {
  459. fill: green !important;
  460. }
  461. :deep(.highlight.djs-shape .djs-visual > path) {
  462. fill: green !important;
  463. fill-opacity: 0.2 !important;
  464. stroke: green !important;
  465. }
  466. :deep(.highlight.djs-connection > .djs-visual > path) {
  467. stroke: green !important;
  468. }
  469. /** 不通过 */
  470. .highlight-reject.djs-shape .djs-visual > :nth-child(1) {
  471. fill: red !important;
  472. stroke: red !important;
  473. fill-opacity: 0.2 !important;
  474. }
  475. .highlight-reject.djs-shape .djs-visual > :nth-child(2) {
  476. fill: red !important;
  477. }
  478. .highlight-reject.djs-shape .djs-visual > path {
  479. fill: red !important;
  480. fill-opacity: 0.2 !important;
  481. stroke: red !important;
  482. }
  483. .highlight-reject.djs-connection > .djs-visual > path {
  484. stroke: red !important;
  485. }
  486. .highlight-reject:not(.djs-connection) .djs-visual > :nth-child(1) {
  487. fill: red !important; /* color elements as green */
  488. }
  489. :deep(.highlight-reject.djs-shape .djs-visual > :nth-child(1)) {
  490. fill: red !important;
  491. stroke: red !important;
  492. fill-opacity: 0.2 !important;
  493. }
  494. :deep(.highlight-reject.djs-shape .djs-visual > :nth-child(2)) {
  495. fill: red !important;
  496. }
  497. :deep(.highlight-reject.djs-shape .djs-visual > path) {
  498. fill: red !important;
  499. fill-opacity: 0.2 !important;
  500. stroke: red !important;
  501. }
  502. :deep(.highlight-reject.djs-connection > .djs-visual > path) {
  503. stroke: red !important;
  504. }
  505. /** 已取消 */
  506. .highlight-cancel.djs-shape .djs-visual > :nth-child(1) {
  507. fill: grey !important;
  508. stroke: grey !important;
  509. fill-opacity: 0.2 !important;
  510. }
  511. .highlight-cancel.djs-shape .djs-visual > :nth-child(2) {
  512. fill: grey !important;
  513. }
  514. .highlight-cancel.djs-shape .djs-visual > path {
  515. fill: grey !important;
  516. fill-opacity: 0.2 !important;
  517. stroke: grey !important;
  518. }
  519. .highlight-cancel.djs-connection > .djs-visual > path {
  520. stroke: grey !important;
  521. }
  522. .highlight-cancel:not(.djs-connection) .djs-visual > :nth-child(1) {
  523. fill: grey !important; /* color elements as green */
  524. }
  525. :deep(.highlight-cancel.djs-shape .djs-visual > :nth-child(1)) {
  526. fill: grey !important;
  527. stroke: grey !important;
  528. fill-opacity: 0.2 !important;
  529. }
  530. :deep(.highlight-cancel.djs-shape .djs-visual > :nth-child(2)) {
  531. fill: grey !important;
  532. }
  533. :deep(.highlight-cancel.djs-shape .djs-visual > path) {
  534. fill: grey !important;
  535. fill-opacity: 0.2 !important;
  536. stroke: grey !important;
  537. }
  538. :deep(.highlight-cancel.djs-connection > .djs-visual > path) {
  539. stroke: grey !important;
  540. }
  541. /**驳回 */
  542. .highlight-back.djs-connection > .djs-visual > path {
  543. stroke: #ffba00 !important;
  544. stroke-dasharray: 4px !important;
  545. fill-opacity: 0.2 !important;
  546. }
  547. .highlight-back.djs-shape .djs-visual > :nth-child(1) {
  548. fill: #ffba00 !important;
  549. stroke: #ffba00 !important;
  550. stroke-dasharray: 4px !important;
  551. fill-opacity: 0.2 !important;
  552. }
  553. :deep(.highlight-back.djs-connection > .djs-visual > path) {
  554. stroke: #ffba00 !important;
  555. stroke-dasharray: 4px !important;
  556. fill-opacity: 0.2 !important;
  557. marker-end: url(#sequenceflow-end-_E7DFDF-_E7DFDF-803g1kf6zwzmcig1y2ulm5egr);
  558. }
  559. :deep(.highlight-back.djs-shape .djs-visual > :nth-child(1)) {
  560. fill: #ffba00 !important;
  561. stroke: #ffba00 !important;
  562. stroke-dasharray: 4px !important;
  563. fill-opacity: 0.2 !important;
  564. }
  565. .element-overlays {
  566. box-sizing: border-box;
  567. padding: 8px;
  568. background: rgba(0, 0, 0, 0.6);
  569. border-radius: 4px;
  570. color: #fafafa;
  571. width: 200px;
  572. }
  573. </style>