|
|
@@ -4,7 +4,7 @@
|
|
|
<OaPulseLoading text="加载审批详情…" />
|
|
|
</div>
|
|
|
<template v-else>
|
|
|
- <div class="oa-detail__body">
|
|
|
+ <div class="oa-detail__body" :class="{ 'oa-detail__body--approve': canApprove }">
|
|
|
<header class="oa-detail__hd">
|
|
|
<h1 class="oa-detail__title">{{ detail?.title || '审批详情' }}</h1>
|
|
|
<span
|
|
|
@@ -28,8 +28,139 @@
|
|
|
<van-button class="oa-detail__btn oa-detail__btn--close" @click="onClose">
|
|
|
关闭
|
|
|
</van-button>
|
|
|
+ <template v-if="canApprove">
|
|
|
+ <van-button
|
|
|
+ class="oa-detail__btn oa-detail__btn--ghost"
|
|
|
+ :loading="acting"
|
|
|
+ :disabled="acting"
|
|
|
+ @click="openComment('reject')"
|
|
|
+ >
|
|
|
+ 拒绝
|
|
|
+ </van-button>
|
|
|
+ <van-button
|
|
|
+ v-if="detail?.allowReturn !== false"
|
|
|
+ class="oa-detail__btn oa-detail__btn--ghost"
|
|
|
+ :loading="acting"
|
|
|
+ :disabled="acting"
|
|
|
+ @click="openComment('return')"
|
|
|
+ >
|
|
|
+ 打回
|
|
|
+ </van-button>
|
|
|
+ <van-button
|
|
|
+ v-if="detail?.allowAddSign !== false"
|
|
|
+ class="oa-detail__btn oa-detail__btn--ghost"
|
|
|
+ :loading="acting"
|
|
|
+ :disabled="acting"
|
|
|
+ @click="openAddSign"
|
|
|
+ >
|
|
|
+ 加签
|
|
|
+ </van-button>
|
|
|
+ <van-button
|
|
|
+ class="oa-detail__btn oa-detail__btn--ghost"
|
|
|
+ :loading="acting"
|
|
|
+ :disabled="acting"
|
|
|
+ @click="openTransfer"
|
|
|
+ >
|
|
|
+ 转交
|
|
|
+ </van-button>
|
|
|
+ <van-button
|
|
|
+ class="oa-detail__btn oa-detail__btn--primary"
|
|
|
+ type="primary"
|
|
|
+ :loading="acting"
|
|
|
+ :disabled="acting"
|
|
|
+ @click="openComment('approve')"
|
|
|
+ >
|
|
|
+ 同意
|
|
|
+ </van-button>
|
|
|
+ </template>
|
|
|
</footer>
|
|
|
</template>
|
|
|
+
|
|
|
+ <!-- 同意 / 拒绝 / 打回意见 -->
|
|
|
+ <van-dialog
|
|
|
+ v-model:show="commentVisible"
|
|
|
+ :title="commentTitle"
|
|
|
+ show-cancel-button
|
|
|
+ confirm-button-text="确定"
|
|
|
+ cancel-button-text="取消"
|
|
|
+ :before-close="onCommentBeforeClose"
|
|
|
+ >
|
|
|
+ <div class="oa-detail__dialog-body">
|
|
|
+ <van-field
|
|
|
+ v-model="commentText"
|
|
|
+ type="textarea"
|
|
|
+ rows="3"
|
|
|
+ autosize
|
|
|
+ maxlength="500"
|
|
|
+ show-word-limit
|
|
|
+ :placeholder="commentPlaceholder"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </van-dialog>
|
|
|
+
|
|
|
+ <!-- 加签 -->
|
|
|
+ <van-popup
|
|
|
+ v-model:show="addSignVisible"
|
|
|
+ position="bottom"
|
|
|
+ round
|
|
|
+ :style="{ height: '72%' }"
|
|
|
+ >
|
|
|
+ <div class="oa-act-sheet">
|
|
|
+ <div class="oa-act-sheet__hd">
|
|
|
+ <span @click="addSignVisible = false">取消</span>
|
|
|
+ <strong>加签</strong>
|
|
|
+ <span class="oa-act-sheet__ok" @click="submitAddSign">确定</span>
|
|
|
+ </div>
|
|
|
+ <div class="oa-act-sheet__body">
|
|
|
+ <p class="oa-act-sheet__label">加签人</p>
|
|
|
+ <OaPersonSelect v-model="addSignUsers" :multiple="true" />
|
|
|
+ <p class="oa-act-sheet__label">说明(可选)</p>
|
|
|
+ <van-field
|
|
|
+ v-model="addSignComment"
|
|
|
+ type="textarea"
|
|
|
+ rows="2"
|
|
|
+ autosize
|
|
|
+ maxlength="500"
|
|
|
+ show-word-limit
|
|
|
+ placeholder="加签说明"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </van-popup>
|
|
|
+
|
|
|
+ <!-- 转交 -->
|
|
|
+ <van-popup
|
|
|
+ v-model:show="transferVisible"
|
|
|
+ position="bottom"
|
|
|
+ round
|
|
|
+ :style="{ height: '72%' }"
|
|
|
+ >
|
|
|
+ <div class="oa-act-sheet">
|
|
|
+ <div class="oa-act-sheet__hd">
|
|
|
+ <span @click="transferVisible = false">取消</span>
|
|
|
+ <strong>转交</strong>
|
|
|
+ <span class="oa-act-sheet__ok" @click="submitTransfer">确定</span>
|
|
|
+ </div>
|
|
|
+ <div class="oa-act-sheet__body">
|
|
|
+ <p class="oa-act-sheet__label">转交给</p>
|
|
|
+ <OaPersonSelect
|
|
|
+ v-model="transferUser"
|
|
|
+ :multiple="false"
|
|
|
+ :exclude-ids="transferExcludeIds"
|
|
|
+ />
|
|
|
+ <p class="oa-act-sheet__label">说明(可选)</p>
|
|
|
+ <van-field
|
|
|
+ v-model="transferComment"
|
|
|
+ type="textarea"
|
|
|
+ rows="2"
|
|
|
+ autosize
|
|
|
+ maxlength="500"
|
|
|
+ show-word-limit
|
|
|
+ placeholder="转交说明"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </van-popup>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
@@ -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;
|
|
|
+}
|
|
|
</style>
|