Bläddra i källkod

feat: [bpm] 新增流程详情 timeline

jason 3 månader sedan
förälder
incheckning
3b9696ac03

+ 5 - 0
src/api/bpm/definition/index.ts

@@ -19,3 +19,8 @@ export interface ProcessDefinition {
 export function getProcessDefinitionList(params?: { suspensionState?: number }) {
   return http.get<ProcessDefinition[]>('/bpm/process-definition/list', params)
 }
+
+/** 获取流程定义详情 */
+export function getProcessDefinition(id?: string, key?: string) {
+  return http.get<ProcessDefinition>('/bpm/process-definition/get', { id, key })
+}

+ 1 - 1
src/api/bpm/processInstance/index.ts

@@ -108,7 +108,7 @@ export function getProcessInstance(id: string) {
 }
 
 /** 获取审批详情 */
-export function getApprovalDetail(params: { processInstanceId: string, activityId?: string, taskId?: string }) {
+export function getApprovalDetail(params: { processDefinitionId?: string, processInstanceId?: string, activityId?: string, taskId?: string, processVariablesStr?: string }) {
   return http.get<ApprovalDetail>('/bpm/process-instance/get-approval-detail', params)
 }
 

+ 33 - 0
src/components/system-select/user-picker.vue

@@ -1,5 +1,24 @@
 <template>
   <wd-select-picker
+    v-if="useDefaultSlot"
+    v-model="selectedId"
+    :label="label"
+    :label-width="label ? '180rpx' : '0'"
+    :columns="userList"
+    value-key="id"
+    label-key="nickname"
+    :type="type"
+    :prop="prop"
+    use-default-slot
+    filterable
+    :placeholder="placeholder"
+    @confirm="handleConfirm"
+  >
+    <slot />
+  </wd-select-picker>
+
+  <wd-select-picker
+    v-else
     v-model="selectedId"
     :label="label"
     :label-width="label ? '180rpx' : '0'"
@@ -25,15 +44,18 @@ const props = withDefaults(defineProps<{
   label?: string
   placeholder?: string
   prop?: string
+  useDefaultSlot?: boolean
 }>(), {
   type: 'checkbox',
   label: '',
   placeholder: '请选择',
   prop: '',
+  useDefaultSlot: false,
 })
 
 const emit = defineEmits<{
   (e: 'update:modelValue', value: number | number[] | undefined): void
+  (e: 'confirm', users: User[]): void
 }>()
 
 const userList = ref<User[]>([])
@@ -74,6 +96,17 @@ async function loadUserList() {
 /** 选择确认 */
 function handleConfirm({ value }: { value: any }) {
   emit('update:modelValue', value)
+
+  // 发出包含完整用户对象的 confirm 事件
+  if (Array.isArray(value)) {
+    const selectedUsers = userList.value.filter(user => value.includes(user.id))
+    emit('confirm', selectedUsers)
+  } else if (value) {
+    const selectedUser = userList.value.find(user => user.id === value)
+    emit('confirm', selectedUser ? [selectedUser] : [])
+  } else {
+    emit('confirm', [])
+  }
 }
 
 /** 初始化 */

+ 198 - 47
src/pages-bpm/oa/leave/create/index.vue

@@ -1,57 +1,76 @@
 <template>
-  <view class="yd-page-container">
+  <view class="yd-page-container pb-[76rpx]">
     <!-- 顶部导航栏 -->
     <wd-navbar
       title="发起请假"
       left-arrow placeholder safe-area-inset-top fixed
       @click-left="handleBack"
     />
+    <view class="mx-24rpx mt-24rpx overflow-hidden rounded-16rpx bg-white">
+      <!-- 表单内容 -->
+      <wd-form ref="formRef" :model="formData" :rules="formRules">
+        <wd-cell-group border title="请假信息">
+          <wd-picker
+            v-model="formData.type"
+            :columns="getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)"
+            label="请假类型"
+            label-width="200rpx"
+            prop="type"
+            :rules="[{ required: true, message: '请选择请假类型' }]"
+            placeholder="请选择请假类型"
+          />
+          <wd-datetime-picker
+            v-model="formData.startTime"
+            label="开始时间"
+            label-width="200rpx"
+            prop="startTime"
+            :rules="[{ required: true, message: '请选择开始时间' }]"
+            placeholder="请选择开始时间"
+          />
+          <wd-datetime-picker
+            v-model="formData.endTime"
+            label="结束时间"
+            label-width="200rpx"
+            prop="endTime"
+            :rules="[{ required: true, message: '请选择结束时间' }]"
+            placeholder="请选择结束时间"
+          />
+          <wd-textarea
+            v-model="formData.reason"
+            label="请假原因"
+            label-width="200rpx"
+            prop="reason"
+            :rules="[{ required: true, message: '请输入请假原因' }]"
+            placeholder="请输入请假原因"
+            :maxlength="200"
+            show-word-limit
+          />
+        </wd-cell-group>
+      </wd-form>
+    </view>
+    <!-- 流程预览卡片 -->
+    <view class="mx-24rpx mb-120rpx mt-24rpx rounded-16rpx bg-white">
+      <view class="p-24rpx">
+        <view class="mb-16rpx flex items-center justify-between">
+          <text class="text-28rpx text-[#333] font-bold">流程预览</text>
+          <wd-loading v-if="processTimeLineLoading" size="32rpx" />
+        </view>
 
-    <!-- 表单内容 -->
-    <wd-form ref="formRef" :model="formData" :rules="formRules">
-      <wd-cell-group border title="请假信息">
-        <wd-picker
-          v-model="formData.type"
-          :columns="getIntDictOptions(DICT_TYPE.BPM_OA_LEAVE_TYPE)"
-          label="请假类型"
-          label-width="200rpx"
-          prop="type"
-          :rules="[{ required: true, message: '请选择请假类型' }]"
-          placeholder="请选择请假类型"
-        />
-        <wd-datetime-picker
-          v-model="formData.startTime"
-          label="开始时间"
-          label-width="200rpx"
-          prop="startTime"
-          :rules="[{ required: true, message: '请选择开始时间' }]"
-          placeholder="请选择开始时间"
-        />
-        <wd-datetime-picker
-          v-model="formData.endTime"
-          label="结束时间"
-          label-width="200rpx"
-          prop="endTime"
-          :rules="[{ required: true, message: '请选择结束时间' }]"
-          placeholder="请选择结束时间"
+        <!-- 流程时间线 -->
+        <ProcessInstanceTimeline
+          v-if="activityNodes.length > 0"
+          :activity-nodes="activityNodes"
+          :show-status-icon="false"
+          :enable-approve-user-select="true"
+          @select-user-confirm="selectUserConfirm"
         />
-        <wd-textarea
-          v-model="formData.reason"
-          label="请假原因"
-          label-width="200rpx"
-          prop="reason"
-          :rules="[{ required: true, message: '请输入请假原因' }]"
-          placeholder="请输入请假原因"
-          :maxlength="200"
-          show-word-limit
-        />
-      </wd-cell-group>
-    </wd-form>
 
-    <!-- TODO:@jason:流程预览卡片 -->
-    <!-- 原始 vben 版本有流程节点预览和发起人选择审批人功能 -->
-    <!-- 参考:yudao-ui-admin-vben-v5/apps/web-antd/src/views/bpm/oa/leave/create.vue 第 40-50 行 -->
-    <!-- uniapp 端暂不实现流程节点预览,因为需要复杂的 ProcessInstanceTimeline 组件 -->
+        <!-- 无流程数据提示 -->
+        <view v-else-if="!processTimeLineLoading" class="py-40rpx text-center">
+          <text class="text-24rpx text-[#999]">暂无流程预览数据</text>
+        </view>
+      </view>
+    </view>
 
     <!-- 底部提交按钮 -->
     <view class="yd-detail-footer">
@@ -67,12 +86,16 @@
 <script lang="ts" setup>
 import type { FormInstance } from 'wot-design-uni/components/wd-form/types'
 import type { Leave } from '@/api/bpm/oa/leave'
-import { ref } from 'vue'
+import type { ApprovalNodeInfo } from '@/api/bpm/processInstance'
+import { computed, onMounted, ref, watch } from 'vue'
 import { useMessage, useToast } from 'wot-design-uni'
+import { getProcessDefinition } from '@/api/bpm/definition'
 import { createLeave } from '@/api/bpm/oa/leave'
+import { getApprovalDetail } from '@/api/bpm/processInstance'
 import { getIntDictOptions } from '@/hooks/useDict'
+import ProcessInstanceTimeline from '@/pages-bpm/processInstance/detail/components/time-line.vue'
 import { navigateBackPlus } from '@/utils'
-import { DICT_TYPE } from '@/utils/constants'
+import { BpmCandidateStrategyEnum, BpmNodeIdEnum, DICT_TYPE } from '@/utils/constants'
 
 definePage({
   style: {
@@ -84,6 +107,16 @@ definePage({
 const toast = useToast()
 const message = useMessage()
 const formLoading = ref(false)
+const processTimeLineLoading = ref(false) // 流程预览加载状态
+
+// 流程相关数据
+const processDefineKey = 'oa_leave' // 流程定义 Key
+const processDefinitionId = ref('')
+const activityNodes = ref<ApprovalNodeInfo[]>([]) // 审批节点信息
+const startUserSelectTasks = ref<any[]>([]) // 发起人需要选择审批人的用户任务列表
+const startUserSelectAssignees = ref<any>({}) // 发起人选择审批人的数据
+const tempStartUserSelectAssignees = ref<any>({}) // 临时保存的审批人数据
+
 const formData = ref<Partial<Leave>>({
   type: undefined,
   startTime: undefined,
@@ -98,6 +131,16 @@ const formRules = {
 }
 const formRef = ref<FormInstance>()
 
+// 计算请假天数
+const leaveDays = computed(() => {
+  if (!formData.value.startTime || !formData.value.endTime) {
+    return 0
+  }
+  const start = new Date(formData.value.startTime)
+  const end = new Date(formData.value.endTime)
+  return Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24))
+})
+
 /** 返回上一页 */
 function handleBack() {
   message.confirm({
@@ -110,6 +153,58 @@ function handleBack() {
   })
 }
 
+/** 获取流程审批详情 */
+async function getProcessApprovalDetail() {
+  if (!processDefinitionId.value) {
+    return
+  }
+
+  processTimeLineLoading.value = true
+  try {
+    const data = await getApprovalDetail({
+      processDefinitionId: processDefinitionId.value,
+      activityId: BpmNodeIdEnum.START_USER_NODE_ID,
+      processVariablesStr: JSON.stringify({
+        day: leaveDays.value,
+      }),
+    })
+
+    if (!data) {
+      toast.show('查询不到审批详情信息!')
+      return
+    }
+
+    // 获取审批节点,显示 Timeline 的数据
+    activityNodes.value = data.activityNodes || []
+
+    // 获取发起人自选的任务
+    startUserSelectTasks.value = data.activityNodes?.filter(
+      (node: ApprovalNodeInfo) =>
+        BpmCandidateStrategyEnum.START_USER_SELECT === node.candidateStrategy,
+    ) || []
+
+    // 恢复之前的选择审批人
+    if (startUserSelectTasks.value.length > 0) {
+      for (const node of startUserSelectTasks.value) {
+        startUserSelectAssignees.value[node.id]
+          = tempStartUserSelectAssignees.value[node.id]
+            && tempStartUserSelectAssignees.value[node.id].length > 0
+            ? tempStartUserSelectAssignees.value[node.id]
+            : []
+      }
+    }
+  } catch (error) {
+    console.error('获取流程审批详情失败:', error)
+  } finally {
+    processTimeLineLoading.value = false
+  }
+}
+
+/** 选择审批人确认 */
+function selectUserConfirm(id: string, userList: any[]) {
+  startUserSelectAssignees.value[id] = userList?.map((item: any) => item.id) || []
+}
+
 /** 提交表单 */
 async function handleSubmit() {
   const { valid } = await formRef.value!.validate()
@@ -121,9 +216,28 @@ async function handleSubmit() {
     return
   }
 
+  // 校验指定审批人
+  if (startUserSelectTasks.value.length > 0) {
+    for (const userTask of startUserSelectTasks.value) {
+      if (
+        Array.isArray(startUserSelectAssignees.value[userTask.id])
+        && startUserSelectAssignees.value[userTask.id].length === 0
+      ) {
+        toast.show(`请选择${userTask.name}的审批人`)
+        return
+      }
+    }
+  }
+
   formLoading.value = true
   try {
-    await createLeave(formData.value)
+    const submitData = { ...formData.value }
+    // 设置指定审批人
+    if (startUserSelectTasks.value.length > 0) {
+      submitData.startUserSelectAssignees = startUserSelectAssignees.value
+    }
+
+    await createLeave(submitData)
     uni.showToast({ title: '提交成功', icon: 'success' })
     setTimeout(() => {
       navigateBackPlus('/pages-bpm/oa/leave/index')
@@ -132,4 +246,41 @@ async function handleSubmit() {
     formLoading.value = false
   }
 }
+
+// 监听表单数据变化,重新预测流程节点
+watch(
+  () => [formData.value.startTime, formData.value.endTime, formData.value.type],
+  (newValue, oldValue) => {
+    if (!oldValue || !oldValue.some(v => v !== undefined)) {
+      return
+    }
+    if (newValue && newValue.some(v => v !== undefined)) {
+      // 记录之前的节点审批人
+      tempStartUserSelectAssignees.value = { ...startUserSelectAssignees.value }
+      startUserSelectAssignees.value = {}
+      // 加载最新的审批详情,主要用于节点预测
+      getProcessApprovalDetail()
+    }
+  },
+  { deep: true },
+)
+
+// 组件初始化
+onMounted(async () => {
+  try {
+    // 获取流程定义详情
+    const processDefinitionDetail = await getProcessDefinition(undefined, processDefineKey)
+    if (!processDefinitionDetail) {
+      toast.show('OA 请假的流程模型未配置,请检查!')
+      return
+    }
+    processDefinitionId.value = processDefinitionDetail.id
+
+    // 获取流程审批详情
+    await getProcessApprovalDetail()
+  } catch (error) {
+    console.error('初始化流程失败:', error)
+    toast.show('初始化流程失败,请稍后重试')
+  }
+})
 </script>

+ 188 - 210
src/pages-bpm/processInstance/detail/components/time-line.vue

@@ -1,218 +1,207 @@
 <template>
-  <view class="mx-24rpx mt-24rpx overflow-hidden rounded-16rpx bg-white">
-    <view class="p-24rpx">
-      <view class="mb-16rpx flex">
-        <text class="text-28rpx text-[#333] font-bold">审批进度</text>
-      </view>
-      <!-- 遍历每个审批节点 -->
-      <view
-        v-for="(activity, index) in activityNodes"
-        :key="activity.id || index"
-        class="relative pb-24rpx pl-60rpx"
-      >
-        <!-- 时间线圆点 -->
-        <view
-          class="absolute left-12rpx top-8rpx h-36rpx w-36rpx flex items-center justify-center rounded-full bg-blue-500"
-        >
-          <!-- 节点类型图标 -->
-          <wd-icon
-            :name="getApprovalNodeTypeIcon(activity.nodeType)"
-            size="20rpx"
-            color="white"
-          />
-        </view>
+  <!-- 遍历每个审批节点 -->
+  <view
+    v-for="(activity, index) in activityNodes"
+    :key="activity.id || index"
+    class="relative pb-24rpx pl-80rpx"
+  >
+    <!-- 时间线圆点 -->
+    <view
+      class="absolute left-12rpx top-8rpx h-52rpx w-52rpx flex items-center justify-center rounded-full bg-blue-500"
+    >
+      <!-- 节点类型图标 -->
+      <wd-icon
+        :name="getApprovalNodeTypeIcon(activity.nodeType)"
+        size="32rpx"
+        color="white"
+      />
+    </view>
 
-        <!-- 状态小图标 -->
-        <view
-          v-if="showStatusIcon"
-          class="absolute left-38rpx top-32rpx h-12rpx w-12rpx flex items-center justify-center border-2 border-white rounded-full"
-          :style="{ backgroundColor: getApprovalNodeColor(activity.status) }"
-        >
-          <wd-icon
-            :name="getApprovalNodeIcon(activity.status, activity.nodeType)"
-            size="12rpx"
-            color="white"
-          />
+    <!-- 状态小图标 -->
+    <view
+      v-if="showStatusIcon"
+      class="absolute left-48rpx top-44rpx h-16rpx w-16rpx flex items-center justify-center border-2 border-white rounded-full"
+      :style="{ backgroundColor: getApprovalNodeColor(activity.status) }"
+    >
+      <wd-icon
+        :name="getApprovalNodeIcon(activity.status, activity.nodeType)"
+        size="12rpx"
+        color="white"
+      />
+    </view>
+
+    <!-- 连接线 -->
+    <view
+      v-if="index < activityNodes.length - 1"
+      class="absolute bottom-0 left-38rpx top-64rpx w-2rpx bg-[#e5e5e5]"
+    />
+
+    <!-- 节点内容 -->
+    <view class="ml-8rpx">
+      <!-- 第一行:节点名称、时间 -->
+      <view class="mb-8rpx flex items-center justify-between">
+        <view class="flex items-center">
+          <text class="text-28rpx text-[#333] font-bold">{{ activity.name }}</text>
+          <text v-if="activity.status === BpmTaskStatusEnum.SKIP" class="ml-8rpx text-24rpx text-[#999]">
+            【跳过】
+          </text>
         </view>
+        <text
+          v-if="activity.status !== BpmTaskStatusEnum.NOT_START && getApprovalNodeTime(activity)"
+          class="text-22rpx text-[#999]"
+        >
+          {{ getApprovalNodeTime(activity) }}
+        </text>
+      </view>
 
-        <!-- 连接线 -->
-        <view
-          v-if="index < activityNodes.length - 1"
-          class="absolute bottom-0 left-28rpx top-48rpx w-2rpx bg-[#e5e5e5]"
-        />
-
-        <!-- 节点内容 -->
-        <view class="ml-8rpx">
-          <!-- 第一行:节点名称、时间 -->
-          <view class="mb-8rpx flex items-center justify-between">
-            <view class="flex items-center">
-              <text class="text-28rpx text-[#333] font-bold">{{ activity.name }}</text>
-              <text v-if="activity.status === BpmTaskStatusEnum.SKIP" class="ml-8rpx text-24rpx text-[#999]">
-                【跳过】
-              </text>
-            </view>
-            <text
-              v-if="activity.status !== BpmTaskStatusEnum.NOT_START && getApprovalNodeTime(activity)"
-              class="text-22rpx text-[#999]"
-            >
-              {{ getApprovalNodeTime(activity) }}
-            </text>
-          </view>
+      <!-- 子流程节点 -->
+      <view v-if="activity.nodeType === BpmNodeTypeEnum.CHILD_PROCESS_NODE" class="mb-16rpx">
+        <wd-button
+          type="primary"
+          plain
+          size="small"
+          :disabled="!activity.processInstanceId"
+          @click="handleChildProcess(activity)"
+        >
+          查看子流程
+        </wd-button>
+      </view>
 
-          <!-- 子流程节点 -->
-          <view v-if="activity.nodeType === BpmNodeTypeEnum.CHILD_PROCESS_NODE" class="mb-16rpx">
-            <wd-button
-              type="primary"
-              plain
-              size="small"
-              :disabled="!activity.processInstanceId"
-              @click="handleChildProcess(activity)"
+      <!-- 需要自定义选择审批人 -->
+      <view v-if="shouldShowCustomUserSelect(activity)" class="mb-16rpx">
+        <view class="flex flex-wrap items-center">
+          <!-- 添加用户按钮 -->
+          <UserPicker
+            :model-value="getSelectedUserIds(activity.id)"
+            type="checkbox"
+            use-default-slot
+            @confirm="(users) => handleCustomUserSelectConfirm(activity.id, users)"
+          >
+            <view
+              class="mb-8rpx mr-16rpx h-48rpx w-48rpx flex items-center justify-center border-indigo-500 rounded-lg border-solid"
             >
-              查看子流程
-            </wd-button>
+              <wd-icon name="user-add" size="32rpx" color="blue" />
+            </view>
+          </UserPicker>
+          <!-- 已选择的用户 -->
+          <view
+            v-for="(user, userIndex) in customApproveUsers[activity.id]"
+            :key="user.id || userIndex"
+            class="mb-8rpx mr-16rpx flex items-center rounded-32rpx bg-[#f5f5f5] pr-16rpx"
+          >
+            <view class="mr-8rpx h-48rpx w-48rpx flex items-center justify-center rounded-full bg-[#1890ff] text-24rpx text-white">
+              {{ user.nickname?.[0] || '?' }}
+            </view>
+            <text class="text-24rpx text-[#333]">{{ user.nickname }}</text>
           </view>
+        </view>
+      </view>
 
-          <!-- 需要自定义选择审批人 -->
-          <view v-if="shouldShowCustomUserSelect(activity)" class="mb-16rpx">
-            <view class="flex flex-wrap items-center">
-              <!-- 添加用户按钮 -->
-              <view
-                class="mb-8rpx mr-16rpx h-64rpx w-64rpx flex items-center justify-center rounded-full bg-[#1890ff] text-white"
-                @click="handleSelectUser(activity.id, customApproveUsers[activity.id] || [])"
-              >
-                <wd-icon name="add-outline" size="32rpx" color="white" />
-              </view>
-
-              <!-- 已选择的用户 -->
-              <view
-                v-for="(user, userIndex) in customApproveUsers[activity.id]"
-                :key="user.id || userIndex"
-                class="mb-8rpx mr-16rpx flex items-center rounded-32rpx bg-[#f5f5f5] pr-16rpx"
-              >
-                <view class="mr-8rpx h-48rpx w-48rpx flex items-center justify-center rounded-full bg-[#1890ff] text-24rpx text-white">
-                  {{ user.nickname?.[0] || '?' }}
+      <!-- 审批人员列表 -->
+      <view v-else class="mb-16rpx">
+        <!-- 情况一:遍历每个审批节点下的【进行中】task 任务 -->
+        <view v-if="activity.tasks && activity.tasks.length > 0">
+          <view
+            v-for="(task, taskIndex) in activity.tasks"
+            :key="taskIndex"
+            class="mb-16rpx"
+          >
+            <!-- 审批人信息 -->
+            <view v-if="task.assigneeUser || task.ownerUser" class="mb-8rpx flex items-center">
+              <!-- TODO @jason 显示用户头像 -->
+              <view class="relative mr-8rpx h-48rpx w-48rpx flex items-center justify-center rounded-full bg-[#1890ff] text-24rpx text-white">
+                {{ (task.assigneeUser?.nickname || task.ownerUser?.nickname)?.[0] || '?' }}
+
+                <!-- 任务状态小图标 -->
+                <view
+                  v-if="showStatusIcon && shouldShowTaskStatusIcon(task.status)"
+                  class="absolute right--4rpx top-36rpx h-16rpx w-16rpx flex items-center justify-center border-2 border-white rounded-full"
+                  :style="{ backgroundColor: getApprovalNodeColor(task.status) }"
+                >
+                  <wd-icon
+                    :name="getApprovalNodeIcon(task.status, activity.nodeType)"
+                    size="12rpx"
+                    color="white"
+                  />
                 </view>
-                <text class="text-24rpx text-[#333]">{{ user.nickname }}</text>
               </view>
-            </view>
-          </view>
 
-          <!-- 审批人员列表 -->
-          <view v-else class="mb-16rpx">
-            <!-- 情况一:遍历每个审批节点下的【进行中】task 任务 -->
-            <view v-if="activity.tasks && activity.tasks.length > 0">
-              <view
-                v-for="(task, taskIndex) in activity.tasks"
-                :key="taskIndex"
-                class="mb-16rpx"
-              >
-                <!-- 审批人信息 -->
-                <view v-if="task.assigneeUser || task.ownerUser" class="mb-8rpx flex items-center">
-                  <!-- TODO @jason 显示用户头像 -->
-                  <view class="relative mr-8rpx h-48rpx w-48rpx flex items-center justify-center rounded-full bg-[#1890ff] text-24rpx text-white">
-                    {{ (task.assigneeUser?.nickname || task.ownerUser?.nickname)?.[0] || '?' }}
-
-                    <!-- 任务状态小图标 -->
-                    <view
-                      v-if="showStatusIcon && shouldShowTaskStatusIcon(task.status)"
-                      class="absolute right--4rpx top-36rpx h-16rpx w-16rpx flex items-center justify-center border-2 border-white rounded-full"
-                      :style="{ backgroundColor: getApprovalNodeColor(task.status) }"
+              <view class="flex-1">
+                <view class="flex items-center justify-between">
+                  <view class="flex items-center">
+                    <text class="text-26rpx text-[#333]">
+                      {{ task.assigneeUser?.nickname || task.ownerUser?.nickname }}
+                    </text>
+                    <text
+                      v-if="task.assigneeUser?.deptName || task.ownerUser?.deptName"
+                      class="ml-8rpx text-22rpx text-[#999]"
                     >
-                      <wd-icon
-                        :name="getApprovalNodeIcon(task.status, activity.nodeType)"
-                        size="12rpx"
-                        color="white"
-                      />
-                    </view>
-                  </view>
-
-                  <view class="flex-1">
-                    <view class="flex items-center justify-between">
-                      <view class="flex items-center">
-                        <text class="text-26rpx text-[#333]">
-                          {{ task.assigneeUser?.nickname || task.ownerUser?.nickname }}
-                        </text>
-                        <text
-                          v-if="task.assigneeUser?.deptName || task.ownerUser?.deptName"
-                          class="ml-8rpx text-22rpx text-[#999]"
-                        >
-                          {{ task.assigneeUser?.deptName || task.ownerUser?.deptName }}
-                        </text>
-                      </view>
-                    </view>
-                    <view class="mt-4rpx flex items-center">
-                      <text :class="getStatusTextClass(task.status)" class="text-24rpx">
-                        {{ getStatusText(task.status) }}
-                      </text>
-                    </view>
+                      {{ task.assigneeUser?.deptName || task.ownerUser?.deptName }}
+                    </text>
                   </view>
                 </view>
-
-                <!-- 审批意见 -->
-                <view
-                  v-if="shouldShowApprovalReason(task, activity.nodeType)"
-                  class="mt-8rpx rounded-8rpx bg-[#f5f5f5] p-16rpx"
-                >
-                  <text class="text-24rpx text-[#666]">审批意见:{{ task.reason }}</text>
-                </view>
-
-                <!-- 签名 -->
-                <view
-                  v-if="task.signPicUrl && activity.nodeType === BpmNodeTypeEnum.USER_TASK_NODE"
-                  class="mt-8rpx rounded-8rpx bg-[#f5f5f5] p-16rpx"
-                >
-                  <text class="text-24rpx text-[#666]">签名:</text>
-                  <image
-                    :src="task.signPicUrl"
-                    class="ml-8rpx h-80rpx w-192rpx"
-                    mode="aspectFit"
-                    @click="previewImage(task.signPicUrl)"
-                  />
+                <view class="mt-4rpx flex items-center">
+                  <text :class="getStatusTextClass(task.status)" class="text-24rpx">
+                    {{ getStatusText(task.status) }}
+                  </text>
                 </view>
               </view>
             </view>
 
-            <!-- 情况二:遍历每个审批节点下的【候选的】task 任务 -->
-            <view v-if="activity.candidateUsers && activity.candidateUsers.length > 0">
+            <!-- 审批意见 -->
+            <view
+              v-if="shouldShowApprovalReason(task, activity.nodeType)"
+              class="mt-8rpx rounded-8rpx bg-[#f5f5f5] p-16rpx"
+            >
+              <text class="text-24rpx text-[#666]">审批意见:{{ task.reason }}</text>
+            </view>
+
+            <!-- 签名 -->
+            <view
+              v-if="task.signPicUrl && activity.nodeType === BpmNodeTypeEnum.USER_TASK_NODE"
+              class="mt-8rpx rounded-8rpx bg-[#f5f5f5] p-16rpx"
+            >
+              <text class="text-24rpx text-[#666]">签名:</text>
+              <image
+                :src="task.signPicUrl"
+                class="ml-8rpx h-80rpx w-192rpx"
+                mode="aspectFit"
+                @click="previewImage(task.signPicUrl)"
+              />
+            </view>
+          </view>
+        </view>
+
+        <!-- 情况二:遍历每个审批节点下的【候选的】task 任务 -->
+        <view v-if="activity.candidateUsers && activity.candidateUsers.length > 0">
+          <view
+            v-for="(user, userIndex) in activity.candidateUsers"
+            :key="userIndex"
+            class="mb-8rpx flex items-center"
+          >
+            <view class="relative mr-8rpx h-48rpx w-48rpx flex items-center justify-center rounded-full bg-[#1890ff] text-24rpx text-white">
+              {{ user.nickname?.[0] || '?' }}
+
+              <!-- 候选状态图标 -->
               <view
-                v-for="(user, userIndex) in activity.candidateUsers"
-                :key="userIndex"
-                class="mb-8rpx flex items-center"
+                v-if="showStatusIcon"
+                class="absolute right--4rpx top-36rpx h-16rpx w-16rpx flex items-center justify-center border-2 border-white rounded-full"
+                :style="{ backgroundColor: getApprovalNodeColor(BpmTaskStatusEnum.NOT_START) }"
               >
-                <view class="relative mr-8rpx h-48rpx w-48rpx flex items-center justify-center rounded-full bg-[#1890ff] text-24rpx text-white">
-                  {{ user.nickname?.[0] || '?' }}
-
-                  <!-- 候选状态图标 -->
-                  <view
-                    v-if="showStatusIcon"
-                    class="absolute right--4rpx top-36rpx h-16rpx w-16rpx flex items-center justify-center border-2 border-white rounded-full"
-                    :style="{ backgroundColor: getApprovalNodeColor(BpmTaskStatusEnum.NOT_START) }"
-                  >
-                    <wd-icon name="time" size="12rpx" color="white" />
-                  </view>
-                </view>
-
-                <view class="flex-1">
-                  <text class="text-26rpx text-[#333]">{{ user.nickname }}</text>
-                  <text v-if="user.deptName" class="ml-8rpx text-22rpx text-[#999]">
-                    {{ user.deptName }}
-                  </text>
-                </view>
+                <wd-icon name="time" size="12rpx" color="white" />
               </view>
             </view>
+
+            <view class="flex-1">
+              <text class="text-26rpx text-[#333]">{{ user.nickname }}</text>
+              <text v-if="user.deptName" class="ml-8rpx text-22rpx text-[#999]">
+                {{ user.deptName }}
+              </text>
+            </view>
           </view>
         </view>
       </view>
     </view>
-    <!-- 用户选择弹窗 -->
-    <UserPicker
-      v-if="showUserPicker"
-      v-model="selectedUserIds"
-      type="checkbox"
-      :visible="showUserPicker"
-      @confirm="handleUserSelectConfirm"
-      @cancel="handleUserSelectCancel"
-    />
   </view>
 </template>
 
@@ -243,8 +232,8 @@ const emit = defineEmits<{
 const statusIconMap: Record<string, { color: string, icon: string }> = {
   '-2': { color: '#909398', icon: 'skip-forward' }, // 跳过
   '-1': { color: '#909398', icon: 'time' }, // 审批未开始
-  '0': { color: '#ff943e', icon: 'view' }, // 待审批
-  '1': { color: '#448ef7', icon: 'view' }, // 审批中
+  '0': { color: '#f59e0b', icon: 'refresh1' }, // 待审批
+  '1': { color: '#f59e0b', icon: 'refresh1' }, // 审批中
   '2': { color: '#00b32a', icon: 'check' }, // 审批通过
   '3': { color: '#f46b6c', icon: 'close' }, // 审批不通过
   '4': { color: '#cccccc', icon: 'delete' }, // 已取消
@@ -355,27 +344,16 @@ function getStatusText(status: number) {
   return textMap[status] || '未知'
 }
 
-/** 打开选择用户弹窗 */
-function handleSelectUser(activityId: string, selectedList: any[]) {
-  selectedActivityNodeId.value = activityId
-  selectedUserIds.value = selectedList.map(item => item.id)
-  showUserPicker.value = true
-}
-
-/** 选择用户完成 */
-function handleUserSelectConfirm(userList: any[]) {
-  if (!selectedActivityNodeId.value) {
-    return
-  }
-  customApproveUsers.value[selectedActivityNodeId.value] = userList || []
-  emit('selectUserConfirm', selectedActivityNodeId.value, userList)
-  showUserPicker.value = false
+/** 用户选择确认 */
+function handleCustomUserSelectConfirm(activityId: string, users: any[]) {
+  customApproveUsers.value[activityId] = users || []
+  emit('selectUserConfirm', activityId, users)
 }
 
-/** 取消选择用户 */
-function handleUserSelectCancel() {
-  showUserPicker.value = false
-  selectedUserIds.value = []
+/** 获取选中的用户ID数组 */
+function getSelectedUserIds(activityId: string): number[] {
+  const users = customApproveUsers.value[activityId] || []
+  return users.map(user => user.id).filter(id => id !== undefined)
 }
 
 /** 跳转子流程 */

+ 11 - 3
src/pages-bpm/processInstance/detail/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <view class="yd-page-container">
+  <view class="yd-page-container pb-[80rpx]">
     <!-- 顶部导航栏 -->
     <wd-navbar
       title="审批详情"
@@ -40,12 +40,20 @@
     <FormDetail :process-definition="processDefinition" :process-instance="processInstance" />
 
     <!-- 区域:审批进度 -->
-    <ProcessInstanceTimeline :activity-nodes="activityNodes" />
+    <view class="mx-24rpx mt-24rpx rounded-16rpx bg-white">
+      <view class="p-24rpx">
+        <view class="mb-16rpx flex">
+          <text class="text-28rpx text-[#333] font-bold">审批进度</text>
+        </view>
+        <!-- 流程时间线 -->
+        <ProcessInstanceTimeline :activity-nodes="activityNodes" />
+      </view>
+    </view>
 
     <!-- TODO 待开发:区域:流程评论 -->
 
     <!-- 区域:底部操作栏 -->
-    <ProcessInstanceOperationButton ref="operationButtonRef" :process-instance="processInstance" />
+    <ProcessInstanceOperationButton ref="operationButtonRef" />
   </view>
 </template>