| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496 |
- <template>
- <div class="containers-bpmn">
- <!-- dark模式下 连接线的箭头样式 -->
- <svg width="0" height="0" style="position: absolute">
- <defs>
- <marker id="markerArrow-dark-mode" viewBox="0 0 20 20" refX="11" refY="10" markerWidth="10" markerHeight="10" orient="auto">
- <path d="M 1 5 L 11 10 L 1 15 Z" class="arrow-dark" />
- </marker>
- </defs>
- </svg>
- <div v-loading="loading" class="app-containers-bpmn">
- <el-container class="h-full">
- <el-container style="align-items: stretch">
- <el-header>
- <div class="process-toolbar">
- <el-space wrap :size="10">
- <el-tooltip effect="dark" content="自适应屏幕" placement="bottom">
- <el-button size="small" icon="Rank" @click="fitViewport" />
- </el-tooltip>
- <el-tooltip effect="dark" content="放大" placement="bottom">
- <el-button size="small" icon="ZoomIn" @click="zoomViewport(true)" />
- </el-tooltip>
- <el-tooltip effect="dark" content="缩小" placement="bottom">
- <el-button size="small" icon="ZoomOut" @click="zoomViewport(false)" />
- </el-tooltip>
- <el-tooltip effect="dark" content="后退" placement="bottom">
- <el-button size="small" icon="Back" @click="bpmnModeler.get('commandStack').undo()" />
- </el-tooltip>
- <el-tooltip effect="dark" content="前进" placement="bottom">
- <el-button size="small" icon="Right" @click="bpmnModeler.get('commandStack').redo()" />
- </el-tooltip>
- </el-space>
- <el-space wrap :size="10" style="float: right; padding-right: 10px">
- <el-button size="small" type="primary" @click="saveXml">保 存</el-button>
- <el-dropdown size="small">
- <el-button size="small" type="primary"> 预 览 </el-button>
- <template #dropdown>
- <el-dropdown-menu>
- <el-dropdown-item icon="Document" @click="previewXML">XML预览</el-dropdown-item>
- <el-dropdown-item icon="View" @click="previewSVG"> SVG预览</el-dropdown-item>
- </el-dropdown-menu>
- </template>
- </el-dropdown>
- <el-dropdown size="small">
- <el-button size="small" type="primary"> 下 载 </el-button>
- <template #dropdown>
- <el-dropdown-menu>
- <el-dropdown-item icon="Download" @click="downloadXML">下载XML</el-dropdown-item>
- <el-dropdown-item icon="Download" @click="downloadSVG"> 下载SVG</el-dropdown-item>
- </el-dropdown-menu>
- </template>
- </el-dropdown>
- </el-space>
- </div>
- </el-header>
- <div ref="canvas" class="canvas" />
- </el-container>
- <div :class="{ 'process-panel': true, 'hide': panelFlag }">
- <div class="process-panel-bar" @click="panelBarClick">
- <div class="open-bar">
- <el-link type="default" :underline="false">
- <svg-icon class-name="open-bar" :icon-class="panelFlag ? 'caret-back' : 'caret-forward'"></svg-icon>
- </el-link>
- </div>
- </div>
- <transition enter-active-class="animate__animated animate__fadeIn">
- <div v-show="showPanel" v-if="bpmnModeler" class="panel-content">
- <PropertyPanel :modeler="bpmnModeler" />
- </div>
- </transition>
- </div>
- </el-container>
- </div>
- </div>
- <div>
- <el-dialog v-model="perviewXMLShow" title="XML预览" width="80%" append-to-body>
- <highlightjs :code="xmlStr" language="XML" />
- </el-dialog>
- </div>
- <div>
- <el-dialog v-model="perviewSVGShow" title="SVG预览" width="80%" append-to-body>
- <div style="text-align: center" v-html="svgData" />
- </el-dialog>
- </div>
- </template>
- <script lang="ts" setup name="BpmnDesign">
- import 'bpmn-js/dist/assets/diagram-js.css';
- import 'bpmn-js/dist/assets/bpmn-font/css/bpmn.css';
- import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-codes.css';
- import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css';
- import './assets/style/index.scss';
- import type { Canvas, Modeler } from 'bpmn';
- import PropertyPanel from './panel/index.vue';
- import BpmnModeler from 'bpmn-js/lib/Modeler.js';
- import defaultXML from './assets/defaultXML';
- import flowableModdle from './assets/moddle/flowable';
- import Modules from './assets/module/index';
- import useModelerStore from '@/store/modules/modeler';
- import useDialog from '@/hooks/useDialog';
- const emit = defineEmits(['closeCallBack', 'saveCallBack']);
- const { visible, title, openDialog, closeDialog } = useDialog({
- title: '编辑流程'
- });
- const modelerStore = useModelerStore();
- const { proxy } = getCurrentInstance() as ComponentInternalInstance;
- const panelFlag = ref(false);
- const showPanel = ref(true);
- const canvas = ref<HTMLDivElement>();
- const panel = ref<HTMLDivElement>();
- const bpmnModeler = ref<Modeler>();
- const zoom = ref(1);
- const perviewXMLShow = ref(false);
- const perviewSVGShow = ref(false);
- const xmlStr = ref('');
- const svgData = ref('');
- const loading = ref(false);
- const panelBarClick = () => {
- // 延迟执行,否则会导致面板收起时,属性面板不显示
- panelFlag.value = !panelFlag.value;
- setTimeout(() => {
- showPanel.value = !panelFlag.value;
- }, 100);
- };
- /**
- * 初始化Canvas
- */
- const initCanvas = () => {
- bpmnModeler.value = new BpmnModeler({
- container: canvas.value,
- // 键盘
- keyboard: {
- bindTo: window // 或者window,注意与外部表单的键盘监听事件是否冲突
- },
- propertiesPanel: {
- parent: panel.value
- },
- additionalModules: Modules,
- moddleExtensions: {
- flowable: flowableModdle
- }
- });
- };
- /**
- * 初始化Model
- */
- const initModel = () => {
- if (modelerStore.getModeler()) {
- modelerStore.getModeler().destroy();
- modelerStore.setModeler(undefined);
- }
- modelerStore.setModeler(bpmnModeler.value);
- };
- /**
- * 新建
- */
- const newDiagram = async () => {
- await proxy?.$modal.confirm('是否确认新建');
- initDiagram();
- };
- /**
- * 初始化
- */
- const initDiagram = (xml?: string) => {
- if (!xml) xml = defaultXML;
- bpmnModeler.value.importXML(xml);
- };
- /**
- * 自适应屏幕
- */
- const fitViewport = () => {
- zoom.value = bpmnModeler.value.get<Canvas>('canvas').zoom('fit-viewport');
- const bbox = document.querySelector<SVGGElement>('.app-containers-bpmn .viewport').getBBox();
- const currentViewBox = bpmnModeler.value.get<Canvas>('canvas').viewbox();
- const elementMid = {
- x: bbox.x + bbox.width / 2 - 65,
- y: bbox.y + bbox.height / 2
- };
- bpmnModeler.value.get<Canvas>('canvas').viewbox({
- x: elementMid.x - currentViewBox.width / 2,
- y: elementMid.y - currentViewBox.height / 2,
- width: currentViewBox.width,
- height: currentViewBox.height
- });
- zoom.value = (bbox.width / currentViewBox.width) * 1.8;
- };
- /**
- * 放大或者缩小
- * @param zoomIn true 放大 | false 缩小
- */
- const zoomViewport = (zoomIn = true) => {
- zoom.value = bpmnModeler.value.get<Canvas>('canvas').zoom();
- zoom.value += zoomIn ? 0.1 : -0.1;
- bpmnModeler.value.get<Canvas>('canvas').zoom(zoom.value);
- };
- /**
- * 下载XML
- */
- const downloadXML = async () => {
- try {
- const { xml } = await bpmnModeler.value.saveXML({ format: true });
- downloadFile(`${getProcessElement().name}.bpmn20.xml`, xml, 'application/xml');
- } catch (e) {
- proxy?.$modal.msgError(e);
- }
- };
- /**
- * 下载SVG
- */
- const downloadSVG = async () => {
- try {
- const { svg } = await bpmnModeler.value.saveSVG();
- downloadFile(getProcessElement().name, svg, 'image/svg+xml');
- } catch (e) {
- proxy?.$modal.msgError(e);
- }
- };
- /**
- * XML预览
- */
- const previewXML = async () => {
- try {
- const { xml } = await bpmnModeler.value.saveXML({ format: true });
- xmlStr.value = xml;
- perviewXMLShow.value = true;
- } catch (e) {
- proxy?.$modal.msgError(e);
- }
- };
- /**
- * SVG预览
- */
- const previewSVG = async () => {
- try {
- const { svg } = await bpmnModeler.value.saveSVG();
- svgData.value = svg;
- perviewSVGShow.value = true;
- } catch (e) {
- proxy?.$modal.msgError(e);
- }
- };
- const curNodeInfo = reactive({
- curType: '', // 任务类型 用户任务
- curNode: '',
- expValue: '' //多用户和部门角色实现
- });
- const downloadFile = (fileName: string, data: any, type: string) => {
- const a = document.createElement('a');
- const url = window.URL.createObjectURL(new Blob([data], { type: type }));
- a.href = url;
- a.download = fileName;
- a.click();
- window.URL.revokeObjectURL(url);
- };
- const getProcessElement = () => {
- const rootElements = bpmnModeler.value?.getDefinitions().rootElements;
- for (let i = 0; i < rootElements.length; i++) {
- if (rootElements[i].$type === 'bpmn:Process') return rootElements[i];
- }
- };
- const getProcess = () => {
- const element = getProcessElement();
- return {
- id: element.id,
- name: element.name
- };
- };
- const saveXml = async () => {
- const { xml } = await bpmnModeler.value.saveXML({ format: true });
- const { svg } = await bpmnModeler.value.saveSVG();
- const process = getProcess();
- let data = {
- xml: xml,
- svg: svg,
- key: process.id,
- name: process.name,
- loading: loading
- };
- emit('saveCallBack', data);
- };
- const open = (xml?: string) => {
- openDialog();
- nextTick(() => {
- initDiagram(xml);
- });
- };
- const close = () => {
- closeDialog();
- };
- onMounted(() => {
- nextTick(() => {
- initCanvas();
- initModel();
- });
- });
- /**
- * 对外暴露子组件方法
- */
- defineExpose({
- initDiagram,
- saveXml,
- open,
- close
- });
- </script>
- <style lang="scss">
- /** 夜间模式 线条的颜色 */
- $stroke-color-dark: white;
- $bpmn-font-size: 12px;
- /** 日间模式 字体颜色 */
- $bpmn-font-color-dark: white;
- /** 夜间模式 字体颜色 */
- $bpmn-font-color-light: #222;
- /* 背景网格 */
- @mixin djs-container {
- 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;
- background-size: 10px 10px !important;
- }
- html[class='light'] {
- /** 从左侧拖动时的背景图 */
- svg.new-parent {
- @include djs-container;
- }
- /** 双击编辑元素时样式保持一致 */
- div.djs-direct-editing-parent {
- border-radius: 10px;
- background-color: transparent !important;
- color: $bpmn-font-color-light;
- }
- g.djs-visual {
- .djs-label {
- fill: $bpmn-font-color-light !important;
- font-size: $bpmn-font-size !important;
- }
- }
- }
- html[class='dark'] {
- /** dark模式下 连接线的箭头样式 */
- .arrow-dark {
- stroke-width: 1px;
- stroke-linecap: round;
- stroke: $stroke-color-dark;
- fill: $stroke-color-dark;
- stroke-linejoin: round;
- }
- /** 从左侧拖动时的背景图 */
- svg.new-parent {
- background-color: black !important;
- @include djs-container;
- }
- /** 双击编辑元素时样式保持一致 */
- div.djs-direct-editing-parent {
- border-radius: 10px;
- background-color: transparent !important;
- color: $bpmn-font-color-dark;
- }
- /** 元素相关设置 */
- g.djs-visual {
- /** 元素边框 需要去除文字(.djs-label) */
- & > *:first-child:not(.djs-label) {
- stroke: $stroke-color-dark !important;
- }
- /** 字体颜色 */
- .djs-label {
- fill: $bpmn-font-color-dark !important;
- font-size: $bpmn-font-size !important;
- }
- /* 连接线样式 */
- path[data-corner-radius] {
- stroke: $stroke-color-dark !important;
- marker-end: url('#markerArrow-dark-mode') !important;
- }
- }
- }
- .containers-bpmn {
- height: 100%;
- .app-containers-bpmn {
- width: 100%;
- height: 100%;
- .canvas {
- width: 100%;
- height: 100%;
- @include djs-container;
- }
- .el-header {
- height: 35px;
- padding: 0;
- }
- .process-panel {
- transition: width 0.25s ease-in;
- .process-panel-bar {
- width: 34px;
- height: 40px;
- .open-bar {
- width: 34px;
- line-height: 40px;
- }
- }
- // 收起面板样式
- &.hide {
- width: 34px;
- overflow: hidden;
- padding: 0;
- .process-panel-bar {
- width: 34px;
- height: 100%;
- box-sizing: border-box;
- display: block;
- text-align: left;
- line-height: 34px;
- }
- .process-panel-bar:hover {
- background-color: var(--bpmn-panel-bar-background-color);
- }
- }
- }
- }
- }
- pre {
- margin: 0;
- height: 100%;
- max-height: calc(80vh - 32px);
- overflow-x: hidden;
- overflow-y: auto;
- .hljs {
- word-break: break-word;
- white-space: pre-wrap;
- padding: 0.5em;
- }
- }
- .open-bar {
- font-size: 20px;
- cursor: pointer;
- text-align: center;
- }
- .process-panel {
- box-sizing: border-box;
- padding: 0 8px 0 8px;
- border-left: 1px solid var(--bpmn-panel-border);
- box-shadow: var(--bpmn-panel-box-shadow) 0 0 8px;
- max-height: 100%;
- width: 25%;
- height: calc(100vh - 100px);
- .el-collapse {
- height: calc(100vh - 182px);
- overflow: auto;
- }
- }
- // 任务栏 透明度
- //:deep(.djs-palette) {
- // opacity: 0.3;
- // transition: all 1s;
- //}
- //
- //:deep(.djs-palette:hover) {
- // opacity: 1;
- // transition: all 1s;
- //}
- </style>
|