فهرست منبع

feat(web-admin): add OA task transfer action on instance detail

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 3 ماه پیش
والد
کامیت
5350db1dc4
2فایلهای تغییر یافته به همراه141 افزوده شده و 27 حذف شده
  1. 11 0
      web-admin/src/api/oaApproval.ts
  2. 130 27
      web-admin/src/views/oa-approval/instances/detail.vue

+ 11 - 0
web-admin/src/api/oaApproval.ts

@@ -288,6 +288,17 @@ export function fetchAddSignOaTask(
   })
   })
 }
 }
 
 
+export function fetchTransferOaTask(
+  taskId: string,
+  data: { targetUserId: string; comment?: string }
+) {
+  return request.post({
+    url: `${BASE}/tasks/${taskId}/transfer`,
+    data,
+    showSuccessMessage: true
+  })
+}
+
 export function fetchWithdrawOaInstance(instanceId: string) {
 export function fetchWithdrawOaInstance(instanceId: string) {
   return request.post({
   return request.post({
     url: `${BASE}/instances/${instanceId}/withdraw`,
     url: `${BASE}/instances/${instanceId}/withdraw`,

+ 130 - 27
web-admin/src/views/oa-approval/instances/detail.vue

@@ -46,6 +46,15 @@
             <el-icon><Plus /></el-icon>
             <el-icon><Plus /></el-icon>
             加签
             加签
           </el-button>
           </el-button>
+          <el-button
+            v-if="canApprove"
+            class="btn-ghost"
+            :loading="acting"
+            @click="openTransfer"
+          >
+            <el-icon><Position /></el-icon>
+            转交
+          </el-button>
           <el-button
           <el-button
             v-if="canCopyRestart"
             v-if="canCopyRestart"
             class="btn-main"
             class="btn-main"
@@ -103,8 +112,6 @@
               v-model="addSignUserIds"
               v-model="addSignUserIds"
               multiple
               multiple
               filterable
               filterable
-              collapse-tags
-              collapse-tags-tooltip
               placeholder="选择加签用户"
               placeholder="选择加签用户"
               style="width: 100%"
               style="width: 100%"
               :loading="userLoading"
               :loading="userLoading"
@@ -132,6 +139,51 @@
           </el-button>
           </el-button>
         </template>
         </template>
       </el-dialog>
       </el-dialog>
+
+      <el-dialog
+        v-model="transferVisible"
+        title="转交"
+        width="520px"
+        class="dialog-form-layout"
+        align-center
+        append-to-body
+        modal-class="protocol-dialog-mask"
+        destroy-on-close
+        :close-on-click-modal="false"
+      >
+        <el-form label-position="top">
+          <el-form-item label="转交给">
+            <el-select
+              v-model="transferUserId"
+              filterable
+              clearable
+              placeholder="选择用户"
+              style="width: 100%"
+              :loading="userLoading"
+            >
+              <el-option
+                v-for="u in transferUserOptions"
+                :key="u.id"
+                :label="`${u.nickName}(${u.userNo})`"
+                :value="u.id"
+              />
+            </el-select>
+          </el-form-item>
+          <el-form-item label="说明(可选)">
+            <el-input v-model="transferComment" type="textarea" :rows="2" />
+          </el-form-item>
+        </el-form>
+        <template #footer>
+          <el-button class="btn-ghost" @click="transferVisible = false">
+            <el-icon><Close /></el-icon>
+            取消
+          </el-button>
+          <el-button class="btn-main" type="primary" :loading="acting" @click="submitTransfer">
+            <el-icon><Check /></el-icon>
+            确定转交
+          </el-button>
+        </template>
+      </el-dialog>
     </section>
     </section>
   </div>
   </div>
 </template>
 </template>
@@ -140,7 +192,7 @@
   import { computed, onMounted, ref } from 'vue'
   import { computed, onMounted, ref } from 'vue'
   import { useRoute, useRouter } from 'vue-router'
   import { useRoute, useRouter } from 'vue-router'
   import { ElMessage, ElMessageBox } from 'element-plus'
   import { ElMessage, ElMessageBox } from 'element-plus'
-  import { ArrowLeft, Check, Close, Plus, RefreshLeft, RefreshRight } from '@element-plus/icons-vue'
+  import { ArrowLeft, Check, Close, Plus, Position, RefreshLeft, RefreshRight } from '@element-plus/icons-vue'
   import { fetchUserList } from '@/api/admin'
   import { fetchUserList } from '@/api/admin'
   import {
   import {
     fetchAddSignOaTask,
     fetchAddSignOaTask,
@@ -148,6 +200,7 @@
     fetchOaInstanceDetail,
     fetchOaInstanceDetail,
     fetchRejectOaTask,
     fetchRejectOaTask,
     fetchReturnOaTask,
     fetchReturnOaTask,
+    fetchTransferOaTask,
     fetchWithdrawOaInstance
     fetchWithdrawOaInstance
   } from '@/api/oaApproval'
   } from '@/api/oaApproval'
   import OaProgressChain from '@/components/oa-approval/OaProgressChain.vue'
   import OaProgressChain from '@/components/oa-approval/OaProgressChain.vue'
@@ -177,6 +230,9 @@
   const addSignVisible = ref(false)
   const addSignVisible = ref(false)
   const addSignUserIds = ref<string[]>([])
   const addSignUserIds = ref<string[]>([])
   const addSignComment = ref('')
   const addSignComment = ref('')
+  const transferVisible = ref(false)
+  const transferUserId = ref('')
+  const transferComment = ref('')
   const userLoading = ref(false)
   const userLoading = ref(false)
   const userOptions = ref<Array<{ id: string; nickName: string; userNo: string }>>([])
   const userOptions = ref<Array<{ id: string; nickName: string; userNo: string }>>([])
 
 
@@ -186,29 +242,38 @@
   const timelineItems = computed(() => {
   const timelineItems = computed(() => {
     const list = detail.value?.progress
     const list = detail.value?.progress
     if (!Array.isArray(list) || !list.length) return []
     if (!Array.isArray(list) || !list.length) return []
-    return list.map((p: any, idx: number) => ({
-      key: `${p.kind}-${p.nodeKey || p.taskId || idx}-${p.roundNo || 1}`,
-      title: p.title || '-',
-      status: p.status || 'info',
-      timestamp: p.timestamp || '',
-      assignee: p.assigneeUserName || p.assigneeUserId || '',
-      resolveSource: p.resolveSource || '',
-      commentText: p.commentText || '',
-      taskType: p.taskType || '',
-      kind: p.kind || '',
-      roundNo: p.roundNo || 1,
-      signMode: p.signMode || '',
-      people: Array.isArray(p.people)
-        ? p.people.map((x: any) => ({
-            id: x.id || '',
-            nickName: x.nickName || x.id || '',
-            status: x.status || '',
-            timestamp: x.timestamp || '',
-            commentText: x.commentText || '',
-            taskId: x.taskId || ''
-          }))
-        : []
-    }))
+    return list
+      .filter((p: any) => {
+        // 打回只保留审批人节点,过滤独立终态「已打回」行
+        const kind = String(p?.kind || '')
+        const status = String(p?.status || '')
+        const title = String(p?.title || '').trim()
+        if (kind === 'terminal' && (status === 'returned' || title === '已打回')) return false
+        return true
+      })
+      .map((p: any, idx: number) => ({
+        key: `${p.kind}-${p.nodeKey || p.taskId || idx}-${p.roundNo || 1}`,
+        title: p.title || '-',
+        status: p.status || 'info',
+        timestamp: p.timestamp || '',
+        assignee: p.assigneeUserName || p.assigneeUserId || '',
+        resolveSource: p.resolveSource || '',
+        commentText: p.commentText || '',
+        taskType: p.taskType || '',
+        kind: p.kind || '',
+        roundNo: p.roundNo || 1,
+        signMode: p.signMode || '',
+        people: Array.isArray(p.people)
+          ? p.people.map((x: any) => ({
+              id: x.id || '',
+              nickName: x.nickName || x.id || '',
+              status: x.status || '',
+              timestamp: x.timestamp || '',
+              commentText: x.commentText || '',
+              taskId: x.taskId || ''
+            }))
+          : []
+      }))
   })
   })
 
 
   const currentUserId = computed(() => {
   const currentUserId = computed(() => {
@@ -230,6 +295,16 @@
 
 
   const canApprove = computed(() => !!myPendingTask.value)
   const canApprove = computed(() => !!myPendingTask.value)
 
 
+  const transferUserOptions = computed(() => {
+    const me = currentUserId.value
+    const pendingIds = new Set(
+      (detail.value?.tasks || [])
+        .filter((t: any) => t.status === 'pending' && t.taskType === 'approval')
+        .map((t: any) => String(t.assigneeUserId || ''))
+    )
+    return userOptions.value.filter((u) => u.id !== me && !pendingIds.has(u.id))
+  })
+
   const canCopyRestart = computed(() => {
   const canCopyRestart = computed(() => {
     if (!isInitiator.value || !detail.value) return false
     if (!isInitiator.value || !detail.value) return false
     return ['returned', 'cancelled', 'rejected'].includes(String(detail.value.status || ''))
     return ['returned', 'cancelled', 'rejected'].includes(String(detail.value.status || ''))
@@ -254,7 +329,8 @@
       done: '已结束',
       done: '已结束',
       submitted: '已提交',
       submitted: '已提交',
       not_reached: '未到达',
       not_reached: '未到达',
-      returned: '已打回'
+      returned: '已打回',
+      transferred: '已转交'
     }
     }
     return map[s] || s
     return map[s] || s
   }
   }
@@ -414,6 +490,33 @@
     }
     }
   }
   }
 
 
+  async function openTransfer() {
+    transferUserId.value = ''
+    transferComment.value = ''
+    transferVisible.value = true
+    if (!userOptions.value.length) await loadUsers()
+  }
+
+  async function submitTransfer() {
+    const task = myPendingTask.value
+    if (!task) return
+    if (!transferUserId.value) {
+      ElMessage.warning('请选择转交人')
+      return
+    }
+    acting.value = true
+    try {
+      await fetchTransferOaTask(task.id, {
+        targetUserId: transferUserId.value,
+        comment: transferComment.value || undefined
+      })
+      transferVisible.value = false
+      await load()
+    } finally {
+      acting.value = false
+    }
+  }
+
   function goBack() {
   function goBack() {
     router.back()
     router.back()
   }
   }