ProcessViewer.vue 17 KB

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