index.vue 12 KB

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