Эх сурвалжийг харах

Merge branch 'feature/bpm' of https://github.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm

# Conflicts:
#	src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue
#	src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue
YunaiV 9 сар өмнө
parent
commit
5814826fa8

+ 100 - 0
src/components/UserSelectForm/index.vue

@@ -0,0 +1,100 @@
+<template>
+  <Dialog v-model="dialogVisible" title="人员选择" width="900">
+    <el-row>
+      <el-col :span="6">
+        <el-tree
+          ref="treeRef"
+          :data="deptList"
+          :expand-on-click-node="false"
+          :props="defaultProps"
+          default-expand-all
+          highlight-current
+          node-key="id"
+          @node-click="handleNodeClick"
+        />
+      </el-col>
+      <el-col :span="17" :offset="1">
+        <el-transfer
+          v-model="selectedUserIdList"
+          filterable
+          filter-placeholder="搜索成员"
+          :data="userList"
+          :props="{ label: 'nickname', key: 'id' }"
+        />
+      </el-col>
+    </el-row>
+    <template #footer>
+      <el-button
+        :disabled="formLoading || !selectedUserIdList?.length"
+        type="primary"
+        @click="submitForm"
+        >确 定</el-button
+      >
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script lang="ts" setup>
+import { defaultProps, handleTree } from '@/utils/tree'
+import * as DeptApi from '@/api/system/dept'
+import * as UserApi from '@/api/system/user'
+
+defineOptions({ name: 'UserSelectForm' })
+const emit = defineEmits<{
+  confirm: [id: any, userList: any[]]
+}>()
+const { t } = useI18n() // 国际
+const deptList = ref<Tree[]>([]) // 部门树形结构化
+const userList: any = ref([]) // 用户列表
+const message = useMessage() // 消息弹窗
+const selectedUserIdList: any = ref([]) // 选中的用户列表
+const dialogVisible = ref(false) // 弹窗的是否展示
+const formLoading = ref(false) // 表单的加载中
+const activityId = ref() // 主键id
+
+/** 打开弹窗 */
+const open = async (id, selectedList?) => {
+  activityId.value = id
+  resetForm()
+  deptList.value = handleTree(await DeptApi.getSimpleDeptList())
+  await getUserList()
+  selectedUserIdList.value = selectedList?.map((item) => item.id)
+  // 修改时,设置数据
+  dialogVisible.value = true
+}
+
+/* 获取用户列表 */
+const getUserList = async (deptId?) => {
+  try {
+    // @ts-ignore
+    const data = await UserApi.getUserPage({ pageSize: 100, pageNo: 1, deptId })
+    userList.value = data.list
+  } finally {
+  }
+}
+
+const submitForm = async () => {
+  // 提交请求
+  formLoading.value = true
+  try {
+    message.success(t('common.updateSuccess'))
+    dialogVisible.value = false
+    const emitUserList = userList.value.filter((user) => selectedUserIdList.value.includes(user.id))
+    // 发送操作成功的事件
+    emit('confirm', activityId.value, emitUserList)
+  } finally {
+    formLoading.value = false
+  }
+}
+const resetForm = () => {
+  deptList.value = []
+  userList.value = []
+  selectedUserIdList.value = []
+}
+
+/** 处理部门被点击 */
+const handleNodeClick = async (row: { [key: string]: any }) => {
+  getUserList(row.id)
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+</script>

+ 25 - 63
src/views/bpm/processInstance/create/ProcessDefinitionDetail.vue

@@ -19,40 +19,7 @@
                       v-model="detailForm.value"
                       :option="detailForm.option"
                       @submit="submitForm"
-                    >
-                      <template #type-startUserSelect>
-                        <el-col :span="24">
-                          <el-card class="mb-10px">
-                            <template #header>指定审批人</template>
-                            <el-form
-                              :model="startUserSelectAssignees"
-                              :rules="startUserSelectAssigneesFormRules"
-                              ref="startUserSelectAssigneesFormRef"
-                            >
-                              <el-form-item
-                                v-for="userTask in startUserSelectTasks"
-                                :key="userTask.id"
-                                :label="`任务【${userTask.name}】`"
-                                :prop="userTask.id"
-                              >
-                                <el-select
-                                  v-model="startUserSelectAssignees[userTask.id]"
-                                  multiple
-                                  placeholder="请选择审批人"
-                                >
-                                  <el-option
-                                    v-for="user in userList"
-                                    :key="user.id"
-                                    :label="user.nickname"
-                                    :value="user.id"
-                                  />
-                                </el-select>
-                              </el-form-item>
-                            </el-form>
-                          </el-card>
-                        </el-col>
-                      </template>
-                    </form-create>
+                    />
                   </el-col>
 
                   <el-col :span="6" :offset="1">
@@ -61,7 +28,9 @@
                       ref="timelineRef"
                       :activity-nodes="activityNodes"
                       :show-status-icon="false"
-                      candidateField="candidateUserList"
+                      :startUserSelectTasks="startUserSelectTasks"
+                      :startUserSelectAssignees="startUserSelectAssignees"
+                      @select-user-confirm="selectUserConfirm"
                     />
                   </el-col>
                 </el-row>
@@ -72,15 +41,15 @@
           <el-tab-pane label="流程图" name="diagram">
             <div class="form-scroll-area">
               <!-- BPMN 流程图预览 -->
-              <ProcessInstanceBpmnViewer 
-                :bpmn-xml="bpmnXML" 
-                v-if="BpmModelType.BPMN === selectProcessDefinition.modelType" 
+              <ProcessInstanceBpmnViewer
+                :bpmn-xml="bpmnXML"
+                v-if="BpmModelType.BPMN === selectProcessDefinition.modelType"
               />
 
               <!-- Simple 流程图预览 -->
-              <ProcessInstanceSimpleViewer 
-                :simple-json="simpleJson" 
-                v-if="BpmModelType.SIMPLE === selectProcessDefinition.modelType" 
+              <ProcessInstanceSimpleViewer
+                :simple-json="simpleJson"
+                v-if="BpmModelType.SIMPLE === selectProcessDefinition.modelType"
               />
             </div>
           </el-tab-pane>
@@ -115,7 +84,6 @@ import type { ApiAttrs } from '@form-create/element-ui/types/config'
 import { useTagsViewStore } from '@/store/modules/tagsView'
 import * as ProcessInstanceApi from '@/api/bpm/processInstance'
 import * as DefinitionApi from '@/api/bpm/definition'
-import * as UserApi from '@/api/system/user'
 
 defineOptions({ name: 'ProcessDefinitionDetail' })
 const props = defineProps<{
@@ -132,11 +100,8 @@ const detailForm: any = ref({
 }) // 流程表单详情
 const fApi = ref<ApiAttrs>()
 // 指定审批人
-const startUserSelectAssigneesFormRef = ref() // 发起人选择审批人的表单 Ref
 const startUserSelectTasks: any = ref([]) // 发起人需要选择审批人的用户任务列表
 const startUserSelectAssignees = ref({}) // 发起人选择审批人的数据
-const startUserSelectAssigneesFormRules = ref({}) // 发起人选择审批人的表单 Rules
-const userList = ref<any[]>([]) // 用户列表
 const bpmnXML: any = ref(null) // BPMN 数据
 const simpleJson = ref<string|undefined>() // Simple 设计器数据 json 格式
 /** 当前的Tab */
@@ -150,7 +115,6 @@ const initProcessInfo = async (row: any, formVariables?: any) => {
   // 重置指定审批人
   startUserSelectTasks.value = []
   startUserSelectAssignees.value = {}
-  startUserSelectAssigneesFormRules.value = {}
 
   // 情况一:流程表单
   if (row.formType == 10) {
@@ -176,24 +140,12 @@ const initProcessInfo = async (row: any, formVariables?: any) => {
       bpmnXML.value = processDefinitionDetail.bpmnXml
       simpleJson.value = processDefinitionDetail.simpleModel
       startUserSelectTasks.value = processDefinitionDetail.startUserSelectTasks
-
       // 设置指定审批人
       if (startUserSelectTasks.value?.length > 0) {
-        detailForm.value.rule.push({
-          type: 'startUserSelect',
-          props: {
-            title: '指定审批人'
-          }
-        })
-        // 设置校验规则
         for (const userTask of startUserSelectTasks.value) {
+          // 初始化数据
           startUserSelectAssignees.value[userTask.id] = []
-          startUserSelectAssigneesFormRules.value[userTask.id] = [
-            { required: true, message: '请选择审批人', trigger: 'blur' }
-          ]
         }
-        // 加载用户列表
-        userList.value = await UserApi.getSimpleUserList()
       }
     }
     // 情况二:业务表单
@@ -223,13 +175,19 @@ const getApprovalDetail = async (row: any) => {
 }
 
 /** 提交按钮 */
-const submitForm = async (formData: any) => {
+const submitForm = async () => {
   if (!fApi.value || !props.selectProcessDefinition) {
     return
   }
   // 如果有指定审批人,需要校验
   if (startUserSelectTasks.value?.length > 0) {
-    await startUserSelectAssigneesFormRef.value.validate()
+    for (const userTask of startUserSelectTasks.value) {
+      if (
+        Array.isArray(startUserSelectAssignees.value[userTask.id]) &&
+        startUserSelectAssignees.value[userTask.id].length === 0
+      )
+        return message.warning(`请选择${userTask.name}的审批人`)
+    }
   }
 
   // 提交请求
@@ -237,7 +195,7 @@ const submitForm = async (formData: any) => {
   try {
     await ProcessInstanceApi.createProcessInstance({
       processDefinitionId: props.selectProcessDefinition.id,
-      variables: formData || detailForm.value.value,
+      variables: detailForm.value.value,
       startUserSelectAssignees: startUserSelectAssignees.value
     })
     // 提示
@@ -256,6 +214,10 @@ const handleCancel = () => {
   emit('cancel')
 }
 
+const selectUserConfirm = (id, userList) => {
+  startUserSelectAssignees.value[id] = userList?.map((item) => item.id)
+}
+
 defineExpose({ initProcessInfo })
 </script>
 
@@ -263,7 +225,7 @@ defineExpose({ initProcessInfo })
 $wrap-padding-height: 20px;
 $wrap-margin-height: 15px;
 $button-height: 51px;
-$process-header-height: 194px;
+$process-header-height: 105px;
 
 .processInstance-wrap-main {
   height: calc(

+ 133 - 56
src/views/bpm/processInstance/create/index.vue

@@ -1,46 +1,74 @@
 <template>
   <!-- 第一步,通过流程定义的列表,选择对应的流程 -->
-  <ContentWrap
-    class="process-definition-container position-relative pb-20px"
-    v-if="!selectProcessDefinition"
-    v-loading="loading"
-  >
-    <el-row :gutter="20" class="!flex-nowrap">
-      <el-col :span="5">
-        <div class="flex flex-col">
-          <div
-            v-for="category in categoryList"
-            :key="category.code"
-            class="flex items-center p-10px cursor-pointer text-14px rounded-md"
-            :class="categoryActive.code === category.code ? 'text-#3e7bff bg-#e8eeff' : ''"
-            @click="handleCategoryClick(category)"
-          >
-            {{ category.name }}
+  <template v-if="!selectProcessDefinition">
+    <el-input
+      v-model="currentSearchKey"
+      class="!w-50% mb-15px"
+      placeholder="请输入流程名称"
+      clearable
+      @keyup.enter="handleQuery"
+      @clear="handleClear"
+    >
+      <template #prefix>
+        <Icon icon="ep:search" />
+      </template>
+    </el-input>
+    <ContentWrap
+      :class="{ 'process-definition-container': filteredProcessDefinitionList?.length }"
+      class="position-relative pb-20px h-700px"
+      v-loading="loading"
+    >
+      <el-row v-if="filteredProcessDefinitionList?.length" :gutter="20" class="!flex-nowrap">
+        <el-col :span="5">
+          <div class="flex flex-col">
+            <div
+              v-for="category in categoryList"
+              :key="category.code"
+              class="flex items-center p-10px cursor-pointer text-14px rounded-md"
+              :class="categoryActive.code === category.code ? 'text-#3e7bff bg-#e8eeff' : ''"
+              @click="handleCategoryClick(category)"
+            >
+              {{ category.name }}
+            </div>
           </div>
-        </div>
-      </el-col>
-      <el-col :span="19">
-        <h3 class="text-16px font-bold mb-10px mt-5px">{{ categoryActive.name }}</h3>
-        <div class="grid grid-cols-3 gap3" v-if="categoryProcessDefinitionList.length">
-          <el-card
-            v-for="definition in categoryProcessDefinitionList"
-            :key="definition.id"
-            shadow="hover"
-            class="cursor-pointer definition-item-card"
-            @click="handleSelect(definition)"
-          >
-            <template #default>
-              <div class="flex">
-                <el-image :src="definition.icon" class="w-32px h-32px" />
-                <el-text class="!ml-10px" size="large">{{ definition.name }}</el-text>
+        </el-col>
+        <el-col :span="19">
+          <el-scrollbar ref="scrollWrapper" height="700">
+            <div
+              class="mb-20px pl-10px"
+              v-for="(definitions, title) in processDefinitionGroup"
+              :key="title"
+              :ref="`category-${title}`"
+            >
+              <h3 class="text-18px font-bold mb-10px mt-5px">{{ title }}</h3>
+              <div class="grid grid-cols-3 gap3">
+                <el-tooltip
+                  v-for="definition in definitions"
+                  :key="definition.id"
+                  :content="definition.description"
+                  placement="top"
+                >
+                  <el-card
+                    shadow="hover"
+                    class="cursor-pointer definition-item-card"
+                    @click="handleSelect(definition)"
+                  >
+                    <template #default>
+                      <div class="flex">
+                        <el-image :src="definition.icon" class="w-32px h-32px" />
+                        <el-text class="!ml-10px" size="large">{{ definition.name }}</el-text>
+                      </div>
+                    </template>
+                  </el-card>
+                </el-tooltip>
               </div>
-            </template>
-          </el-card>
-        </div>
-        <el-empty v-else />
-      </el-col>
-    </el-row>
-  </ContentWrap>
+            </div>
+          </el-scrollbar>
+        </el-col>
+      </el-row>
+      <el-empty class="!py-200px" :image-size="200" description="没有找到搜索结果" v-else />
+    </ContentWrap>
+  </template>
 
   <!-- 第二步,填写表单,进行流程的提交 -->
   <ProcessDefinitionDetail
@@ -56,12 +84,14 @@ import * as DefinitionApi from '@/api/bpm/definition'
 import * as ProcessInstanceApi from '@/api/bpm/processInstance'
 import { CategoryApi } from '@/api/bpm/category'
 import ProcessDefinitionDetail from './ProcessDefinitionDetail.vue'
+import { groupBy } from 'lodash-es'
 
 defineOptions({ name: 'BpmProcessInstanceCreate' })
 
+const { proxy } = getCurrentInstance() as any
 const route = useRoute() // 路由
 const message = useMessage() // 消息
-
+const currentSearchKey = ref('') // 当前搜索关键字
 const processInstanceId: any = route.query.processInstanceId
 const loading = ref(true) // 加载中
 const categoryList: any = ref([]) // 分类的列表
@@ -71,15 +101,10 @@ const processDefinitionList = ref([]) // 流程定义的列表
 const getList = async () => {
   loading.value = true
   try {
-    // 流程分类
-    categoryList.value = await CategoryApi.getCategorySimpleList()
-    if (categoryList.value.length > 0) {
-      categoryActive.value = categoryList.value[0]
-    }
-    // 流程定义
-    processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({
-      suspensionState: 1
-    })
+    // 所有流程分类数据
+    await getCategoryList()
+    // 所有流程定义数据
+    await getDefinitionList()
 
     // 如果 processInstanceId 非空,说明是重新发起
     if (processInstanceId?.length > 0) {
@@ -102,11 +127,55 @@ const getList = async () => {
   }
 }
 
-/** 选中分类对应的流程定义列表 */
-const categoryProcessDefinitionList: any = computed(() => {
-  return processDefinitionList.value.filter(
-    (item: any) => item.category == categoryActive.value.code
-  )
+// 获取所有流程分类数据
+const getCategoryList = async () => {
+  try {
+    // 流程分类
+    categoryList.value = await CategoryApi.getCategorySimpleList()
+    if (categoryList.value.length > 0) {
+      categoryActive.value = categoryList.value[0]
+    }
+  } finally {
+  }
+}
+
+// 获取所有流程定义数据
+const getDefinitionList = async () => {
+  try {
+    // 流程定义
+    processDefinitionList.value = await DefinitionApi.getProcessDefinitionList({
+      suspensionState: 1
+    })
+    // 初始化过滤列表为全部流程定义
+    filteredProcessDefinitionList.value = processDefinitionList.value
+  } finally {
+  }
+}
+
+const filteredProcessDefinitionList = ref([]) // 用于存储搜索过滤后的流程定义
+// 直接进行前端搜索
+const handleQuery = () => {
+  if (currentSearchKey.value.trim()) {
+    // 如果有搜索关键字,进行过滤
+    filteredProcessDefinitionList.value = processDefinitionList.value.filter(
+      (definition: any) =>
+        definition.name.toLowerCase().includes(currentSearchKey.value.toLowerCase()) // 假设搜索依据是流程定义的名称
+    )
+  } else {
+    // 如果没有搜索关键字,恢复所有数据
+    filteredProcessDefinitionList.value = processDefinitionList.value
+  }
+}
+
+// 监听input `clearable` 事件
+const handleClear = () => {
+  filteredProcessDefinitionList.value = processDefinitionList.value
+}
+
+// 流程定义的分组
+const processDefinitionGroup: any = computed(() => {
+  if (!processDefinitionList.value?.length) return {}
+  return groupBy(filteredProcessDefinitionList.value, 'categoryName')
 })
 
 // ========== 表单相关 ==========
@@ -122,8 +191,16 @@ const handleSelect = async (row, formVariables?) => {
   processDefinitionDetailRef.value?.initProcessInfo(row, formVariables)
 }
 // 左侧分类切换
-const handleCategoryClick = (val: number) => {
-  categoryActive.value = val
+const handleCategoryClick = (category) => {
+  categoryActive.value = category
+  const categoryRef = proxy.$refs[`category-${category.name}`] // 获取点击分类对应的 DOM 元素
+  if (categoryRef?.length) {
+    const scrollWrapper = proxy.$refs.scrollWrapper // 获取右侧滚动容器
+    const categoryOffsetTop = categoryRef[0].offsetTop
+
+    // 滚动到对应位置
+    scrollWrapper.scrollTo({ top: categoryOffsetTop, behavior: 'smooth' })
+  }
 }
 
 /** 初始化 */

+ 91 - 43
src/views/bpm/processInstance/detail/ProcessInstanceTimeline.vue

@@ -24,7 +24,7 @@
           </div>
         </div>
       </template>
-      <div class="flex flex-col items-start">
+      <div class="flex flex-col items-start gap2" :id="`activity-task-${activity.id}`">
         <!-- 第一行:节点名称、时间 -->
         <div class="flex w-full">
           <div class="font-bold"> {{ activity.name }}</div>
@@ -36,53 +36,79 @@
             {{ getApprovalNodeTime(activity) }}
           </div>
         </div>
-        <div class="flex items-center flex-wrap mt-1">
+        <!-- 需要自定义选择审批人 -->
+        <div
+          class="flex flex-wrap gap2 items-center"
+          v-if="
+            startUserSelectTasks?.length > 0 && Array.isArray(startUserSelectAssignees[activity.id])
+          "
+        >
+          <!--  && activity.nodeType === NodeType.USER_TASK_NODE -->
+          <el-button
+            class="!px-8px"
+            @click="handleSelectUser(activity.id, customApprover[activity.id])"
+          >
+            <Icon icon="fa:user-plus" />
+          </el-button>
+          <div
+            v-for="(user, idx1) in customApprover[activity.id]"
+            :key="idx1"
+            class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600 position-relative"
+          >
+            <el-avatar :size="28" v-if="user.avatar" :src="user.avatar" />
+            <el-avatar :size="28" v-else>
+              {{ user.nickname.substring(0, 1) }}
+            </el-avatar>
+            {{ user.nickname }}
+          </div>
+        </div>
+        <div v-else class="flex items-center flex-wrap mt-1 gap2">
           <!-- 情况一:遍历每个审批节点下的【进行中】task 任务 -->
-          <div v-for="(task, idx) in activity.tasks" :key="idx" class="flex items-center">
-            <div class="flex flex-col pr-2 gap2">
+          <div v-for="(task, idx) in activity.tasks" :key="idx" class="flex flex-col pr-2 gap2">
+            <div
+              class="position-relative flex flex-wrap gap2"
+              v-if="task.assigneeUser || task.ownerUser"
+            >
+              <!-- 信息:头像昵称 -->
               <div
-                class="position-relative pt-2 flex flex-wrap gap2"
-                v-if="task.assigneeUser || task.ownerUser"
+                class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600 position-relative"
               >
-                <!-- 信息:头像昵称 -->
+                <template v-if="task.assigneeUser?.avatar || task.assigneeUser?.nickname">
+                  <el-avatar
+                    :size="28"
+                    v-if="task.assigneeUser?.avatar"
+                    :src="task.assigneeUser?.avatar"
+                  />
+                  <el-avatar :size="28" v-else>
+                    {{ task.assigneeUser?.nickname.substring(0, 1) }}
+                  </el-avatar>
+                  {{ task.assigneeUser?.nickname }}
+                </template>
+                <template v-else-if="task.ownerUser?.avatar || task.ownerUser?.nickname">
+                  <el-avatar
+                    :size="28"
+                    v-if="task.ownerUser?.avatar"
+                    :src="task.ownerUser?.avatar"
+                  />
+                  <el-avatar :size="28" v-else>
+                    {{ task.ownerUser?.nickname.substring(0, 1) }}
+                  </el-avatar>
+                  {{ task.ownerUser?.nickname }}
+                </template>
+                <!-- 信息:任务 ICON -->
                 <div
-                  class="bg-gray-100 h-35px rounded-3xl flex items-center p-8px gap-2 dark:color-gray-600 position-relative"
+                  v-if="onlyStatusIconShow.includes(task.status)"
+                  class="position-absolute top-22px left-26px bg-#fff rounded-full flex items-center p-2px"
                 >
-                  <template v-if="task.assigneeUser?.avatar || task.assigneeUser?.nickname">
-                    <el-avatar
-                      :size="28"
-                      v-if="task.assigneeUser?.avatar"
-                      :src="task.assigneeUser?.avatar"
-                    />
-                    <el-avatar :size="28" v-else>
-                      {{ task.assigneeUser?.nickname.substring(0, 1) }}
-                    </el-avatar>
-                    {{ task.assigneeUser?.nickname }}
-                  </template>
-                  <template v-else-if="task.ownerUser?.avatar || task.ownerUser?.nickname">
-                    <el-avatar
-                      :size="28"
-                      v-if="task.ownerUser?.avatar"
-                      :src="task.ownerUser?.avatar"
-                    />
-                    <el-avatar :size="28" v-else>
-                      {{ task.ownerUser?.nickname.substring(0, 1) }}
-                    </el-avatar>
-                    {{ task.ownerUser?.nickname }}
-                  </template>
-                  <!-- 信息:任务 ICON -->
-                  <div
-                    v-if="onlyStatusIconShow.includes(task.status)"
-                    class="position-absolute top-22px left-26px bg-#fff rounded-full flex items-center p-2px"
-                  >
-                    <Icon
-                      :size="12"
-                      :icon="statusIconMap2[task.status]?.icon"
-                      :color="statusIconMap2[task.status]?.color"
-                    />
-                  </div>
+                  <Icon
+                    :size="12"
+                    :icon="statusIconMap2[task.status]?.icon"
+                    :color="statusIconMap2[task.status]?.color"
+                  />
                 </div>
               </div>
+            </div>
+            <teleport defer :to="`#activity-task-${activity.id}`">
               <div
                 v-if="
                   task.reason &&
@@ -92,7 +118,7 @@
               >
                 审批意见:{{ task.reason }}
               </div>
-            </div>
+            </teleport>
           </div>
           <!-- 情况二:遍历每个审批节点下的【候选的】task 任务。例如说,1)依次审批,2)未来的审批任务等 -->
           <div
@@ -121,6 +147,9 @@
       </div>
     </el-timeline-item>
   </el-timeline>
+
+  <!-- 用户选择弹窗 -->
+  <UserSelectForm ref="userSelectFormRef" @confirm="handleUserSelectConfirm" />
 </template>
 
 <script lang="ts" setup>
@@ -141,9 +170,13 @@ withDefaults(
   defineProps<{
     activityNodes: ProcessInstanceApi.ApprovalNodeInfo[] // 审批节点信息
     showStatusIcon?: boolean // 是否显示头像右下角状态图标
+    startUserSelectTasks?: any[] // 发起人需要选择审批人的用户任务列表
+    startUserSelectAssignees?: any // 发起人选择审批人的数据
   }>(),
   {
-    showStatusIcon: true // 默认值为 true
+    showStatusIcon: true, // 默认值为 true
+    startUserSelectTasks: () => [], // 默认值为空数组
+    startUserSelectAssignees: () => {}
   }
 )
 
@@ -241,4 +274,19 @@ const getApprovalNodeTime = (node: ProcessInstanceApi.ApprovalNodeInfo) => {
     return `${formatDate(node.startTime)}`
   }
 }
+
+// 选择自定义审批人
+const userSelectFormRef = ref()
+const handleSelectUser = (activityId, selectedList) => {
+  userSelectFormRef.value.open(activityId, selectedList)
+}
+const emit = defineEmits<{
+  selectUserConfirm: [id: any, userList: any[]]
+}>()
+const customApprover: any = ref({})
+// 选择完成
+const handleUserSelectConfirm = (activityId, userList) => {
+  customApprover.value[activityId] = userList || []
+  emit('selectUserConfirm', activityId, userList)
+}
 </script>