From d5287765ec0c6964373d9074a553acf7bb943fad Mon Sep 17 00:00:00 2001 From: zlwu <704541015@qq.com> Date: Wed, 12 Aug 2026 16:25:23 +0800 Subject: [PATCH] feat(oa): add pending list and approve reject return add-sign transfer Co-authored-by: Cursor --- mobile-h5/pages.json | 4 +- mobile-h5/pages/oa/pending/index.vue | 424 +++++++++++++++++- .../src/oa/components/OaPersonSelect.vue | 18 +- mobile-oa-h5/src/views/OaDetail.vue | 405 ++++++++++++++++- 4 files changed, 833 insertions(+), 18 deletions(-) diff --git a/mobile-h5/pages.json b/mobile-h5/pages.json index 123dd4e7..c1c59232 100644 --- a/mobile-h5/pages.json +++ b/mobile-h5/pages.json @@ -79,7 +79,9 @@ "style": { "navigationBarTitleText": "待办中心", "navigationBarBackgroundColor": "#ecfdf5", - "navigationBarTextStyle": "black" + "navigationBarTextStyle": "black", + "enablePullDownRefresh": true, + "backgroundTextStyle": "dark" } }, { diff --git a/mobile-h5/pages/oa/pending/index.vue b/mobile-h5/pages/oa/pending/index.vue index 29f4439c..e7017b38 100644 --- a/mobile-h5/pages/oa/pending/index.vue +++ b/mobile-h5/pages/oa/pending/index.vue @@ -1,12 +1,424 @@ + + + diff --git a/mobile-oa-h5/src/oa/components/OaPersonSelect.vue b/mobile-oa-h5/src/oa/components/OaPersonSelect.vue index 3b13eeba..269e3c91 100644 --- a/mobile-oa-h5/src/oa/components/OaPersonSelect.vue +++ b/mobile-oa-h5/src/oa/components/OaPersonSelect.vue @@ -104,6 +104,8 @@ const props = defineProps({ disabled: { type: Boolean, default: false }, multiple: { type: Boolean, default: true }, max: { type: [Number, String], default: null }, + /** 不可选用户 id(如转交时排除自己及本节点已有待办审批人) */ + excludeIds: { type: Array, default: () => [] }, formCreateInject: { type: Object, default: null } }) const emit = defineEmits(['update:modelValue', 'change']) @@ -117,14 +119,26 @@ const nameMap = ref(new Map()) const userOptions = ref([]) const multiple = computed(() => !!props.multiple) +const excludeSet = computed(() => { + const set = new Set() + for (const x of props.excludeIds || []) { + const id = String(x || '').trim() + if (id) set.add(id) + } + return set +}) const normalized = computed(() => normalizeIdNameModelValue(props.modelValue, multiple.value) ) const displayText = computed(() => formatIdNameDisplay(normalized.value)) const filtered = computed(() => { + const excluded = excludeSet.value + const base = excluded.size + ? userOptions.value.filter((u) => !excluded.has(u.id)) + : userOptions.value const q = keyword.value.trim().toLowerCase() - if (!q) return userOptions.value - return userOptions.value.filter( + if (!q) return base + return base.filter( (u) => u.label.toLowerCase().includes(q) || String(u.sub || '') diff --git a/mobile-oa-h5/src/views/OaDetail.vue b/mobile-oa-h5/src/views/OaDetail.vue index 6f612a12..6e2f3cab 100644 --- a/mobile-oa-h5/src/views/OaDetail.vue +++ b/mobile-oa-h5/src/views/OaDetail.vue @@ -4,7 +4,7 @@ + + + +
+ +
+
+ + + +
+
+ 取消 + 加签 + 确定 +
+
+

加签人

+ +

说明(可选)

+ +
+
+
+ + + +
+
+ 取消 + 转交 + 确定 +
+
+

转交给

+ +

说明(可选)

+ +
+
+
@@ -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; +}