ProcessViewer.vue 18 KB

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