index.vue 13 KB

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