view.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. <script setup lang="ts">
  2. import BpmnViewer from "bpmn-js/lib/Viewer"
  3. import MoveCanvasModule from "diagram-js/lib/navigation/movecanvas"
  4. import ZoomScrollModule from "diagram-js/lib/navigation/zoomscroll"
  5. import type { ModuleDeclaration } from "didi"
  6. import type { Canvas, ModdleElement } from "vb-bpmn"
  7. import type EventBus from "diagram-js/lib/core/EventBus"
  8. import type Overlays from "diagram-js/lib/features/overlays/Overlays"
  9. import apis from "@a"
  10. const canvas = ref<HTMLElement>()
  11. const modeler = ref<BpmnViewer>()
  12. const taskList = ref([])
  13. const zoom = ref(1)
  14. const xml = ref("")
  15. const bpmnVisible = ref(true)
  16. const historyList = ref([])
  17. // 让图能自适应屏幕
  18. function onFitViewport() {
  19. fitViewport()
  20. }
  21. // 放大缩小
  22. function onZoomViewport(zoomIn = true) {
  23. zoom.value = modeler.value!.get<Canvas>("canvas").zoom()
  24. zoom.value += zoomIn ? 0.1 : -0.1
  25. modeler.value!.get<Canvas>("canvas").zoom(zoom.value)
  26. }
  27. // 让图能自适应屏幕
  28. function fitViewport() {
  29. zoom.value = modeler.value!.get<Canvas>("canvas").zoom("fit-viewport")
  30. const bbox = document.querySelector<SVGGElement>(".flow-containers .viewport")!.getBBox()
  31. const currentViewBox = (modeler.value!.get("canvas") as any).viewbox()
  32. const elementMid = {
  33. x: bbox.x + bbox.width / 2 - 65,
  34. y: bbox.y + bbox.height / 2
  35. }
  36. modeler.value!.get<Canvas>("canvas").viewbox({
  37. x: elementMid.x - currentViewBox.width / 2,
  38. y: elementMid.y - currentViewBox.height / 2,
  39. width: currentViewBox.width,
  40. height: currentViewBox.height
  41. })
  42. zoom.value = (bbox.width / currentViewBox.width) * 1.8
  43. }
  44. //上色
  45. function fillColor() {
  46. const canvas = modeler.value!.get<Canvas>("canvas")
  47. bpmnNodeList(modeler.value?.getDefinitions().rootElements[0].flowElements, canvas)
  48. //递归上色
  49. function bpmnNodeList(flowElements, canvas) {
  50. flowElements.forEach((n) => {
  51. if (n.$type === "bpmn:UserTask") {
  52. const completeTask: any = taskList.value.find((m: any) => m.key === n.id)
  53. if (completeTask) {
  54. canvas.addMarker(n.id, completeTask.completed ? "highlight" : "highlight-todo")
  55. n.outgoing?.forEach((nn) => {
  56. const targetTask: any = taskList.value.find((m: any) => m.key === nn.targetRef.id)
  57. if (targetTask) {
  58. canvas.addMarker(nn.id, targetTask.completed ? "highlight" : "highlight-todo")
  59. } else if (nn.targetRef.$type === "bpmn:ExclusiveGateway") {
  60. canvas.addMarker(nn.id, completeTask.completed ? "highlight" : "highlight-todo")
  61. canvas.addMarker(
  62. nn.targetRef.id,
  63. completeTask.completed ? "highlight" : "highlight-todo"
  64. )
  65. nn.targetRef.outgoing.forEach((e) => {
  66. gateway(e.id, e.targetRef.$type, e.targetRef.id, canvas, completeTask.completed)
  67. })
  68. } else if (nn.targetRef.$type === "bpmn:ParallelGateway") {
  69. canvas.addMarker(nn.id, completeTask.completed ? "highlight" : "highlight-todo")
  70. canvas.addMarker(
  71. nn.targetRef.id,
  72. completeTask.completed ? "highlight" : "highlight-todo"
  73. )
  74. nn.targetRef.outgoing.forEach((e) => {
  75. gateway(e.id, e.targetRef.$type, e.targetRef.id, canvas, completeTask.completed)
  76. })
  77. } else if (nn.targetRef.$type === "bpmn:InclusiveGateway") {
  78. canvas.addMarker(nn.id, completeTask.completed ? "highlight" : "highlight-todo")
  79. canvas.addMarker(
  80. nn.targetRef.id,
  81. completeTask.completed ? "highlight" : "highlight-todo"
  82. )
  83. nn.targetRef.outgoing.forEach((e) => {
  84. gateway(e.id, e.targetRef.$type, e.targetRef.id, canvas, completeTask.completed)
  85. })
  86. }
  87. })
  88. }
  89. } else if (n.$type === "bpmn:ExclusiveGateway") {
  90. n.outgoing.forEach((nn) => {
  91. const targetTask: any = taskList.value.find((m: any) => m.key === nn.targetRef.id)
  92. if (targetTask) {
  93. canvas.addMarker(nn.id, targetTask.completed ? "highlight" : "highlight-todo")
  94. }
  95. })
  96. } else if (n.$type === "bpmn:ParallelGateway") {
  97. n.outgoing.forEach((nn) => {
  98. const targetTask: any = taskList.value.find((m: any) => m.key === nn.targetRef.id)
  99. if (targetTask) {
  100. canvas.addMarker(nn.id, targetTask.completed ? "highlight" : "highlight-todo")
  101. }
  102. })
  103. } else if (n.$type === "bpmn:InclusiveGateway") {
  104. n.outgoing.forEach((nn) => {
  105. const targetTask: any = taskList.value.find((m: any) => m.key === nn.targetRef.id)
  106. if (targetTask) {
  107. canvas.addMarker(nn.id, targetTask.completed ? "highlight" : "highlight-todo")
  108. }
  109. })
  110. } else if (n.$type === "bpmn:SubProcess") {
  111. const completeTask: any = taskList.value.find((m: any) => m.key === n.id)
  112. if (completeTask) {
  113. canvas.addMarker(n.id, completeTask.completed ? "highlight" : "highlight-todo")
  114. }
  115. bpmnNodeList(n.flowElements, canvas)
  116. } else if (n.$type === "bpmn:StartEvent") {
  117. canvas.addMarker(n.id, "startEvent")
  118. if (n.outgoing) {
  119. n.outgoing.forEach((nn) => {
  120. const completeTask = taskList.value.find((m: any) => m.key === nn.targetRef.id)
  121. if (completeTask) {
  122. canvas.addMarker(nn.id, "highlight")
  123. canvas.addMarker(n.id, "highlight")
  124. }
  125. })
  126. }
  127. } else if (n.$type === "bpmn:EndEvent") {
  128. canvas.addMarker(n.id, "endEvent")
  129. const completeTask: any = taskList.value.find((m: any) => m.key === n.id)
  130. if (completeTask) {
  131. canvas.addMarker(completeTask.key, "highlight")
  132. canvas.addMarker(n.id, "highlight")
  133. return
  134. }
  135. }
  136. })
  137. }
  138. function gateway(id, targetRefType, targetRefId, canvas, completed) {
  139. if (targetRefType === "bpmn:ExclusiveGateway") {
  140. canvas.addMarker(id, completed ? "highlight" : "highlight-todo")
  141. canvas.addMarker(targetRefId, completed ? "highlight" : "highlight-todo")
  142. }
  143. if (targetRefType === "bpmn:ParallelGateway") {
  144. canvas.addMarker(id, completed ? "highlight" : "highlight-todo")
  145. canvas.addMarker(targetRefId, completed ? "highlight" : "highlight-todo")
  146. }
  147. if (targetRefType === "bpmn:InclusiveGateway") {
  148. canvas.addMarker(id, completed ? "highlight" : "highlight-todo")
  149. canvas.addMarker(targetRefId, completed ? "highlight" : "highlight-todo")
  150. }
  151. }
  152. }
  153. function initDiagram(instanceId: string) {
  154. nextTick(() => {
  155. if (modeler.value) {
  156. modeler.value.destroy()
  157. }
  158. modeler.value = new BpmnViewer({
  159. container: canvas.value,
  160. additionalModules: [
  161. {
  162. //禁止滚轮滚动
  163. zoomScroll: ["value", ""]
  164. },
  165. ZoomScrollModule,
  166. MoveCanvasModule
  167. ] as ModuleDeclaration[]
  168. })
  169. apis.workflow.processInstanceApi.getHistoryList(instanceId).then((res) => {
  170. xml.value = res.data.xml
  171. taskList.value = res.data.taskList
  172. historyList.value = res.data.historyList
  173. createDiagram(xml.value)
  174. })
  175. })
  176. function createDiagram(data: any) {
  177. modeler
  178. .value!.importXML(data)
  179. .then(() => {
  180. fitViewport()
  181. fillColor()
  182. const eventBus = modeler.value!.get<EventBus>("eventBus")
  183. const overlays = modeler.value!.get<Overlays>("overlays")
  184. eventBus.on<ModdleElement>("element.hover", (e: any) => {
  185. let data = historyList.value.find((t: any) => t.taskDefinitionKey === e.element.id)
  186. if (e.element.type === "bpmn:UserTask" && data) {
  187. setTimeout(() => {
  188. genNodeDetailBox(e, overlays, data)
  189. }, 10)
  190. }
  191. })
  192. eventBus.on("element.out", (e) => {
  193. overlays.clear()
  194. })
  195. })
  196. .catch((err) => {
  197. console.log(err)
  198. })
  199. const genNodeDetailBox = (e: any, overlays: any, data: any) => {
  200. overlays.add(e.element.id, {
  201. position: { top: e.element.height, left: 0 },
  202. html: `<div class="overlays">
  203. <p>审批人员:${data.nickName || ""}<p/>
  204. <p>节点状态:${data.statusName || ""}</p>
  205. <p>开始时间:${data.startTime || ""}</p>
  206. <p>结束时间:${data.endTime || ""}</p>
  207. <p>审批耗时:${data.runDuration || ""}</p>
  208. </div>`
  209. })
  210. }
  211. }
  212. }
  213. function open(instanceId: string) {
  214. bpmnVisible.value = true
  215. initDiagram(instanceId)
  216. }
  217. function init() {
  218. //
  219. }
  220. onMounted(init)
  221. defineExpose({
  222. open
  223. })
  224. </script>
  225. <template>
  226. <div class="bpmnDialogContainers">
  227. <el-header style="border-bottom: 1px solid rgb(218 218 218); height: auto">
  228. <div class="header-div">
  229. <div>
  230. <vb-tooltip content="自适应屏幕" placement="bottom">
  231. <el-button size="small" type="default" @click="onFitViewport">
  232. <template #icon>
  233. <i class="bi bi-arrows-fullscreen fs-7"></i>
  234. </template>
  235. </el-button>
  236. </vb-tooltip>
  237. <vb-tooltip content="放大" placement="bottom">
  238. <el-button size="small" type="default" @click="onZoomViewport(true)">
  239. <template #icon>
  240. <i class="bi bi-zoom-in fs-7"></i>
  241. </template>
  242. </el-button>
  243. </vb-tooltip>
  244. <vb-tooltip content="缩小" placement="bottom">
  245. <el-button size="small" type="default" @click="onZoomViewport(false)">
  246. <template #icon>
  247. <i class="bi bi-zoom-out fs-7"></i>
  248. </template>
  249. </el-button>
  250. </vb-tooltip>
  251. </div>
  252. <div>
  253. <div class="tips-label">
  254. <div class="un-complete">未完成</div>
  255. <div class="in-progress">进行中</div>
  256. <div class="complete">已完成</div>
  257. </div>
  258. </div>
  259. </div>
  260. </el-header>
  261. <div class="flow-containers">
  262. <el-container class="bpmn-el-container" style="align-items: stretch">
  263. <el-main style="padding: 0">
  264. <div ref="canvas" class="canvas" />
  265. </el-main>
  266. </el-container>
  267. </div>
  268. </div>
  269. </template>
  270. <style lang="scss" scoped>
  271. .canvas {
  272. width: 100%;
  273. height: 100%;
  274. }
  275. .header-div {
  276. display: flex;
  277. padding: 10px 0;
  278. justify-content: space-between;
  279. .tips-label {
  280. display: flex;
  281. div {
  282. margin-right: 10px;
  283. padding: 5px;
  284. font-size: 12px;
  285. }
  286. .un-complete {
  287. border: 1px solid #000;
  288. }
  289. .in-progress {
  290. background-color: rgb(255, 237, 204);
  291. border: 1px dashed orange;
  292. }
  293. .complete {
  294. background-color: rgb(204, 230, 204);
  295. border: 1px solid green;
  296. }
  297. }
  298. }
  299. .view-mode {
  300. .el-header,
  301. .el-aside,
  302. .djs-palette,
  303. .bjs-powered-by {
  304. display: none;
  305. }
  306. .el-loading-mask {
  307. background-color: initial;
  308. }
  309. .el-loading-spinner {
  310. display: none;
  311. }
  312. }
  313. .bpmn-el-container {
  314. height: calc(100vh - 350px);
  315. }
  316. .flow-containers {
  317. width: 100%;
  318. height: 100%;
  319. overflow-y: auto;
  320. .canvas {
  321. width: 100%;
  322. height: 100%;
  323. }
  324. .load {
  325. margin-right: 10px;
  326. }
  327. :deep(.el-form-item__label) {
  328. font-size: 13px;
  329. }
  330. :deep(.djs-palette) {
  331. left: 0 !important;
  332. top: 0;
  333. border-top: none;
  334. }
  335. :deep(.djs-container svg) {
  336. min-height: 650px;
  337. }
  338. :deep(.startEvent.djs-shape .djs-visual > :nth-child(1)) {
  339. fill: #77df6d !important;
  340. }
  341. :deep(.endEvent.djs-shape .djs-visual > :nth-child(1)) {
  342. fill: #ee7b77 !important;
  343. }
  344. :deep(.highlight.djs-shape .djs-visual > :nth-child(1)) {
  345. fill: green !important;
  346. stroke: green !important;
  347. fill-opacity: 0.2 !important;
  348. }
  349. :deep(.highlight.djs-shape .djs-visual > :nth-child(2)) {
  350. fill: green !important;
  351. }
  352. :deep(.highlight.djs-shape .djs-visual > path) {
  353. fill: green !important;
  354. fill-opacity: 0.2 !important;
  355. stroke: green !important;
  356. }
  357. :deep(.highlight.djs-connection > .djs-visual > path) {
  358. stroke: green !important;
  359. }
  360. // 边框滚动动画
  361. @keyframes path-animation {
  362. from {
  363. stroke-dashoffset: 100%;
  364. }
  365. to {
  366. stroke-dashoffset: 0%;
  367. }
  368. }
  369. :deep(.highlight-todo.djs-connection > .djs-visual > path) {
  370. animation: path-animation 60s;
  371. animation-timing-function: linear;
  372. animation-iteration-count: infinite;
  373. stroke-dasharray: 4px !important;
  374. stroke: orange !important;
  375. fill-opacity: 0.2 !important;
  376. marker-end: url("#sequenceflow-end-_E7DFDF-_E7DFDF-803g1kf6zwzmcig1y2ulm5egr");
  377. }
  378. :deep(.highlight-todo.djs-shape .djs-visual > :nth-child(1)) {
  379. animation: path-animation 60s;
  380. animation-timing-function: linear;
  381. animation-iteration-count: infinite;
  382. stroke-dasharray: 4px !important;
  383. stroke: orange !important;
  384. fill: orange !important;
  385. fill-opacity: 0.2 !important;
  386. }
  387. }
  388. :deep(.overlays) {
  389. width: 250px;
  390. margin-top: 5px;
  391. background: #c6e2ff;
  392. border-radius: 8px;
  393. border: 1px solid #0095e8;
  394. color: #0095e8;
  395. padding: 10px 15px;
  396. cursor: pointer;
  397. p {
  398. font-weight: bold;
  399. line-height: 28px;
  400. margin: 0;
  401. padding: 0;
  402. }
  403. }
  404. </style>