ProcessViewer.vue 17 KB

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