兰宝车间质量管理系统-前端
LiuHao
2024-04-15 945eec5418ee9fa87a1cd01b3cdd1a6844a46287
update 优化bpmn位置
已添加1个文件
已重命名24个文件
已删除1个文件
已修改2个文件
1165 ■■■■ 文件已修改
src/bpmn/assets/defaultXML.ts 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/assets/lang/zh.ts 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/assets/moddle/flowable.ts 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/assets/module/ContextPad/CustomContextPadProvider.ts 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/assets/module/Palette/CustomPaletteProvider.ts 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/assets/module/Renderer/CustomRenderer.ts 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/assets/module/Translate/index.ts 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/assets/module/index.ts 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/assets/showConfig.ts 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/assets/style/index.scss 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/hooks/usePanel.ts 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/hooks/useParseElement.ts 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/index.vue 498 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/panel/GatewayPanel.vue 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/panel/ParticipantPanel.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/panel/ProcessPanel.vue 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/panel/SequenceFlowPanel.vue 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/panel/StartEndPanel.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/panel/SubProcessPanel.vue 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/panel/TaskPanel.vue 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/panel/index.vue 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/panel/property/DueDate.vue 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/panel/property/ExecutionListener.vue 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/panel/property/ListenerParam.vue 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/panel/property/TaskListener.vue 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/components/BpmnDesign/index.vue 521 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/workflow/model/design.vue 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/workflow/model/index.vue 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/bpmn/assets/defaultXML.ts
src/bpmn/assets/lang/zh.ts
src/bpmn/assets/moddle/flowable.ts
src/bpmn/assets/module/ContextPad/CustomContextPadProvider.ts
src/bpmn/assets/module/Palette/CustomPaletteProvider.ts
src/bpmn/assets/module/Renderer/CustomRenderer.ts
src/bpmn/assets/module/Translate/index.ts
ÎļþÃû´Ó src/components/BpmnDesign/assets/module/Translate/index.ts ÐÞ¸Ä
@@ -1,4 +1,4 @@
import zh from '@/components/BpmnDesign/assets/lang/zh';
import zh from '../../lang/zh';
const customTranslate = (template: any, replacements: any) => {
  replacements = replacements || {};
src/bpmn/assets/module/index.ts
src/bpmn/assets/showConfig.ts
src/bpmn/assets/style/index.scss
src/bpmn/hooks/usePanel.ts
ÎļþÃû´Ó src/components/BpmnDesign/hooks/usePanel.ts ÐÞ¸Ä
@@ -1,4 +1,4 @@
import showConfig from '@/components/BpmnDesign/assets/showConfig';
import showConfig from '../assets/showConfig';
import { ModdleElement } from 'bpmn';
import useModelerStore from '@/store/modules/modeler';
import { MultiInstanceTypeEnum } from '@/enums/bpmn/IndexEnums';
@@ -116,7 +116,7 @@
    }
  };
  const formKeyChange = (newVal: string) => {
      updateProperties({ formKey: newVal });
    updateProperties({ formKey: newVal });
  };
  const constant = {
    MultiInstanceType: [
src/bpmn/hooks/useParseElement.ts
src/bpmn/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,498 @@
<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-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-tooltip effect="dark" content="新建" placement="bottom">
                  <el-button size="small" icon="CirclePlus" @click="newDiagram" />
                </el-tooltip>
                <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>
            </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 { 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: #f5f7fa;
        }
      }
    }
  }
}
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 #eeeeee;
  box-shadow: #cccccc 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>
src/bpmn/panel/GatewayPanel.vue
ÎļþÃû´Ó src/components/BpmnDesign/panel/GatewayPanel.vue ÐÞ¸Ä
@@ -39,11 +39,11 @@
  </div>
</template>
<script setup lang="ts">
import useParseElement from '@/components/BpmnDesign/hooks/useParseElement';
import usePanel from '@/components/BpmnDesign/hooks/usePanel';
import useParseElement from '../hooks/useParseElement';
import usePanel from '../hooks/usePanel';
import { Modeler, ModdleElement } from 'bpmn';
import { GatewayPanel } from 'bpmnDesign';
import ExecutionListener from '@/components/BpmnDesign/panel/property/ExecutionListener.vue';
import ExecutionListener from './property/ExecutionListener.vue';
interface PropType {
  element: ModdleElement;
src/bpmn/panel/ParticipantPanel.vue
ÎļþÃû´Ó src/components/BpmnDesign/panel/ParticipantPanel.vue ÐÞ¸Ä
@@ -39,8 +39,9 @@
  </div>
</template>
<script setup lang="ts">
import useParseElement from '@/components/BpmnDesign/hooks/useParseElement';
import usePanel from '@/components/BpmnDesign/hooks/usePanel';
import useParseElement from '../hooks/useParseElement';
import usePanel from '../hooks/usePanel';
import ExecutionListener from './property/ExecutionListener.vue';
import { ModdleElement } from 'bpmn';
import { ParticipantPanel } from 'bpmnDesign';
src/bpmn/panel/ProcessPanel.vue
ÎļþÃû´Ó src/components/BpmnDesign/panel/ProcessPanel.vue ÐÞ¸Ä
@@ -41,8 +41,8 @@
<script setup lang="ts">
import ExecutionListener from './property/ExecutionListener.vue';
import useParseElement from '@/components/BpmnDesign/hooks/useParseElement';
import usePanel from '@/components/BpmnDesign/hooks/usePanel';
import useParseElement from '../hooks/useParseElement';
import usePanel from '../hooks/usePanel';
import { Modeler, ModdleElement } from 'bpmn';
import { ProcessPanel } from 'bpmnDesign';
src/bpmn/panel/SequenceFlowPanel.vue
ÎļþÃû´Ó src/components/BpmnDesign/panel/SequenceFlowPanel.vue ÐÞ¸Ä
@@ -45,11 +45,12 @@
  </div>
</template>
<script setup lang="ts">
import useParseElement from '@/components/BpmnDesign/hooks/useParseElement';
import usePanel from '@/components/BpmnDesign/hooks/usePanel';
import useParseElement from '../hooks/useParseElement';
import useModelerStore from '@/store/modules/modeler';
import usePanel from '../hooks/usePanel';
import ExecutionListener from './property/ExecutionListener.vue';
import { Modeler, ModdleElement } from 'bpmn';
import { SequenceFlowPanel } from 'bpmnDesign';
import useModelerStore from '@/store/modules/modeler';
interface PropType {
  element: ModdleElement;
src/bpmn/panel/StartEndPanel.vue
ÎļþÃû´Ó src/components/BpmnDesign/panel/StartEndPanel.vue ÐÞ¸Ä
@@ -39,8 +39,9 @@
  </div>
</template>
<script setup lang="ts">
import useParseElement from '@/components/BpmnDesign/hooks/useParseElement';
import usePanel from '@/components/BpmnDesign/hooks/usePanel';
import ExecutionListener from './property/ExecutionListener.vue';
import useParseElement from '../hooks/useParseElement';
import usePanel from '../hooks/usePanel';
import { Modeler, ModdleElement } from 'bpmn';
import { StartEndPanel } from 'bpmnDesign';
src/bpmn/panel/SubProcessPanel.vue
ÎļþÃû´Ó src/components/BpmnDesign/panel/SubProcessPanel.vue ÐÞ¸Ä
@@ -108,8 +108,9 @@
  </div>
</template>
<script setup lang="ts">
import useParseElement from '@/components/BpmnDesign/hooks/useParseElement';
import usePanel from '@/components/BpmnDesign/hooks/usePanel';
import ExecutionListener from './property/ExecutionListener.vue';
import useParseElement from '../hooks/useParseElement';
import usePanel from '../hooks/usePanel';
import { ModdleElement } from 'bpmn';
import { SubProcessPanel } from 'bpmnDesign';
import { MultiInstanceTypeEnum } from '@/enums/bpmn/IndexEnums';
src/bpmn/panel/TaskPanel.vue
ÎļþÃû´Ó src/components/BpmnDesign/panel/TaskPanel.vue ÐÞ¸Ä
@@ -21,9 +21,14 @@
            <el-form-item v-if="showConfig.skipExpression" prop="skipExpression" label="跳过表达式">
              <el-input v-model="formData.skipExpression" @change="skipExpressionChange"> </el-input>
            </el-form-item>
            <el-form-item prop="formKey" label="表单地址" v-loading="formManageListLoading">
              <el-select @change="formKeyChange" v-model="formData.formKey" clearable filterable placeholder="请选择表单"  style="width: 260px" >
                <el-option  v-for="item in formManageList"  :key="item.id"  :label="item.formTypeName+':'+item.formName" :value="item.formType+':'+item.id" />
            <el-form-item v-loading="formManageListLoading" prop="formKey" label="表单地址">
              <el-select v-model="formData.formKey" clearable filterable placeholder="请选择表单" style="width: 260px" @change="formKeyChange">
                <el-option
                  v-for="item in formManageList"
                  :key="item.id"
                  :label="item.formTypeName + ':' + item.formName"
                  :value="item.formType + ':' + item.id"
                />
              </el-select>
            </el-form-item>
          </div>
@@ -231,11 +236,13 @@
  </div>
</template>
<script setup lang="ts">
import useParseElement from '@/components/BpmnDesign/hooks/useParseElement';
import usePanel from '@/components/BpmnDesign/hooks/usePanel';
import useParseElement from '../hooks/useParseElement';
import usePanel from '../hooks/usePanel';
import UserSelect from '@/components/UserSelect';
import RoleSelect from '@/components/RoleSelect';
import DueDate from '@/components/BpmnDesign/panel/property/DueDate.vue';
import ExecutionListener from './property/ExecutionListener.vue';
import TaskListener from './property/TaskListener.vue';
import DueDate from './property/DueDate.vue';
import { ModdleElement } from 'bpmn';
import { TaskPanel } from 'bpmnDesign';
import { AllocationTypeEnum, MultiInstanceTypeEnum, SpecifyDescEnum } from '@/enums/bpmn/IndexEnums';
@@ -464,11 +471,11 @@
];
const listFormManage = async () => {
  formManageListLoading.value = true
  formManageListLoading.value = true;
  const res = await selectListFormManage();
  formManageList.value = res.data;
  formManageListLoading.value = false
}
  formManageListLoading.value = false;
};
onMounted(() => {
  nextTick(() => {
    listFormManage();
src/bpmn/panel/index.vue
src/bpmn/panel/property/DueDate.vue
src/bpmn/panel/property/ExecutionListener.vue
ÎļþÃû´Ó src/components/BpmnDesign/panel/property/ExecutionListener.vue ÐÞ¸Ä
@@ -66,7 +66,10 @@
            <el-option v-for="item in typeSelect" :key="item.id" :value="item.value" :label="item.label"></el-option>
          </el-select>
        </el-form-item>
        <el-form-item :label="typeSelect.filter(e=>e.value === formData.type)[0]?typeSelect.filter(e=>e.value === formData.type)[0]?.label:'表达式'" prop="className">
        <el-form-item
          :label="typeSelect.filter((e) => e.value === formData.type)[0] ? typeSelect.filter((e) => e.value === formData.type)[0]?.label : '表达式'"
          prop="className"
        >
          <el-input v-model="formData.className" type="text"></el-input>
        </el-form-item>
      </el-form>
@@ -90,7 +93,7 @@
import { ExecutionListenerVO } from 'bpmnDesign';
import { Moddle, Modeler, ModdleElement } from 'bpmn';
import usePanel from '@/components/BpmnDesign/hooks/usePanel';
import usePanel from '../../hooks/usePanel';
import useDialog from '@/hooks/useDialog';
import useModelerStore from '@/store/modules/modeler';
src/bpmn/panel/property/ListenerParam.vue
src/bpmn/panel/property/TaskListener.vue
ÎļþÃû´Ó src/components/BpmnDesign/panel/property/TaskListener.vue ÐÞ¸Ä
@@ -67,7 +67,10 @@
            <el-option v-for="item in typeSelect" :key="item.id" :value="item.value" :label="item.label"></el-option>
          </el-select>
        </el-form-item>
        <el-form-item :label="typeSelect.filter(e=>e.value === formData.type)[0]?typeSelect.filter(e=>e.value === formData.type)[0]?.label:'表达式'" prop="className">
        <el-form-item
          :label="typeSelect.filter((e) => e.value === formData.type)[0] ? typeSelect.filter((e) => e.value === formData.type)[0]?.label : '表达式'"
          prop="className"
        >
          <el-input v-model="formData.className" type="text"></el-input>
        </el-form-item>
      </el-form>
@@ -91,7 +94,7 @@
import { TaskListenerVO } from 'bpmnDesign';
import { ModdleElement } from 'bpmn';
import usePanel from '@/components/BpmnDesign/hooks/usePanel';
import usePanel from '../../hooks/usePanel';
import useDialog from '@/hooks/useDialog';
import useModelerStore from '@/store/modules/modeler';
src/components/BpmnDesign/index.vue
@@ -1,498 +1,71 @@
<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-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-tooltip effect="dark" content="新建" placement="bottom">
                  <el-button size="small" icon="CirclePlus" @click="newDiagram" />
                </el-tooltip>
                <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>
            </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" />
  <div class="design">
    <el-dialog v-model="visible" width="100%" fullscreen :title="title">
      <div class="modeler">
        <bpmn-design ref="bpmnDesignRef" @save-call-back="saveCallBack"></bpmn-design>
      </div>
    </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 { Canvas, Modeler } from 'bpmn';
import PropertyPanel from './panel/index.vue';
import BpmnModeler from 'bpmn-js/lib/Modeler.js';
import defaultXML from '@/components/BpmnDesign/assets/defaultXML';
import flowableModdle from '@/components/BpmnDesign/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();
<script lang="ts" setup name="Design">
import { getInfo, editModelXml } from '@/api/workflow/model';
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);
import { ModelForm } from '@/api/workflow/model/types';
import BpmnDesign from '@/bpmn/index.vue';
import useDialog from '@/hooks/useDialog';
const bpmnDesignRef = ref<InstanceType<typeof BpmnDesign>>();
const modelForm = ref<ModelForm>();
const emit = defineEmits(['closeCallBack']);
const { visible, title } = useDialog({
  title: '编辑流程'
});
const modelId = ref('');
const open = async (id) => {
  visible.value = true;
  modelId.value = id;
  const { data } = await getInfo(id);
  modelForm.value = data;
  bpmnDesignRef.value.initDiagram(modelForm.value.xml);
};
/**
 * åˆå§‹åŒ–Canvas
 */
const initCanvas = () => {
  bpmnModeler.value = new BpmnModeler({
    container: canvas.value,
    // é”®ç›˜
    keyboard: {
      bindTo: window // æˆ–者window,注意与外部表单的键盘监听事件是否冲突
    },
    propertiesPanel: {
      parent: panel.value
    },
    additionalModules: Modules,
    moddleExtensions: {
      flowable: flowableModdle
//保存模型
const saveCallBack = async (data) => {
  await proxy?.$modal.confirm('是否确认保存?');
  data.loading.value = true;
  modelForm.value.id = modelId.value;
  modelForm.value.xml = data.xml;
  modelForm.value.svg = data.svg;
  modelForm.value.key = data.key;
  modelForm.value.name = data.name;
  editModelXml(modelForm.value).then((res) => {
    if (res.code === 200) {
      visible.value = false;
      proxy?.$modal.msgSuccess('保存成功');
      emit('closeCallBack', data);
    }
  });
  data.loading.value = false;
};
/**
 * åˆå§‹åŒ–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
  open
});
</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;
<style lang="scss" scoped>
.design {
  :deep(.el-dialog .el-dialog__body) {
    max-height: 100% !important;
    min-height: calc(100vh - 80px);
    padding: 10px 0 10px 0 !important;
  }
  /** åŒå‡»ç¼–辑元素时样式保持一致 */
  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;
    }
  :deep(.el-dialog__header) {
    padding: 0 0 5px 0 !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: #f5f7fa;
        }
      }
    }
  }
}
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 #eeeeee;
  box-shadow: #cccccc 0 0 8px;
  max-height: 100%;
  width: 25%;
  height: calc(100vh - 80px);
  .el-collapse {
    height: calc(100vh - 162px);
    overflow: auto;
  }
}
// ä»»åŠ¡æ  é€æ˜Žåº¦
//:deep(.djs-palette) {
//  opacity: 0.3;
//  transition: all 1s;
//}
//
//:deep(.djs-palette:hover) {
//  opacity: 1;
//  transition: all 1s;
//}
</style>
src/views/workflow/model/design.vue
ÎļþÒÑɾ³ý
src/views/workflow/model/index.vue
@@ -138,7 +138,7 @@
</template>
<script lang="ts" setup name="Model">
import Design from './design.vue';
import Design from '../../../components/BpmnDesign/index.vue';
import { listModel, addModel, delModel, modelDeploy, getInfo, update } from '@/api/workflow/model';
import { ModelQuery, ModelForm, ModelVO } from '@/api/workflow/model/types';
import { listCategory } from '@/api/workflow/category';