ProcessViewer.vue 19 KB

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