ProcessViewer.vue 18 KB

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