index.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. <template>
  2. <div class="containers-bpmn">
  3. <!-- dark模式下 连接线的箭头样式 -->
  4. <svg width="0" height="0" style="position: absolute">
  5. <defs>
  6. <marker id="markerArrow-dark-mode" viewBox="0 0 20 20" refX="11" refY="10" markerWidth="10" markerHeight="10" orient="auto">
  7. <path d="M 1 5 L 11 10 L 1 15 Z" class="arrow-dark" />
  8. </marker>
  9. </defs>
  10. </svg>
  11. <div v-loading="loading" class="app-containers-bpmn">
  12. <el-container class="h-full">
  13. <el-container style="align-items: stretch">
  14. <el-header>
  15. <div class="process-toolbar">
  16. <el-space wrap :size="10">
  17. <el-tooltip effect="dark" content="自适应屏幕" placement="bottom">
  18. <el-button size="small" icon="Rank" @click="fitViewport" />
  19. </el-tooltip>
  20. <el-tooltip effect="dark" content="放大" placement="bottom">
  21. <el-button size="small" icon="ZoomIn" @click="zoomViewport(true)" />
  22. </el-tooltip>
  23. <el-tooltip effect="dark" content="缩小" placement="bottom">
  24. <el-button size="small" icon="ZoomOut" @click="zoomViewport(false)" />
  25. </el-tooltip>
  26. <el-tooltip effect="dark" content="后退" placement="bottom">
  27. <el-button size="small" icon="Back" @click="bpmnModeler.get('commandStack').undo()" />
  28. </el-tooltip>
  29. <el-tooltip effect="dark" content="前进" placement="bottom">
  30. <el-button size="small" icon="Right" @click="bpmnModeler.get('commandStack').redo()" />
  31. </el-tooltip>
  32. </el-space>
  33. <el-space wrap :size="10" style="float: right; padding-right: 10px">
  34. <el-button size="small" type="primary" @click="saveXml">保 存</el-button>
  35. <el-dropdown size="small">
  36. <el-button size="small" type="primary"> 预 览 </el-button>
  37. <template #dropdown>
  38. <el-dropdown-menu>
  39. <el-dropdown-item icon="Document" @click="previewXML">XML预览</el-dropdown-item>
  40. <el-dropdown-item icon="View" @click="previewSVG"> SVG预览</el-dropdown-item>
  41. </el-dropdown-menu>
  42. </template>
  43. </el-dropdown>
  44. <el-dropdown size="small">
  45. <el-button size="small" type="primary"> 下 载 </el-button>
  46. <template #dropdown>
  47. <el-dropdown-menu>
  48. <el-dropdown-item icon="Download" @click="downloadXML">下载XML</el-dropdown-item>
  49. <el-dropdown-item icon="Download" @click="downloadSVG"> 下载SVG</el-dropdown-item>
  50. </el-dropdown-menu>
  51. </template>
  52. </el-dropdown>
  53. </el-space>
  54. </div>
  55. </el-header>
  56. <div ref="canvas" class="canvas" />
  57. </el-container>
  58. <div :class="{ 'process-panel': true, 'hide': panelFlag }">
  59. <div class="process-panel-bar" @click="panelBarClick">
  60. <div class="open-bar">
  61. <el-link type="default" :underline="false">
  62. <svg-icon class-name="open-bar" :icon-class="panelFlag ? 'caret-back' : 'caret-forward'"></svg-icon>
  63. </el-link>
  64. </div>
  65. </div>
  66. <transition enter-active-class="animate__animated animate__fadeIn">
  67. <div v-show="showPanel" v-if="bpmnModeler" class="panel-content">
  68. <PropertyPanel :modeler="bpmnModeler" />
  69. </div>
  70. </transition>
  71. </div>
  72. </el-container>
  73. </div>
  74. </div>
  75. <div>
  76. <el-dialog v-model="perviewXMLShow" title="XML预览" width="80%" append-to-body>
  77. <highlightjs :code="xmlStr" language="XML" />
  78. </el-dialog>
  79. </div>
  80. <div>
  81. <el-dialog v-model="perviewSVGShow" title="SVG预览" width="80%" append-to-body>
  82. <div style="text-align: center" v-html="svgData" />
  83. </el-dialog>
  84. </div>
  85. </template>
  86. <script lang="ts" setup name="BpmnDesign">
  87. import 'bpmn-js/dist/assets/diagram-js.css';
  88. import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css';
  89. import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css';
  90. import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css';
  91. import './assets/style/index.scss';
  92. import type { Canvas, Modeler } from 'bpmn';
  93. import PropertyPanel from './panel/index.vue';
  94. import BpmnModeler from 'bpmn-js/lib/Modeler.js';
  95. import defaultXML from './assets/defaultXML';
  96. import flowableModdle from './assets/moddle/flowable';
  97. import Modules from './assets/module/index';
  98. import useModelerStore from '@/store/modules/modeler';
  99. import useDialog from '@/hooks/useDialog';
  100. const emit = defineEmits(['closeCallBack', 'saveCallBack']);
  101. const { visible, title, openDialog, closeDialog } = useDialog({
  102. title: '编辑流程'
  103. });
  104. const modelerStore = useModelerStore();
  105. const { proxy } = getCurrentInstance() as ComponentInternalInstance;
  106. const panelFlag = ref(false);
  107. const showPanel = ref(true);
  108. const canvas = ref<HTMLDivElement>();
  109. const panel = ref<HTMLDivElement>();
  110. const bpmnModeler = ref<Modeler>();
  111. const zoom = ref(1);
  112. const perviewXMLShow = ref(false);
  113. const perviewSVGShow = ref(false);
  114. const xmlStr = ref('');
  115. const svgData = ref('');
  116. const loading = ref(false);
  117. const panelBarClick = () => {
  118. // 延迟执行,否则会导致面板收起时,属性面板不显示
  119. panelFlag.value = !panelFlag.value;
  120. setTimeout(() => {
  121. showPanel.value = !panelFlag.value;
  122. }, 100);
  123. };
  124. /**
  125. * 初始化Canvas
  126. */
  127. const initCanvas = () => {
  128. bpmnModeler.value = new BpmnModeler({
  129. container: canvas.value,
  130. // 键盘
  131. keyboard: {
  132. bindTo: window // 或者window,注意与外部表单的键盘监听事件是否冲突
  133. },
  134. propertiesPanel: {
  135. parent: panel.value
  136. },
  137. additionalModules: Modules,
  138. moddleExtensions: {
  139. flowable: flowableModdle
  140. }
  141. });
  142. };
  143. /**
  144. * 初始化Model
  145. */
  146. const initModel = () => {
  147. if (modelerStore.getModeler()) {
  148. modelerStore.getModeler().destroy();
  149. modelerStore.setModeler(undefined);
  150. }
  151. modelerStore.setModeler(bpmnModeler.value);
  152. };
  153. /**
  154. * 新建
  155. */
  156. const newDiagram = async () => {
  157. await proxy?.$modal.confirm('是否确认新建');
  158. initDiagram();
  159. };
  160. /**
  161. * 初始化
  162. */
  163. const initDiagram = (xml?: string) => {
  164. if (!xml) xml = defaultXML;
  165. bpmnModeler.value.importXML(xml);
  166. };
  167. /**
  168. * 自适应屏幕
  169. */
  170. const fitViewport = () => {
  171. zoom.value = bpmnModeler.value.get<Canvas>('canvas').zoom('fit-viewport');
  172. const bbox = document.querySelector<SVGGElement>('.app-containers-bpmn .viewport').getBBox();
  173. const currentViewBox = bpmnModeler.value.get<Canvas>('canvas').viewbox();
  174. const elementMid = {
  175. x: bbox.x + bbox.width / 2 - 65,
  176. y: bbox.y + bbox.height / 2
  177. };
  178. bpmnModeler.value.get<Canvas>('canvas').viewbox({
  179. x: elementMid.x - currentViewBox.width / 2,
  180. y: elementMid.y - currentViewBox.height / 2,
  181. width: currentViewBox.width,
  182. height: currentViewBox.height
  183. });
  184. zoom.value = (bbox.width / currentViewBox.width) * 1.8;
  185. };
  186. /**
  187. * 放大或者缩小
  188. * @param zoomIn true 放大 | false 缩小
  189. */
  190. const zoomViewport = (zoomIn = true) => {
  191. zoom.value = bpmnModeler.value.get<Canvas>('canvas').zoom();
  192. zoom.value += zoomIn ? 0.1 : -0.1;
  193. bpmnModeler.value.get<Canvas>('canvas').zoom(zoom.value);
  194. };
  195. /**
  196. * 下载XML
  197. */
  198. const downloadXML = async () => {
  199. try {
  200. const { xml } = await bpmnModeler.value.saveXML({ format: true });
  201. downloadFile(`${getProcessElement().name}.bpmn20.xml`, xml, 'application/xml');
  202. } catch (e) {
  203. proxy?.$modal.msgError(e);
  204. }
  205. };
  206. /**
  207. * 下载SVG
  208. */
  209. const downloadSVG = async () => {
  210. try {
  211. const { svg } = await bpmnModeler.value.saveSVG();
  212. downloadFile(getProcessElement().name, svg, 'image/svg+xml');
  213. } catch (e) {
  214. proxy?.$modal.msgError(e);
  215. }
  216. };
  217. /**
  218. * XML预览
  219. */
  220. const previewXML = async () => {
  221. try {
  222. const { xml } = await bpmnModeler.value.saveXML({ format: true });
  223. xmlStr.value = xml;
  224. perviewXMLShow.value = true;
  225. } catch (e) {
  226. proxy?.$modal.msgError(e);
  227. }
  228. };
  229. /**
  230. * SVG预览
  231. */
  232. const previewSVG = async () => {
  233. try {
  234. const { svg } = await bpmnModeler.value.saveSVG();
  235. svgData.value = svg;
  236. perviewSVGShow.value = true;
  237. } catch (e) {
  238. proxy?.$modal.msgError(e);
  239. }
  240. };
  241. const curNodeInfo = reactive({
  242. curType: '', // 任务类型 用户任务
  243. curNode: '',
  244. expValue: '' //多用户和部门角色实现
  245. });
  246. const downloadFile = (fileName: string, data: any, type: string) => {
  247. const a = document.createElement('a');
  248. const url = window.URL.createObjectURL(new Blob([data], { type: type }));
  249. a.href = url;
  250. a.download = fileName;
  251. a.click();
  252. window.URL.revokeObjectURL(url);
  253. };
  254. const getProcessElement = () => {
  255. const rootElements = bpmnModeler.value?.getDefinitions().rootElements;
  256. for (let i = 0; i < rootElements.length; i++) {
  257. if (rootElements[i].$type === 'bpmn:Process') return rootElements[i];
  258. }
  259. };
  260. const getProcess = () => {
  261. const element = getProcessElement();
  262. return {
  263. id: element.id,
  264. name: element.name
  265. };
  266. };
  267. const saveXml = async () => {
  268. const { xml } = await bpmnModeler.value.saveXML({ format: true });
  269. const { svg } = await bpmnModeler.value.saveSVG();
  270. const process = getProcess();
  271. let data = {
  272. xml: xml,
  273. svg: svg,
  274. key: process.id,
  275. name: process.name,
  276. loading: loading
  277. };
  278. emit('saveCallBack', data);
  279. };
  280. const open = (xml?: string) => {
  281. openDialog();
  282. nextTick(() => {
  283. initDiagram(xml);
  284. });
  285. };
  286. const close = () => {
  287. closeDialog();
  288. };
  289. onMounted(() => {
  290. nextTick(() => {
  291. initCanvas();
  292. initModel();
  293. });
  294. });
  295. /**
  296. * 对外暴露子组件方法
  297. */
  298. defineExpose({
  299. initDiagram,
  300. saveXml,
  301. open,
  302. close
  303. });
  304. </script>
  305. <style lang="scss">
  306. /** 夜间模式 线条的颜色 */
  307. $stroke-color-dark: white;
  308. $bpmn-font-size: 12px;
  309. /** 日间模式 字体颜色 */
  310. $bpmn-font-color-dark: white;
  311. /** 夜间模式 字体颜色 */
  312. $bpmn-font-color-light: #222;
  313. /* 背景网格 */
  314. @mixin djs-container {
  315. background-image: linear-gradient(90deg, hsl(0deg 0% 78.4% / 15%) 10%, transparent 0), linear-gradient(hsl(0deg 0% 78.4% / 15%) 10%, transparent 0) !important;
  316. background-size: 10px 10px !important;
  317. }
  318. html[class='light'] {
  319. /** 从左侧拖动时的背景图 */
  320. svg.new-parent {
  321. @include djs-container;
  322. }
  323. /** 双击编辑元素时样式保持一致 */
  324. div.djs-direct-editing-parent {
  325. border-radius: 10px;
  326. background-color: transparent !important;
  327. color: $bpmn-font-color-light;
  328. }
  329. g.djs-visual {
  330. .djs-label {
  331. fill: $bpmn-font-color-light !important;
  332. font-size: $bpmn-font-size !important;
  333. }
  334. }
  335. }
  336. html[class='dark'] {
  337. /** dark模式下 连接线的箭头样式 */
  338. .arrow-dark {
  339. stroke-width: 1px;
  340. stroke-linecap: round;
  341. stroke: $stroke-color-dark;
  342. fill: $stroke-color-dark;
  343. stroke-linejoin: round;
  344. }
  345. /** 从左侧拖动时的背景图 */
  346. svg.new-parent {
  347. background-color: black !important;
  348. @include djs-container;
  349. }
  350. /** 双击编辑元素时样式保持一致 */
  351. div.djs-direct-editing-parent {
  352. border-radius: 10px;
  353. background-color: transparent !important;
  354. color: $bpmn-font-color-dark;
  355. }
  356. /** 元素相关设置 */
  357. g.djs-visual {
  358. /** 元素边框 需要去除文字(.djs-label) */
  359. & > *:first-child:not(.djs-label) {
  360. stroke: $stroke-color-dark !important;
  361. }
  362. /** 字体颜色 */
  363. .djs-label {
  364. fill: $bpmn-font-color-dark !important;
  365. font-size: $bpmn-font-size !important;
  366. }
  367. /* 连接线样式 */
  368. path[data-corner-radius] {
  369. stroke: $stroke-color-dark !important;
  370. marker-end: url('#markerArrow-dark-mode') !important;
  371. }
  372. }
  373. }
  374. .containers-bpmn {
  375. height: 100%;
  376. .app-containers-bpmn {
  377. width: 100%;
  378. height: 100%;
  379. .canvas {
  380. width: 100%;
  381. height: 100%;
  382. @include djs-container;
  383. }
  384. .el-header {
  385. height: 35px;
  386. padding: 0;
  387. }
  388. .process-panel {
  389. transition: width 0.25s ease-in;
  390. .process-panel-bar {
  391. width: 34px;
  392. height: 40px;
  393. .open-bar {
  394. width: 34px;
  395. line-height: 40px;
  396. }
  397. }
  398. // 收起面板样式
  399. &.hide {
  400. width: 34px;
  401. overflow: hidden;
  402. padding: 0;
  403. .process-panel-bar {
  404. width: 34px;
  405. height: 100%;
  406. box-sizing: border-box;
  407. display: block;
  408. text-align: left;
  409. line-height: 34px;
  410. }
  411. .process-panel-bar:hover {
  412. background-color: var(--bpmn-panel-bar-background-color);
  413. }
  414. }
  415. }
  416. }
  417. }
  418. pre {
  419. margin: 0;
  420. height: 100%;
  421. max-height: calc(80vh - 32px);
  422. overflow-x: hidden;
  423. overflow-y: auto;
  424. .hljs {
  425. word-break: break-word;
  426. white-space: pre-wrap;
  427. padding: 0.5em;
  428. }
  429. }
  430. .open-bar {
  431. font-size: 20px;
  432. cursor: pointer;
  433. text-align: center;
  434. }
  435. .process-panel {
  436. box-sizing: border-box;
  437. padding: 0 8px 0 8px;
  438. border-left: 1px solid var(--bpmn-panel-border);
  439. box-shadow: var(--bpmn-panel-box-shadow) 0 0 8px;
  440. max-height: 100%;
  441. width: 25%;
  442. height: calc(100vh - 100px);
  443. .el-collapse {
  444. height: calc(100vh - 182px);
  445. overflow: auto;
  446. }
  447. }
  448. // 任务栏 透明度
  449. //:deep(.djs-palette) {
  450. // opacity: 0.3;
  451. // transition: all 1s;
  452. //}
  453. //
  454. //:deep(.djs-palette:hover) {
  455. // opacity: 1;
  456. // transition: all 1s;
  457. //}
  458. </style>