+
{{ detail?.title || '审批详情' }}
关闭
+
+
+ 拒绝
+
+
+ 打回
+
+
+ 加签
+
+
+ 转交
+
+
+ 同意
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -37,21 +168,48 @@
import { computed, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { showToast } from 'vant'
-import { fetchOaInstanceDetail } from '../api/oa.js'
+import {
+ fetchOaInstanceDetail,
+ fetchCurrentUser,
+ fetchApproveOaTask,
+ fetchRejectOaTask,
+ fetchReturnOaTask,
+ fetchAddSignOaTask,
+ fetchTransferOaTask
+} from '../api/oa.js'
import { prepareMobileFormSchema } from '../oa/schema-prep.js'
import { statusText, statusTone } from '../oa/status-label.js'
import OaProgressTimeline from '../oa/components/OaProgressTimeline.vue'
import OaPulseLoading from '../oa/components/OaPulseLoading.vue'
+import OaPersonSelect from '../oa/components/OaPersonSelect.vue'
const route = useRoute()
const loading = ref(true)
+const acting = ref(false)
const detail = ref(null)
const formData = ref({})
const formRule = ref([])
const formOption = ref({})
+const currentUserId = ref('')
+
+const commentVisible = ref(false)
+const commentKind = ref('approve') // approve | reject | return
+const commentText = ref('')
+
+const addSignVisible = ref(false)
+const addSignUsers = ref([])
+const addSignComment = ref('')
+
+const transferVisible = ref(false)
+const transferUser = ref(null)
+const transferComment = ref('')
-/** instanceId 必填;mode 默认 view;taskId 本任务忽略办理动作 */
const instanceId = computed(() => String(route.query.instanceId || '').trim())
+const queryTaskId = computed(() => String(route.query.taskId || '').trim())
+const mode = computed(() => {
+ const m = String(route.query.mode || 'view').trim()
+ return m || 'view'
+})
const timelineItems = computed(() => {
const list = detail.value?.progress
@@ -74,6 +232,50 @@ const timelineItems = computed(() => {
}))
})
+function isPendingApprovalForMe(t) {
+ const uid = currentUserId.value
+ if (!uid || !t) return false
+ return (
+ t.taskType === 'approval' &&
+ t.status === 'pending' &&
+ String(t.assigneeUserId || '') === uid
+ )
+}
+
+/** 优先 query.taskId(须为本用户待办审批任务),否则取本用户第一条 pending approval */
+const myPendingTask = computed(() => {
+ const list = detail.value?.tasks || []
+ const qid = queryTaskId.value
+ if (qid) {
+ const matched = list.find((t) => String(t.id || '') === qid && isPendingApprovalForMe(t))
+ if (matched) return matched
+ }
+ return list.find((t) => isPendingApprovalForMe(t))
+})
+
+const canApprove = computed(() => !!myPendingTask.value && mode.value === 'approve')
+
+/** 转交:排除自己 + 本节点已有 pending approval 的办理人 */
+const transferExcludeIds = computed(() => {
+ const me = currentUserId.value
+ const pendingIds = (detail.value?.tasks || [])
+ .filter((t) => t.status === 'pending' && t.taskType === 'approval')
+ .map((t) => String(t.assigneeUserId || '').trim())
+ .filter(Boolean)
+ return [...new Set([me, ...pendingIds].filter(Boolean))]
+})
+
+const commentTitle = computed(() => {
+ if (commentKind.value === 'reject') return '拒绝'
+ if (commentKind.value === 'return') return '打回'
+ return '同意'
+})
+
+const commentPlaceholder = computed(() => {
+ if (commentKind.value === 'return') return '请填写打回意见(必填)'
+ return '审批意见(可选)'
+})
+
/** 通知宿主关闭(对齐 OaStart closeToMini) */
function closeToMini(payload) {
const data = payload || { action: 'oa-detail-close' }
@@ -119,6 +321,125 @@ function parseFormData(d) {
return {}
}
+function openComment(kind) {
+ if (!myPendingTask.value) return
+ commentKind.value = kind
+ commentText.value = ''
+ commentVisible.value = true
+}
+
+function onCommentBeforeClose(action) {
+ if (action !== 'confirm') return true
+ if (commentKind.value === 'return') {
+ const c = String(commentText.value || '').trim()
+ if (!c) {
+ showToast('打回意见必填')
+ return false
+ }
+ }
+ void submitComment()
+ return false
+}
+
+async function submitComment() {
+ const task = myPendingTask.value
+ if (!task) {
+ commentVisible.value = false
+ return
+ }
+ const comment = String(commentText.value || '').trim()
+ acting.value = true
+ try {
+ const kind = commentKind.value
+ if (kind === 'approve') {
+ await fetchApproveOaTask(task.id, comment || undefined)
+ commentVisible.value = false
+ showToast('已同意')
+ closeToMini({ action: 'oa-detail-done', message: '已同意' })
+ } else if (kind === 'reject') {
+ await fetchRejectOaTask(task.id, comment || undefined)
+ commentVisible.value = false
+ showToast('已拒绝')
+ closeToMini({ action: 'oa-detail-done', message: '已拒绝' })
+ } else if (kind === 'return') {
+ await fetchReturnOaTask(task.id, comment)
+ commentVisible.value = false
+ showToast('已打回')
+ closeToMini({ action: 'oa-detail-done', message: '已打回' })
+ }
+ } catch (e) {
+ showToast(e?.message || '操作失败')
+ } finally {
+ acting.value = false
+ }
+}
+
+function openAddSign() {
+ if (!myPendingTask.value) return
+ addSignUsers.value = []
+ addSignComment.value = ''
+ addSignVisible.value = true
+}
+
+async function submitAddSign() {
+ const task = myPendingTask.value
+ if (!task) return
+ const userIds = (Array.isArray(addSignUsers.value) ? addSignUsers.value : [])
+ .map((u) => String(u?.id || '').trim())
+ .filter(Boolean)
+ if (!userIds.length) {
+ showToast('请选择加签人')
+ return
+ }
+ acting.value = true
+ try {
+ const c = String(addSignComment.value || '').trim()
+ await fetchAddSignOaTask(task.id, {
+ userIds,
+ comment: c || undefined
+ })
+ addSignVisible.value = false
+ showToast('已加签')
+ closeToMini({ action: 'oa-detail-done', message: '已加签' })
+ } catch (e) {
+ showToast(e?.message || '加签失败')
+ } finally {
+ acting.value = false
+ }
+}
+
+function openTransfer() {
+ if (!myPendingTask.value) return
+ transferUser.value = null
+ transferComment.value = ''
+ transferVisible.value = true
+}
+
+async function submitTransfer() {
+ const task = myPendingTask.value
+ if (!task) return
+ const targetUserId = String(transferUser.value?.id || '').trim()
+ if (!targetUserId) {
+ showToast('请选择转交人')
+ return
+ }
+ acting.value = true
+ try {
+ const c = String(transferComment.value || '').trim()
+ await fetchTransferOaTask(task.id, {
+ targetUserId,
+ comment: c || undefined
+ })
+ transferVisible.value = false
+ showToast('已转交')
+ closeToMini({ action: 'oa-detail-done', message: '已转交' })
+ } catch (e) {
+ showToast(e?.message || '转交失败')
+ } finally {
+ acting.value = false
+ }
+}
+
async function bootstrap() {
loading.value = true
try {
@@ -127,9 +448,12 @@ async function bootstrap() {
showToast('缺少 instanceId')
return
}
- // mode 默认 view;taskId 本任务忽略(办理动作留给后续)
- const d = await fetchOaInstanceDetail(id)
+ const [me, d] = await Promise.all([
+ fetchCurrentUser().catch(() => null),
+ fetchOaInstanceDetail(id)
+ ])
+ currentUserId.value = String(me?.id || me?.userId || '').trim()
detail.value = d
try {
document.title = d?.title || d?.appName || '审批详情'
@@ -178,6 +502,9 @@ onMounted(() => {
flex: 1;
padding: 12px 12px 96px;
}
+.oa-detail__body--approve {
+ padding-bottom: 128px;
+}
.oa-detail__hd {
display: flex;
flex-wrap: wrap;
@@ -230,20 +557,26 @@ onMounted(() => {
left: 0;
z-index: 20;
display: flex;
+ flex-wrap: wrap;
align-items: center;
- padding: 12px 14px calc(12px + env(safe-area-inset-bottom));
+ gap: 8px;
+ padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
background: #fff;
border-top: 1px solid #eef2f6;
box-shadow: 0 -4px 16px rgba(15, 23, 42, 0.04);
}
.oa-detail__btn {
margin: 0;
- height: 44px;
+ height: 40px;
+ min-width: 0;
+ flex: 1 1 auto;
border-radius: 12px !important;
- font-size: 15px;
+ font-size: 14px;
+ padding: 0 10px !important;
}
.oa-detail__btn--close {
- flex: 1;
+ flex: 0 0 auto;
+ min-width: 64px;
border: none !important;
background: #f1f5f9 !important;
color: #334155 !important;
@@ -252,4 +585,58 @@ onMounted(() => {
.oa-detail__btn--close:active {
opacity: 0.85;
}
+.oa-detail__btn--ghost {
+ border: 1px solid #e2e8f0 !important;
+ background: #fff !important;
+ color: #334155 !important;
+ font-weight: 600;
+}
+.oa-detail__btn--primary {
+ flex: 1 1 88px;
+ border: none !important;
+ background: var(--brand-green, #22c55e) !important;
+ color: #fff !important;
+ font-weight: 700;
+}
+.oa-detail__dialog-body {
+ padding: 8px 16px 4px;
+}
+.oa-act-sheet {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+ background: #fff;
+}
+.oa-act-sheet__hd {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 14px 16px;
+ border-bottom: 1px solid #f1f5f9;
+ font-size: 15px;
+ color: #64748b;
+}
+.oa-act-sheet__hd strong {
+ color: #0f172a;
+ font-size: 16px;
+}
+.oa-act-sheet__ok {
+ color: var(--brand-green, #22c55e);
+ font-weight: 700;
+}
+.oa-act-sheet__body {
+ flex: 1;
+ overflow: auto;
+ padding: 12px 16px 24px;
+ -webkit-overflow-scrolling: touch;
+}
+.oa-act-sheet__label {
+ margin: 12px 0 8px;
+ font-size: 13px;
+ font-weight: 600;
+ color: #334155;
+}
+.oa-act-sheet__label:first-child {
+ margin-top: 0;
+}