feat(oa): terminate on return and copy form to start a new instance

Align with DingTalk: returned/rejected/cancelled are terminal; progress
shows return on the approver task only; done list includes returned;
deprecate same-instance resubmit and source-instance linking.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-07-15 15:19:20 +08:00
1 parent 8b2d8284c7
commit c9711d0dce
20 files changed
+74586 -32336

No files matched your search

-12
View File
@@ -210,7 +210,6 @@ export function fetchStartOaInstance(data: {
initiatorDeptId?: string
initiatorSelectUserIds?: string[]
relatedInstanceIds?: string[]
sourceInstanceId?: string
}) {
return request.post({
url: `${BASE}/instances`,
@@ -269,17 +268,6 @@ export function fetchReturnOaTask(taskId: string, comment: string) {
})
}
export function fetchResubmitOaInstance(
id: string,
data: { formData?: Record<string, any>; initiatorSelectUserIds?: string[] }
) {
return request.post({
url: `${BASE}/instances/${id}/resubmit`,
data,
showSuccessMessage: true
})
}
export function fetchAddSignOaTask(
taskId: string,
data: { userIds: string[]; comment?: string }
@@ -45,12 +45,16 @@
查看
</el-button>
<el-button
v-if="kind === 'mine' && row.status === 'cancelled' && row.appId"
v-if="
kind === 'mine' &&
row.appId &&
['returned', 'cancelled', 'rejected'].includes(row.status)
"
class="btn-op-item"
@click="openResubmitFromCancelled(row)"
@click="openCopyRestart(row)"
>
<el-icon><RefreshRight /></el-icon>
修改并重新提交
复制并重新发起
</el-button>
</template>
</el-table-column>
@@ -76,9 +80,9 @@
const rows = ref<any[]>([])
function statusType(s: string) {
if (s === 'pending' || s === 'running' || s === 'returned') return 'warning'
if (s === 'pending' || s === 'running') return 'warning'
if (s === 'approved' || s === 'completed' || s === 'done') return 'success'
if (s === 'rejected' || s === 'cancelled') return 'danger'
if (s === 'rejected' || s === 'cancelled' || s === 'returned') return 'danger'
return 'info'
}
@@ -92,7 +96,7 @@
completed: '已完成',
cancelled: '已撤销',
done: '已结束',
returned: '待修改'
returned: '已打回'
}
return map[s] || s
}
@@ -103,11 +107,11 @@
router.push({ path: `/system-manage/oa-approval/instances/${id}` })
}
function openResubmitFromCancelled(row: any) {
function openCopyRestart(row: any) {
if (!row?.appId || !row?.id) return
router.push({
path: '/system-manage/oa-approval/start',
query: { appId: row.appId, sourceInstanceId: row.id }
query: { appId: row.appId, copyFromInstanceId: row.id }
})
}
@@ -47,14 +47,13 @@
加签
</el-button>
<el-button
v-if="detail?.status === 'returned' && isInitiator"
v-if="canCopyRestart"
class="btn-main"
type="primary"
:loading="acting"
@click="openResubmit"
@click="goCopyRestart"
>
<el-icon><Check /></el-icon>
修改并再次提交
<el-icon><RefreshRight /></el-icon>
复制并重新发起
</el-button>
<el-button
v-if="detail?.status === 'running' && isInitiator"
@@ -69,16 +68,6 @@
</div>
<div v-loading="loading" class="oa-detail__body">
<p v-if="detail?.sourceInstanceId" class="oa-detail__link-tip">
本单由已撤销审批重提,
<el-button link type="primary" @click="goInstance(detail.sourceInstanceId)">查看原单</el-button>
</p>
<p v-if="detail?.resubmittedInstanceId" class="oa-detail__link-tip">
已有重提单,
<el-button link type="primary" @click="goInstance(detail.resubmittedInstanceId)">
查看重提单
</el-button>
</p>
<section class="oa-detail__section">
<h3>表单内容</h3>
<div class="oa-detail__form-wrap">
@@ -96,18 +85,13 @@
<h3>审批进度</h3>
<div class="oa-detail__timeline-wrap">
<el-timeline v-if="timelineItems.length">
<template v-for="(item, idx) in timelineItems" :key="item.key">
<div
v-if="idx === 0 || item.roundNo !== timelineItems[idx - 1].roundNo"
class="oa-tl__round"
>
第 {{ item.roundNo }} 轮
</div>
<el-timeline-item
:type="timelineType(item.status)"
:timestamp="item.timestamp"
placement="top"
>
<el-timeline-item
v-for="item in timelineItems"
:key="item.key"
:type="timelineType(item.status)"
:timestamp="item.timestamp"
placement="top"
>
<div class="oa-tl">
<div class="oa-tl__title">
{{ item.title }}
@@ -115,7 +99,6 @@
{{ statusText(item.status) }}
</el-tag>
<el-tag v-if="item.taskType === 'cc'" size="small" type="info">抄送</el-tag>
<el-tag v-if="item.kind === 'return'" size="small" type="warning">打回</el-tag>
</div>
<div
v-if="item.assignee && item.kind !== 'pending_node' && item.kind !== 'terminal'"
@@ -128,8 +111,7 @@
</div>
<div v-if="item.commentText" class="oa-tl__comment">意见:{{ item.commentText }}</div>
</div>
</el-timeline-item>
</template>
</el-timeline-item>
</el-timeline>
<p v-else class="oa-detail__empty">暂无任务记录</p>
</div>
@@ -190,14 +172,13 @@
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { ArrowLeft, Check, Close, RefreshLeft, Plus } from '@element-plus/icons-vue'
import { ArrowLeft, Check, Close, Plus, RefreshLeft, RefreshRight } from '@element-plus/icons-vue'
import { fetchUserList } from '@/api/admin'
import {
fetchAddSignOaTask,
fetchApproveOaTask,
fetchOaInstanceDetail,
fetchRejectOaTask,
fetchResubmitOaInstance,
fetchReturnOaTask,
fetchWithdrawOaInstance
} from '@/api/oaApproval'
@@ -264,10 +245,15 @@
const canApprove = computed(() => !!myPendingTask.value)
const canCopyRestart = computed(() => {
if (!isInitiator.value || !detail.value) return false
return ['returned', 'cancelled', 'rejected'].includes(String(detail.value.status || ''))
})
function statusType(s: string) {
if (s === 'pending' || s === 'running' || s === 'returned') return 'warning'
if (s === 'pending' || s === 'running') return 'warning'
if (s === 'approved' || s === 'completed' || s === 'done' || s === 'submitted') return 'success'
if (s === 'rejected' || s === 'cancelled') return 'danger'
if (s === 'rejected' || s === 'cancelled' || s === 'returned') return 'danger'
if (s === 'not_reached') return 'info'
return 'info'
}
@@ -283,17 +269,15 @@
done: '已结束',
submitted: '已提交',
not_reached: '未到达',
returned: '待修改'
returned: '已打回'
}
// 任务被打回后取消:展示「已取消」
if (s === 'cancelled') return '已撤销'
return map[s] || s
}
function timelineType(s: string) {
if (s === 'approved' || s === 'completed' || s === 'done' || s === 'submitted') return 'success'
if (s === 'rejected' || s === 'cancelled') return 'danger'
if (s === 'pending' || s === 'returned') return 'warning'
if (s === 'rejected' || s === 'cancelled' || s === 'returned') return 'danger'
if (s === 'pending') return 'warning'
if (s === 'not_reached') return 'info'
return 'primary'
}
@@ -395,26 +379,17 @@
}
}
async function openResubmit() {
await ElMessageBox.confirm(
'将使用当前表单内容再次提交并重新走审批。若需改字段,请先在下方确认表单后确定。',
'再次提交',
{ type: 'warning' }
)
acting.value = true
try {
await fetchResubmitOaInstance(String(route.params.id || ''), {
formData: { ...(viewFormData.value || detail.value?.formDataJson || {}) }
})
await load()
} finally {
acting.value = false
function goCopyRestart() {
const appId = String(detail.value?.appId || '').trim()
const id = String(route.params.id || '').trim()
if (!appId || !id) {
ElMessage.warning('无法复制:缺少应用或实例信息')
return
}
}
function goInstance(id: string) {
if (!id) return
router.push({ path: `/system-manage/oa-approval/instances/${id}` })
router.push({
path: '/system-manage/oa-approval/start',
query: { appId, copyFromInstanceId: id }
})
}
async function loadUsers() {
@@ -184,7 +184,6 @@
const formOption = ref<Record<string, any>>({ submitBtn: false, resetBtn: false })
const fapi = ref<any>(null)
const submitting = ref(false)
const sourceInstanceId = ref('')
const needInitiatorSelect = ref(false)
const initiatorSelectUserIds = ref<string[]>([])
const userLoading = ref(false)
@@ -350,11 +349,9 @@
formData: { ...formData.value },
initiatorSelectUserIds: needInitiatorSelect.value
? [...initiatorSelectUserIds.value]
: undefined,
sourceInstanceId: sourceInstanceId.value || undefined
: undefined
})
dlgVisible.value = false
sourceInstanceId.value = ''
if (data?.id) {
router.push({ path: `/system-manage/oa-approval/instances/${data.id}` })
}
@@ -370,15 +367,14 @@
templates.value = tpl?.list || []
groupOptions.value = groupsRes?.list || []
const qAppId = String(route.query.appId || '').trim()
const qSource = String(route.query.sourceInstanceId || '').trim()
const qCopyFrom = String(route.query.copyFromInstanceId || '').trim()
if (qAppId) {
const row = templates.value.find((t: any) => t.id === qAppId)
if (row) {
sourceInstanceId.value = qSource
await onPick(row)
if (qSource) {
if (qCopyFrom) {
try {
const old = await fetchOaInstanceDetail(qSource)
const old = await fetchOaInstanceDetail(qCopyFrom)
formData.value = { ...(old?.formDataJson || {}) }
} catch {
/* 预填失败仍可手工填 */