Browse Source

feat: 重构自定义配置,支持对不同类型节点绩效配置

Lesan 8 tháng trước cách đây
mục cha
commit
3c7526de2f

+ 2 - 2
src/components/bpmnProcessDesigner/package/penal/PropertiesPanel.vue

@@ -54,8 +54,8 @@
         <template #title><Icon icon="ep:promotion" />其他</template>
         <element-other-config :id="elementId" />
       </el-collapse-item>
-      <el-collapse-item name="customConfig" v-if="elementType.indexOf('Task') !== -1" key="customConfig">
-        <template #title><Icon icon="ep:circle-plus-filled" />自定义配置</template>
+      <el-collapse-item name="customConfig" key="customConfig">
+        <template #title><Icon icon="ep:tools" />自定义配置</template>
         <element-custom-config :id="elementId" :type="elementType" />
       </el-collapse-item>
     </el-collapse>

+ 20 - 635
src/components/bpmnProcessDesigner/package/penal/custom-config/ElementCustomConfig.vue

@@ -1,656 +1,41 @@
-<!-- UserTask 自定义配置:
-     1. 审批人与提交人为同一人时
-     2. 审批人拒绝时
-     3. 审批人为空时
-     4. 操作按钮
-     5. 字段权限
--->
 <template>
   <div class="panel-tab__content">
-    <el-divider content-position="left">审批人拒绝时</el-divider>
-    <el-form-item prop="rejectHandlerType">
-      <el-radio-group
-        v-model="rejectHandlerType"
-        :disabled="returnTaskList.length === 0"
-        @change="updateRejectHandlerType"
-      >
-        <div class="flex-col">
-          <div v-for="(item, index) in REJECT_HANDLER_TYPES" :key="index">
-            <el-radio :key="item.value" :value="item.value" :label="item.label" />
-          </div>
-        </div>
-      </el-radio-group>
-    </el-form-item>
-    <el-form-item
-      v-if="rejectHandlerType == RejectHandlerType.RETURN_USER_TASK"
-      label="驳回节点"
-      prop="returnNodeId"
-    >
-      <el-select v-model="returnNodeId" clearable style="width: 100%" @change="updateReturnNodeId">
-        <el-option
-          v-for="item in returnTaskList"
-          :key="item.id"
-          :label="item.name"
-          :value="item.id"
-        />
-      </el-select>
-    </el-form-item>
-
-    <el-divider content-position="left">审批人为空时</el-divider>
-    <el-form-item prop="assignEmptyHandlerType">
-      <el-radio-group v-model="assignEmptyHandlerType" @change="updateAssignEmptyHandlerType">
-        <div class="flex-col">
-          <div v-for="(item, index) in ASSIGN_EMPTY_HANDLER_TYPES" :key="index">
-            <el-radio :key="item.value" :value="item.value" :label="item.label" />
-          </div>
-        </div>
-      </el-radio-group>
-    </el-form-item>
-    <el-form-item
-      v-if="assignEmptyHandlerType == AssignEmptyHandlerType.ASSIGN_USER"
-      label="指定用户"
-      prop="assignEmptyHandlerUserIds"
-      span="24"
-    >
-      <el-select
-        v-model="assignEmptyUserIds"
-        clearable
-        multiple
-        style="width: 100%"
-        @change="updateAssignEmptyUserIds"
-      >
-        <el-option
-          v-for="item in userOptions"
-          :key="item.id"
-          :label="item.nickname"
-          :value="item.id"
-        />
-      </el-select>
-    </el-form-item>
-
-    <el-divider content-position="left">审批人与提交人为同一人时</el-divider>
-    <el-radio-group v-model="assignStartUserHandlerType" @change="updateAssignStartUserHandlerType">
-      <div class="flex-col">
-        <div v-for="(item, index) in ASSIGN_START_USER_HANDLER_TYPES" :key="index">
-          <el-radio :key="item.value" :value="item.value" :label="item.label" />
-        </div>
-      </div>
-    </el-radio-group>
-
-    <el-divider content-position="left">操作按钮</el-divider>
-    <div class="button-setting-pane">
-      <div class="button-setting-title">
-        <div class="button-title-label">操作按钮</div>
-        <div class="pl-4 button-title-label">显示名称</div>
-        <div class="button-title-label">启用</div>
-      </div>
-      <div class="button-setting-item" v-for="(item, index) in buttonsSettingEl" :key="index">
-        <div class="button-setting-item-label"> {{ OPERATION_BUTTON_NAME.get(item.id) }} </div>
-        <div class="button-setting-item-label">
-          <input
-            type="text"
-            class="editable-title-input"
-            @blur="btnDisplayNameBlurEvent(index)"
-            v-mountedFocus
-            v-model="item.displayName"
-            :placeholder="item.displayName"
-            v-if="btnDisplayNameEdit[index]"
-          />
-          <el-button v-else text @click="changeBtnDisplayName(index)"
-            >{{ item.displayName }} &nbsp;<Icon icon="ep:edit"
-          /></el-button>
-        </div>
-        <div class="button-setting-item-label">
-          <el-switch v-model="item.enable" />
-        </div>
-      </div>
-    </div>
-
-    <el-divider content-position="left">字段权限</el-divider>
-    <div class="field-setting-pane" v-if="formType === 10">
-      <div class="field-permit-title">
-        <div class="setting-title-label first-title"> 字段名称 </div>
-        <div class="other-titles">
-          <span class="setting-title-label">只读</span>
-          <span class="setting-title-label">可编辑</span>
-          <span class="setting-title-label">隐藏</span>
-        </div>
-      </div>
-      <div class="field-setting-item" v-for="(item, index) in fieldsPermissionEl" :key="index">
-        <div class="field-setting-item-label"> {{ item.title }} </div>
-        <el-radio-group class="field-setting-item-group" v-model="item.permission">
-          <div class="item-radio-wrap">
-            <el-radio
-              :value="FieldPermissionType.READ"
-              size="large"
-              :label="FieldPermissionType.READ"
-              ><span></span
-            ></el-radio>
-          </div>
-          <div class="item-radio-wrap">
-            <el-radio
-              :value="FieldPermissionType.WRITE"
-              size="large"
-              :label="FieldPermissionType.WRITE"
-              ><span></span
-            ></el-radio>
-          </div>
-          <div class="item-radio-wrap">
-            <el-radio
-              :value="FieldPermissionType.NONE"
-              size="large"
-              :label="FieldPermissionType.NONE"
-              ><span></span
-            ></el-radio>
-          </div>
-        </el-radio-group>
-      </div>
-    </div>
+    <component :is="customConfigComponent" v-bind="$props" />
   </div>
 </template>
 
 <script lang="ts" setup>
-import {
-  ASSIGN_START_USER_HANDLER_TYPES,
-  RejectHandlerType,
-  REJECT_HANDLER_TYPES,
-  ASSIGN_EMPTY_HANDLER_TYPES,
-  AssignEmptyHandlerType,
-  OPERATION_BUTTON_NAME,
-  DEFAULT_BUTTON_SETTING,
-  FieldPermissionType
-} from '@/components/SimpleProcessDesignerV2/src/consts'
-import * as UserApi from '@/api/system/user'
-import { cloneDeep } from 'lodash-es'
+import { CustomConfigMap } from './data'
 
 defineOptions({ name: 'ElementCustomConfig' })
+
 const props = defineProps({
   id: String,
   type: String
 })
-const prefix = inject('prefix')
-
-// 审批人与提交人为同一人时
-const assignStartUserHandlerTypeEl = ref()
-const assignStartUserHandlerType = ref()
 
-// 审批人拒绝时
-const rejectHandlerTypeEl = ref()
-const rejectHandlerType = ref()
-const returnNodeIdEl = ref()
-const returnNodeId = ref()
-const returnTaskList = ref([])
-
-// 审批人为空时
-const assignEmptyHandlerTypeEl = ref()
-const assignEmptyHandlerType = ref()
-const assignEmptyUserIdsEl = ref()
-const assignEmptyUserIds = ref()
-
-// 操作按钮
-const buttonsSettingEl = ref()
-const { buttonsSetting, btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } =
-  useButtonsSetting()
-
-// 字段权限
-const fieldsPermissionEl = ref()
-const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission(
-  FieldPermissionType.READ
-)
-
-const elExtensionElements = ref()
-const otherExtensions = ref()
-const bpmnElement = ref()
 const bpmnInstances = () => (window as any)?.bpmnInstances
-
-const resetCustomConfigList = () => {
-  bpmnElement.value = bpmnInstances().bpmnElement
-
-  // 获取可回退的列表
-  returnTaskList.value = findAllPredecessorsExcludingStart(
-    bpmnElement.value.id,
-    bpmnInstances().modeler
-  )
-
-  // 获取元素扩展属性 或者 创建扩展属性
-  elExtensionElements.value =
-    bpmnElement.value.businessObject?.extensionElements ??
-    bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] })
-
-  // 审批人与提交人为同一人时
-  assignStartUserHandlerTypeEl.value =
-    elExtensionElements.value.values?.filter(
-      (ex) => ex.$type === `${prefix}:AssignStartUserHandlerType`
-    )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignStartUserHandlerType`, { value: 1 })
-  assignStartUserHandlerType.value = assignStartUserHandlerTypeEl.value.value
-
-  // 审批人拒绝时
-  rejectHandlerTypeEl.value =
-    elExtensionElements.value.values?.filter(
-      (ex) => ex.$type === `${prefix}:RejectHandlerType`
-    )?.[0] || bpmnInstances().moddle.create(`${prefix}:RejectHandlerType`, { value: 1 })
-  rejectHandlerType.value = rejectHandlerTypeEl.value.value
-  returnNodeIdEl.value =
-    elExtensionElements.value.values?.filter(
-      (ex) => ex.$type === `${prefix}:RejectReturnTaskId`
-    )?.[0] || bpmnInstances().moddle.create(`${prefix}:RejectReturnTaskId`, { value: '' })
-  returnNodeId.value = returnNodeIdEl.value.value
-
-  // 审批人为空时
-  assignEmptyHandlerTypeEl.value =
-    elExtensionElements.value.values?.filter(
-      (ex) => ex.$type === `${prefix}:AssignEmptyHandlerType`
-    )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignEmptyHandlerType`, { value: 1 })
-  assignEmptyHandlerType.value = assignEmptyHandlerTypeEl.value.value
-  assignEmptyUserIdsEl.value =
-    elExtensionElements.value.values?.filter(
-      (ex) => ex.$type === `${prefix}:AssignEmptyUserIds`
-    )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignEmptyUserIds`, { value: '' })
-  assignEmptyUserIds.value = assignEmptyUserIdsEl.value.value.split(',').map((item) => {
-    // 如果数字超出了最大安全整数范围,则将其作为字符串处理
-    let num = Number(item)
-    return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER ? item : num
-  })
-
-  // 操作按钮
-  buttonsSettingEl.value = elExtensionElements.value.values?.filter(
-    (ex) => ex.$type === `${prefix}:ButtonsSetting`
-  )
-  if (buttonsSettingEl.value.length === 0) {
-    DEFAULT_BUTTON_SETTING.forEach((item) => {
-      buttonsSettingEl.value.push(
-        bpmnInstances().moddle.create(`${prefix}:ButtonsSetting`, {
-          'flowable:id': item.id,
-          'flowable:displayName': item.displayName,
-          'flowable:enable': item.enable
-        })
-      )
-    })
-  }
-
-  // 字段权限
-  if (formType.value === 10) {
-    fieldsPermissionEl.value = elExtensionElements.value.values?.filter(
-      (ex) => ex.$type === `${prefix}:FieldsPermission`
-    )
-    if (fieldsPermissionEl.value.length === 0) {
-      getNodeConfigFormFields()
-      fieldsPermissionConfig.value.forEach((el) => {
-        fieldsPermissionEl.value.push(
-          bpmnInstances().moddle.create(`${prefix}:FieldsPermission`, el)
-        )
-      })
-    }
-  }
-
-  // 保留剩余扩展元素,便于后面更新该元素对应属性
-  otherExtensions.value =
-    elExtensionElements.value.values?.filter(
-      (ex) =>
-        ex.$type !== `${prefix}:AssignStartUserHandlerType` &&
-        ex.$type !== `${prefix}:RejectHandlerType` &&
-        ex.$type !== `${prefix}:RejectReturnTaskId` &&
-        ex.$type !== `${prefix}:AssignEmptyHandlerType` &&
-        ex.$type !== `${prefix}:AssignEmptyUserIds` &&
-        ex.$type !== `${prefix}:ButtonsSetting` &&
-        ex.$type !== `${prefix}:FieldsPermission`
-    ) ?? []
-
-  // 更新元素扩展属性,避免后续报错
-  updateElementExtensions()
-}
-
-const updateAssignStartUserHandlerType = () => {
-  assignStartUserHandlerTypeEl.value.value = assignStartUserHandlerType.value
-
-  updateElementExtensions()
-}
-
-const updateRejectHandlerType = () => {
-  rejectHandlerTypeEl.value.value = rejectHandlerType.value
-
-  returnNodeId.value = returnTaskList.value[0].id
-  returnNodeIdEl.value.value = returnNodeId.value
-
-  updateElementExtensions()
-}
-
-const updateReturnNodeId = () => {
-  returnNodeIdEl.value.value = returnNodeId.value
-
-  updateElementExtensions()
-}
-
-const updateAssignEmptyHandlerType = () => {
-  assignEmptyHandlerTypeEl.value.value = assignEmptyHandlerType.value
-
-  updateElementExtensions()
-}
-
-const updateAssignEmptyUserIds = () => {
-  assignEmptyUserIdsEl.value.value = assignEmptyUserIds.value.toString()
-
-  updateElementExtensions()
-}
-
-const updateElementExtensions = () => {
-  const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
-    values: [
-      ...otherExtensions.value,
-      assignStartUserHandlerTypeEl.value,
-      rejectHandlerTypeEl.value,
-      returnNodeIdEl.value,
-      assignEmptyHandlerTypeEl.value,
-      assignEmptyUserIdsEl.value,
-      ...buttonsSettingEl.value,
-      ...fieldsPermissionEl.value
-    ]
-  })
-  bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
-    extensionElements: extensions
-  })
-}
+const customConfigComponent = ref<any>(null)
 
 watch(
-  () => props.id,
-  (val) => {
-    val &&
-      val.length &&
-      nextTick(() => {
-        resetCustomConfigList()
-      })
-  },
-  { immediate: true }
-)
-
-function findAllPredecessorsExcludingStart(elementId, modeler) {
-  const elementRegistry = modeler.get('elementRegistry')
-  const allConnections = elementRegistry.filter((element) => element.type === 'bpmn:SequenceFlow')
-  const predecessors = new Set() // 使用 Set 来避免重复节点
-  const visited = new Set() // 用于记录已访问的节点
-
-  // 检查是否是开始事件节点
-  function isStartEvent(element) {
-    return element.type === 'bpmn:StartEvent'
-  }
-
-  function findPredecessorsRecursively(element) {
-    // 如果该节点已经访问过,直接返回,避免循环
-    if (visited.has(element)) {
-      return
-    }
-
-    // 标记当前节点为已访问
-    visited.add(element)
-
-    // 获取与当前节点相连的所有连接
-    const incomingConnections = allConnections.filter((connection) => connection.target === element)
-
-    incomingConnections.forEach((connection) => {
-      const source = connection.source // 获取前置节点
-
-      // 只添加不是开始事件的前置节点
-      if (!isStartEvent(source)) {
-        predecessors.add(source.businessObject)
-        // 递归查找前置节点
-        findPredecessorsRecursively(source)
+  () => props.type,
+  () => {
+    if (props.type) {
+      const element = bpmnInstances().bpmnElement.businessObject
+      let elementType = props.type
+      if (element.eventDefinitions) {
+        // 处理类似共用BoundaryEvent类型的TimerEvent
+        elementType += element.eventDefinitions[0].$type.split(':')[1]
       }
-    })
-  }
-
-  const targetElement = elementRegistry.get(elementId)
-  if (targetElement) {
-    findPredecessorsRecursively(targetElement)
-  }
-
-  return Array.from(predecessors) // 返回前置节点数组
-}
-
-function useButtonsSetting() {
-  const buttonsSetting = ref<ButtonSetting[]>()
-  // 操作按钮显示名称可编辑
-  const btnDisplayNameEdit = ref<boolean[]>([])
-  const changeBtnDisplayName = (index: number) => {
-    btnDisplayNameEdit.value[index] = true
-  }
-  const btnDisplayNameBlurEvent = (index: number) => {
-    btnDisplayNameEdit.value[index] = false
-    const buttonItem = buttonsSetting.value![index]
-    buttonItem.displayName = buttonItem.displayName || OPERATION_BUTTON_NAME.get(buttonItem.id)!
-  }
-  return {
-    buttonsSetting,
-    btnDisplayNameEdit,
-    changeBtnDisplayName,
-    btnDisplayNameBlurEvent
-  }
-}
-
-// 表单字段权限设置
-function useFormFieldsPermission(defaultPermission) {
-  // 字段权限配置. 需要有 field, title,  permissioin 属性
-  const fieldsPermissionConfig = ref<Array<Record<string, string>>>([])
-
-  const formType = inject<Ref<number>>('formType') // 表单类型
-
-  const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
-
-  const getNodeConfigFormFields = (nodeFormFields?: Array<Record<string, string>>) => {
-    nodeFormFields = toRaw(nodeFormFields)
-    fieldsPermissionConfig.value =
-      cloneDeep(nodeFormFields) || getDefaultFieldsPermission(unref(formFields))
-  }
-  // 默认的表单权限: 获取表单的所有字段,设置字段默认权限为只读
-  const getDefaultFieldsPermission = (formFields?: string[]) => {
-    const defaultFieldsPermission: Array<Record<string, string>> = []
-    if (formFields) {
-      formFields.forEach((fieldStr: string) => {
-        parseFieldsSetDefaultPermission(JSON.parse(fieldStr), defaultFieldsPermission)
-      })
-    }
-    return defaultFieldsPermission
-  }
-  // 解析字段。赋给默认权限
-  const parseFieldsSetDefaultPermission = (
-    rule: Record<string, any>,
-    fieldsPermission: Array<Record<string, string>>,
-    parentTitle: string = ''
-  ) => {
-    const { /**type,*/ field, title: tempTitle, children } = rule
-    if (field && tempTitle) {
-      let title = tempTitle
-      if (parentTitle) {
-        title = `${parentTitle}.${tempTitle}`
+      const config = CustomConfigMap[elementType]
+      if (config) {
+        customConfigComponent.value = config.componet
+        return
       }
-      fieldsPermission.push({
-        field,
-        title,
-        permission: defaultPermission
-      })
-      // TODO 子表单 需要处理子表单字段
-      // if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) {
-      //   // 解析子表单的字段
-      //   rule.props.rule.forEach((item) => {
-      //     parseFieldsSetDefaultPermission(item, fieldsPermission, title)
-      //   })
-      // }
-    }
-    if (children && Array.isArray(children)) {
-      children.forEach((rule) => {
-        parseFieldsSetDefaultPermission(rule, fieldsPermission)
-      })
     }
-  }
-
-  return {
-    formType,
-    fieldsPermissionConfig,
-    getNodeConfigFormFields
-  }
-}
-
-const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
-onMounted(async () => {
-  // 获得用户列表
-  userOptions.value = await UserApi.getSimpleUserList()
-})
+  },
+  { immediate: true }
+)
 </script>
 
-<style lang="scss" scoped>
-.button-setting-pane {
-  display: flex;
-  flex-direction: column;
-  font-size: 14px;
-  margin-top: 8px;
-
-  .button-setting-desc {
-    padding-right: 8px;
-    margin-bottom: 16px;
-    font-size: 16px;
-    font-weight: 700;
-  }
-
-  .button-setting-title {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    height: 45px;
-    padding-left: 12px;
-    background-color: #f8fafc0a;
-    border: 1px solid #1f38581a;
-
-    & > :first-child {
-      width: 100px !important;
-      text-align: left !important;
-    }
-
-    & > :last-child {
-      text-align: center !important;
-    }
-
-    .button-title-label {
-      width: 150px;
-      font-size: 13px;
-      font-weight: 700;
-      color: #000;
-      text-align: left;
-    }
-  }
-
-  .button-setting-item {
-    align-items: center;
-    display: flex;
-    justify-content: space-between;
-    height: 38px;
-    padding-left: 12px;
-    border: 1px solid #1f38581a;
-    border-top: 0;
-
-    & > :first-child {
-      width: 100px !important;
-    }
-
-    & > :last-child {
-      text-align: center !important;
-    }
-
-    .button-setting-item-label {
-      width: 150px;
-      overflow: hidden;
-      text-align: left;
-      text-overflow: ellipsis;
-      white-space: nowrap;
-    }
-
-    .editable-title-input {
-      height: 24px;
-      max-width: 130px;
-      margin-left: 4px;
-      line-height: 24px;
-      border: 1px solid #d9d9d9;
-      border-radius: 4px;
-      transition: all 0.3s;
-
-      &:focus {
-        border-color: #40a9ff;
-        outline: 0;
-        box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
-      }
-    }
-  }
-}
-
-.field-setting-pane {
-  display: flex;
-  flex-direction: column;
-  font-size: 14px;
-
-  .field-setting-desc {
-    padding-right: 8px;
-    margin-bottom: 16px;
-    font-size: 16px;
-    font-weight: 700;
-  }
-
-  .field-permit-title {
-    display: flex;
-    justify-content: space-between;
-    align-items: center;
-    height: 45px;
-    padding-left: 12px;
-    line-height: 45px;
-    background-color: #f8fafc0a;
-    border: 1px solid #1f38581a;
-
-    .first-title {
-      text-align: left !important;
-    }
-
-    .other-titles {
-      display: flex;
-      justify-content: space-between;
-    }
-
-    .setting-title-label {
-      display: inline-block;
-      width: 100px;
-      padding: 5px 0;
-      font-size: 13px;
-      font-weight: 700;
-      color: #000;
-      text-align: center;
-    }
-  }
-
-  .field-setting-item {
-    align-items: center;
-    display: flex;
-    justify-content: space-between;
-    height: 38px;
-    padding-left: 12px;
-    border: 1px solid #1f38581a;
-    border-top: 0;
-
-    .field-setting-item-label {
-      display: inline-block;
-      width: 100px;
-      min-height: 16px;
-      overflow: hidden;
-      text-overflow: ellipsis;
-      white-space: nowrap;
-      cursor: text;
-    }
-
-    .field-setting-item-group {
-      display: flex;
-      justify-content: space-between;
-
-      .item-radio-wrap {
-        display: inline-block;
-        width: 100px;
-        text-align: center;
-      }
-    }
-  }
-}
-</style>
+<style lang="scss" scoped></style>

+ 13 - 0
src/components/bpmnProcessDesigner/package/penal/custom-config/components/BoundaryEventTimer.vue

@@ -0,0 +1,13 @@
+<template>
+  <div>
+    定时边界事件(非中断)
+  </div>
+</template>
+
+<script lang="ts" setup>
+// TODO 未来继续完善
+defineOptions({ name: 'ElementCustomConfig4BoundaryEventTimer' })
+</script>
+
+<style lang="scss" scoped>
+</style>

+ 656 - 0
src/components/bpmnProcessDesigner/package/penal/custom-config/components/UserTask.vue

@@ -0,0 +1,656 @@
+<!-- UserTask 自定义配置:
+     1. 审批人与提交人为同一人时
+     2. 审批人拒绝时
+     3. 审批人为空时
+     4. 操作按钮
+     5. 字段权限
+-->
+<template>
+  <div>
+    <el-divider content-position="left">审批人拒绝时</el-divider>
+    <el-form-item prop="rejectHandlerType">
+      <el-radio-group
+        v-model="rejectHandlerType"
+        :disabled="returnTaskList.length === 0"
+        @change="updateRejectHandlerType"
+      >
+        <div class="flex-col">
+          <div v-for="(item, index) in REJECT_HANDLER_TYPES" :key="index">
+            <el-radio :key="item.value" :value="item.value" :label="item.label" />
+          </div>
+        </div>
+      </el-radio-group>
+    </el-form-item>
+    <el-form-item
+      v-if="rejectHandlerType == RejectHandlerType.RETURN_USER_TASK"
+      label="驳回节点"
+      prop="returnNodeId"
+    >
+      <el-select v-model="returnNodeId" clearable style="width: 100%" @change="updateReturnNodeId">
+        <el-option
+          v-for="item in returnTaskList"
+          :key="item.id"
+          :label="item.name"
+          :value="item.id"
+        />
+      </el-select>
+    </el-form-item>
+
+    <el-divider content-position="left">审批人为空时</el-divider>
+    <el-form-item prop="assignEmptyHandlerType">
+      <el-radio-group v-model="assignEmptyHandlerType" @change="updateAssignEmptyHandlerType">
+        <div class="flex-col">
+          <div v-for="(item, index) in ASSIGN_EMPTY_HANDLER_TYPES" :key="index">
+            <el-radio :key="item.value" :value="item.value" :label="item.label" />
+          </div>
+        </div>
+      </el-radio-group>
+    </el-form-item>
+    <el-form-item
+      v-if="assignEmptyHandlerType == AssignEmptyHandlerType.ASSIGN_USER"
+      label="指定用户"
+      prop="assignEmptyHandlerUserIds"
+      span="24"
+    >
+      <el-select
+        v-model="assignEmptyUserIds"
+        clearable
+        multiple
+        style="width: 100%"
+        @change="updateAssignEmptyUserIds"
+      >
+        <el-option
+          v-for="item in userOptions"
+          :key="item.id"
+          :label="item.nickname"
+          :value="item.id"
+        />
+      </el-select>
+    </el-form-item>
+
+    <el-divider content-position="left">审批人与提交人为同一人时</el-divider>
+    <el-radio-group v-model="assignStartUserHandlerType" @change="updateAssignStartUserHandlerType">
+      <div class="flex-col">
+        <div v-for="(item, index) in ASSIGN_START_USER_HANDLER_TYPES" :key="index">
+          <el-radio :key="item.value" :value="item.value" :label="item.label" />
+        </div>
+      </div>
+    </el-radio-group>
+
+    <el-divider content-position="left">操作按钮</el-divider>
+    <div class="button-setting-pane">
+      <div class="button-setting-title">
+        <div class="button-title-label">操作按钮</div>
+        <div class="pl-4 button-title-label">显示名称</div>
+        <div class="button-title-label">启用</div>
+      </div>
+      <div class="button-setting-item" v-for="(item, index) in buttonsSettingEl" :key="index">
+        <div class="button-setting-item-label"> {{ OPERATION_BUTTON_NAME.get(item.id) }} </div>
+        <div class="button-setting-item-label">
+          <input
+            type="text"
+            class="editable-title-input"
+            @blur="btnDisplayNameBlurEvent(index)"
+            v-mountedFocus
+            v-model="item.displayName"
+            :placeholder="item.displayName"
+            v-if="btnDisplayNameEdit[index]"
+          />
+          <el-button v-else text @click="changeBtnDisplayName(index)"
+            >{{ item.displayName }} &nbsp;<Icon icon="ep:edit"
+          /></el-button>
+        </div>
+        <div class="button-setting-item-label">
+          <el-switch v-model="item.enable" />
+        </div>
+      </div>
+    </div>
+
+    <el-divider content-position="left">字段权限</el-divider>
+    <div class="field-setting-pane" v-if="formType === 10">
+      <div class="field-permit-title">
+        <div class="setting-title-label first-title"> 字段名称 </div>
+        <div class="other-titles">
+          <span class="setting-title-label">只读</span>
+          <span class="setting-title-label">可编辑</span>
+          <span class="setting-title-label">隐藏</span>
+        </div>
+      </div>
+      <div class="field-setting-item" v-for="(item, index) in fieldsPermissionEl" :key="index">
+        <div class="field-setting-item-label"> {{ item.title }} </div>
+        <el-radio-group class="field-setting-item-group" v-model="item.permission">
+          <div class="item-radio-wrap">
+            <el-radio
+              :value="FieldPermissionType.READ"
+              size="large"
+              :label="FieldPermissionType.READ"
+              ><span></span
+            ></el-radio>
+          </div>
+          <div class="item-radio-wrap">
+            <el-radio
+              :value="FieldPermissionType.WRITE"
+              size="large"
+              :label="FieldPermissionType.WRITE"
+              ><span></span
+            ></el-radio>
+          </div>
+          <div class="item-radio-wrap">
+            <el-radio
+              :value="FieldPermissionType.NONE"
+              size="large"
+              :label="FieldPermissionType.NONE"
+              ><span></span
+            ></el-radio>
+          </div>
+        </el-radio-group>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+import {
+  ASSIGN_START_USER_HANDLER_TYPES,
+  RejectHandlerType,
+  REJECT_HANDLER_TYPES,
+  ASSIGN_EMPTY_HANDLER_TYPES,
+  AssignEmptyHandlerType,
+  OPERATION_BUTTON_NAME,
+  DEFAULT_BUTTON_SETTING,
+  FieldPermissionType
+} from '@/components/SimpleProcessDesignerV2/src/consts'
+import * as UserApi from '@/api/system/user'
+import { cloneDeep } from 'lodash-es'
+
+defineOptions({ name: 'ElementCustomConfig4UserTask' })
+const props = defineProps({
+  id: String,
+  type: String
+})
+const prefix = inject('prefix')
+
+// 审批人与提交人为同一人时
+const assignStartUserHandlerTypeEl = ref()
+const assignStartUserHandlerType = ref()
+
+// 审批人拒绝时
+const rejectHandlerTypeEl = ref()
+const rejectHandlerType = ref()
+const returnNodeIdEl = ref()
+const returnNodeId = ref()
+const returnTaskList = ref([])
+
+// 审批人为空时
+const assignEmptyHandlerTypeEl = ref()
+const assignEmptyHandlerType = ref()
+const assignEmptyUserIdsEl = ref()
+const assignEmptyUserIds = ref()
+
+// 操作按钮
+const buttonsSettingEl = ref()
+const { buttonsSetting, btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } =
+  useButtonsSetting()
+
+// 字段权限
+const fieldsPermissionEl = ref()
+const { formType, fieldsPermissionConfig, getNodeConfigFormFields } = useFormFieldsPermission(
+  FieldPermissionType.READ
+)
+
+const elExtensionElements = ref()
+const otherExtensions = ref()
+const bpmnElement = ref()
+const bpmnInstances = () => (window as any)?.bpmnInstances
+
+const resetCustomConfigList = () => {
+  bpmnElement.value = bpmnInstances().bpmnElement
+
+  // 获取可回退的列表
+  returnTaskList.value = findAllPredecessorsExcludingStart(
+    bpmnElement.value.id,
+    bpmnInstances().modeler
+  )
+
+  // 获取元素扩展属性 或者 创建扩展属性
+  elExtensionElements.value =
+    bpmnElement.value.businessObject?.extensionElements ??
+    bpmnInstances().moddle.create('bpmn:ExtensionElements', { values: [] })
+
+  // 审批人与提交人为同一人时
+  assignStartUserHandlerTypeEl.value =
+    elExtensionElements.value.values?.filter(
+      (ex) => ex.$type === `${prefix}:AssignStartUserHandlerType`
+    )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignStartUserHandlerType`, { value: 1 })
+  assignStartUserHandlerType.value = assignStartUserHandlerTypeEl.value.value
+
+  // 审批人拒绝时
+  rejectHandlerTypeEl.value =
+    elExtensionElements.value.values?.filter(
+      (ex) => ex.$type === `${prefix}:RejectHandlerType`
+    )?.[0] || bpmnInstances().moddle.create(`${prefix}:RejectHandlerType`, { value: 1 })
+  rejectHandlerType.value = rejectHandlerTypeEl.value.value
+  returnNodeIdEl.value =
+    elExtensionElements.value.values?.filter(
+      (ex) => ex.$type === `${prefix}:RejectReturnTaskId`
+    )?.[0] || bpmnInstances().moddle.create(`${prefix}:RejectReturnTaskId`, { value: '' })
+  returnNodeId.value = returnNodeIdEl.value.value
+
+  // 审批人为空时
+  assignEmptyHandlerTypeEl.value =
+    elExtensionElements.value.values?.filter(
+      (ex) => ex.$type === `${prefix}:AssignEmptyHandlerType`
+    )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignEmptyHandlerType`, { value: 1 })
+  assignEmptyHandlerType.value = assignEmptyHandlerTypeEl.value.value
+  assignEmptyUserIdsEl.value =
+    elExtensionElements.value.values?.filter(
+      (ex) => ex.$type === `${prefix}:AssignEmptyUserIds`
+    )?.[0] || bpmnInstances().moddle.create(`${prefix}:AssignEmptyUserIds`, { value: '' })
+  assignEmptyUserIds.value = assignEmptyUserIdsEl.value.value.split(',').map((item) => {
+    // 如果数字超出了最大安全整数范围,则将其作为字符串处理
+    let num = Number(item)
+    return num > Number.MAX_SAFE_INTEGER || num < -Number.MAX_SAFE_INTEGER ? item : num
+  })
+
+  // 操作按钮
+  buttonsSettingEl.value = elExtensionElements.value.values?.filter(
+    (ex) => ex.$type === `${prefix}:ButtonsSetting`
+  )
+  if (buttonsSettingEl.value.length === 0) {
+    DEFAULT_BUTTON_SETTING.forEach((item) => {
+      buttonsSettingEl.value.push(
+        bpmnInstances().moddle.create(`${prefix}:ButtonsSetting`, {
+          'flowable:id': item.id,
+          'flowable:displayName': item.displayName,
+          'flowable:enable': item.enable
+        })
+      )
+    })
+  }
+
+  // 字段权限
+  if (formType.value === 10) {
+    fieldsPermissionEl.value = elExtensionElements.value.values?.filter(
+      (ex) => ex.$type === `${prefix}:FieldsPermission`
+    )
+    if (fieldsPermissionEl.value.length === 0) {
+      getNodeConfigFormFields()
+      fieldsPermissionConfig.value.forEach((el) => {
+        fieldsPermissionEl.value.push(
+          bpmnInstances().moddle.create(`${prefix}:FieldsPermission`, el)
+        )
+      })
+    }
+  }
+
+  // 保留剩余扩展元素,便于后面更新该元素对应属性
+  otherExtensions.value =
+    elExtensionElements.value.values?.filter(
+      (ex) =>
+        ex.$type !== `${prefix}:AssignStartUserHandlerType` &&
+        ex.$type !== `${prefix}:RejectHandlerType` &&
+        ex.$type !== `${prefix}:RejectReturnTaskId` &&
+        ex.$type !== `${prefix}:AssignEmptyHandlerType` &&
+        ex.$type !== `${prefix}:AssignEmptyUserIds` &&
+        ex.$type !== `${prefix}:ButtonsSetting` &&
+        ex.$type !== `${prefix}:FieldsPermission`
+    ) ?? []
+
+  // 更新元素扩展属性,避免后续报错
+  updateElementExtensions()
+}
+
+const updateAssignStartUserHandlerType = () => {
+  assignStartUserHandlerTypeEl.value.value = assignStartUserHandlerType.value
+
+  updateElementExtensions()
+}
+
+const updateRejectHandlerType = () => {
+  rejectHandlerTypeEl.value.value = rejectHandlerType.value
+
+  returnNodeId.value = returnTaskList.value[0].id
+  returnNodeIdEl.value.value = returnNodeId.value
+
+  updateElementExtensions()
+}
+
+const updateReturnNodeId = () => {
+  returnNodeIdEl.value.value = returnNodeId.value
+
+  updateElementExtensions()
+}
+
+const updateAssignEmptyHandlerType = () => {
+  assignEmptyHandlerTypeEl.value.value = assignEmptyHandlerType.value
+
+  updateElementExtensions()
+}
+
+const updateAssignEmptyUserIds = () => {
+  assignEmptyUserIdsEl.value.value = assignEmptyUserIds.value.toString()
+
+  updateElementExtensions()
+}
+
+const updateElementExtensions = () => {
+  const extensions = bpmnInstances().moddle.create('bpmn:ExtensionElements', {
+    values: [
+      ...otherExtensions.value,
+      assignStartUserHandlerTypeEl.value,
+      rejectHandlerTypeEl.value,
+      returnNodeIdEl.value,
+      assignEmptyHandlerTypeEl.value,
+      assignEmptyUserIdsEl.value,
+      ...buttonsSettingEl.value,
+      ...fieldsPermissionEl.value
+    ]
+  })
+  bpmnInstances().modeling.updateProperties(toRaw(bpmnElement.value), {
+    extensionElements: extensions
+  })
+}
+
+watch(
+  () => props.id,
+  (val) => {
+    val &&
+      val.length &&
+      nextTick(() => {
+        resetCustomConfigList()
+      })
+  },
+  { immediate: true }
+)
+
+function findAllPredecessorsExcludingStart(elementId, modeler) {
+  const elementRegistry = modeler.get('elementRegistry')
+  const allConnections = elementRegistry.filter((element) => element.type === 'bpmn:SequenceFlow')
+  const predecessors = new Set() // 使用 Set 来避免重复节点
+  const visited = new Set() // 用于记录已访问的节点
+
+  // 检查是否是开始事件节点
+  function isStartEvent(element) {
+    return element.type === 'bpmn:StartEvent'
+  }
+
+  function findPredecessorsRecursively(element) {
+    // 如果该节点已经访问过,直接返回,避免循环
+    if (visited.has(element)) {
+      return
+    }
+
+    // 标记当前节点为已访问
+    visited.add(element)
+
+    // 获取与当前节点相连的所有连接
+    const incomingConnections = allConnections.filter((connection) => connection.target === element)
+
+    incomingConnections.forEach((connection) => {
+      const source = connection.source // 获取前置节点
+
+      // 只添加不是开始事件的前置节点
+      if (!isStartEvent(source)) {
+        predecessors.add(source.businessObject)
+        // 递归查找前置节点
+        findPredecessorsRecursively(source)
+      }
+    })
+  }
+
+  const targetElement = elementRegistry.get(elementId)
+  if (targetElement) {
+    findPredecessorsRecursively(targetElement)
+  }
+
+  return Array.from(predecessors) // 返回前置节点数组
+}
+
+function useButtonsSetting() {
+  const buttonsSetting = ref<ButtonSetting[]>()
+  // 操作按钮显示名称可编辑
+  const btnDisplayNameEdit = ref<boolean[]>([])
+  const changeBtnDisplayName = (index: number) => {
+    btnDisplayNameEdit.value[index] = true
+  }
+  const btnDisplayNameBlurEvent = (index: number) => {
+    btnDisplayNameEdit.value[index] = false
+    const buttonItem = buttonsSetting.value![index]
+    buttonItem.displayName = buttonItem.displayName || OPERATION_BUTTON_NAME.get(buttonItem.id)!
+  }
+  return {
+    buttonsSetting,
+    btnDisplayNameEdit,
+    changeBtnDisplayName,
+    btnDisplayNameBlurEvent
+  }
+}
+
+// 表单字段权限设置
+function useFormFieldsPermission(defaultPermission) {
+  // 字段权限配置. 需要有 field, title,  permissioin 属性
+  const fieldsPermissionConfig = ref<Array<Record<string, string>>>([])
+
+  const formType = inject<Ref<number>>('formType') // 表单类型
+
+  const formFields = inject<Ref<string[]>>('formFields') // 流程表单字段
+
+  const getNodeConfigFormFields = (nodeFormFields?: Array<Record<string, string>>) => {
+    nodeFormFields = toRaw(nodeFormFields)
+    fieldsPermissionConfig.value =
+      cloneDeep(nodeFormFields) || getDefaultFieldsPermission(unref(formFields))
+  }
+  // 默认的表单权限: 获取表单的所有字段,设置字段默认权限为只读
+  const getDefaultFieldsPermission = (formFields?: string[]) => {
+    const defaultFieldsPermission: Array<Record<string, string>> = []
+    if (formFields) {
+      formFields.forEach((fieldStr: string) => {
+        parseFieldsSetDefaultPermission(JSON.parse(fieldStr), defaultFieldsPermission)
+      })
+    }
+    return defaultFieldsPermission
+  }
+  // 解析字段。赋给默认权限
+  const parseFieldsSetDefaultPermission = (
+    rule: Record<string, any>,
+    fieldsPermission: Array<Record<string, string>>,
+    parentTitle: string = ''
+  ) => {
+    const { /**type,*/ field, title: tempTitle, children } = rule
+    if (field && tempTitle) {
+      let title = tempTitle
+      if (parentTitle) {
+        title = `${parentTitle}.${tempTitle}`
+      }
+      fieldsPermission.push({
+        field,
+        title,
+        permission: defaultPermission
+      })
+      // TODO 子表单 需要处理子表单字段
+      // if (type === 'group' && rule.props?.rule && Array.isArray(rule.props.rule)) {
+      //   // 解析子表单的字段
+      //   rule.props.rule.forEach((item) => {
+      //     parseFieldsSetDefaultPermission(item, fieldsPermission, title)
+      //   })
+      // }
+    }
+    if (children && Array.isArray(children)) {
+      children.forEach((rule) => {
+        parseFieldsSetDefaultPermission(rule, fieldsPermission)
+      })
+    }
+  }
+
+  return {
+    formType,
+    fieldsPermissionConfig,
+    getNodeConfigFormFields
+  }
+}
+
+const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
+onMounted(async () => {
+  // 获得用户列表
+  userOptions.value = await UserApi.getSimpleUserList()
+})
+</script>
+
+<style lang="scss" scoped>
+.button-setting-pane {
+  display: flex;
+  flex-direction: column;
+  font-size: 14px;
+  margin-top: 8px;
+
+  .button-setting-desc {
+    padding-right: 8px;
+    margin-bottom: 16px;
+    font-size: 16px;
+    font-weight: 700;
+  }
+
+  .button-setting-title {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    height: 45px;
+    padding-left: 12px;
+    background-color: #f8fafc0a;
+    border: 1px solid #1f38581a;
+
+    & > :first-child {
+      width: 100px !important;
+      text-align: left !important;
+    }
+
+    & > :last-child {
+      text-align: center !important;
+    }
+
+    .button-title-label {
+      width: 150px;
+      font-size: 13px;
+      font-weight: 700;
+      color: #000;
+      text-align: left;
+    }
+  }
+
+  .button-setting-item {
+    align-items: center;
+    display: flex;
+    justify-content: space-between;
+    height: 38px;
+    padding-left: 12px;
+    border: 1px solid #1f38581a;
+    border-top: 0;
+
+    & > :first-child {
+      width: 100px !important;
+    }
+
+    & > :last-child {
+      text-align: center !important;
+    }
+
+    .button-setting-item-label {
+      width: 150px;
+      overflow: hidden;
+      text-align: left;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+
+    .editable-title-input {
+      height: 24px;
+      max-width: 130px;
+      margin-left: 4px;
+      line-height: 24px;
+      border: 1px solid #d9d9d9;
+      border-radius: 4px;
+      transition: all 0.3s;
+
+      &:focus {
+        border-color: #40a9ff;
+        outline: 0;
+        box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
+      }
+    }
+  }
+}
+
+.field-setting-pane {
+  display: flex;
+  flex-direction: column;
+  font-size: 14px;
+
+  .field-setting-desc {
+    padding-right: 8px;
+    margin-bottom: 16px;
+    font-size: 16px;
+    font-weight: 700;
+  }
+
+  .field-permit-title {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    height: 45px;
+    padding-left: 12px;
+    line-height: 45px;
+    background-color: #f8fafc0a;
+    border: 1px solid #1f38581a;
+
+    .first-title {
+      text-align: left !important;
+    }
+
+    .other-titles {
+      display: flex;
+      justify-content: space-between;
+    }
+
+    .setting-title-label {
+      display: inline-block;
+      width: 100px;
+      padding: 5px 0;
+      font-size: 13px;
+      font-weight: 700;
+      color: #000;
+      text-align: center;
+    }
+  }
+
+  .field-setting-item {
+    align-items: center;
+    display: flex;
+    justify-content: space-between;
+    height: 38px;
+    padding-left: 12px;
+    border: 1px solid #1f38581a;
+    border-top: 0;
+
+    .field-setting-item-label {
+      display: inline-block;
+      width: 100px;
+      min-height: 16px;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+      cursor: text;
+    }
+
+    .field-setting-item-group {
+      display: flex;
+      justify-content: space-between;
+
+      .item-radio-wrap {
+        display: inline-block;
+        width: 100px;
+        text-align: center;
+      }
+    }
+  }
+}
+</style>

+ 13 - 0
src/components/bpmnProcessDesigner/package/penal/custom-config/data.ts

@@ -0,0 +1,13 @@
+import UserTask from './components/UserTask.vue'
+import BoundaryEventTimer from './components/BoundaryEventTimer.vue'
+
+export const CustomConfigMap = {
+  UserTask: {
+    name: '用户任务',
+    componet: UserTask
+  },
+  BoundaryEventTimerEventDefinition: {
+    name: '定时边界事件(非中断)',
+    componet: BoundaryEventTimer
+  }
+}