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

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-07-23 15:17:25 +08:00
1 parent 35c60f95e8
commit 5350db1dc4
2 files changed
+141 -27

No files matched your search

+11
View File
@@ -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) {
return request.post({
url: `${BASE}/instances/${instanceId}/withdraw`,
@@ -46,6 +46,15 @@
<el-icon><Plus /></el-icon>
加签
</el-button>
<el-button
v-if="canApprove"
class="btn-ghost"
:loading="acting"
@click="openTransfer"
>
<el-icon><Position /></el-icon>
转交
</el-button>
<el-button
v-if="canCopyRestart"
class="btn-main"
@@ -103,8 +112,6 @@
v-model="addSignUserIds"
multiple
filterable
collapse-tags
collapse-tags-tooltip
placeholder="选择加签用户"
style="width: 100%"
:loading="userLoading"
@@ -132,6 +139,51 @@
</el-button>
</template>
</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>
</div>
</template>
@@ -140,7 +192,7 @@
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
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 {
fetchAddSignOaTask,
@@ -148,6 +200,7 @@
fetchOaInstanceDetail,
fetchRejectOaTask,
fetchReturnOaTask,
fetchTransferOaTask,
fetchWithdrawOaInstance
} from '@/api/oaApproval'
import OaProgressChain from '@/components/oa-approval/OaProgressChain.vue'
@@ -177,6 +230,9 @@
const addSignVisible = ref(false)
const addSignUserIds = ref<string[]>([])
const addSignComment = ref('')
const transferVisible = ref(false)
const transferUserId = ref('')
const transferComment = ref('')
const userLoading = ref(false)
const userOptions = ref<Array<{ id: string; nickName: string; userNo: string }>>([])
@@ -186,29 +242,38 @@
const timelineItems = computed(() => {
const list = detail.value?.progress
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(() => {
@@ -230,6 +295,16 @@
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(() => {
if (!isInitiator.value || !detail.value) return false
return ['returned', 'cancelled', 'rejected'].includes(String(detail.value.status || ''))
@@ -254,7 +329,8 @@
done: '已结束',
submitted: '已提交',
not_reached: '未到达',
returned: '已打回'
returned: '已打回',
transferred: '已转交'
}
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() {
router.back()
}