index.vue 12 KB

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