Browse Source

11482-【CR】【投资系统】增加审批流程-之前更改流程设计插件时用到的

hxy 3 months ago
parent
commit
c60fdea615

File diff suppressed because it is too large
+ 80 - 0
ruoyi-admin/src/main/java/com/ruoyi/web/controller/tool/FormWidgetProcessor.java


+ 44 - 0
ruoyi-ui/src/api/flowable/listener.js

@@ -0,0 +1,44 @@
+import request from '@/utils/request'
+
+// 查询流程监听列表
+export function listListener(query) {
+  return request({
+    url: '/system/listener/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询流程监听详细
+export function getListener(id) {
+  return request({
+    url: '/system/listener/' + id,
+    method: 'get'
+  })
+}
+
+// 新增流程监听
+export function addListener(data) {
+  return request({
+    url: '/system/listener',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改流程监听
+export function updateListener(data) {
+  return request({
+    url: '/system/listener',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除流程监听
+export function delListener(id) {
+  return request({
+    url: '/system/listener/' + id,
+    method: 'delete'
+  })
+}

+ 120 - 0
ruoyi-ui/src/components/Process/common/bpmnUtils.js

@@ -0,0 +1,120 @@
+import { NodeName } from '../lang/zh'
+
+// 创建监听器实例
+export function createListenerObject(moddle, options, isTask, prefix) {
+  const listenerObj = Object.create(null);
+  listenerObj.event = options.event;
+  isTask && (listenerObj.id = options.id); // 任务监听器特有的 id 字段
+  switch (options.listenerType) {
+    case "scriptListener":
+      listenerObj.script = createScriptObject(moddle, options, prefix);
+      break;
+    case "expressionListener":
+      listenerObj.expression = options.expression;
+      break;
+    case "delegateExpressionListener":
+      listenerObj.delegateExpression = options.delegateExpression;
+      break;
+    default:
+      listenerObj.class = options.class;
+  }
+  // 注入字段
+  if (options.fields) {
+    listenerObj.fields = options.fields.map(field => {
+      return createFieldObject(moddle, field, prefix);
+    });
+  }
+  // 任务监听器的 定时器 设置
+  if (isTask && options.event === "timeout" && !!options.eventDefinitionType) {
+    const timeDefinition = moddle.create("bpmn:FormalExpression", {
+      body: options.eventTimeDefinitions
+    });
+    const TimerEventDefinition = moddle.create("bpmn:TimerEventDefinition", {
+      id: `TimerEventDefinition_${uuid(8)}`,
+      [`time${options.eventDefinitionType.replace(/^\S/, s => s.toUpperCase())}`]: timeDefinition
+    });
+    listenerObj.eventDefinitions = [TimerEventDefinition];
+  }
+  return moddle.create(`${prefix}:${isTask ? "TaskListener" : "ExecutionListener"}`, listenerObj);
+}
+
+// 处理内置流程监听器
+export function createSystemListenerObject(moddle, options, isTask, prefix) {
+  const listenerObj = Object.create(null);
+  listenerObj.event = options.eventType;
+  listenerObj.listenerType = options.valueType;
+  switch (options.valueType) {
+    case "scriptListener":
+      listenerObj.script = createScriptObject(moddle, options, prefix);
+      break;
+    case "expressionListener":
+      listenerObj.expression = options.expression;
+      break;
+    case "delegateExpressionListener":
+      listenerObj.delegateExpression = options.delegateExpression;
+      break;
+    default:
+      listenerObj.class = options.value;
+  }
+  return moddle.create(`${prefix}:${isTask ? "TaskListener" : "ExecutionListener"}`, listenerObj);
+}
+
+// 转换成字段
+export function changeListenerObject(options) {
+  const listenerObj = Object.create(null);
+  listenerObj.event = options.eventType;
+  listenerObj.listenerType = options.valueType;
+  switch (options.valueType) {
+    case "scriptListener":
+      // listenerObj.script = createScriptObject(moddle, options, prefix);
+      break;
+    case "expressionListener":
+      listenerObj.expression = options.expression;
+      break;
+    case "delegateExpressionListener":
+      listenerObj.delegateExpression = options.delegateExpression;
+      break;
+    default:
+      listenerObj.class = options.value;
+  }
+  return listenerObj;
+}
+
+// 创建 监听器的注入字段 实例
+export function createFieldObject(moddle, option, prefix) {
+  const { name, fieldType, string, expression } = option;
+  const fieldConfig = fieldType === "string" ? { name, string } : { name, expression };
+  return moddle.create(`${prefix}:Field`, fieldConfig);
+}
+
+// 创建脚本实例
+export function createScriptObject(moddle, options, prefix) {
+  const { scriptType, scriptFormat, value, resource } = options;
+  const scriptConfig = scriptType === "inlineScript" ? { scriptFormat, value } : { scriptFormat, resource };
+  return moddle.create(`${prefix}:Script`, scriptConfig);
+}
+
+// 更新元素扩展属性
+export function updateElementExtensions(moddle, modeling, element, extensionList) {
+  const extensions = moddle.create("bpmn:ExtensionElements", {
+    values: extensionList
+  });
+  modeling.updateProperties(element, {
+    extensionElements: extensions
+  });
+}
+
+// 创建一个id
+export function uuid(length = 8, chars) {
+  let result = "";
+  let charsString = chars || "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+  for (let i = length; i > 0; --i) {
+    result += charsString[Math.floor(Math.random() * charsString.length)];
+  }
+  return result;
+}
+
+// 转换流程节点名称
+export function translateNodeName(node){
+  return NodeName[node];
+}

+ 187 - 0
ruoyi-ui/src/components/Process/designer.vue

@@ -0,0 +1,187 @@
+<template>
+  <div>
+    <template slot="header">
+      <div class="card-header">
+        <span>{{ translateNodeName(elementType) }}</span>
+      </div>
+    </template>
+    <el-collapse v-model="activeName" >
+        <!--   常规信息     -->
+        <el-collapse-item name="common">
+          <template slot="title"><i class="el-icon-info"></i> 常规信息</template>
+          <common-panel :id="elementId"/>
+        </el-collapse-item>
+
+        <!--   任务信息     -->
+        <el-collapse-item name="Task" v-if="elementType.indexOf('Task') !== -1">
+          <template slot="title"><i class="el-icon-s-claim"></i> 任务配置</template>
+          <user-task-panel :id="elementId"/>
+        </el-collapse-item>
+
+        <!--   表单     -->
+        <el-collapse-item name="form" v-if="formVisible">
+          <template slot="title"><i class="el-icon-s-order"></i> 表单配置</template>
+          <form-panel :id="elementId"/>
+        </el-collapse-item>
+
+        <!--   执行监听器     -->
+        <el-collapse-item name="executionListener">
+          <template slot="title"><i class="el-icon-s-promotion"></i> 执行监听器
+             <el-badge :value="executionListenerCount" class="item" type="primary"/>
+           </template>
+          <execution-listener :id="elementId" @getExecutionListenerCount="getExecutionListenerCount"/>
+        </el-collapse-item>
+
+        <!--   任务监听器     -->
+        <el-collapse-item name="taskListener" v-if="elementType === 'UserTask'" >
+          <template slot="title"><i class="el-icon-s-flag"></i> 任务监听器
+            <el-badge :value="taskListenerCount" class="item" type="primary"/>
+          </template>
+          <task-listener :id="elementId" @getTaskListenerCount="getTaskListenerCount"/>
+        </el-collapse-item>
+
+        <!--   多实例     -->
+        <el-collapse-item name="multiInstance" v-if="elementType.indexOf('Task') !== -1" >
+          <template slot="title"><i class="el-icon-s-grid"></i> 多实例</template>
+          <multi-instance :id="elementId"/>
+        </el-collapse-item>
+        <!--   流转条件     -->
+        <el-collapse-item name="condition" v-if="conditionVisible" >
+          <template slot="title"><i class="el-icon-share"></i> 流转条件</template>
+          <condition-panel :id="elementId"/>
+        </el-collapse-item>
+
+        <!--   扩展属性     -->
+        <el-collapse-item name="properties" >
+          <template slot="title"><i class="el-icon-circle-plus"></i> 扩展属性</template>
+          <properties-panel :id="elementId"/>
+        </el-collapse-item>
+
+    </el-collapse>
+  </div>
+</template>
+
+<script>
+import ExecutionListener from './panel/executionListener'
+import TaskListener from './panel/taskListener'
+import MultiInstance from './panel/multiInstance'
+import CommonPanel from './panel/commonPanel'
+import UserTaskPanel from './panel/taskPanel'
+import ConditionPanel from './panel/conditionPanel'
+import FormPanel from './panel/formPanel'
+import OtherPanel from './panel/otherPanel'
+import PropertiesPanel from './panel/PropertiesPanel2.vue'
+
+import { translateNodeName } from "./common/bpmnUtils";
+import FlowUser from "@/components/flow/User/index.vue";
+import FlowRole from "@/components/flow/Role/index.vue";
+import FlowExp from "@/components/flow/Expression/index.vue";
+export default {
+  name: "Designer",
+  components: {
+    ExecutionListener,
+    TaskListener,
+    MultiInstance,
+    CommonPanel,
+    UserTaskPanel,
+    ConditionPanel,
+    FormPanel,
+    OtherPanel,
+    PropertiesPanel,
+    FlowUser,
+    FlowRole,
+    FlowExp,
+  },
+  data() {
+    return {
+      activeName : 'common',
+      executionListenerCount: 0,
+      taskListenerCount:0,
+      elementId:"",
+      elementType:"",
+      conditionVisible:false,// 流转条件设置
+      formVisible:false, // 表单配置
+      rules:{
+        id: [
+          { required: true, message: '节点Id 不能为空', trigger: 'blur' },
+        ],
+        name: [
+          { required: true, message: '节点名称不能为空', trigger: 'blur' },
+        ],
+      },
+    }
+  },
+
+  /** 传值监听 */
+  watch: {
+    elementId: {
+      handler() {
+        this.activeName = "common";
+      }
+    },
+  },
+  created() {
+    this.initModels();
+  },
+  methods: {
+    // 初始化流程设计器
+    initModels() {
+      this.getActiveElement();
+    },
+
+    // 注册节点事件
+    getActiveElement() {
+      // 初始第一个选中元素 bpmn:Process
+      this.initFormOnChanged(null);
+      this.modelerStore.modeler.on("import.done", e => {
+        this.initFormOnChanged(null);
+      });
+      // 监听选择事件,修改当前激活的元素以及表单
+      this.modelerStore.modeler.on("selection.changed", ({newSelection}) => {
+        this.initFormOnChanged(newSelection[0] || null);
+      });
+      this.modelerStore.modeler.on("element.changed", ({element}) => {
+        // 保证 修改 "默认流转路径" 类似需要修改多个元素的事件发生的时候,更新表单的元素与原选中元素不一致。
+        if (element && element.id === this.elementId) {
+          this.initFormOnChanged(element);
+        }
+      });
+    },
+
+    // 初始化数据
+    initFormOnChanged(element) {
+      let activatedElement = element;
+      if (!activatedElement) {
+        activatedElement =
+          this.modelerStore.elRegistry.find(el => el.type === "bpmn:Process") ??
+          this.modelerStore.elRegistry.find(el => el.type === "bpmn:Collaboration");
+      }
+      if (!activatedElement) return;
+      this.modelerStore.element = activatedElement;
+      this.elementId = activatedElement.id;
+      this.elementType = activatedElement.type.split(":")[1] || "";
+      this.conditionVisible = !!(
+        this.elementType === "SequenceFlow" &&
+        activatedElement.source &&
+        activatedElement.source.type.indexOf("StartEvent") === -1
+      );
+      this.formVisible = this.elementType === "UserTask" || this.elementType === "StartEvent";
+    },
+
+    /** 获取执行监听器数量 */
+    getExecutionListenerCount(value) {
+      this.executionListenerCount = value;
+    },
+    /** 获取任务监听器数量 */
+    getTaskListenerCount(value) {
+      this.taskListenerCount = value;
+    },
+    translateNodeName(val){
+      return translateNodeName(val);
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+</style>

File diff suppressed because it is too large
+ 353 - 0
ruoyi-ui/src/components/Process/index1.vue


+ 140 - 0
ruoyi-ui/src/components/Process/panel/ButtonsPanel.vue

@@ -0,0 +1,140 @@
+<template>
+  <div class="panel-tab__content">
+    <el-divider content-position="center">按钮设置</el-divider>
+    <el-table :data="elementButtonList" size="mini" max-height="240" border fit>
+      <el-table-column label="序号" width="50px" type="index" />
+      <el-table-column label="属性名" prop="name" min-width="100px" show-overflow-tooltip />
+      <el-table-column label="属性值" prop="value" min-width="100px" show-overflow-tooltip />
+      <el-table-column label="操作" width="90px">
+        <template slot-scope="{ row, $index }">
+          <el-button size="mini" type="text" @click="openAttributesForm(row, $index)">编辑</el-button>
+          <el-divider direction="vertical" />
+          <el-button size="mini" type="text" style="color: #ff4d4f" @click="removeAttributes(row, $index)">移除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <div class="element-drawer__button_save">
+      <el-button size="mini" type="primary" icon="el-icon-plus" @click="openAttributesForm(null, -1)">添加按钮</el-button>
+    </div>
+
+    <el-dialog :visible.sync="buttonFormModelVisible" title="按钮配置" width="600px" append-to-body destroy-on-close>
+      <el-form :model="buttonForm" label-width="80px" size="mini" ref="attributeFormRef" @submit.native.prevent>
+        <el-form-item label="属性名:" prop="label">
+          <el-input v-model="buttonForm.label" clearable />
+        </el-form-item>
+        <el-form-item label="属性值:" prop="value">
+          <el-input v-model="buttonForm.value" clearable />
+        </el-form-item>
+      </el-form>
+      <template slot="footer">
+        <el-button size="mini" @click="buttonFormModelVisible = false">取 消</el-button>
+        <el-button size="mini" type="primary" @click="saveAttribute">确 定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {StrUtil} from "@/utils/StrUtil";
+
+export default {
+  name: "ButtonsPanel",
+  props: {
+    id: {
+      type: String,
+      required: true
+    },
+  },
+  data() {
+    return {
+      elementButtonList: [],
+      otherExtensionList: [],
+      buttonForm: {},
+      editingPropertyIndex: -1,
+      buttonFormModelVisible: false
+    };
+  },
+  watch: {
+    id: {
+      immediate: true,
+      handler(val) {
+        if (StrUtil.isNotBlank(val)) {
+          this.resetAttributesList();
+        }
+      }
+    }
+  },
+  methods: {
+    resetAttributesList() {
+      this.bpmnElement = this.modelerStore.element;
+      this.otherExtensionList = []; // 其他扩展配置
+      this.bpmnElementProperties =
+        this.bpmnElement.businessObject?.extensionElements?.values?.filter(ex => {
+          if (ex.$type !== `flowable:Buttons`) {
+            this.otherExtensionList.push(ex);
+          }
+          return ex.$type === `flowable:Buttons`;
+        }) ?? [];
+
+      // 保存所有的 扩展属性字段
+      this.bpmnElementButtonList = this.bpmnElementProperties.reduce((pre, current) => pre.concat(current.values), []);
+      // 复制 显示
+      this.elementButtonList = JSON.parse(JSON.stringify(this.bpmnElementButtonList ?? []));
+    },
+    openAttributesForm(attr, index) {
+      this.editingPropertyIndex = index;
+      this.buttonForm = index === -1 ? {} : JSON.parse(JSON.stringify(attr));
+      this.buttonFormModelVisible = true;
+      this.$nextTick(() => {
+        if (this.$refs["attributeFormRef"]) this.$refs["attributeFormRef"].clearValidate();
+      });
+    },
+    removeAttributes(attr, index) {
+      this.$confirm("确认移除该属性吗?", "提示", {
+        confirmButtonText: "确 认",
+        cancelButtonText: "取 消"
+      })
+        .then(() => {
+          this.elementButtonList.splice(index, 1);
+          this.bpmnElementButtonList.splice(index, 1);
+          // 新建一个属性字段的保存列表
+          const propertiesObject = this.modelerStore.moddle.create(`flowable:Properties`, {
+            values: this.bpmnElementButtonList
+          });
+          this.updateElementExtensions(propertiesObject);
+          this.resetAttributesList();
+        })
+        .catch(() => console.info("操作取消"));
+    },
+    saveAttribute() {
+      const { name, value } = this.buttonForm;
+      console.log(this.bpmnElementButtonList);
+      if (this.editingPropertyIndex !== -1) {
+        this.modelerStore.modeling.updateModdleProperties(this.bpmnElement, this.bpmnElementButtonList[this.editingPropertyIndex], {
+          name,
+          value
+        });
+      } else {
+        // 新建属性字段
+        const newPropertyObject = this.modelerStore.moddle.create(`flowable:Button`, { name, value });
+        // 新建一个属性字段的保存列表
+        const propertiesObject = this.modelerStore.moddle.create(`flowable:Buttons`, {
+          values: this.bpmnElementButtonList.concat([newPropertyObject])
+        });
+        this.updateElementExtensions(propertiesObject);
+      }
+      this.buttonFormModelVisible = false;
+      this.resetAttributesList();
+    },
+    updateElementExtensions(properties) {
+      const extensions = this.modelerStore.moddle.create("bpmn:ExtensionElements", {
+        values: this.otherExtensionList.concat([properties])
+      });
+
+      this.modelerStore.modeling.updateProperties(this.bpmnElement, {
+        extensionElements: extensions
+      });
+    }
+  }
+};
+</script>

+ 139 - 0
ruoyi-ui/src/components/Process/panel/PropertiesPanel2.vue

@@ -0,0 +1,139 @@
+<template>
+  <div class="panel-tab__content">
+    <el-table :data="elementPropertyList" size="mini" max-height="240" border fit>
+      <el-table-column label="序号" width="50px" type="index" />
+      <el-table-column label="属性名" prop="name" min-width="100px" show-overflow-tooltip />
+      <el-table-column label="属性值" prop="value" min-width="100px" show-overflow-tooltip />
+      <el-table-column label="操作" width="90px">
+        <template slot-scope="{ row, $index }">
+          <el-button size="mini" type="text" @click="openAttributesForm(row, $index)">编辑</el-button>
+          <el-divider direction="vertical" />
+          <el-button size="mini" type="text" style="color: #ff4d4f" @click="removeAttributes(row, $index)">移除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <div class="element-drawer__button">
+      <el-button size="mini" type="primary" icon="el-icon-plus" @click="openAttributesForm(null, -1)">添加属性</el-button>
+    </div>
+
+    <el-dialog :visible.sync="propertyFormModelVisible" title="属性配置" width="600px" append-to-body destroy-on-close>
+      <el-form :model="propertyForm" label-width="80px" size="mini" ref="attributeFormRef" @submit.native.prevent>
+        <el-form-item label="属性名:" prop="name">
+          <el-input v-model="propertyForm.name" clearable />
+        </el-form-item>
+        <el-form-item label="属性值:" prop="value">
+          <el-input v-model="propertyForm.value" clearable />
+        </el-form-item>
+      </el-form>
+      <template slot="footer">
+        <el-button size="mini" @click="propertyFormModelVisible = false">取 消</el-button>
+        <el-button size="mini" type="primary" @click="saveAttribute">确 定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {StrUtil} from "@/utils/StrUtil";
+
+export default {
+  name: "PropertiesPanel",
+  props: {
+    id: {
+      type: String,
+      required: true
+    },
+  },
+  data() {
+    return {
+      elementPropertyList: [],
+      otherExtensionList: [],
+      propertyForm: {},
+      editingPropertyIndex: -1,
+      propertyFormModelVisible: false
+    };
+  },
+  watch: {
+    id: {
+      immediate: true,
+      handler(val) {
+        if (StrUtil.isNotBlank(val)) {
+          this.resetAttributesList();
+        }
+      }
+    }
+  },
+  methods: {
+    resetAttributesList() {
+      this.bpmnElement = this.modelerStore.element;
+      this.otherExtensionList = []; // 其他扩展配置
+      this.bpmnElementProperties =
+        this.bpmnElement.businessObject?.extensionElements?.values?.filter(ex => {
+          if (ex.$type !== `flowable:Properties`) {
+            this.otherExtensionList.push(ex);
+          }
+          return ex.$type === `flowable:Properties`;
+        }) ?? [];
+
+      // 保存所有的 扩展属性字段
+      this.bpmnElementPropertyList = this.bpmnElementProperties.reduce((pre, current) => pre.concat(current.values), []);
+      // 复制 显示
+      this.elementPropertyList = JSON.parse(JSON.stringify(this.bpmnElementPropertyList ?? []));
+    },
+    openAttributesForm(attr, index) {
+      this.editingPropertyIndex = index;
+      this.propertyForm = index === -1 ? {} : JSON.parse(JSON.stringify(attr));
+      this.propertyFormModelVisible = true;
+      this.$nextTick(() => {
+        if (this.$refs["attributeFormRef"]) this.$refs["attributeFormRef"].clearValidate();
+      });
+    },
+    removeAttributes(attr, index) {
+      this.$confirm("确认移除该属性吗?", "提示", {
+        confirmButtonText: "确 认",
+        cancelButtonText: "取 消"
+      })
+        .then(() => {
+          this.elementPropertyList.splice(index, 1);
+          this.bpmnElementPropertyList.splice(index, 1);
+          // 新建一个属性字段的保存列表
+          const propertiesObject = this.modelerStore.moddle.create(`flowable:Properties`, {
+            values: this.bpmnElementPropertyList
+          });
+          this.updateElementExtensions(propertiesObject);
+          this.resetAttributesList();
+        })
+        .catch(() => console.info("操作取消"));
+    },
+    saveAttribute() {
+      const { name, value } = this.propertyForm;
+      console.log(this.bpmnElementPropertyList);
+      if (this.editingPropertyIndex !== -1) {
+        this.modelerStore.modeling.updateModdleProperties(this.bpmnElement, this.bpmnElementPropertyList[this.editingPropertyIndex], {
+          name,
+          value
+        });
+      } else {
+        // 新建属性字段
+        const newPropertyObject = this.modelerStore.moddle.create(`flowable:Property`, { name, value });
+        // 新建一个属性字段的保存列表
+        const propertiesObject = this.modelerStore.moddle.create(`flowable:Properties`, {
+          values: this.bpmnElementPropertyList.concat([newPropertyObject])
+        });
+        this.updateElementExtensions(propertiesObject);
+      }
+      this.propertyFormModelVisible = false;
+      this.resetAttributesList();
+    },
+    updateElementExtensions(properties) {
+      const extensions = this.modelerStore.moddle.create("bpmn:ExtensionElements", {
+        values: this.otherExtensionList.concat([properties])
+      });
+
+      this.modelerStore.modeling.updateProperties(this.bpmnElement, {
+        extensionElements: extensions
+      });
+    }
+  }
+};
+</script>

+ 133 - 0
ruoyi-ui/src/components/Process/panel/commonPanel.vue

@@ -0,0 +1,133 @@
+<template>
+  <div>
+  <el-form :model="bpmnFormData" label-width="80px" :rules="rules"  size="small">
+      <el-form-item :label="bpmnFormData.$type === 'bpmn:Process'? '流程标识': '节点ID'" prop="id">
+        <el-input v-model="bpmnFormData.id" @change="updateElementTask('id')"/>
+      </el-form-item>
+      <el-form-item :label="bpmnFormData.$type === 'bpmn:Process'? '流程名称': '节点名称'" prop="name">
+        <el-input v-model="bpmnFormData.name"  @change="updateElementTask('name')"/>
+      </el-form-item>
+
+      <!--流程的基础属性-->
+      <template v-if="bpmnFormData.$type === 'bpmn:Process'">
+        <el-form-item label="流程分类" prop="processCategory">
+          <el-select v-model="bpmnFormData.processCategory" placeholder="请选择流程分类" @change="updateElementTask('processCategory')">
+            <el-option
+                v-for="dict in dict.type.sys_process_category"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+      </template>
+      <el-form-item v-if="bpmnFormData.$type === 'bpmn:SubProcess'" label="状态">
+        <el-switch v-model="bpmnFormData.isExpanded" active-text="展开" inactive-text="折叠" @change="updateElementTask('isExpanded')" />
+      </el-form-item>
+      <el-form-item label="节点描述">
+        <el-input
+          :rows="2"
+          type="textarea"
+          v-model="bpmnFormData.documentationValue"
+          @change="updateDocumentation"
+        />
+      </el-form-item>
+  </el-form>
+  </div>
+</template>
+
+<script>
+import {StrUtil} from '@/utils/StrUtil'
+
+export default {
+  name: "CommonPanel",
+  dicts: ['sys_process_category'],
+  /** 组件传值  */
+  props : {
+    id: {
+      type: String,
+      required: true
+    },
+  },
+  data() {
+    return {
+      rules:{
+        id: [
+          { required: true, message: '节点Id 不能为空', trigger: 'blur' },
+        ],
+        name: [
+          { required: true, message: '节点名称不能为空', trigger: 'blur' },
+        ],
+      },
+      bpmnFormData: {}
+    }
+  },
+  /** 传值监听 */
+  watch: {
+    id: {
+      handler(newVal) {
+        if (StrUtil.isNotBlank(newVal)) {
+          this.resetTaskForm();
+        }
+      },
+      immediate: true, // 立即生效
+    },
+  },
+
+  created() {
+  },
+  methods: {
+    resetTaskForm() {
+      // this.bpmnFormData = JSON.parse(JSON.stringify(this.modelerStore.element.businessObject));
+      this.bpmnFormData = Object.assign({}, this.modelerStore.element.businessObject);
+
+      // 使用 $set 确保 documentationValue 是响应式的
+      this.$set(this.bpmnFormData, 'documentationValue', this.modelerStore.element.businessObject.documentation?.[0]?.text || '');
+    },
+    updateElementTask(key) {
+      const taskAttr = Object.create(null);
+      taskAttr[key] = this.bpmnFormData[key] || null;
+      this.modelerStore.modeling.updateProperties(this.modelerStore.element, taskAttr);
+    },
+    updateDocumentation() {
+      // 确保 modelerStore 是 BPMN.js 的 Modeler 实例
+      const modeler = this.modelerStore.modeler; // 获取实际的 modeler 实例
+      const moddle = modeler.get('moddle');      // 通过 modeler 获取 moddle
+      const modeling = modeler.get('modeling');  // 通过 modeler 获取 modeling
+
+      // 创建新的文档对象
+      const documentation = moddle.create('bpmn:Documentation', {
+        text: this.bpmnFormData.documentationValue
+      });
+
+      // 获取当前元素的扩展元素
+      let extensionElements = this.modelerStore.element.businessObject.extensionElements;
+
+      if (!extensionElements) {
+        // 如果没有扩展元素,创建一个新的
+        extensionElements = moddle.create('bpmn:ExtensionElements', {
+          values: []
+        });
+      }
+
+      // 更新文档
+      modeling.updateProperties(this.modelerStore.element, {
+        documentation: [documentation],
+        extensionElements: extensionElements
+      });
+
+      // 强制更新模型
+      this.modelerStore.modeler.get('commandStack').execute('element.updateProperties', {
+        element: this.modelerStore.element,
+        properties: {
+          documentation: [documentation]
+        }
+      });
+
+      this.$emit('save');
+    }
+  }
+}
+
+
+</script>

+ 175 - 0
ruoyi-ui/src/components/Process/panel/conditionPanel.vue

@@ -0,0 +1,175 @@
+<template>
+  <div>
+    <el-form label-width="100px" size="small" @submit.native.prevent>
+      <el-form-item>
+        <template slot="label">
+            <span>
+               流转类型
+               <el-tooltip placement="top">
+                  <template slot="content">
+                     <div>
+                              普通流转路径:流程执行过程中,一个元素被访问后,会沿着其所有出口顺序流继续执行。
+                        <br />默认流转路径:只有当没有其他顺序流可以选择时,才会选择默认顺序流作为活动的出口顺序流。流程会忽略默认顺序流上的条件。
+                        <br />条件流转路径:是计算其每个出口顺序流上的条件。当条件计算为true时,选择该出口顺序流。如果该方法选择了多条顺序流,则会生成多个执行,流程会以并行方式继续。
+                     </div>
+                  </template>
+                  <i class="el-icon-question" />
+               </el-tooltip>
+            </span>
+        </template>
+        <el-select v-model="bpmnFormData.type" @change="updateFlowType">
+          <el-option label="普通流转路径" value="normal" />
+          <el-option label="默认流转路径" value="default" />
+          <el-option label="条件流转路径" value="condition" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="条件格式" v-if="bpmnFormData.type === 'condition'" key="condition">
+        <el-select v-model="bpmnFormData.conditionType">
+          <el-option label="表达式" value="expression" />
+          <el-option label="脚本" value="script" />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="表达式" v-if="bpmnFormData.conditionType && bpmnFormData.conditionType === 'expression'" key="express">
+        <el-input v-model="bpmnFormData.body" clearable @change="updateFlowCondition" />
+      </el-form-item>
+      <template v-if="bpmnFormData.conditionType && bpmnFormData.conditionType === 'script'">
+        <el-form-item label="脚本语言" key="language">
+          <el-input v-model="bpmnFormData.language" clearable @change="updateFlowCondition" />
+        </el-form-item>
+        <el-form-item label="脚本类型" key="scriptType">
+          <el-select v-model="bpmnFormData.scriptType">
+            <el-option label="内联脚本" value="inlineScript" />
+            <el-option label="外部脚本" value="externalScript" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="脚本" v-if="bpmnFormData.scriptType === 'inlineScript'" key="body">
+          <el-input v-model="bpmnFormData.body" type="textarea" clearable @change="updateFlowCondition" />
+        </el-form-item>
+        <el-form-item label="资源地址" v-if="bpmnFormData.scriptType === 'externalScript'" key="resource">
+          <el-input v-model="bpmnFormData.resource" clearable @change="updateFlowCondition" />
+        </el-form-item>
+      </template>
+    </el-form>
+  </div>
+</template>
+
+<script>
+
+import {StrUtil} from "@/utils/StrUtil";
+export default {
+  name: "BpmnModel",
+  /** 组件传值  */
+  props : {
+    id: {
+      type: String,
+      required: true
+    },
+  },
+  data() {
+    return {
+      bpmnElementSource: {},
+      bpmnElementSourceRef: {},
+      bpmnFormData: {}
+    }
+  },
+
+  /** 传值监听 */
+  watch: {
+    id: {
+      handler(newVal) {
+        if (StrUtil.isNotBlank(newVal)) {
+          this.resetFlowCondition();
+        }
+      },
+      immediate: true, // 立即生效
+    },
+  },
+  created() {
+
+  },
+  methods: {
+    // 方法区
+    resetFlowCondition() {
+      this.bpmnFormData = {
+        body: null
+      };
+      this.bpmnElementSource = this.modelerStore.element.source;
+      this.bpmnElementSourceRef = this.modelerStore.element.businessObject.sourceRef;
+      if (this.bpmnElementSourceRef && this.bpmnElementSourceRef.default && this.bpmnElementSourceRef.default.id === this.modelerStore.element.id) {
+        // 默认
+        this.$set(this.bpmnFormData, "type", "default");
+      } else if (!this.modelerStore.element.businessObject.conditionExpression) {
+        // 普通
+        this.$set(this.bpmnFormData, "type", "normal");
+      } else {
+        // 带条件
+        const conditionExpression = this.modelerStore.element.businessObject.conditionExpression;
+        this.bpmnFormData = {...conditionExpression, type: "condition"};
+        // resource 可直接标识 是否是外部资源脚本
+        if (this.bpmnFormData.resource) {
+          this.$set(this.bpmnFormData, "conditionType", "script");
+          this.$set(this.bpmnFormData, "scriptType", "externalScript");
+          return;
+        }
+        if (conditionExpression.language) {
+          this.$set(this.bpmnFormData, "conditionType", "script");
+          this.$set(this.bpmnFormData, "scriptType", "inlineScript");
+          return;
+        }
+        this.$set(this.bpmnFormData, "conditionType", "expression");
+      }
+    },
+
+    updateFlowType(flowType) {
+      // 正常条件类
+      if (flowType === "condition") {
+        const flowConditionRef = this.modelerStore.moddle.create("bpmn:FormalExpression");
+        this.modelerStore.modeling.updateProperties(this.modelerStore.element, {
+          conditionExpression: flowConditionRef
+        });
+        return;
+      }
+      // 默认路径
+      if (flowType === "default") {
+        this.modelerStore.modeling.updateProperties(this.modelerStore.element, {
+          conditionExpression: null
+        });
+        this.modelerStore.modeling.updateProperties(this.bpmnElementSource, {
+          default: this.modelerStore.element
+        });
+        // 清空条件格式
+        this.bpmnFormData.conditionType = null;
+        return;
+      }
+      // 清空条件格式
+      this.bpmnFormData.conditionType = null;
+      // 正常路径,如果来源节点的默认路径是当前连线时,清除父元素的默认路径配置
+      if (this.bpmnElementSourceRef.default && this.bpmnElementSourceRef.default.id === this.modelerStore.element.id) {
+        this.modelerStore.modeling.updateProperties(this.bpmnElementSource, {
+          default: null
+        });
+      }
+      this.modelerStore.modeling.updateProperties(this.modelerStore.element, {
+        conditionExpression: null
+      });
+    },
+
+    updateFlowCondition() {
+      let {conditionType, scriptType, body, resource, language} = this.bpmnFormData;
+      let condition;
+      if (conditionType === "expression") {
+        condition = this.modelerStore.moddle.create("bpmn:FormalExpression", {body});
+      } else {
+        if (scriptType === "inlineScript") {
+          condition = this.modelerStore.moddle.create("bpmn:FormalExpression", {body, language});
+          this.$set(this.bpmnFormData, "resource", "");
+        } else {
+          this.$set(this.bpmnFormData, "body", "");
+          condition = this.modelerStore.moddle.create("bpmn:FormalExpression", {resource, language});
+        }
+      }
+      this.modelerStore.modeling.updateProperties(this.modelerStore.element, {conditionExpression: condition});
+    }
+  }
+}
+</script>

+ 472 - 0
ruoyi-ui/src/components/Process/panel/executionListener.vue

@@ -0,0 +1,472 @@
+<template>
+  <div class="panel-tab__content">
+    <el-table :data="elementListenersList" size="mini" border>
+      <el-table-column label="序号" width="50px" type="index" />
+      <el-table-column label="类型" width="60px" prop="event" />
+      <el-table-column label="监听类型" width="80px" show-overflow-tooltip :formatter="row => listenerTypeObject[row.listenerType]" />
+      <el-table-column label="操作">
+        <template slot-scope="scope">
+          <el-button size="mini" type="primary" @click="openListenerForm(scope.row, scope.$index)">编辑</el-button>
+          <el-divider direction="vertical" />
+          <el-button size="mini" type="danger" @click="removeListener(scope.row, scope.$index)">移除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <div class="element-drawer__button_save">
+      <el-button type="primary" icon="el-icon-plus" size="small" @click="listenerSystemVisible = true">内置监听器</el-button>
+      <el-button type="primary" icon="el-icon-plus" size="small" @click="openListenerForm(null)" >自定义监听器</el-button>
+    </div>
+
+    <!-- 监听器 编辑/创建 部分 -->
+    <el-drawer :visible.sync="listenerFormModelVisible" title="执行监听器" size="480px" append-to-body destroy-on-close>
+      <el-form :model="listenerForm" size="small" label-width="96px" ref="listenerFormRef" @submit.native.prevent>
+        <el-form-item label="事件类型" prop="event" :rules="{ required: true, trigger: ['blur', 'change'] }">
+          <el-select v-model="listenerForm.event">
+            <el-option label="start" value="start" />
+            <el-option label="end" value="end" />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="监听器类型" prop="listenerType" :rules="{ required: true, trigger: ['blur', 'change'] }">
+          <el-select v-model="listenerForm.listenerType">
+            <el-option v-for="i in Object.keys(listenerTypeObject)" :key="i" :label="listenerTypeObject[i]" :value="i" />
+          </el-select>
+        </el-form-item>
+        <el-form-item
+            v-if="listenerForm.listenerType === 'classListener'"
+            label="Java类"
+            prop="class"
+            key="listener-class"
+            :rules="{ required: true, trigger: ['blur', 'change'] }"
+        >
+          <el-input v-model="listenerForm.class" clearable />
+        </el-form-item>
+        <el-form-item
+            v-if="listenerForm.listenerType === 'expressionListener'"
+            label="表达式"
+            prop="expression"
+            key="listener-expression"
+            :rules="{ required: true, trigger: ['blur', 'change'] }"
+        >
+          <el-input v-model="listenerForm.expression" clearable />
+        </el-form-item>
+        <el-form-item
+            v-if="listenerForm.listenerType === 'delegateExpressionListener'"
+            label="代理表达式"
+            prop="delegateExpression"
+            key="listener-delegate"
+            :rules="{ required: true, trigger: ['blur', 'change'] }"
+        >
+          <el-input v-model="listenerForm.delegateExpression" clearable />
+        </el-form-item>
+        <template v-if="listenerForm.listenerType === 'scriptListener'">
+          <el-form-item
+              label="脚本格式"
+              prop="scriptFormat"
+              key="listener-script-format"
+              :rules="{ required: true, trigger: ['blur', 'change'], message: '请填写脚本格式' }"
+          >
+            <el-input v-model="listenerForm.scriptFormat" clearable />
+          </el-form-item>
+          <el-form-item
+              label="脚本类型"
+              prop="scriptType"
+              key="listener-script-type"
+              :rules="{ required: true, trigger: ['blur', 'change'], message: '请选择脚本类型' }"
+          >
+            <el-select v-model="listenerForm.scriptType">
+              <el-option label="内联脚本" value="inlineScript" />
+              <el-option label="外部脚本" value="externalScript" />
+            </el-select>
+          </el-form-item>
+          <el-form-item
+              v-if="listenerForm.scriptType === 'inlineScript'"
+              label="脚本内容"
+              prop="value"
+              key="listener-script"
+              :rules="{ required: true, trigger: ['blur', 'change'], message: '请填写脚本内容' }"
+          >
+            <el-input v-model="this.listenerForm" clearable />
+          </el-form-item>
+          <el-form-item
+              v-if="listenerForm.scriptType === 'externalScript'"
+              label="资源地址"
+              prop="resource"
+              key="listener-resource"
+              :rules="{ required: true, trigger: ['blur', 'change'], message: '请填写资源地址' }"
+          >
+            <el-input v-model="listenerForm.resource" clearable />
+          </el-form-item>
+        </template>
+      </el-form>
+      <el-divider />
+      <p class="listener-filed__title">
+        <span><el-icon><BellFilled /></el-icon>注入字段:</span>
+        <el-button type="primary" size="mini" @click="openListenerFieldForm(null)">添加字段</el-button>
+      </p>
+      <el-table :data="fieldsListOfListener" size="mini" max-height="240" border fit style="flex: none">
+        <el-table-column label="序号" width="50px" type="index" />
+        <el-table-column label="字段名称" width="80px" prop="name" />
+        <el-table-column label="字段类型" width="80px" show-overflow-tooltip :formatter="row => fieldTypeObject[row.fieldType]" />
+        <el-table-column label="值内容" width="80px" show-overflow-tooltip :formatter="row => row.string || row.expression" />
+        <el-table-column label="操作">
+          <template slot-scope="scope">
+            <el-button size="mini" type="primary" @click="openListenerFieldForm(scope.row, scope.$index)">编辑</el-button>
+            <el-divider direction="vertical" />
+            <el-button size="mini" type="danger" @click="removeListenerField(scope.row, scope.$index)">移除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div class="element-drawer__button">
+        <el-button size="small" @click="listenerFormModelVisible = false">取 消</el-button>
+        <el-button size="small" type="primary" @click="saveListenerConfig">保 存</el-button>
+      </div>
+    </el-drawer>
+
+    <!-- 注入西段 编辑/创建 部分 -->
+    <el-dialog title="字段配置" :visible.sync="listenerFieldFormModelVisible" width="600px" append-to-body destroy-on-close>
+      <el-form :model="listenerFieldForm" label-width="96px" ref="listenerFieldFormRef" style="height: 136px" @submit.native.prevent>
+        <el-form-item label="字段名称:" prop="name" :rules="{ required: true, trigger: ['blur', 'change'] }">
+          <el-input v-model="listenerFieldForm.name" clearable />
+        </el-form-item>
+        <el-form-item label="字段类型:" prop="fieldType" :rules="{ required: true, trigger: ['blur', 'change'] }">
+          <el-select v-model="listenerFieldForm.fieldType">
+            <el-option v-for="i in Object.keys(fieldTypeObject)" :key="i" :label="fieldTypeObject[i]" :value="i" />
+          </el-select>
+        </el-form-item>
+        <el-form-item
+            v-if="listenerFieldForm.fieldType === 'string'"
+            label="字段值:"
+            prop="string"
+            key="field-string"
+            :rules="{ required: true, trigger: ['blur', 'change'] }"
+        >
+          <el-input v-model="listenerFieldForm.string" clearable />
+        </el-form-item>
+        <el-form-item
+            v-if="listenerFieldForm.fieldType === 'expression'"
+            label="表达式:"
+            prop="expression"
+            key="field-expression"
+            :rules="{ required: true, trigger: ['blur', 'change'] }"
+        >
+          <el-input v-model="listenerFieldForm.expression" clearable />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+          <el-button size="small" @click="listenerFieldFormModelVisible= false">取 消</el-button>
+          <el-button size="small" type="primary" @click="saveListenerFiled">确 定</el-button>
+      </div>
+    </el-dialog>
+
+
+    <!-- 内置监听器 -->
+    <el-drawer :visible.sync="listenerSystemVisible" title="执行监听器" size="580px" append-to-body destroy-on-close>
+      <el-table v-loading="loading" :data="listenerList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="名称" align="center" prop="name" />
+        <el-table-column label="类型" align="center" prop="eventType"/>
+        <el-table-column label="监听类型" align="center" prop="valueType">
+          <template slot-scope="scope">
+            <dict-tag :options="dict.type.sys_listener_value_type" :value="scope.row.valueType"/>
+          </template>
+        </el-table-column>
+        <el-table-column label="执行内容" align="center" prop="value" :show-overflow-tooltip="true"/>
+      </el-table>
+
+      <pagination
+          v-show="total>0"
+          :total="total"
+          layout="prev, pager, next"
+          :page.sync="queryParams.pageNum"
+          :limit.sync="queryParams.pageSize"
+          @pagination="getList"
+      />
+
+      <div class="element-drawer__button">
+        <el-button size="small" @click="listenerSystemVisible = false">取 消</el-button>
+        <el-button size="small" type="primary" :disabled="listenerSystemChecked" @click="saveSystemListener">保 存</el-button>
+      </div>
+    </el-drawer>
+  </div>
+</template>
+
+<script>
+import { listListener } from "@/api/flowable/listener";
+import {
+  changeListenerObject,
+  createListenerObject,
+  createSystemListenerObject,
+  updateElementExtensions
+} from "../common/bpmnUtils";
+
+import {StrUtil} from "@/utils/StrUtil";
+
+export default {
+  name: "ExecutionListener",
+  // 内置监听器相关信息
+  dicts: ['sys_listener_value_type', 'sys_listener_event_type'],
+  /** 组件传值  */
+  props : {
+    id: {
+      type: String,
+      required: true
+    },
+  },
+  data() {
+    return {
+      elementListenersList: [], // 监听器列表
+      listenerForm: {},// 监听器详情表单
+      listenerFormModelVisible: false, // 监听器 编辑 侧边栏显示状态
+      fieldsListOfListener: [],
+      bpmnElementListeners: [],
+      otherExtensionList: [],
+      listenerFieldForm: {}, // 监听器 注入字段 详情表单
+      listenerFieldFormModelVisible: false, // 监听器 注入字段表单弹窗 显示状态
+      editingListenerIndex: -1, // 监听器所在下标,-1 为新增
+      editingListenerFieldIndex: -1, // 字段所在下标,-1 为新增
+
+      listenerList: [],
+      checkedListenerData: [],
+      listenerSystemVisible: false,
+      listenerSystemChecked: true,
+      loading: true,
+      total: 0,
+      listenerTypeObject: {
+        classListener: "Java 类",
+        expressionListener: "表达式",
+        delegateExpressionListener: "代理表达式",
+        scriptListener: "脚本"
+      },
+      eventType: {
+        create: "创建",
+        assignment: "指派",
+        complete: "完成",
+        delete: "删除",
+        update: "更新",
+        timeout: "超时"
+      },
+      fieldTypeObject: {
+        string: "字符串",
+        expression: "表达式"
+      },
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        type: 2,
+      },
+    }
+  },
+
+  /** 传值监听 */
+  watch: {
+    id: {
+      handler(newVal) {
+        if (StrUtil.isNotBlank(newVal)) {
+          this.resetListenersList();
+        }
+      },
+      immediate: true, // 立即生效
+    },
+  },
+  created() {
+    this.getList();
+
+  },
+  methods: {
+    resetListenersList() {
+      this.bpmnElementListeners =
+        this.modelerStore.element.businessObject?.extensionElements?.values?.filter(ex => ex.$type === `flowable:ExecutionListener`) ?? [];
+      this.elementListenersList = this.bpmnElementListeners.map(listener => this.initListenerType(listener));
+      this.$emit('getExecutionListenerCount', this.elementListenersList.length)
+    },
+
+    // 打开 监听器详情 侧边栏
+    openListenerForm(listener, index) {
+      if (listener) {
+        this.listenerForm = this.initListenerForm(listener);
+        this.editingListenerIndex = index;
+      } else {
+        this.listenerForm = {};
+        this.editingListenerIndex = -1; // 标记为新增
+      }
+      if (listener && listener.fields) {
+        this.fieldsListOfListener = listener.fields.map(field => ({
+          ...field,
+          fieldType: field.string ? "string" : "expression"
+        }));
+      } else {
+        this.fieldsListOfListener = [];
+        this.$set(this.listenerForm, "fields", []);
+      }
+      // 打开侧边栏并清楚验证状态
+      this.listenerFormModelVisible = true;
+      this.$nextTick(() => {
+        if (this.$refs["listenerFormRef"]) this.$refs["listenerFormRef"].clearValidate();
+      });
+    },
+
+    // 打开监听器字段编辑弹窗
+    openListenerFieldForm(field, index) {
+      this.listenerFieldForm = field ? JSON.parse(JSON.stringify(field)) : {};
+      this.editingListenerFieldIndex = field ? index : -1;
+      this.listenerFieldFormModelVisible = true;
+      this.$nextTick(() => {
+        if (this.$refs["listenerFieldFormRef"]) this.$refs["listenerFieldFormRef"].clearValidate();
+      });
+    },
+
+    // 保存监听器注入字段
+    async saveListenerFiled() {
+      let validateStatus = await this.$refs["listenerFieldFormRef"].validate();
+      if (!validateStatus) return; // 验证不通过直接返回
+      if (this.editingListenerFieldIndex === -1) {
+        this.fieldsListOfListener.push(this.listenerFieldForm);
+        this.listenerForm.fields.push(this.listenerFieldForm);
+      } else {
+        this.fieldsListOfListener.splice(this.editingListenerFieldIndex, 1, this.listenerFieldForm);
+        this.listenerForm.fields.splice(this.editingListenerFieldIndex, 1, this.listenerFieldForm);
+      }
+      this.listenerFieldFormModelVisible = false;
+      this.$nextTick(() => (this.listenerFieldForm = {}));
+    },
+
+    // 移除监听器字段
+    removeListenerField(field, index) {
+      this.$confirm("确认移除该字段吗?", "提示", {
+        confirmButtonText: "确 认",
+        cancelButtonText: "取 消"
+      }).then(() => {
+        this.fieldsListOfListener.splice(index, 1);
+        this.listenerForm.fields.splice(index, 1);
+      }).catch(() => console.info("操作取消"));
+    },
+
+    // 移除监听器
+    removeListener(listener, index) {
+      this.$confirm("确认移除该监听器吗?", "提示", {
+        confirmButtonText: "确 认",
+        cancelButtonText: "取 消"
+      }).then(() => {
+        this.bpmnElementListeners.splice(index, 1);
+        this.elementListenersList.splice(index, 1);
+        updateElementExtensions(this.modelerStore.moddle, this.modelerStore.modeling, this.modelerStore.element, this.otherExtensionList.concat(this.bpmnElementListeners));
+        this.$emit('getExecutionListenerCount', this.elementListenersList.length)
+      }).catch((r) => console.info(r, "操作取消"));
+    },
+
+    // 保存监听器配置
+    async saveListenerConfig() {
+      let validateStatus = await this.$refs["listenerFormRef"].validate();
+      if (!validateStatus) return; // 验证不通过直接返回
+      const listenerObject = createListenerObject(this.modelerStore.moddle, this.listenerForm, false, "flowable");
+      if (this.editingListenerIndex === -1) {
+        this.bpmnElementListeners.push(listenerObject);
+        this.elementListenersList.push(this.listenerForm);
+      } else {
+        this.bpmnElementListeners.splice(this.editingListenerIndex, 1, listenerObject);
+        this.elementListenersList.splice(this.editingListenerIndex, 1, this.listenerForm);
+      }
+      // 保存其他配置
+      this.otherExtensionList = this.modelerStore.element.businessObject?.extensionElements?.values?.filter(ex => ex.$type !== `flowable:ExecutionListener`) ?? [];
+      updateElementExtensions(this.modelerStore.moddle, this.modelerStore.modeling, this.modelerStore.element, this.otherExtensionList.concat(this.bpmnElementListeners));
+      this.$emit('getExecutionListenerCount', this.elementListenersList.length)
+      // 4. 隐藏侧边栏
+      this.listenerFormModelVisible = false;
+      this.listenerForm = {};
+    },
+
+    initListenerType(listener) {
+      let listenerType;
+      if (listener.class) listenerType = "classListener";
+      if (listener.expression) listenerType = "expressionListener";
+      if (listener.delegateExpression) listenerType = "delegateExpressionListener";
+      if (listener.script) listenerType = "scriptListener";
+      return {
+        ...JSON.parse(JSON.stringify(listener)),
+        ...(listener.script ?? {}),
+        listenerType: listenerType
+      };
+    },
+
+    // 初始化表单数据
+    initListenerForm(listener) {
+      let self = {
+        ...listener
+      };
+      if (listener.script) {
+        self = {
+          ...listener,
+          ...listener.script,
+          scriptType: listener.script.resource ? "externalScript" : "inlineScript"
+        };
+      }
+      if (listener.event === "timeout" && listener.eventDefinitions) {
+        if (listener.eventDefinitions.length) {
+          let k = "";
+          for (let key in listener.eventDefinitions[0]) {
+            console.log(listener.eventDefinitions, key);
+            if (key.indexOf("time") !== -1) {
+              k = key;
+              self.eventDefinitionType = key.replace("time", "").toLowerCase();
+            }
+          }
+          console.log(k);
+          self.eventTimeDefinitions = listener.eventDefinitions[0][k].body;
+        }
+      }
+      return self;
+    },
+
+
+    /** 查询流程达式列表 */
+    getList() {
+      this.loading = true;
+      listListener(this.queryParams).then(response => {
+        this.listenerList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      // ids.value = selection.map(item => item.id);
+      // TODO 应该使用 push?
+      this.checkedListenerData = selection;
+      this.listenerSystemChecked = selection.length !== 1;
+    },
+
+    // 保存内置监听器
+    saveSystemListener() {
+      if (this.checkedListenerData.length > 0) {
+        this.checkedListenerData.forEach(value => {
+          // 保存其他配置
+          const listenerObject = createSystemListenerObject(this.modelerStore.moddle, value, false, "flowable");
+          this.bpmnElementListeners.push(listenerObject);
+          this.elementListenersList.push(changeListenerObject(value));
+          this.otherExtensionList = this.modelerStore.element.businessObject?.extensionElements?.values?.filter(ex => ex.$type !== `flowable:TaskListener`) ?? [];
+          updateElementExtensions(this.modelerStore.moddle, this.modelerStore.modeling, this.modelerStore.element, this.otherExtensionList.concat(this.bpmnElementListeners));
+        })
+        // 回传显示数量
+        this.$emit('getExecutionListenerCount', this.elementListenersList.length)
+      }
+      this.checkedListenerData = [];
+      this.listenerSystemChecked = true;
+      // 隐藏侧边栏
+      this.listenerSystemVisible = false;
+    }
+  }
+}
+
+</script>
+
+<style lang="scss">
+@import '../style/process-panel';
+.flow-containers  .el-badge__content.is-fixed {
+  top: 18px;
+}
+.dialog-footer button:first-child {
+  margin-right: 10px;
+}
+</style>

+ 84 - 0
ruoyi-ui/src/components/Process/panel/formPanel.vue

@@ -0,0 +1,84 @@
+<template>
+  <div>
+    <el-form label-width="80px" size="small" @submit.native.prevent>
+      <el-form-item label="流程表单">
+        <el-select v-model="bpmnFormData.formKey" clearable class="m-2" placeholder="挂载节点表单" @change="updateElementFormKey">
+          <el-option
+              v-for="item in formList"
+              :key="item.value"
+              :label="item.formName"
+              :value="item.formId"
+          />
+        </el-select>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+
+import { listAllForm } from '@/api/flowable/form'
+import {StrUtil} from "@/utils/StrUtil";
+export default {
+  name: "FormPanel",
+  /** 组件传值  */
+  props : {
+    id: {
+      type: String,
+      required: true
+    },
+  },
+  data() {
+    return {
+      formList: [], // 表单数据
+      bpmnFormData: {
+          formKey: ''
+      }
+    }
+  },
+
+  /** 传值监听 */
+  watch: {
+    id: {
+      handler(newVal) {
+        if (StrUtil.isNotBlank(newVal)) {
+          // 加载表单列表
+          this.getListForm();
+          this.resetFlowForm();
+        }
+      },
+      immediate: true, // 立即生效
+    },
+  },
+  created() {
+
+  },
+  methods: {
+
+    // 方法区
+    resetFlowForm() {
+      this.bpmnFormData.formKey = this.modelerStore.element.businessObject.formKey;
+    },
+
+    updateElementFormKey(val) {
+      if (StrUtil.isBlank(val)) {
+        delete this.modelerStore.element.businessObject[`formKey`]
+      } else {
+        this.modelerStore.modeling.updateProperties(this.modelerStore.element, {'formKey': val});
+      }
+    },
+
+    // 获取表单信息
+    getListForm() {
+      listAllForm().then(res => {
+        res.data.forEach(item => {
+          item.formId = item.formId.toString();
+        })
+        this.formList = res.data;
+      })
+    }
+  }
+}
+
+
+</script>

+ 236 - 0
ruoyi-ui/src/components/Process/panel/multiInstance.vue

@@ -0,0 +1,236 @@
+<template>
+  <div class="panel-tab__content">
+    <el-form label-width="70px" @submit.native.prevent size="small">
+      <el-form-item label="参数说明">
+        <el-button size="small" type="primary" @click="dialogVisible = true">查看</el-button>
+      </el-form-item>
+      <el-form-item label="回路特性">
+        <el-select v-model="loopCharacteristics" @change="changeLoopCharacteristicsType">
+          <!--bpmn:MultiInstanceLoopCharacteristics-->
+          <el-option label="并行多重事件" value="ParallelMultiInstance" />
+          <el-option label="时序多重事件" value="SequentialMultiInstance" />
+          <!--bpmn:StandardLoopCharacteristics-->
+          <el-option label="循环事件" value="StandardLoop" />
+          <el-option label="无" value="Null" />
+        </el-select>
+      </el-form-item>
+      <template v-if="loopCharacteristics === 'ParallelMultiInstance' || loopCharacteristics === 'SequentialMultiInstance'">
+        <el-form-item label="循环基数" key="loopCardinality">
+          <el-input v-model="loopInstanceForm.loopCardinality" clearable @change="updateLoopCardinality" />
+        </el-form-item>
+        <el-form-item label="集合" key="collection">
+          <el-input v-model="loopInstanceForm.collection" clearable @change="updateLoopBase" />
+        </el-form-item>
+        <el-form-item label="元素变量" key="elementVariable">
+          <el-input v-model="loopInstanceForm.elementVariable" clearable @change="updateLoopBase" />
+        </el-form-item>
+        <el-form-item label="完成条件" key="completionCondition">
+          <el-input v-model="loopInstanceForm.completionCondition" clearable @change="updateLoopCondition" />
+        </el-form-item>
+        <el-form-item label="异步状态" key="async">
+          <el-checkbox v-model="loopInstanceForm.asyncBefore" label="异步前" @change="updateLoopAsync('asyncBefore')" />
+          <el-checkbox v-model="loopInstanceForm.asyncAfter" label="异步后" @change="updateLoopAsync('asyncAfter')" />
+          <el-checkbox
+              v-model="loopInstanceForm.exclusive"
+              v-if="loopInstanceForm.asyncAfter || loopInstanceForm.asyncBefore"
+              label="排除"
+              @change="updateLoopAsync('exclusive')"
+          />
+        </el-form-item>
+        <el-form-item label="重试周期" prop="timeCycle" v-if="loopInstanceForm.asyncAfter || loopInstanceForm.asyncBefore" key="timeCycle">
+          <el-input v-model="loopInstanceForm.timeCycle" clearable @change="updateLoopTimeCycle" />
+        </el-form-item>
+      </template>
+    </el-form>
+
+  <!-- 参数说明 -->
+  <el-dialog title="多实例参数" :visible.sync="dialogVisible" width="680px" @closed="$emit('close')">
+    <el-descriptions :column="1" border>
+      <el-descriptions-item label="使用说明">按照BPMN2.0规范的要求,用于为每个实例创建执行的父执行,会提供下列变量:</el-descriptions-item>
+      <el-descriptions-item label="collection(集合变量)">传入List参数, 一般为用户ID集合</el-descriptions-item>
+      <el-descriptions-item label="elementVariable(元素变量)">List中单个参数的名称</el-descriptions-item>
+      <el-descriptions-item label="loopCardinality(基数)">List循环次数</el-descriptions-item>
+      <el-descriptions-item label="isSequential(串并行)">Parallel: 并行多实例,Sequential: 串行多实例</el-descriptions-item>
+      <el-descriptions-item label="completionCondition(完成条件)">任务出口条件</el-descriptions-item>
+      <el-descriptions-item label="nrOfInstances(实例总数)">实例总数</el-descriptions-item>
+      <el-descriptions-item label="nrOfActiveInstances(未完成数)">当前活动的(即未完成的),实例数量。对于顺序多实例,这个值总为1</el-descriptions-item>
+      <el-descriptions-item label="nrOfCompletedInstances(已完成数)">已完成的实例数量</el-descriptions-item>
+      <el-descriptions-item label="loopCounter">给定实例在for-each循环中的index</el-descriptions-item>
+    </el-descriptions>
+  </el-dialog>
+  </div>
+</template>
+<script>
+import {StrUtil} from '@/utils/StrUtil'
+
+
+export default {
+  name: "MultiInstance",
+  /** 组件传值  */
+  props: {
+    id: {
+      type: String,
+      required: true
+    },
+  },
+  data() {
+    return {
+      dialogVisible: false,
+      loopCharacteristics: "",
+      loopInstanceForm: {},
+      multiLoopInstance: {},
+      defaultLoopInstanceForm: {
+        completionCondition: "",
+        loopCardinality: "",
+        extensionElements: [],
+        asyncAfter: false,
+        asyncBefore: false,
+        exclusive: false
+      },
+    }
+  },
+
+  /** 传值监听 */
+  watch: {
+    id: {
+      handler(newVal) {
+        if (StrUtil.isNotBlank(newVal)) {
+          this.getElementLoop(this.modelerStore.element.businessObject);        }
+      },
+      immediate: true, // 立即生效
+    },
+  },
+  created() {
+
+  },
+  methods: {
+    // 方法区
+    getElementLoop(businessObject) {
+      if (!businessObject.loopCharacteristics) {
+        this.loopCharacteristics = "Null";
+        this.loopInstanceForm = {};
+        return;
+      }
+      if (businessObject.loopCharacteristics.$type === "bpmn:StandardLoopCharacteristics") {
+        this.loopCharacteristics = "StandardLoop";
+        this.loopInstanceForm = {};
+        return;
+      }
+      if (businessObject.loopCharacteristics.isSequential) {
+        this.loopCharacteristics = "SequentialMultiInstance";
+      } else {
+        this.loopCharacteristics = "ParallelMultiInstance";
+      }
+      // 合并配置
+      this.loopInstanceForm = {
+        ...this.defaultLoopInstanceForm,
+        ...businessObject.loopCharacteristics,
+        completionCondition: businessObject.loopCharacteristics?.completionCondition?.body ?? "",
+        loopCardinality: businessObject.loopCharacteristics?.loopCardinality?.body ?? ""
+      };
+      // 保留当前元素 businessObject 上的 loopCharacteristics 实例
+      this.multiLoopInstance = this.modelerStore.element.businessObject.loopCharacteristics;
+      // 更新表单
+      if (
+        businessObject.loopCharacteristics.extensionElements &&
+        businessObject.loopCharacteristics.extensionElements.values &&
+        businessObject.loopCharacteristics.extensionElements.values.length
+      ) {
+        this.$set(this.loopInstanceForm, "timeCycle", businessObject.loopCharacteristics.extensionElements.values[0].body);
+      }
+    },
+
+    changeLoopCharacteristicsType(type) {
+      // 切换类型取消原表单配置
+      this.loopInstanceForm = {...this.defaultLoopInstanceForm};
+      // 取消多实例配置
+      if (type === "Null") {
+        this.modelerStore.modeling.updateProperties(this.modelerStore.element, {loopCharacteristics: null});
+        return;
+      }
+      // 配置循环
+      if (type === "StandardLoop") {
+        const loopCharacteristicsObject = this.modelerStore.moddle.create("bpmn:StandardLoopCharacteristics");
+        this.modelerStore.modeling.updateProperties(this.modelerStore.element, {
+          loopCharacteristics: loopCharacteristicsObject
+        });
+        this.multiLoopInstance = null;
+        return;
+      }
+      // 时序
+      if (type === "SequentialMultiInstance") {
+        this.multiLoopInstance = this.modelerStore.moddle.create("bpmn:MultiInstanceLoopCharacteristics", {
+          isSequential: true
+        });
+      } else {
+        this.multiLoopInstance = this.modelerStore.moddle.create("bpmn:MultiInstanceLoopCharacteristics");
+      }
+      this.modelerStore.modeling.updateProperties(this.modelerStore.element, {
+        loopCharacteristics: this.multiLoopInstance
+      });
+    },
+
+    // 循环基数
+    updateLoopCardinality(cardinality) {
+      let loopCardinality = null;
+      if (cardinality && cardinality.length) {
+        loopCardinality = this.modelerStore.moddle.create("bpmn:FormalExpression", {body: cardinality});
+      }
+      this.modelerStore.modeling.updateModdleProperties(this.modelerStore.element, this.multiLoopInstance, {
+        loopCardinality
+      });
+    },
+
+    // 完成条件
+    updateLoopCondition(condition) {
+      let completionCondition = null;
+      if (condition && condition.length) {
+        completionCondition = this.modelerStore.moddle.create("bpmn:FormalExpression", {body: condition});
+      }
+      this.modelerStore.modeling.updateModdleProperties(this.modelerStore.element, this.multiLoopInstance, {
+        completionCondition
+      });
+    },
+
+    // 重试周期
+    updateLoopTimeCycle(timeCycle) {
+      const extensionElements = this.modelerStore.moddle.create("bpmn:ExtensionElements", {
+        values: [
+          this.modelerStore.moddle.create(`flowable:FailedJobRetryTimeCycle`, {
+            body: timeCycle
+          })
+        ]
+      });
+      this.modelerStore.modeling.updateModdleProperties(this.modelerStore.element, this.multiLoopInstance, {
+        extensionElements
+      });
+    },
+
+    // 直接更新的基础信息
+    updateLoopBase() {
+      this.modelerStore.modeling.updateModdleProperties(this.modelerStore.element, this.multiLoopInstance, {
+        collection: this.loopInstanceForm.collection || null,
+        elementVariable: this.loopInstanceForm.elementVariable || null
+      });
+    },
+
+    // 各异步状态
+    updateLoopAsync(key) {
+      const {asyncBefore, asyncAfter} = this.loopInstanceForm;
+      let asyncAttr = Object.create(null);
+      if (!asyncBefore && !asyncAfter) {
+        this.$set(this.loopInstanceForm, "exclusive", false);
+        asyncAttr = {asyncBefore: false, asyncAfter: false, exclusive: false, extensionElements: null};
+      } else {
+        asyncAttr[key] = this.loopInstanceForm[key];
+      }
+      this.modelerStore.modeling.updateModdleProperties(this.modelerStore.element, this.multiLoopInstance, asyncAttr);
+    }
+  }
+}
+
+</script>
+<style lang="scss">
+@import '../style/process-panel';
+</style>
+

+ 65 - 0
ruoyi-ui/src/components/Process/panel/otherPanel.vue

@@ -0,0 +1,65 @@
+<template>
+  <div>
+  <el-form label-width="80px" size="small" @submit.native.prevent>
+    <el-form-item label="跳过表达式">
+      <el-input v-model="bpmnFormData.skipExpression" @change="updateElementTask('skipExpression')"/>
+    </el-form-item>
+    <el-form-item label="是否为补偿">
+      <el-input v-model="bpmnFormData.isForCompensation" @change="updateElementTask('isForCompensation')"/>
+    </el-form-item>
+    <el-form-item label="服务任务可触发">
+      <el-input v-model="bpmnFormData.triggerable" @change="updateElementTask('triggerable')"/>
+    </el-form-item>
+  </el-form>
+  </div>
+</template>
+
+<script>
+
+import {StrUtil} from "@/utils/StrUtil";
+export default {
+  name: "OtherPanel",
+  /** 组件传值  */
+  props : {
+    id: {
+      type: String,
+      required: true
+    },
+  },
+  data() {
+    return {
+      bpmnFormData: {}
+    }
+  },
+
+  /** 传值监听 */
+  watch: {
+    id: {
+      handler(newVal) {
+        if (StrUtil.isNotBlank(newVal)) {
+          this.resetTaskForm();
+        }
+      },
+      immediate: true, // 立即生效
+    },
+  },
+  created() {
+
+  },
+  methods: {
+    // 方法区
+    resetFlowForm() {
+      this.bpmnFormData = JSON.parse(JSON.stringify(this.modelerStore.element.businessObject));
+    },
+
+    updateElementTask(key) {
+      const taskAttr = Object.create(null);
+      taskAttr[key] = this.bpmnFormData[key] || null;
+      this.modelerStore.modeling.updateProperties(this.modelerStore.element, taskAttr);
+    }
+  }
+}
+
+
+
+</script>

+ 529 - 0
ruoyi-ui/src/components/Process/panel/taskListener.vue

@@ -0,0 +1,529 @@
+<template>
+  <div class="panel-tab__content">
+    <el-table :data="elementListenersList" border size="mini">
+      <el-table-column label="序号" width="50px" type="index" />
+      <el-table-column label="类型" width="60px" show-overflow-tooltip :formatter="row => listenerEventTypeObject[row.event]" />
+<!--      <el-table-column label="事件id" min-width="70px" prop="id" show-overflow-tooltip />-->
+      <el-table-column label="监听类型" width="85px" show-overflow-tooltip :formatter="row => listenerTypeObject[row.listenerType]" />
+      <el-table-column label="操作" >
+        <template  slot-scope="scope">
+          <el-button size="mini" type="primary" @click="openListenerForm(scope.row, scope.$index)">编辑</el-button>
+          <el-divider direction="vertical" />
+          <el-button size="mini" type="danger" @click="removeListener(scope.row, scope.$index)">移除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <div class="element-drawer__button_save">
+      <el-button type="primary" icon="el-icon-plus" size="small" @click="listenerSystemVisible = true">内置监听器</el-button>
+      <el-button type="primary" icon="el-icon-plus" size="small" @click="openListenerForm(null)">自定义监听器</el-button>
+    </div>
+
+    <!-- 监听器 编辑/创建 部分 -->
+    <el-drawer :visible.sync="listenerFormModelVisible" title="任务监听器" size="480px" append-to-body destroy-on-close>
+      <el-form :model="listenerForm" size="small" label-width="110px" ref="listenerFormRef" @submit.native.prevent>
+        <el-form-item prop="event" :rules="{ required: true, trigger: ['blur', 'change'] }">
+          <template slot="label">
+            <span>
+               事件类型
+               <el-tooltip placement="top">
+                  <template slot="content">
+                     <div>
+                              create(创建):当任务已经创建,并且所有任务参数都已经设置时触发。
+                        <br />assignment(指派):当任务已经指派给某人时触发。请注意:当流程执行到达用户任务时,
+                        <br />在触发create事件之前,会首先触发assignment事件。这顺序看起来不太自然,
+                        <br />但是有实际原因的:当收到create事件时,我们通常希望能看到任务的所有参数,包括办理人。
+                        <br />complete(完成):当任务已经完成,从运行时数据中删除前触发。
+                        <br />delete(删除):在任务即将被删除前触发。请注意任务由completeTask正常完成时也会触发。
+                     </div>
+                  </template>
+                  <i class="el-icon-question" />
+               </el-tooltip>
+            </span>
+          </template>
+          <el-select v-model="listenerForm.event">
+            <el-option v-for="i in Object.keys(listenerEventTypeObject)" :key="i" :label="listenerEventTypeObject[i]" :value="i" />
+          </el-select>
+        </el-form-item>
+<!--        <el-form-item label="监听器ID" prop="id" :rules="{ required: true, trigger: ['blur', 'change'] }">-->
+<!--          <el-input v-model="listenerForm.id" clearable />-->
+<!--        </el-form-item>-->
+        <el-form-item label="监听器类型" prop="listenerType" :rules="{ required: true, trigger: ['blur', 'change'] }">
+          <template slot="label">
+            <span>
+               监听类型
+               <el-tooltip placement="top">
+                  <template slot="content">
+                     <div>
+                              class:需要调用的委托类。这个类必须实现org.flowable.engine.delegate.TaskListener接口。
+                        <br />assignment(指派):当任务已经指派给某人时触发。请注意:当流程执行到达用户任务时,
+                        <br />  在触发create事件之前,会首先触发assignment事件。这顺序看起来不太自然,
+                        <br />  但是有实际原因的:当收到create事件时,我们通常希望能看到任务的所有参数,包括办理人。
+                        <br />complete(完成):当任务已经完成,从运行时数据中删除前触发。
+                        <br />delete(删除):在任务即将被删除前触发。请注意任务由completeTask正常完成时也会触发。
+                     </div>
+                  </template>
+                  <i class="el-icon-question" />
+               </el-tooltip>
+            </span>
+          </template>
+          <el-select v-model="listenerForm.listenerType">
+            <el-option v-for="i in Object.keys(listenerTypeObject)" :key="i" :label="listenerTypeObject[i]" :value="i" />
+          </el-select>
+        </el-form-item>
+        <el-form-item
+            v-if="listenerForm.listenerType === 'classListener'"
+            label="Java类"
+            prop="class"
+            key="listener-class"
+            :rules="{ required: true, trigger: ['blur', 'change'] }"
+        >
+          <el-input v-model="listenerForm.class" clearable />
+        </el-form-item>
+        <el-form-item
+            v-if="listenerForm.listenerType === 'expressionListener'"
+            label="表达式"
+            prop="expression"
+            key="listener-expression"
+            :rules="{ required: true, trigger: ['blur', 'change'] }"
+        >
+          <el-input v-model="listenerForm.expression" clearable />
+        </el-form-item>
+        <el-form-item
+            v-if="listenerForm.listenerType === 'delegateExpressionListener'"
+            label="代理表达式"
+            prop="delegateExpression"
+            key="listener-delegate"
+            :rules="{ required: true, trigger: ['blur', 'change'] }"
+        >
+          <el-input v-model="listenerForm.delegateExpression" clearable />
+        </el-form-item>
+        <template v-if="listenerForm.listenerType === 'scriptListener'">
+          <el-form-item
+              label="脚本格式"
+              prop="scriptFormat"
+              key="listener-script-format"
+              :rules="{ required: true, trigger: ['blur', 'change'], message: '请填写脚本格式' }"
+          >
+            <el-input v-model="listenerForm.scriptFormat" clearable />
+          </el-form-item>
+          <el-form-item
+              label="脚本类型"
+              prop="scriptType"
+              key="listener-script-type"
+              :rules="{ required: true, trigger: ['blur', 'change'], message: '请选择脚本类型' }"
+          >
+            <el-select v-model="listenerForm.scriptType">
+              <el-option label="内联脚本" value="inlineScript" />
+              <el-option label="外部脚本" value="externalScript" />
+            </el-select>
+          </el-form-item>
+          <el-form-item
+              v-if="listenerForm.scriptType === 'inlineScript'"
+              label="脚本内容"
+              prop="value"
+              key="listener-script"
+              :rules="{ required: true, trigger: ['blur', 'change'], message: '请填写脚本内容' }"
+          >
+            <el-input v-model="listenerForm.value" clearable />
+          </el-form-item>
+          <el-form-item
+              v-if="listenerForm.scriptType === 'externalScript'"
+              label="资源地址"
+              prop="resource"
+              key="listener-resource"
+              :rules="{ required: true, trigger: ['blur', 'change'], message: '请填写资源地址' }"
+          >
+            <el-input v-model="listenerForm.resource" clearable />
+          </el-form-item>
+        </template>
+
+        <template v-if="listenerForm.event === 'timeout'">
+          <el-form-item label="定时器类型" prop="eventDefinitionType" key="eventDefinitionType">
+            <el-select v-model="listenerForm.eventDefinitionType">
+              <el-option label="日期" value="date" />
+              <el-option label="持续时长" value="duration" />
+              <el-option label="循环" value="cycle" />
+              <el-option label="无" value="null" />
+            </el-select>
+          </el-form-item>
+          <el-form-item
+              v-if="!!listenerForm.eventDefinitionType && listenerForm.eventDefinitionType !== 'null'"
+              label="定时器"
+              prop="eventTimeDefinitions"
+              key="eventTimeDefinitions"
+              :rules="{ required: true, trigger: ['blur', 'change'], message: '请填写定时器配置' }"
+          >
+            <el-input v-model="listenerForm.eventTimeDefinitions" clearable />
+          </el-form-item>
+        </template>
+      </el-form>
+
+      <el-divider />
+      <p class="listener-filed__title">
+        <span><i class="el-icon-menu"></i>注入字段:</span>
+        <el-button size="small" type="primary" @click="openListenerFieldForm(null)">添加字段</el-button>
+      </p>
+      <el-table :data="fieldsListOfListener" size="mini" max-height="240" border fit style="flex: none">
+        <el-table-column label="序号" width="50px" type="index" />
+        <el-table-column label="字段名称" width="80px" prop="name" />
+        <el-table-column label="字段类型" width="80px" show-overflow-tooltip :formatter="row => fieldTypeObject[row.fieldType]" />
+        <el-table-column label="值内容" width="80px" show-overflow-tooltip :formatter="row => row.string || row.expression" />
+        <el-table-column label="操作">
+          <template slot-scope="scope">
+            <el-button size="mini" type="primary"  @click="openListenerFieldForm(scope.row, scope.$index)">编辑</el-button>
+            <el-divider direction="vertical" />
+            <el-button size="mini" type="danger" @click="removeListenerField(scope.row, scope.$index)">移除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <div class="element-drawer__button">
+        <el-button size="small" @click="listenerFormModelVisible = false">取 消</el-button>
+        <el-button size="small" type="primary" @click="saveListenerConfig">保 存</el-button>
+      </div>
+    </el-drawer>
+
+    <!-- 注入西段 编辑/创建 部分 -->
+    <el-dialog title="字段配置" :visible.sync="listenerFieldFormModelVisible" width="600px" append-to-body destroy-on-close>
+      <el-form :model="listenerFieldForm"  label-width="96px" ref="listenerFieldFormRef" style="height: 136px" @submit.native.prevent>
+        <el-form-item label="字段名称:" prop="name" :rules="{ required: true, trigger: ['blur', 'change'] }">
+          <el-input v-model="listenerFieldForm.name" clearable />
+        </el-form-item>
+        <el-form-item label="字段类型:" prop="fieldType" :rules="{ required: true, trigger: ['blur', 'change'] }">
+          <el-select v-model="listenerFieldForm.fieldType">
+            <el-option v-for="i in Object.keys(fieldTypeObject)" :key="i" :label="fieldTypeObject[i]" :value="i" />
+          </el-select>
+        </el-form-item>
+        <el-form-item
+            v-if="listenerFieldForm.fieldType === 'string'"
+            label="字段值:"
+            prop="string"
+            key="field-string"
+            :rules="{ required: true, trigger: ['blur', 'change'] }"
+        >
+          <el-input v-model="listenerFieldForm.string" clearable />
+        </el-form-item>
+        <el-form-item
+            v-if="listenerFieldForm.fieldType === 'expression'"
+            label="表达式:"
+            prop="expression"
+            key="field-expression"
+            :rules="{ required: true, trigger: ['blur', 'change'] }"
+        >
+          <el-input v-model="listenerFieldForm.expression" clearable />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+          <el-button size="small" @click="listenerFieldFormModelVisible= false">取 消</el-button>
+          <el-button size="small" type="primary" @click="saveListenerFiled">确 定</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 内置监听器 -->
+    <el-drawer :visible.sync="listenerSystemVisible" title="任务监听器" size="580px" append-to-body destroy-on-close>
+      <el-table v-loading="loading" :data="listenerList" @selection-change="handleSelectionChange">
+        <el-table-column type="selection" width="55" align="center" />
+        <el-table-column label="名称" align="center" prop="name" />
+        <el-table-column label="类型" align="center" prop="eventType"/>
+        <el-table-column label="监听类型" align="center" prop="valueType">
+          <template slot-scope="scope">
+            <dict-tag :options="dict.type.sys_listener_value_type" :value="scope.row.valueType"/>
+          </template>
+        </el-table-column>
+        <el-table-column label="执行内容" align="center" prop="value" :show-overflow-tooltip="true"/>
+      </el-table>
+
+      <pagination
+          v-show="total>0"
+          :total="total"
+          layout="prev, pager, next"
+          :page.sync="queryParams.pageNum"
+          :limit.sync="queryParams.pageSize"
+          @pagination="getList"
+      />
+
+      <div class="element-drawer__button">
+        <el-button size="small" @click="listenerSystemVisible = false">取 消</el-button>
+        <el-button size="small" type="primary" :disabled="listenerSystemChecked" @click="saveSystemListener">保 存</el-button>
+      </div>
+    </el-drawer>
+  </div>
+</template>
+<script>
+import { listListener } from "@/api/flowable/listener";
+import {
+  changeListenerObject,
+  createListenerObject,
+  createSystemListenerObject,
+  updateElementExtensions
+} from "../common/bpmnUtils";
+
+import {StrUtil} from "@/utils/StrUtil";
+
+export default {
+  name: "TaskListener",
+  // 内置监听器相关信息
+  dicts: ['sys_listener_value_type', 'sys_listener_event_type'],
+  /** 组件传值  */
+  props : {
+    id: {
+      type: String,
+      required: true
+    },
+  },
+  data() {
+    return {
+      elementListenersList: [], // 监听器列表
+      listenerForm: {},// 监听器详情表单
+      listenerFormModelVisible: false, // 监听器 编辑 侧边栏显示状态
+      fieldsListOfListener: [],
+      bpmnElementListeners: [],
+      otherExtensionList: [],
+      listenerFieldForm: {}, // 监听器 注入字段 详情表单
+      listenerFieldFormModelVisible: false, // 监听器 注入字段表单弹窗 显示状态
+      editingListenerIndex: -1, // 监听器所在下标,-1 为新增
+      editingListenerFieldIndex: -1, // 字段所在下标,-1 为新增
+
+      listenerList: [],
+      checkedListenerData: [],
+      listenerSystemVisible: false,
+      listenerSystemChecked: true,
+      loading: true,
+      total: 0,
+      listenerTypeObject: {
+        classListener: "Java 类",
+        expressionListener: "表达式",
+        delegateExpressionListener: "代理表达式",
+        scriptListener: "脚本"
+      },
+      listenerEventTypeObject:{
+        create: "创建",
+        assignment: "指派",
+        complete: "完成",
+        delete: "删除",
+        // update: "更新",
+        // timeout: "超时"
+      },
+      fieldTypeObject:{
+        string: "字符串",
+        expression: "表达式"
+      },
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        type: 1,
+      },
+    }
+  },
+
+  /** 传值监听 */
+  watch: {
+    id: {
+      handler(newVal) {
+        if (StrUtil.isNotBlank(newVal)) {
+          this.resetListenersList();
+        }
+      },
+      immediate: true, // 立即生效
+    },
+  },
+  created() {
+    this.getList();
+
+  },
+  methods: {
+    resetListenersList() {
+      this.bpmnElementListeners =
+        this.modelerStore.element.businessObject?.extensionElements?.values?.filter(ex => ex.$type === `flowable:TaskListener`) ?? [];
+      this.elementListenersList = this.bpmnElementListeners.map(listener => this.initListenerType(listener));
+      this.$emit('getTaskListenerCount', this.elementListenersList.length)
+    },
+
+    // 打开 监听器详情 侧边栏
+    openListenerForm(listener, index) {
+      if (listener) {
+        this.listenerForm = this.initListenerForm(listener);
+        this.editingListenerIndex = index;
+      } else {
+        this.listenerForm = {};
+        this.editingListenerIndex = -1; // 标记为新增
+      }
+      if (listener && listener.fields) {
+        this.fieldsListOfListener = listener.fields.map(field => ({
+          ...field,
+          fieldType: field.string ? "string" : "expression"
+        }));
+      } else {
+        this.fieldsListOfListener = [];
+        this.$set(this.listenerForm, "fields", []);
+      }
+      // 打开侧边栏并清楚验证状态
+      this.listenerFormModelVisible = true;
+      this.$nextTick(() => {
+        if (this.$refs["listenerFormRef"]) this.$refs["listenerFormRef"].clearValidate();
+      });
+    },
+
+    // 打开监听器字段编辑弹窗
+    openListenerFieldForm(field, index) {
+      this.listenerFieldForm = field ? JSON.parse(JSON.stringify(field)) : {};
+      this.editingListenerFieldIndex = field ? index : -1;
+      this.listenerFieldFormModelVisible = true;
+      this.$nextTick(() => {
+        if (this.$refs["listenerFieldFormRef"]) this.$refs["listenerFieldFormRef"].clearValidate();
+      });
+    },
+
+    // 保存监听器注入字段
+    async saveListenerFiled() {
+      let validateStatus = await this.$refs["listenerFieldFormRef"].validate();
+      if (!validateStatus) return; // 验证不通过直接返回
+      if (this.editingListenerFieldIndex === -1) {
+        this.fieldsListOfListener.push(this.listenerFieldForm);
+        this.listenerForm.fields.push(this.listenerFieldForm);
+      } else {
+        this.fieldsListOfListener.splice(this.editingListenerFieldIndex, 1, this.listenerFieldForm);
+        this.listenerForm.fields.splice(this.editingListenerFieldIndex, 1, this.listenerFieldForm);
+      }
+      this.listenerFieldFormModelVisible = false;
+      this.$nextTick(() => (this.listenerFieldForm = {}));
+    },
+
+    // 移除监听器字段
+    removeListenerField(field, index) {
+      this.$confirm("确认移除该字段吗?", "提示", {
+        confirmButtonText: "确 认",
+        cancelButtonText: "取 消"
+      }).then(() => {
+        this.fieldsListOfListener.splice(index, 1);
+        this.listenerForm.fields.splice(index, 1);
+      }).catch(() => console.info("操作取消"));
+    },
+
+    // 移除监听器
+    removeListener(listener, index) {
+      this.$confirm("确认移除该监听器吗?", "提示", {
+        confirmButtonText: "确 认",
+        cancelButtonText: "取 消"
+      }).then(() => {
+        this.bpmnElementListeners.splice(index, 1);
+        this.elementListenersList.splice(index, 1);
+        updateElementExtensions(this.modelerStore.moddle, this.modelerStore.modeling, this.modelerStore.element, this.otherExtensionList.concat(this.bpmnElementListeners));
+        this.$emit('getTaskListenerCount', this.elementListenersList.length)
+      }).catch((r) => console.info(r, "操作取消"));
+    },
+
+    // 保存监听器配置
+    async saveListenerConfig() {
+      let validateStatus = await this.$refs["listenerFormRef"].validate();
+      if (!validateStatus) return; // 验证不通过直接返回
+      const listenerObject = createListenerObject(this.modelerStore.moddle, this.listenerForm, false, "flowable");
+      if (this.editingListenerIndex === -1) {
+        this.bpmnElementListeners.push(listenerObject);
+        this.elementListenersList.push(this.listenerForm);
+      } else {
+        this.bpmnElementListeners.splice(this.editingListenerIndex, 1, listenerObject);
+        this.elementListenersList.splice(this.editingListenerIndex, 1, this.listenerForm);
+      }
+      // 保存其他配置
+      this.otherExtensionList = this.modelerStore.element.businessObject?.extensionElements?.values?.filter(ex => ex.$type !== `flowable:TaskListener`) ?? [];
+      updateElementExtensions(this.modelerStore.moddle, this.modelerStore.modeling, this.modelerStore.element, this.otherExtensionList.concat(this.bpmnElementListeners));
+      this.$emit('getTaskListenerCount', this.elementListenersList.length)
+      // 4. 隐藏侧边栏
+      this.listenerFormModelVisible = false;
+      this.listenerForm = {};
+    },
+
+    initListenerType(listener) {
+      let listenerType;
+      if (listener.class) listenerType = "classListener";
+      if (listener.expression) listenerType = "expressionListener";
+      if (listener.delegateExpression) listenerType = "delegateExpressionListener";
+      if (listener.script) listenerType = "scriptListener";
+      return {
+        ...JSON.parse(JSON.stringify(listener)),
+        ...(listener.script ?? {}),
+        listenerType: listenerType
+      };
+    },
+
+    // 初始化表单数据
+    initListenerForm(listener) {
+      let self = {
+        ...listener
+      };
+      if (listener.script) {
+        self = {
+          ...listener,
+          ...listener.script,
+          scriptType: listener.script.resource ? "externalScript" : "inlineScript"
+        };
+      }
+      if (listener.event === "timeout" && listener.eventDefinitions) {
+        if (listener.eventDefinitions.length) {
+          let k = "";
+          for (let key in listener.eventDefinitions[0]) {
+            console.log(listener.eventDefinitions, key);
+            if (key.indexOf("time") !== -1) {
+              k = key;
+              self.eventDefinitionType = key.replace("time", "").toLowerCase();
+            }
+          }
+          console.log(k);
+          self.eventTimeDefinitions = listener.eventDefinitions[0][k].body;
+        }
+      }
+      return self;
+    },
+
+
+    /** 查询流程达式列表 */
+    getList() {
+      this.loading = true;
+      listListener(this.queryParams).then(response => {
+        this.listenerList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      // ids.value = selection.map(item => item.id);
+      // TODO 应该使用 push?
+      this.checkedListenerData = selection;
+      this.listenerSystemChecked = selection.length !== 1;
+    },
+
+    // 保存内置监听器
+    saveSystemListener() {
+      if (this.checkedListenerData.length > 0) {
+        this.checkedListenerData.forEach(value => {
+          // 保存其他配置
+          const listenerObject = createSystemListenerObject(this.modelerStore.moddle, value, true, "flowable");
+          this.bpmnElementListeners.push(listenerObject);
+          this.elementListenersList.push(changeListenerObject(value));
+          this.otherExtensionList = this.modelerStore.element.businessObject?.extensionElements?.values?.filter(ex => ex.$type !== `flowable:TaskListener`) ?? [];
+          updateElementExtensions(this.modelerStore.moddle, this.modelerStore.modeling, this.modelerStore.element, this.otherExtensionList.concat(this.bpmnElementListeners));
+        })
+        // 回传显示数量
+        this.$emit('getTaskListenerCount', this.elementListenersList.length)
+      }
+      this.checkedListenerData = [];
+      this.listenerSystemChecked = true;
+      // 隐藏侧边栏
+      this.listenerSystemVisible = false;
+    }
+  }
+}
+</script>
+
+<style lang="scss">
+@import '../style/process-panel';
+.flow-containers  .el-badge__content.is-fixed {
+  top: 18px;
+}
+.dialog-footer button:first-child {
+  margin-right: 10px;
+}
+</style>

+ 424 - 0
ruoyi-ui/src/components/Process/panel/taskPanel.vue

@@ -0,0 +1,424 @@
+<template>
+  <div>
+    <el-form label-width="80px" size="small">
+      <el-form-item label="异步">
+        <el-switch v-model="bpmnFormData.async" active-text="是" inactive-text="否" @change="updateElementTask('async')"/>
+      </el-form-item>
+      <el-form-item label="用户类型">
+        <el-select v-model="bpmnFormData.userType" placeholder="选择人员" @change="updateUserType">
+          <el-option
+            v-for="item in userTypeOption"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="指定人员" v-if="bpmnFormData.userType === 'assignee'">
+        <el-input-tag v-model="bpmnFormData.assignee" :value="bpmnFormData.assignee"/>
+        <el-button-group class="ml-4" style="margin-top: 4px">
+          <!--指定人员-->
+          <el-tooltip class="box-item" effect="dark" content="指定人员" placement="bottom">
+            <el-button size="mini" type="primary" icon="el-icon-user" @click="singleUserCheck"/>
+          </el-tooltip>
+          <!--选择表达式-->
+          <el-tooltip class="box-item" effect="dark" content="选择表达式" placement="bottom">
+            <el-button size="mini" type="warning" icon="el-icon-postcard" @click="singleExpCheck"/>
+          </el-tooltip>
+        </el-button-group>
+      </el-form-item>
+
+      <el-form-item label="候选人员" v-else-if="bpmnFormData.userType === 'candidateUsers'">
+        <el-input-tag v-model="bpmnFormData.candidateUsers" :value="bpmnFormData.candidateUsers"/>
+        <el-button-group class="ml-4" style="margin-top: 4px">
+          <!--候选人员-->
+          <el-tooltip class="box-item" effect="dark" content="候选人员" placement="bottom">
+            <el-button size="mini" type="primary" icon="el-icon-user" @click="multipleUserCheck"/>
+          </el-tooltip>
+          <!--选择表达式-->
+          <el-tooltip class="box-item" effect="dark" content="选择表达式" placement="bottom">
+            <el-button size="mini" type="warning" icon="el-icon-postcard" @click="singleExpCheck"/>
+          </el-tooltip>
+        </el-button-group>
+      </el-form-item>
+
+      <el-form-item label="候选角色" v-else>
+        <el-input-tag v-model="bpmnFormData.candidateGroups" :value="bpmnFormData.candidateGroups"/>
+        <el-button-group class="ml-4" style="margin-top: 4px">
+          <!--候选角色-->
+          <el-tooltip class="box-item" effect="dark" content="候选角色" placement="bottom">
+            <el-button size="mini" type="primary" icon="el-icon-user"  @click="multipleRoleCheck"/>
+          </el-tooltip>
+          <!--选择表达式-->
+          <el-tooltip class="box-item" effect="dark" content="选择表达式" placement="bottom">
+            <el-button size="mini" type="warning" icon="el-icon-postcard" @click="singleExpCheck"/>
+          </el-tooltip>
+        </el-button-group>
+      </el-form-item>
+
+      <el-form-item label="优先级">
+        <el-input v-model="bpmnFormData.priority" @change="updateElementTask('priority')"/>
+      </el-form-item>
+      <el-form-item label="到期时间">
+        <el-input v-model="bpmnFormData.dueDate" @change="updateElementTask('dueDate')"/>
+      </el-form-item>
+    </el-form>
+
+    <!--选择人员-->
+    <el-dialog
+      title="选择人员"
+      :visible.sync="userVisible"
+      width="60%"
+      :close-on-press-escape="false"
+      :show-close="false"
+    >
+      <flow-user v-if="userVisible" :checkType="checkType" :selectValues="selectData.assignee || selectData.candidateUsers" @handleUserSelect="userSelect"></flow-user>
+      <div slot="footer" class="dialog-footer">
+        <el-button size="small" @click="userVisible = false">取 消</el-button>
+        <el-button size="small" type="primary" @click="checkUserComplete">确 定</el-button>
+      </div>
+    </el-dialog>
+
+    <!--选择角色-->
+    <el-dialog
+      title="选择候选角色"
+      :visible.sync="roleVisible"
+      width="60%"
+      :close-on-press-escape="false"
+      :show-close="false"
+    >
+      <flow-role v-if="roleVisible" :selectValues="selectData.candidateGroups" @handleRoleSelect="roleSelect"></flow-role>
+      <div slot="footer" class="dialog-footer">
+        <el-button size="small" @click="roleVisible = false">取 消</el-button>
+        <el-button size="small" type="primary" @click="checkRoleComplete">确 定</el-button>
+      </div>
+    </el-dialog>
+
+    <!--选择表达式-->
+    <el-dialog
+      title="选择表达式"
+      :visible.sync="expVisible"
+      width="60%"
+      :close-on-press-escape="false"
+      :show-close="false"
+    >
+      <flow-exp v-if="expVisible" :selectValues="selectData.exp" @handleSingleExpSelect="expSelect"></flow-exp>
+      <div slot="footer" class="dialog-footer">
+        <el-button size="small" @click="expVisible = false">取 消</el-button>
+        <el-button size="small" type="primary" @click="checkExpComplete">确 定</el-button>
+      </div>
+    </el-dialog>
+
+
+  </div>
+</template>
+
+<script>
+import FlowUser from '@/components/flow/User'
+import FlowRole from '@/components/flow/Role'
+import FlowExp from '@/components/flow/Expression'
+import ElInputTag from '@/components/flow/ElInputTag'
+import {StrUtil} from '@/utils/StrUtil'
+
+export default {
+  name: "TaskPanel",
+  components: {
+    FlowUser,
+    FlowRole,
+    FlowExp,
+    ElInputTag
+  },
+  /** 组件传值  */
+  props : {
+    id: {
+      type: String,
+      required: true
+    },
+  },
+  data() {
+    return {
+      userVisible: false,
+      roleVisible: false,
+      expVisible: false,
+      isIndeterminate: true,
+      checkType: 'single', // 选类
+      userType: '',
+      userTypeOption: [
+        {label: '指定人员', value: 'assignee'},
+        {label: '候选人员', value: 'candidateUsers'},
+        {label: '候选角色', value: 'candidateGroups'}
+      ],
+      checkAll: false,
+      bpmnFormData: {
+        userType: "",
+        assignee: "",
+        candidateUsers: "",
+        candidateGroups: "",
+        dueDate: "",
+        priority: "",
+        dataType: "",
+        expId: "",
+      },
+      // 数据回显
+      selectData: {
+        assignee: null,
+        candidateUsers: null,
+        candidateGroups: null,
+        exp: null,
+      },
+      otherExtensionList:[],
+    }
+  },
+
+  /** 传值监听 */
+  watch: {
+    id: {
+      handler(newVal) {
+        if (StrUtil.isNotBlank(newVal)) {
+          this.resetTaskForm();
+        }
+      },
+      immediate: true, // 立即生效
+    },
+  },
+  created() {
+
+  },
+  methods: {
+    // 初始化表单
+    resetTaskForm() {
+      // 初始化设为空值
+      this.bpmnFormData = {
+        userType: "",
+        assignee: "",
+        candidateUsers: "",
+        candidateGroups: "",
+        dueDate: "",
+        priority: "",
+        dataType: "",
+        expId: "",
+      }
+      this.selectData = {
+        assignee: null,
+        candidateUsers: null,
+        candidateGroups: null,
+        exp: null,
+      }
+      // 流程节点信息上取值
+      for (let key in this.bpmnFormData) {
+        const value = this.modelerStore.element?.businessObject[key] || this.bpmnFormData[key];
+        this.$set(this.bpmnFormData, key, value);
+      }
+      // 人员信息回显
+      this.checkValuesEcho(this.bpmnFormData);
+    },
+
+    // 更新节点信息
+    updateElementTask(key) {
+      const taskAttr = Object.create(null);
+      taskAttr[key] = this.bpmnFormData[key] || "";
+      this.modelerStore.modeling.updateProperties(this.modelerStore.element, taskAttr);
+    },
+
+    // 更新自定义流程节点/参数信息
+    updateCustomElement(key, value) {
+      const taskAttr = Object.create(null);
+      taskAttr[key] = value;
+      this.modelerStore.modeling.updateProperties(this.modelerStore.element, taskAttr);
+    },
+
+    // 更新人员类型
+    updateUserType(val) {
+      // 删除xml中已选择数据类型节点
+      this.deleteFlowAttar();
+      delete this.modelerStore.element.businessObject[`userType`]
+      // 清除已选人员数据
+      this.bpmnFormData[val] = null;
+      this.selectData = {
+        assignee: null,
+        candidateUsers: null,
+        candidateGroups: null,
+        exp: null,
+      }
+      // 写入userType节点信息到xml
+      this.updateCustomElement('userType', val);
+    },
+
+    // 设计器右侧表单数据回显
+    checkValuesEcho(formData) {
+      if (StrUtil.isNotBlank(formData.expId)) {
+        this.getExpList(formData.expId, formData.userType);
+      } else {
+        if ("candidateGroups" === formData.userType) {
+          this.getRoleList(formData[formData.userType], formData.userType);
+        } else {
+          this.getUserList(formData[formData.userType], formData.userType);
+        }
+      }
+    },
+
+    // 获取表达式信息
+    getExpList(val, key) {
+      if (StrUtil.isNotBlank(val)) {
+        this.bpmnFormData[key] = this.modelerStore.expList?.find(item => item.id.toString() === val).name;
+        this.selectData.exp = this.modelerStore.expList?.find(item => item.id.toString() === val).id;
+      }
+    },
+
+    // 获取人员信息
+    getUserList(val, key) {
+      if (StrUtil.isNotBlank(val)) {
+        const newArr = this.modelerStore.userList?.filter(i => val.toString().split(',').includes(i.userId.toString()))
+        this.bpmnFormData[key] = newArr.map(item => item.nickName).join(',');
+        if ('assignee' === key) {
+          this.selectData[key] = newArr.find(item => item.userId.toString() === val.toString()).userId;
+        } else {
+          this.selectData[key] = newArr.map(item => item.userId);
+        }
+      }
+    },
+
+    // 获取角色信息
+    getRoleList(val, key) {
+      if (StrUtil.isNotBlank(val)) {
+        const newArr = this.modelerStore.roleList?.filter(i => val.split(',').includes(i.roleId.toString()))
+        this.bpmnFormData[key] = newArr.map(item => item.roleName).join(',');
+        if ('assignee' === key) {
+          this.selectData[key] = newArr.find(item => item.roleId.toString() === val).roleId;
+        } else {
+          this.selectData[key] = newArr.map(item => item.roleId);
+        }
+      }
+    },
+
+    // ------ 流程审批人员信息弹出框 start---------
+
+    /*单选人员*/
+    singleUserCheck() {
+      this.userVisible = true;
+      this.checkType = "single";
+    },
+
+    /*多选人员*/
+    multipleUserCheck() {
+      this.userVisible = true;
+      this.checkType = "multiple";
+    },
+
+    /*单选角色*/
+    singleRoleCheck() {
+      this.roleVisible = true;
+      this.checkType = "single";
+    },
+
+    /*多选角色*/
+    multipleRoleCheck() {
+      this.roleVisible = true;
+    },
+
+    /*单选表达式*/
+    singleExpCheck() {
+      this.expVisible = true;
+    },
+
+    // 表达式选中数据
+    expSelect(selection) {
+      if (selection) {
+        this.deleteFlowAttar();
+        this.bpmnFormData[this.bpmnFormData.userType] = selection.name;
+        this.updateCustomElement('dataType', selection.dataType);
+        this.updateCustomElement('expId', selection.id.toString());
+        this.updateCustomElement(this.bpmnFormData.userType, selection.expression);
+        this.handleSelectData("exp", selection.id);
+      }
+    },
+
+    // 用户选中数据 TODO: 后面更改为 点击确认按钮再赋值人员信息
+    userSelect(selection) {
+      if (selection) {
+        this.deleteFlowAttar();
+        this.updateCustomElement('dataType', 'fixed');
+        if (selection instanceof Array) {
+          const userIds = selection.map(item => item.userId);
+          const nickName = selection.map(item => item.nickName);
+          // userType = candidateUsers
+          this.bpmnFormData[this.bpmnFormData.userType] = nickName.join(',');
+          this.updateCustomElement(this.bpmnFormData.userType, userIds.join(','));
+          this.handleSelectData(this.bpmnFormData.userType, userIds);
+        } else {
+          // userType = assignee
+          this.bpmnFormData[this.bpmnFormData.userType] = selection.nickName;
+          this.updateCustomElement(this.bpmnFormData.userType, selection.userId);
+          this.handleSelectData(this.bpmnFormData.userType, selection.userId);
+        }
+      }
+    },
+
+    // 角色选中数据
+    roleSelect(selection, name) {
+      if (selection && name) {
+        this.deleteFlowAttar();
+        this.bpmnFormData[this.bpmnFormData.userType] = name;
+        this.updateCustomElement('dataType', 'fixed');
+        // userType = candidateGroups
+        this.updateCustomElement(this.bpmnFormData.userType, selection);
+        this.handleSelectData(this.bpmnFormData.userType, selection);
+      }
+    },
+
+    // 处理人员回显
+    handleSelectData(key, value) {
+      for (let oldKey in this.selectData) {
+        if (key !== oldKey) {
+          this.$set(this.selectData, oldKey, null);
+        } else {
+          this.$set(this.selectData, oldKey, value);
+        }
+      }
+    },
+
+    /*用户选中赋值*/
+    checkUserComplete() {
+      this.userVisible = false;
+      this.checkType = "";
+    },
+
+    /*候选角色选中赋值*/
+    checkRoleComplete() {
+      this.roleVisible = false;
+    },
+
+    /*表达式选中赋值*/
+    checkExpComplete() {
+      this.expVisible = false;
+    },
+
+    // 删除节点
+    deleteFlowAttar() {
+      delete this.modelerStore.element.businessObject[`dataType`]
+      delete this.modelerStore.element.businessObject[`expId`]
+      delete this.modelerStore.element.businessObject[`assignee`]
+      delete this.modelerStore.element.businessObject[`candidateUsers`]
+      delete this.modelerStore.element.businessObject[`candidateGroups`]
+    },
+
+    // 去重数据
+    unique(arr, code) {
+      const res = new Map();
+      return arr.filter((item) => !res.has(item[code]) && res.set(item[code], 1));
+    },
+
+    // 更新扩展属性信息
+    updateElementExtensions(properties) {
+      const extensions = this.modelerStore.moddle.create("bpmn:ExtensionElements", {
+        values: this.otherExtensionList.concat([properties])
+      });
+
+      this.modelerStore.modeling.updateProperties(this.modelerStore.element, {
+        extensionElements: extensions
+      });
+    }
+  }
+}
+</script>

File diff suppressed because it is too large
+ 183 - 0
ruoyi-ui/src/components/Process/style/flow-viewer.scss


+ 123 - 0
ruoyi-ui/src/components/Process/style/process-panel.scss

@@ -0,0 +1,123 @@
+//.process-panel__container {
+//  box-sizing: border-box;
+//  padding: 0 8px;
+//  border-left: 1px solid #eeeeee;
+//  box-shadow: 0 0 8px #cccccc;
+//  max-height: 100%;
+//  overflow-y: scroll;
+//}
+.panel-tab__title {
+  font-weight: 600;
+  padding: 0 8px;
+  font-size: 1.1em;
+  line-height: 1.2em;
+  i {
+    margin-right: 8px;
+    font-size: 1.2em;
+  }
+}
+.panel-tab__content {
+  width: 100%;
+  box-sizing: border-box;
+  //border-top: 1px solid #eeeeee;
+  padding: 8px 16px;
+  .panel-tab__content--title {
+    display: flex;
+    justify-content: space-between;
+    padding-bottom: 8px;
+    span {
+      flex: 1;
+      text-align: left;
+    }
+  }
+}
+.element-property {
+  width: 100%;
+  display: flex;
+  align-items: flex-start;
+  margin: 8px 0;
+  .element-property__label {
+    display: block;
+    width: 90px;
+    text-align: right;
+    overflow: hidden;
+    padding-right: 12px;
+    line-height: 32px;
+    font-size: 14px;
+    box-sizing: border-box;
+  }
+  .element-property__value {
+    flex: 1;
+    line-height: 32px;
+  }
+  .el-form-item {
+    width: 100%;
+    margin-bottom: 0;
+    padding-bottom: 18px;
+  }
+}
+.list-property {
+  flex-direction: column;
+  .element-listener-item {
+    width: 100%;
+    display: inline-grid;
+    grid-template-columns: 16px auto 32px 32px;
+    grid-column-gap: 8px;
+  }
+  .element-listener-item + .element-listener-item {
+    margin-top: 8px;
+  }
+}
+.listener-filed__title {
+  width: 100%;
+  justify-content: space-between;
+  align-items: center;
+  margin-top: 0;
+  span {
+    font-size: 14px;
+  }
+  i {
+    margin-right: 8px;
+  }
+}
+.element-drawer__button {
+  margin-top: 8px;
+  display: inline-flex;
+  justify-content: space-around;
+}
+.element-drawer__button > .el-button {
+  width: 100%;
+}
+
+.element-drawer__button_save {
+  margin-top: 8px;
+  width: 100%;
+  display: inline-flex;
+  justify-content: space-around;
+}
+.element-drawer__button_save > .el-button {
+  width: 100%;
+}
+
+.el-collapse-item__content {
+  padding-bottom: 0;
+}
+.el-input.is-disabled .el-input__inner {
+  color: #999999;
+}
+.el-form-item.el-form-item--mini {
+  margin-bottom: 0;
+  & + .el-form-item {
+    margin-top: 16px;
+  }
+}
+.el-drawer__header{
+  margin-bottom: 0;
+  border-bottom: 1px solid #e8e8e8;
+  padding: 16px 16px 8px 16px;
+  font-size: 18px;
+  color: #303133;
+}
+.el-drawer__body{
+  padding: 10px;
+}

+ 191 - 0
ruoyi-ui/src/components/flow/ElInputTag/index.vue

@@ -0,0 +1,191 @@
+<template>
+  <div
+      class="el-input-tag input-tag-wrapper"
+      :class="[size ? 'el-input-tag--' + size : '']"
+      @click="focusTagInput">
+    <el-tag
+        v-for="(tag, idx) in innerTags"
+        v-bind="$attrs"
+        :key="idx"
+        :size="size"
+        effect="dark"
+        closable
+        :disable-transitions="false"
+        @close="remove(idx)">
+      {{tag}}
+    </el-tag>
+    <input
+        v-if="!readOnly"
+        class="tag-input"
+        :placeholder="placeholder"
+        @input="inputTag"
+        :value="newTag"
+        @keydown.delete.stop = "removeLastTag"
+    />
+<!--    @keydown = "addNew"
+        @blur = "addNew"-->
+  </div>
+</template>
+
+
+<script>
+import {StrUtil} from '@/utils/StrUtil'
+
+export default {
+  name: "ElInputTag",
+  /** 组件传值  */
+  props : {
+    value: {
+      type: String,
+      default: ""
+    },
+    addTagOnKeys: {
+      type: Array,
+      default: () => []
+    },
+    size: {
+      type: String,
+      default: 'small'
+    },
+    placeholder: String,
+  },
+  data() {
+    return {
+       newTag :"",
+       innerTags :[],
+       readOnly :true,
+    }
+  },
+  /** 传值监听 */
+  watch: {
+    value: {
+      handler(newVal) {
+        if (StrUtil.isNotBlank(newVal)) {
+          this.innerTags = newVal.split(',');
+        }else {
+          this.innerTags = [];
+        }
+      },
+      immediate: true, // 立即生效
+    },
+  },
+  methods: {
+    focusTagInput() {
+      if (this.readOnly || !this.$el.querySelector('.tag-input')) {
+        return
+      } else {
+        this.$el.querySelector('.tag-input').focus()
+      }
+    },
+
+    inputTag(ev) {
+     this.newTag = ev.target.value
+    },
+
+    addNew(e) {
+      if (e && (!this.addTagOnKeys.includes(e.keyCode)) && (e.type !== 'blur')) {
+        return
+      }
+      if (e) {
+        e.stopPropagation()
+        e.preventDefault()
+      }
+      let addSuccess = false
+      if (this.newTag.includes(',')) {
+       this.newTag.split(',').forEach(item => {
+          if (this.addTag(item.trim())) {
+            addSuccess = true
+          }
+        })
+      } else {
+        if (this.addTag(this.newTag.trim())) {
+          addSuccess = true
+        }
+      }
+      if (addSuccess) {
+        this.tagChange()
+       this.newTag = ''
+      }
+    },
+
+    addTag(tag) {
+      tag = tag.trim()
+      if (tag && !this.innerTags.includes(tag)) {
+        this.innerTags.push(tag)
+        return true
+      }
+      return false
+    },
+
+    remove(index) {
+      this.innerTags.splice(index, 1)
+      this.tagChange();
+    },
+
+    removeLastTag() {
+      if (this.newTag) {
+        return
+      }
+      this.innerTags.pop()
+      this.tagChange()
+    },
+
+    tagChange() {
+      this.$emit('input', this.innerTags)
+    }
+  }
+}
+
+</script>
+
+<style scoped>
+.el-form-item.is-error .el-input-tag {
+  border-color: #f56c6c;
+}
+.input-tag-wrapper {
+  position: relative;
+  font-size: 14px;
+  background-color: #fff;
+  background-image: none;
+  border-radius: 4px;
+  border: 1px solid #dcdfe6;
+  box-sizing: border-box;
+  color: #606266;
+  display: inline-block;
+  outline: none;
+  padding: 0 10px 0 5px;
+  transition: border-color .2s cubic-bezier(.645,.045,.355,1);
+  width: 100%;
+}
+.el-tag {
+  margin-right: 4px;
+}
+
+.tag-input {
+  background: transparent;
+  border: 0;
+  font-size: inherit;
+  outline: none;
+  padding-left: 0;
+  width: 100px;
+}
+.el-input-tag {
+  min-height: 42px;
+}
+.el-input-tag--small {
+  min-height: 32px;
+  line-height: 32px;
+  font-size: 12px;
+}
+
+.el-input-tag--default {
+  min-height: 34px;
+  line-height: 34px;
+}
+
+.el-input-tag--large {
+  min-height: 36px;
+  line-height: 36px;
+}
+
+</style>