index.vue 13 KB

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