From a1d7e81859f554f3a53680cc35f0f49bf1f77098 Mon Sep 17 00:00:00 2001
From: wwf <1971391498@qq.com>
Date: 星期四, 14 五月 2026 14:37:02 +0800
Subject: [PATCH] 导入项目

---
 src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue | 1068 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 1,068 insertions(+), 0 deletions(-)

diff --git a/src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue b/src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue
new file mode 100644
index 0000000..53058c4
--- /dev/null
+++ b/src/components/SimpleProcessDesignerV2/src/nodes-config/UserTaskNodeConfig.vue
@@ -0,0 +1,1068 @@
+<template>
+  <el-drawer
+    :append-to-body="true"
+    v-model="settingVisible"
+    :show-close="false"
+    :size="580"
+    :before-close="saveConfig"
+    class="justify-start"
+  >
+    <template #header>
+      <div class="config-header">
+        <input
+          v-if="showInput"
+          type="text"
+          class="config-editable-input"
+          @blur="blurEvent()"
+          v-mountedFocus
+          v-model="nodeName"
+          :placeholder="nodeName"
+        />
+        <div v-else class="node-name">
+          {{ nodeName }}
+          <Icon class="ml-1" icon="ep:edit-pen" :size="16" @click="clickIcon()" />
+        </div>
+        <div class="divide-line"></div>
+      </div>
+    </template>
+    <div v-if="currentNode.type === NodeType.USER_TASK_NODE" class="flex flex-items-center mb-3">
+      <span class="font-size-16px mr-3">瀹℃壒绫诲瀷 :</span>
+      <el-radio-group v-model="approveType">
+        <el-radio
+          v-for="(item, index) in APPROVE_TYPE"
+          :key="index"
+          :value="item.value"
+          :label="item.value"
+        >
+          {{ item.label }}
+        </el-radio>
+      </el-radio-group>
+    </div>
+    <el-tabs type="border-card" v-model="activeTabName" v-if="approveType === ApproveType.USER">
+      <el-tab-pane :label="`${nodeTypeName}浜篳" name="user">
+        <div>
+          <el-form ref="formRef" :model="configForm" label-position="top" :rules="formRules">
+            <el-form-item :label="`${nodeTypeName}浜鸿缃甡" prop="candidateStrategy">
+              <el-radio-group
+                v-model="configForm.candidateStrategy"
+                @change="changeCandidateStrategy"
+              >
+                <el-row>
+                  <el-col v-for="(dict, index) in CANDIDATE_STRATEGY" :key="index" :span="8">
+                    <el-radio :value="dict.value" :label="dict.value">
+                      {{ dict.label }}
+                    </el-radio>
+                  </el-col>
+                </el-row>
+              </el-radio-group>
+            </el-form-item>
+            <el-form-item
+              v-if="configForm.candidateStrategy == CandidateStrategy.ROLE"
+              label="鎸囧畾瑙掕壊"
+              prop="roleIds"
+            >
+              <el-select
+                filterable
+                v-model="configForm.roleIds"
+                clearable
+                multiple
+                style="width: 100%"
+              >
+                <el-option
+                  v-for="item in roleOptions"
+                  :key="item.id"
+                  :label="item.name"
+                  :value="item.id"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item
+              v-if="
+                configForm.candidateStrategy == CandidateStrategy.DEPT_MEMBER ||
+                configForm.candidateStrategy == CandidateStrategy.DEPT_LEADER ||
+                configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER
+              "
+              label="鎸囧畾閮ㄩ棬"
+              prop="deptIds"
+              span="24"
+            >
+              <el-tree-select
+                ref="treeRef"
+                v-model="configForm.deptIds"
+                :data="deptTreeOptions"
+                :props="defaultProps"
+                empty-text="鍔犺浇涓紝璇风◢鍚�"
+                multiple
+                node-key="id"
+                :check-strictly="true"
+                style="width: 100%"
+                show-checkbox
+              />
+            </el-form-item>
+            <el-form-item
+              v-if="configForm.candidateStrategy == CandidateStrategy.POST"
+              label="鎸囧畾宀椾綅"
+              prop="postIds"
+              span="24"
+            >
+              <el-select
+                filterable
+                v-model="configForm.postIds"
+                clearable
+                multiple
+                style="width: 100%"
+              >
+                <el-option
+                  v-for="item in postOptions"
+                  :key="item.id"
+                  :label="item.name"
+                  :value="item.id!"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item
+              v-if="configForm.candidateStrategy == CandidateStrategy.USER"
+              label="鎸囧畾鐢ㄦ埛"
+              prop="userIds"
+              span="24"
+            >
+              <el-select
+                filterable
+                v-model="configForm.userIds"
+                clearable
+                multiple
+                style="width: 100%"
+              >
+                <el-option
+                  v-for="item in userOptions"
+                  :key="item.id"
+                  :label="item.nickname"
+                  :value="item.id"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item
+              v-if="configForm.candidateStrategy === CandidateStrategy.USER_GROUP"
+              label="鎸囧畾鐢ㄦ埛缁�"
+              prop="userGroups"
+            >
+              <el-select
+                filterable
+                v-model="configForm.userGroups"
+                clearable
+                multiple
+                style="width: 100%"
+              >
+                <el-option
+                  v-for="item in userGroupOptions"
+                  :key="item.id"
+                  :label="item.name"
+                  :value="item.id"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item
+              v-if="configForm.candidateStrategy === CandidateStrategy.FORM_USER"
+              label="琛ㄥ崟鍐呯敤鎴峰瓧娈�"
+              prop="formUser"
+            >
+              <el-select filterable v-model="configForm.formUser" clearable style="width: 100%">
+                <el-option
+                  v-for="(item, idx) in userFieldOnFormOptions"
+                  :key="idx"
+                  :label="item.title"
+                  :value="item.field"
+                  :disabled="!item.required"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item
+              v-if="configForm.candidateStrategy === CandidateStrategy.FORM_DEPT_LEADER"
+              label="琛ㄥ崟鍐呴儴闂ㄥ瓧娈�"
+              prop="formDept"
+            >
+              <el-select filterable v-model="configForm.formDept" clearable style="width: 100%">
+                <el-option
+                  v-for="(item, idx) in deptFieldOnFormOptions"
+                  :key="idx"
+                  :label="item.title"
+                  :value="item.field"
+                  :disabled="!item.required"
+                />
+              </el-select>
+            </el-form-item>
+            <el-form-item
+              v-if="
+                configForm.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER ||
+                configForm.candidateStrategy == CandidateStrategy.START_USER_DEPT_LEADER ||
+                configForm.candidateStrategy ==
+                  CandidateStrategy.START_USER_MULTI_LEVEL_DEPT_LEADER ||
+                configForm.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER
+              "
+              :label="deptLevelLabel!"
+              prop="deptLevel"
+              span="24"
+            >
+              <el-select filterable v-model="configForm.deptLevel" clearable>
+                <el-option
+                  v-for="(item, index) in MULTI_LEVEL_DEPT"
+                  :key="index"
+                  :label="item.label"
+                  :value="item.value"
+                />
+              </el-select>
+            </el-form-item>
+            <!-- TODO @jason锛氬悗缁鏀寔閫夋嫨宸茬粡瀛樺ソ鐨勮〃杈惧紡 -->
+            <el-form-item
+              v-if="configForm.candidateStrategy === CandidateStrategy.EXPRESSION"
+              label="娴佺▼琛ㄨ揪寮�"
+              prop="expression"
+            >
+              <el-input
+                type="textarea"
+                v-model="configForm.expression"
+                clearable
+                style="width: 100%"
+              />
+            </el-form-item>
+            <el-form-item :label="`澶氫汉${nodeTypeName}鏂瑰紡`" prop="approveMethod">
+              <el-radio-group v-model="configForm.approveMethod" @change="approveMethodChanged">
+                <div class="flex-col">
+                  <div
+                    v-for="(item, index) in APPROVE_METHODS"
+                    :key="index"
+                    class="flex items-center"
+                  >
+                    <el-radio :value="item.value" :label="item.value">
+                      {{ item.label }}
+                    </el-radio>
+                    <el-form-item prop="approveRatio">
+                      <el-input-number
+                        v-model="configForm.approveRatio"
+                        :min="10"
+                        :max="100"
+                        :step="10"
+                        size="small"
+                        v-if="
+                          item.value === ApproveMethodType.APPROVE_BY_RATIO &&
+                          configForm.approveMethod === ApproveMethodType.APPROVE_BY_RATIO
+                        "
+                      />
+                    </el-form-item>
+                  </div>
+                </div>
+              </el-radio-group>
+            </el-form-item>
+
+            <div v-if="currentNode.type === NodeType.USER_TASK_NODE">
+              <el-divider content-position="left">瀹℃壒浜烘嫆缁濇椂</el-divider>
+              <el-form-item prop="rejectHandlerType">
+                <el-radio-group v-model="configForm.rejectHandlerType">
+                  <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="configForm.rejectHandlerType == RejectHandlerType.RETURN_USER_TASK"
+                label="椹冲洖鑺傜偣"
+                prop="returnNodeId"
+              >
+                <el-select
+                  filterable
+                  v-model="configForm.returnNodeId"
+                  clearable
+                  style="width: 100%"
+                >
+                  <el-option
+                    v-for="item in returnTaskList"
+                    :key="item.id"
+                    :label="item.name"
+                    :value="item.id"
+                  />
+                </el-select>
+              </el-form-item>
+            </div>
+
+            <div v-if="currentNode.type === NodeType.USER_TASK_NODE">
+              <el-divider content-position="left">瀹℃壒浜鸿秴鏃舵湭澶勭悊鏃�</el-divider>
+              <el-form-item label="鍚敤寮�鍏�" prop="timeoutHandlerEnable">
+                <el-switch
+                  v-model="configForm.timeoutHandlerEnable"
+                  active-text="寮�鍚�"
+                  inactive-text="鍏抽棴"
+                  @change="timeoutHandlerChange"
+                />
+              </el-form-item>
+              <el-form-item
+                label="鎵ц鍔ㄤ綔"
+                prop="timeoutHandlerType"
+                v-if="configForm.timeoutHandlerEnable"
+              >
+                <el-radio-group
+                  v-model="configForm.timeoutHandlerType"
+                  @change="timeoutHandlerTypeChanged"
+                >
+                  <el-radio-button
+                    v-for="item in TIMEOUT_HANDLER_TYPES"
+                    :key="item.value"
+                    :value="item.value"
+                    :label="item.label"
+                  />
+                </el-radio-group>
+              </el-form-item>
+              <el-form-item label="瓒呮椂鏃堕棿璁剧疆" v-if="configForm.timeoutHandlerEnable">
+                <span class="mr-2">褰撹秴杩�</span>
+                <el-form-item prop="timeDuration">
+                  <el-input-number
+                    class="mr-2"
+                    :style="{ width: '100px' }"
+                    v-model="configForm.timeDuration"
+                    :min="1"
+                    controls-position="right"
+                  />
+                </el-form-item>
+                <el-select
+                  filterable
+                  v-model="timeUnit"
+                  class="mr-2"
+                  :style="{ width: '100px' }"
+                  @change="timeUnitChange"
+                >
+                  <el-option
+                    v-for="item in TIME_UNIT_TYPES"
+                    :key="item.value"
+                    :label="item.label"
+                    :value="item.value"
+                  />
+                </el-select>
+                鏈鐞�
+              </el-form-item>
+              <el-form-item
+                label="鏈�澶ф彁閱掓鏁�"
+                prop="maxRemindCount"
+                v-if="configForm.timeoutHandlerEnable && configForm.timeoutHandlerType === 1"
+              >
+                <el-input-number v-model="configForm.maxRemindCount" :min="1" :max="10" />
+              </el-form-item>
+            </div>
+
+            <el-divider content-position="left">{{ nodeTypeName }}浜轰负绌烘椂</el-divider>
+            <el-form-item prop="assignEmptyHandlerType">
+              <el-radio-group v-model="configForm.assignEmptyHandlerType">
+                <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="configForm.assignEmptyHandlerType == AssignEmptyHandlerType.ASSIGN_USER"
+              label="鎸囧畾鐢ㄦ埛"
+              prop="assignEmptyHandlerUserIds"
+              span="24"
+            >
+              <el-select
+                filterable
+                v-model="configForm.assignEmptyHandlerUserIds"
+                clearable
+                multiple
+                style="width: 100%"
+              >
+                <el-option
+                  v-for="item in userOptions"
+                  :key="item.id"
+                  :label="item.nickname"
+                  :value="item.id"
+                />
+              </el-select>
+            </el-form-item>
+
+            <div v-if="currentNode.type === NodeType.USER_TASK_NODE">
+              <el-divider content-position="left">瀹℃壒浜轰笌鎻愪氦浜轰负鍚屼竴浜烘椂</el-divider>
+              <el-form-item prop="assignStartUserHandlerType">
+                <el-radio-group v-model="configForm.assignStartUserHandlerType">
+                  <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-form-item>
+            </div>
+
+            <div v-if="currentNode.type === NodeType.USER_TASK_NODE">
+              <el-divider content-position="left">鏄惁闇�瑕佺鍚�</el-divider>
+              <el-form-item prop="signEnable">
+                <el-switch v-model="configForm.signEnable" active-text="鏄�" inactive-text="鍚�" />
+              </el-form-item>
+            </div>
+
+            <div v-if="currentNode.type === NodeType.USER_TASK_NODE">
+              <el-divider content-position="left">瀹℃壒鎰忚</el-divider>
+              <el-form-item prop="reasonRequire">
+                <el-switch
+                  v-model="configForm.reasonRequire"
+                  active-text="蹇呭~"
+                  inactive-text="闈炲繀濉�"
+                />
+              </el-form-item>
+            </div>
+            <div>
+              <el-divider content-position="left">璺宠繃琛ㄨ揪寮�</el-divider>
+              <el-form-item prop="skipExpression">
+                <el-input v-model="configForm.skipExpression" type="textarea"  />
+              </el-form-item>
+            </div>
+          </el-form>
+        </div>
+      </el-tab-pane>
+      <el-tab-pane
+        label="鎿嶄綔鎸夐挳璁剧疆"
+        v-if="currentNode.type === NodeType.USER_TASK_NODE"
+        name="buttons"
+      >
+        <div class="button-setting-pane">
+          <div class="button-setting-desc">鎿嶄綔鎸夐挳</div>
+          <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 buttonsSetting" :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-tab-pane>
+      <el-tab-pane label="琛ㄥ崟瀛楁鏉冮檺" name="fields" v-if="formType === 10">
+        <div class="field-setting-pane">
+          <div class="field-setting-desc">瀛楁鏉冮檺</div>
+          <div class="field-permit-title">
+            <div class="setting-title-label first-title"> 瀛楁鍚嶇О </div>
+            <div class="other-titles">
+              <span class="setting-title-label cursor-pointer" @click="updatePermission('READ')">
+                鍙
+              </span>
+              <span class="setting-title-label cursor-pointer" @click="updatePermission('WRITE')">
+                鍙紪杈�
+              </span>
+              <span class="setting-title-label cursor-pointer" @click="updatePermission('NONE')">
+                闅愯棌
+              </span>
+            </div>
+          </div>
+          <div
+            class="field-setting-item"
+            v-for="(item, index) in fieldsPermissionConfig"
+            :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>
+      </el-tab-pane>
+      <el-tab-pane label="鐩戝惉鍣�" name="listener">
+        <UserTaskListener
+          ref="userTaskListenerRef"
+          v-model="configForm"
+          :form-field-options="formFieldOptions"
+        />
+      </el-tab-pane>
+    </el-tabs>
+    <template #footer>
+      <el-divider />
+      <div>
+        <el-button type="primary" @click="saveConfig">纭� 瀹�</el-button>
+        <el-button @click="closeDrawer">鍙� 娑�</el-button>
+      </div>
+    </template>
+  </el-drawer>
+</template>
+
+<script setup lang="ts">
+import {
+  SimpleFlowNode,
+  APPROVE_TYPE,
+  ApproveType,
+  APPROVE_METHODS,
+  CandidateStrategy,
+  NodeType,
+  ApproveMethodType,
+  TimeUnitType,
+  RejectHandlerType,
+  TIMEOUT_HANDLER_TYPES,
+  TIME_UNIT_TYPES,
+  REJECT_HANDLER_TYPES,
+  DEFAULT_BUTTON_SETTING,
+  OPERATION_BUTTON_NAME,
+  ButtonSetting,
+  MULTI_LEVEL_DEPT,
+  CANDIDATE_STRATEGY,
+  ASSIGN_START_USER_HANDLER_TYPES,
+  TimeoutHandlerType,
+  ASSIGN_EMPTY_HANDLER_TYPES,
+  AssignEmptyHandlerType,
+  FieldPermissionType,
+  ProcessVariableEnum,
+  TRANSACTOR_DEFAULT_BUTTON_SETTING
+} from '../consts'
+
+import {
+  useWatchNode,
+  useNodeName,
+  useFormFieldsPermission,
+  useNodeForm,
+  UserTaskFormType,
+  useDrawer
+} from '../node'
+import { defaultProps } from '@/utils/tree'
+import { cloneDeep } from 'lodash-es'
+import { convertTimeUnit, getApproveTypeText } from '../utils'
+import UserTaskListener from './components/UserTaskListener.vue'
+defineOptions({
+  name: 'UserTaskNodeConfig'
+})
+const props = defineProps({
+  flowNode: {
+    type: Object as () => SimpleFlowNode,
+    required: true
+  }
+})
+const emits = defineEmits<{
+  'find:returnTaskNodes': [nodeList: SimpleFlowNode[]]
+}>()
+const deptLevelLabel = computed(() => {
+  let label = '閮ㄩ棬璐熻矗浜烘潵婧�'
+  if (configForm.value.candidateStrategy == CandidateStrategy.MULTI_LEVEL_DEPT_LEADER) {
+    label = label + '(鎸囧畾閮ㄩ棬鍚戜笂)'
+  } else if (configForm.value.candidateStrategy == CandidateStrategy.FORM_DEPT_LEADER) {
+    label = label + '(琛ㄥ崟鍐呴儴闂ㄥ悜涓�)'
+  } else {
+    label = label + '(鍙戣捣浜洪儴闂ㄥ悜涓�)'
+  }
+  return label
+})
+// 鐩戞帶鑺傜偣鐨勫彉鍖�
+const currentNode = useWatchNode(props)
+// 鎶藉眽閰嶇疆
+const { settingVisible, closeDrawer, openDrawer } = useDrawer()
+// 鑺傜偣鍚嶇О閰嶇疆
+const { nodeName, showInput, clickIcon, blurEvent } = useNodeName(NodeType.USER_TASK_NODE)
+// 婵�娲荤殑 Tab 鏍囩椤�
+const activeTabName = ref('user')
+// 琛ㄥ崟瀛楁鏉冮檺璁剧疆
+const { formType, fieldsPermissionConfig, formFieldOptions, getNodeConfigFormFields } =
+  useFormFieldsPermission(FieldPermissionType.READ)
+// 琛ㄥ崟鍐呯敤鎴峰瓧娈甸�夐」, 蹇呴』鏄繀濉拰鐢ㄦ埛閫夋嫨鍣�
+const userFieldOnFormOptions = computed(() => {
+  // 鍥哄畾娣诲姞鍙戣捣浜� ID 瀛楁
+  formFieldOptions.unshift({
+    field: ProcessVariableEnum.START_USER_ID,
+    title: '鍙戣捣浜�',
+    type: 'UserSelect',
+    required: true
+  })
+  return formFieldOptions.filter((item) => item.type === 'UserSelect')
+})
+// 琛ㄥ崟鍐呴儴闂ㄥ瓧娈甸�夐」, 蹇呴』鏄繀濉拰閮ㄩ棬閫夋嫨鍣�
+const deptFieldOnFormOptions = computed(() => {
+  return formFieldOptions.filter((item) => item.type === 'DeptSelect')
+})
+// 鎿嶄綔鎸夐挳璁剧疆
+const { buttonsSetting, btnDisplayNameEdit, changeBtnDisplayName, btnDisplayNameBlurEvent } =
+  useButtonsSetting()
+const approveType = ref(ApproveType.USER)
+// 瀹℃壒浜鸿〃鍗曡缃�
+const formRef = ref() // 琛ㄥ崟 Ref
+// 琛ㄥ崟鏍¢獙瑙勫垯
+const formRules = reactive({
+  candidateStrategy: [{ required: true, message: '瀹℃壒浜鸿缃笉鑳戒负绌�', trigger: 'change' }],
+  userIds: [{ required: true, message: '鐢ㄦ埛涓嶈兘涓虹┖', trigger: 'change' }],
+  roleIds: [{ required: true, message: '瑙掕壊涓嶈兘涓虹┖', trigger: 'change' }],
+  deptIds: [{ required: true, message: '閮ㄩ棬涓嶈兘涓虹┖', trigger: 'change' }],
+  userGroups: [{ required: true, message: '鐢ㄦ埛缁勪笉鑳戒负绌�', trigger: 'change' }],
+  formUser: [{ required: true, message: '琛ㄥ崟鍐呯敤鎴峰瓧娈典笉鑳戒负绌�', trigger: 'change' }],
+  formDept: [{ required: true, message: '琛ㄥ崟鍐呴儴闂ㄥ瓧娈典笉鑳戒负绌�', trigger: 'change' }],
+  postIds: [{ required: true, message: '宀椾綅涓嶈兘涓虹┖', trigger: 'change' }],
+  expression: [{ required: true, message: '娴佺▼琛ㄨ揪寮忎笉鑳戒负绌�', trigger: 'blur' }],
+  approveMethod: [{ required: true, message: '澶氫汉瀹℃壒鏂瑰紡涓嶈兘涓虹┖', trigger: 'change' }],
+  approveRatio: [{ required: true, message: '閫氳繃姣斾緥涓嶈兘涓虹┖', trigger: 'blur' }],
+  returnNodeId: [{ required: true, message: '椹冲洖鑺傜偣涓嶈兘涓虹┖', trigger: 'change' }],
+  timeoutHandlerEnable: [{ required: true }],
+  timeoutHandlerType: [{ required: true }],
+  timeDuration: [{ required: true, message: '瓒呮椂鏃堕棿涓嶈兘涓虹┖', trigger: 'blur' }],
+  maxRemindCount: [{ required: true, message: '鎻愰啋娆℃暟涓嶈兘涓虹┖', trigger: 'blur' }],
+  assignEmptyHandlerType: [{ required: true }],
+  assignEmptyHandlerUserIds: [{ required: true, message: '鐢ㄦ埛涓嶈兘涓虹┖', trigger: 'change' }],
+  assignStartUserHandlerType: [{ required: true }]
+})
+
+const {
+  configForm: tempConfigForm,
+  roleOptions,
+  postOptions,
+  userOptions,
+  userGroupOptions,
+  deptTreeOptions,
+  handleCandidateParam,
+  parseCandidateParam,
+  getShowText
+} = useNodeForm(currentNode.value.type)
+const configForm = tempConfigForm as Ref<UserTaskFormType>
+
+// 鏀瑰彉瀹℃壒浜鸿缃瓥鐣�
+const changeCandidateStrategy = () => {
+  configForm.value.userIds = []
+  configForm.value.deptIds = []
+  configForm.value.roleIds = []
+  configForm.value.postIds = []
+  configForm.value.userGroups = []
+  configForm.value.deptLevel = 1
+  configForm.value.formUser = ''
+  configForm.value.formDept = ''
+  configForm.value.approveMethod = ApproveMethodType.SEQUENTIAL_APPROVE
+}
+
+// 瀹℃壒鏂瑰紡鏀瑰彉
+const approveMethodChanged = () => {
+  configForm.value.rejectHandlerType = RejectHandlerType.FINISH_PROCESS
+  if (configForm.value.approveMethod === ApproveMethodType.APPROVE_BY_RATIO) {
+    configForm.value.approveRatio = 100
+  }
+  formRef.value.clearValidate('approveRatio')
+}
+// 瀹℃壒鎷掔粷 鍙��鍥炵殑鑺傜偣
+const returnTaskList = ref<SimpleFlowNode[]>([])
+// 瀹℃壒浜鸿秴鏃舵湭澶勭悊璁剧疆
+const {
+  timeoutHandlerChange,
+  cTimeoutType,
+  timeoutHandlerTypeChanged,
+  timeUnit,
+  timeUnitChange,
+  isoTimeDuration,
+  cTimeoutMaxRemindCount
+} = useTimeoutHandler()
+
+const userTaskListenerRef = ref()
+
+/** 鑺傜偣绫诲瀷鍚嶇О */
+const nodeTypeName = computed(() => {
+  return currentNode.value.type === NodeType.TRANSACTOR_NODE ? '鍔炵悊' : '瀹℃壒'
+})
+
+/** 淇濆瓨閰嶇疆 */
+const saveConfig = async () => {
+  // activeTabName.value = 'user'
+  // 璁剧疆瀹℃壒鑺傜偣鍚嶇О
+  currentNode.value.name = nodeName.value!
+  // 璁剧疆瀹℃壒绫诲瀷
+  currentNode.value.approveType = approveType.value
+  // 濡傛灉涓嶆槸浜哄伐瀹℃壒銆傝繑鍥�
+  if (approveType.value !== ApproveType.USER) {
+    currentNode.value.showText = getApproveTypeText(approveType.value)
+    settingVisible.value = false
+    return true
+  }
+
+  if (!formRef) return false
+  if (!userTaskListenerRef) return false
+  const valid = (await formRef.value.validate()) && (await userTaskListenerRef.value.validate())
+  if (!valid) return false
+  const showText = getShowText()
+  if (!showText) return false
+
+  currentNode.value.candidateStrategy = configForm.value.candidateStrategy
+  // 澶勭悊 candidateParam 鍙傛暟
+  currentNode.value.candidateParam = handleCandidateParam()
+  // 璁剧疆瀹℃壒鏂瑰紡
+  currentNode.value.approveMethod = configForm.value.approveMethod
+  if (configForm.value.approveMethod === ApproveMethodType.APPROVE_BY_RATIO) {
+    currentNode.value.approveRatio = configForm.value.approveRatio
+  }
+  // 璁剧疆鎷掔粷澶勭悊
+  currentNode.value.rejectHandler = {
+    type: configForm.value.rejectHandlerType!,
+    returnNodeId: configForm.value.returnNodeId
+  }
+  // 璁剧疆瓒呮椂澶勭悊
+  currentNode.value.timeoutHandler = {
+    enable: configForm.value.timeoutHandlerEnable!,
+    type: cTimeoutType.value,
+    timeDuration: isoTimeDuration.value,
+    maxRemindCount: cTimeoutMaxRemindCount.value
+  }
+  // 璁剧疆瀹℃壒浜轰负绌烘椂
+  currentNode.value.assignEmptyHandler = {
+    type: configForm.value.assignEmptyHandlerType!,
+    userIds:
+      configForm.value.assignEmptyHandlerType === AssignEmptyHandlerType.ASSIGN_USER
+        ? configForm.value.assignEmptyHandlerUserIds
+        : undefined
+  }
+  // 璁剧疆瀹℃壒浜轰笌鍙戣捣浜虹浉鍚屾椂
+  currentNode.value.assignStartUserHandlerType = configForm.value.assignStartUserHandlerType
+  // 璁剧疆琛ㄥ崟鏉冮檺
+  currentNode.value.fieldsPermission = fieldsPermissionConfig.value
+  // 璁剧疆鎸夐挳鏉冮檺
+  currentNode.value.buttonsSetting = buttonsSetting.value
+  // 鍒涘缓浠诲姟鐩戝惉鍣�
+  currentNode.value.taskCreateListener = {
+    enable: configForm.value.taskCreateListenerEnable ?? false,
+    path: configForm.value.taskCreateListenerPath,
+    header: configForm.value.taskCreateListener?.header,
+    body: configForm.value.taskCreateListener?.body
+  }
+  // 鎸囨淳浠诲姟鐩戝惉鍣�
+  currentNode.value.taskAssignListener = {
+    enable: configForm.value.taskAssignListenerEnable ?? false,
+    path: configForm.value.taskAssignListenerPath,
+    header: configForm.value.taskAssignListener?.header,
+    body: configForm.value.taskAssignListener?.body
+  }
+  // 瀹屾垚浠诲姟鐩戝惉鍣�
+  currentNode.value.taskCompleteListener = {
+    enable: configForm.value.taskCompleteListenerEnable ?? false,
+    path: configForm.value.taskCompleteListenerPath,
+    header: configForm.value.taskCompleteListener?.header,
+    body: configForm.value.taskCompleteListener?.body
+  }
+  // 绛惧悕
+  currentNode.value.signEnable = configForm.value.signEnable
+  // 瀹℃壒鎰忚
+  currentNode.value.reasonRequire = configForm.value.reasonRequire
+  // 璺宠繃琛ㄨ揪寮�
+  currentNode.value.skipExpression = configForm.value.skipExpression
+
+  currentNode.value.showText = showText
+  settingVisible.value = false
+  return true
+}
+
+/** 鏄剧ず瀹℃壒鑺傜偣閰嶇疆锛� 鐢辩埗缁勪欢浼犺繃鏉� */
+const showUserTaskNodeConfig = (node: SimpleFlowNode) => {
+  nodeName.value = node.name
+  // 1 瀹℃壒绫诲瀷
+  approveType.value = node.approveType ? node.approveType : ApproveType.USER
+  // 濡傛灉瀹℃壒绫诲瀷涓嶆槸浜哄伐瀹℃壒杩斿洖
+  if (approveType.value !== ApproveType.USER) {
+    return
+  }
+
+  //2.1 瀹℃壒浜鸿缃�
+  configForm.value.candidateStrategy = node.candidateStrategy!
+  // 瑙f瀽鍊欓�変汉鍙傛暟
+  parseCandidateParam(node.candidateStrategy!, node?.candidateParam)
+  // 2.2 璁剧疆瀹℃壒鏂瑰紡
+  configForm.value.approveMethod = node.approveMethod!
+  if (node.approveMethod == ApproveMethodType.APPROVE_BY_RATIO) {
+    configForm.value.approveRatio = node.approveRatio!
+  }
+  // 2.3 璁剧疆瀹℃壒鎷掔粷澶勭悊
+  configForm.value.rejectHandlerType = node.rejectHandler?.type
+  configForm.value.returnNodeId = node.rejectHandler?.returnNodeId
+  const matchNodeList = []
+  emits('find:returnTaskNodes', matchNodeList)
+  returnTaskList.value = matchNodeList
+  // 2.4 璁剧疆瀹℃壒瓒呮椂澶勭悊
+  configForm.value.timeoutHandlerEnable = node.timeoutHandler?.enable
+  if (node.timeoutHandler?.enable && node.timeoutHandler?.timeDuration) {
+    const strTimeDuration = node.timeoutHandler.timeDuration
+    let parseTime = strTimeDuration.slice(2, strTimeDuration.length - 1)
+    let parseTimeUnit = strTimeDuration.slice(strTimeDuration.length - 1)
+    configForm.value.timeDuration = parseInt(parseTime)
+    timeUnit.value = convertTimeUnit(parseTimeUnit)
+  }
+  configForm.value.timeoutHandlerType = node.timeoutHandler?.type
+  configForm.value.maxRemindCount = node.timeoutHandler?.maxRemindCount
+  // 2.5 璁剧疆瀹℃壒浜轰负绌烘椂
+  configForm.value.assignEmptyHandlerType = node.assignEmptyHandler?.type
+  configForm.value.assignEmptyHandlerUserIds = node.assignEmptyHandler?.userIds
+  // 2.6 璁剧疆鐢ㄦ埛浠诲姟鐨勫鎵逛汉涓庡彂璧蜂汉鐩稿悓鏃�
+  configForm.value.assignStartUserHandlerType = node.assignStartUserHandlerType
+  // 3. 鎿嶄綔鎸夐挳璁剧疆
+  buttonsSetting.value =
+    cloneDeep(node.buttonsSetting) ||
+    (node.type === NodeType.TRANSACTOR_NODE
+      ? TRANSACTOR_DEFAULT_BUTTON_SETTING
+      : DEFAULT_BUTTON_SETTING)
+  // 4. 琛ㄥ崟瀛楁鏉冮檺閰嶇疆
+  getNodeConfigFormFields(node.fieldsPermission)
+  // 5. 鐩戝惉鍣�
+  // 5.1 鍒涘缓浠诲姟
+  configForm.value.taskCreateListenerEnable = node.taskCreateListener?.enable
+  configForm.value.taskCreateListenerPath = node.taskCreateListener?.path
+  configForm.value.taskCreateListener = {
+    header: node.taskCreateListener?.header ?? [],
+    body: node.taskCreateListener?.body ?? []
+  }
+  // 5.2 鎸囨淳浠诲姟
+  configForm.value.taskAssignListenerEnable = node.taskAssignListener?.enable
+  configForm.value.taskAssignListenerPath = node.taskAssignListener?.path
+  configForm.value.taskAssignListener = {
+    header: node.taskAssignListener?.header ?? [],
+    body: node.taskAssignListener?.body ?? []
+  }
+  // 5.3 瀹屾垚浠诲姟
+  configForm.value.taskCompleteListenerEnable = node.taskCompleteListener?.enable
+  configForm.value.taskCompleteListenerPath = node.taskCompleteListener?.path
+  configForm.value.taskCompleteListener = {
+    header: node.taskCompleteListener?.header ?? [],
+    body: node.taskCompleteListener?.body ?? []
+  }
+  // 6. 绛惧悕
+  configForm.value.signEnable = node?.signEnable ?? false
+  // 7. 瀹℃壒鎰忚
+  configForm.value.reasonRequire = node?.reasonRequire ?? false
+  // 8. 璺宠繃琛ㄨ揪寮�
+  configForm.value.skipExpression = node?.skipExpression ?? ''
+}
+
+defineExpose({ openDrawer, showUserTaskNodeConfig }) // 鏆撮湶鏂规硶缁欑埗缁勪欢
+
+/** 鎿嶄綔鎸夐挳璁剧疆 */
+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 useTimeoutHandler() {
+  // 鏃堕棿鍗曚綅
+  const timeUnit = ref(TimeUnitType.HOUR)
+
+  // 瓒呮椂寮�鍏虫敼鍙�
+  const timeoutHandlerChange = () => {
+    if (configForm.value.timeoutHandlerEnable) {
+      timeUnit.value = 2
+      configForm.value.timeDuration = 6
+      configForm.value.timeoutHandlerType = 1
+      configForm.value.maxRemindCount = 1
+    }
+  }
+  // 瓒呮椂鎵ц鐨勫姩浣�
+  const cTimeoutType = computed(() => {
+    if (!configForm.value.timeoutHandlerEnable) {
+      return undefined
+    }
+    return configForm.value.timeoutHandlerType
+  })
+
+  // 瓒呮椂澶勭悊鍔ㄤ綔鏀瑰彉
+  const timeoutHandlerTypeChanged = () => {
+    if (configForm.value.timeoutHandlerType === TimeoutHandlerType.REMINDER) {
+      configForm.value.maxRemindCount = 1 // 瓒呮椂鎻愰啋娆℃暟锛岄粯璁や负1
+    }
+  }
+
+  // 鏃堕棿鍗曚綅鏀瑰彉
+  const timeUnitChange = () => {
+    // 鍒嗛挓锛岄粯璁ゆ槸 60 鍒嗛挓
+    if (timeUnit.value === TimeUnitType.MINUTE) {
+      configForm.value.timeDuration = 60
+    }
+    // 灏忔椂锛岄粯璁ゆ槸 6 涓皬鏃�
+    if (timeUnit.value === TimeUnitType.HOUR) {
+      configForm.value.timeDuration = 6
+    }
+    // 澶╋紝 榛樿 1澶�
+    if (timeUnit.value === TimeUnitType.DAY) {
+      configForm.value.timeDuration = 1
+    }
+  }
+  // 瓒呮椂鏃堕棿鐨� ISO 琛ㄧず
+  const isoTimeDuration = computed(() => {
+    if (!configForm.value.timeoutHandlerEnable) {
+      return undefined
+    }
+    let strTimeDuration = 'PT'
+    if (timeUnit.value === TimeUnitType.MINUTE) {
+      strTimeDuration += configForm.value.timeDuration + 'M'
+    }
+    if (timeUnit.value === TimeUnitType.HOUR) {
+      strTimeDuration += configForm.value.timeDuration + 'H'
+    }
+    if (timeUnit.value === TimeUnitType.DAY) {
+      strTimeDuration += configForm.value.timeDuration + 'D'
+    }
+    return strTimeDuration
+  })
+
+  // 瓒呮椂鏈�澶ф彁閱掓鏁�
+  const cTimeoutMaxRemindCount = computed(() => {
+    if (!configForm.value.timeoutHandlerEnable) {
+      return undefined
+    }
+    if (configForm.value.timeoutHandlerType !== TimeoutHandlerType.REMINDER) {
+      return undefined
+    }
+    return configForm.value.maxRemindCount
+  })
+
+  return {
+    timeoutHandlerChange,
+    cTimeoutType,
+    timeoutHandlerTypeChanged,
+    timeUnit,
+    timeUnitChange,
+    isoTimeDuration,
+    cTimeoutMaxRemindCount
+  }
+}
+
+/** 鎵归噺鏇存柊鏉冮檺 */
+const updatePermission = (type: string) => {
+  fieldsPermissionConfig.value.forEach((field) => {
+    field.permission =
+      type === 'READ'
+        ? FieldPermissionType.READ
+        : type === 'WRITE'
+          ? FieldPermissionType.WRITE
+          : FieldPermissionType.NONE
+  })
+}
+</script>
+
+<style lang="scss" scoped>
+.button-setting-pane {
+  display: flex;
+  flex-direction: column;
+  font-size: 14px;
+
+  .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%);
+      }
+    }
+  }
+}
+</style>

--
Gitblit v1.8.0