feat(oa): support withdraw and copy-restart on mobile mine flows
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
d5287765ec
commit
ecdf73fe23
5 files changed
+244
-17
No files matched your search
@@ -41,9 +41,9 @@
|
||||
:status-text="statusText(row.instanceStatus || row.status)"
|
||||
:status-tone="statusTone(row.instanceStatus || row.status)"
|
||||
:meta-lines="metaLines(row)"
|
||||
:actions="cardActions"
|
||||
:actions="rowActions(row)"
|
||||
@open="openView(row)"
|
||||
@action="openView(row)"
|
||||
@action="onCardAction($event, row)"
|
||||
/>
|
||||
<view v-if="loadingMore" class="oa-task-list__end">加载更多…</view>
|
||||
<view v-else-if="!hasMore" class="oa-task-list__end">已经到底了</view>
|
||||
@@ -55,10 +55,12 @@
|
||||
import OaTaskCard from '../../../components/OaTaskCard.vue'
|
||||
import { fetchOaTasks } from '../../../utils/oa-api.js'
|
||||
import { forceRelogin, getToken } from '../../../utils/auth.js'
|
||||
import { stashOaDetailTitle } from '../../../utils/oa-h5.js'
|
||||
import { stashOaDetailTitle, stashOaStartTitle } from '../../../utils/oa-h5.js'
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
const COPY_RESTART_STATUSES = ['returned', 'cancelled', 'rejected']
|
||||
|
||||
const STATUS_MAP = {
|
||||
pending: '待处理',
|
||||
running: '进行中',
|
||||
@@ -83,8 +85,7 @@
|
||||
loading: false,
|
||||
loadingMore: false,
|
||||
hasMore: true,
|
||||
_searchTimer: null,
|
||||
cardActions: [{ key: 'view', label: '查看', primary: true }]
|
||||
_searchTimer: null
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
@@ -141,6 +142,15 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
rowActions(row) {
|
||||
const actions = [{ key: 'view', label: '查看', primary: true }]
|
||||
const status = String((row && (row.instanceStatus || row.status)) || '')
|
||||
const appId = String((row && row.appId) || '').trim()
|
||||
if (COPY_RESTART_STATUSES.includes(status) && appId) {
|
||||
actions.push({ key: 'copyRestart', label: '复制并重新发起' })
|
||||
}
|
||||
return actions
|
||||
},
|
||||
onSearchInput(e) {
|
||||
const v =
|
||||
e && e.detail && e.detail.value != null
|
||||
@@ -165,6 +175,14 @@
|
||||
this.appName = ''
|
||||
this.onSearchConfirm()
|
||||
},
|
||||
onCardAction(act, row) {
|
||||
if (!act || !row) return
|
||||
if (act.key === 'copyRestart') {
|
||||
this.openCopyRestart(row)
|
||||
return
|
||||
}
|
||||
this.openView(row)
|
||||
},
|
||||
openView(row) {
|
||||
if (!row) return
|
||||
// mine 行是实例:Web 用 row.id;兼容 instanceId
|
||||
@@ -181,6 +199,23 @@
|
||||
'&mode=view'
|
||||
})
|
||||
},
|
||||
openCopyRestart(row) {
|
||||
if (!row) return
|
||||
const appId = String(row.appId || '').trim()
|
||||
const fromInstanceId = String(row.instanceId || row.id || '').trim()
|
||||
if (!appId || !fromInstanceId) {
|
||||
uni.showToast({ title: '无法复制:缺少应用或实例', icon: 'none' })
|
||||
return
|
||||
}
|
||||
stashOaStartTitle(row.appName || row.title)
|
||||
uni.navigateTo({
|
||||
url:
|
||||
'/pages/oa/webview/index?appId=' +
|
||||
encodeURIComponent(appId) +
|
||||
'&fromInstanceId=' +
|
||||
encodeURIComponent(fromInstanceId)
|
||||
})
|
||||
},
|
||||
async loadList(reset) {
|
||||
if (!getToken()) {
|
||||
forceRelogin('请先登录')
|
||||
|
||||
@@ -39,6 +39,7 @@ export default {
|
||||
const mode = String((query && query.mode) || '') || 'view'
|
||||
const appId = String((query && query.appId) || '')
|
||||
const draftId = String((query && query.draftId) || '')
|
||||
const fromInstanceId = String((query && query.fromInstanceId) || '')
|
||||
|
||||
let src = ''
|
||||
|
||||
@@ -48,14 +49,14 @@ export default {
|
||||
uni.setNavigationBarTitle({ title: cachedName })
|
||||
}
|
||||
src = buildOaDetailUrl({ instanceId, taskId, mode })
|
||||
} else if (draftId || appId) {
|
||||
} else if (draftId || appId || fromInstanceId) {
|
||||
const cachedName = takeOaStartTitle()
|
||||
if (cachedName) {
|
||||
uni.setNavigationBarTitle({ title: cachedName })
|
||||
} else if (appId) {
|
||||
this.resolveTitle(appId)
|
||||
}
|
||||
src = buildOaStartUrl({ appId, draftId })
|
||||
src = buildOaStartUrl({ appId, draftId, fromInstanceId })
|
||||
} else {
|
||||
uni.showToast({ title: '参数错误', icon: 'none' })
|
||||
setTimeout(() => {
|
||||
@@ -109,6 +110,35 @@ export default {
|
||||
if (this._closing) return
|
||||
const action = String((payload && payload.action) || '')
|
||||
if (action && !CLOSE_ACTIONS.has(action)) return
|
||||
|
||||
// 复制重开:离开详情 webview 栈时,改开发起页(同页 redirect)
|
||||
if (action === 'oa-detail-restart') {
|
||||
const appId = String((payload && payload.appId) || '').trim()
|
||||
const fromInstanceId = String(
|
||||
(payload && (payload.fromInstanceId || payload.instanceId)) || ''
|
||||
).trim()
|
||||
if (!appId && !fromInstanceId) return
|
||||
this._closing = true
|
||||
const q = []
|
||||
if (appId) q.push('appId=' + encodeURIComponent(appId))
|
||||
if (fromInstanceId) {
|
||||
q.push('fromInstanceId=' + encodeURIComponent(fromInstanceId))
|
||||
}
|
||||
uni.redirectTo({
|
||||
url: '/pages/oa/webview/index?' + q.join('&'),
|
||||
fail: () => {
|
||||
this._closing = false
|
||||
uni.navigateTo({
|
||||
url: '/pages/oa/webview/index?' + q.join('&'),
|
||||
fail: () => {
|
||||
this._closing = false
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this._closing = true
|
||||
const msg = String((payload && payload.message) || '').trim()
|
||||
if (msg) {
|
||||
|
||||
@@ -28,12 +28,13 @@ export function buildFormSinkUrl() {
|
||||
return withQuery('/oa-form-sink', token ? { token } : {})
|
||||
}
|
||||
|
||||
/** 正式发起页 URL(与水槽同一套拼法,仅多 appId/draftId) */
|
||||
export function buildOaStartUrl({ appId, draftId } = {}) {
|
||||
/** 正式发起页 URL(与水槽同一套拼法,仅多 appId/draftId/fromInstanceId) */
|
||||
export function buildOaStartUrl({ appId, draftId, fromInstanceId } = {}) {
|
||||
const token = getToken()
|
||||
const params = {}
|
||||
if (appId) params.appId = appId
|
||||
if (draftId) params.draftId = draftId
|
||||
if (fromInstanceId) params.fromInstanceId = fromInstanceId
|
||||
if (token) params.token = token
|
||||
return withQuery('/oa-start', params)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,13 @@
|
||||
<OaPulseLoading text="加载审批详情…" />
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="oa-detail__body" :class="{ 'oa-detail__body--approve': canApprove }">
|
||||
<div
|
||||
class="oa-detail__body"
|
||||
:class="{
|
||||
'oa-detail__body--approve': canApprove,
|
||||
'oa-detail__body--mine-act': (canWithdraw || canCopyRestart) && !canApprove
|
||||
}"
|
||||
>
|
||||
<header class="oa-detail__hd">
|
||||
<h1 class="oa-detail__title">{{ detail?.title || '审批详情' }}</h1>
|
||||
<span
|
||||
@@ -73,6 +79,24 @@
|
||||
同意
|
||||
</van-button>
|
||||
</template>
|
||||
<van-button
|
||||
v-if="canWithdraw"
|
||||
class="oa-detail__btn oa-detail__btn--ghost"
|
||||
:loading="acting"
|
||||
:disabled="acting"
|
||||
@click="withdraw"
|
||||
>
|
||||
撤销
|
||||
</van-button>
|
||||
<van-button
|
||||
v-if="canCopyRestart"
|
||||
class="oa-detail__btn oa-detail__btn--primary"
|
||||
type="primary"
|
||||
:disabled="acting"
|
||||
@click="goCopyRestart"
|
||||
>
|
||||
复制并重新发起
|
||||
</van-button>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
@@ -166,8 +190,8 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { showToast } from 'vant'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { showConfirmDialog, showToast } from 'vant'
|
||||
import {
|
||||
fetchOaInstanceDetail,
|
||||
fetchCurrentUser,
|
||||
@@ -175,7 +199,8 @@ import {
|
||||
fetchRejectOaTask,
|
||||
fetchReturnOaTask,
|
||||
fetchAddSignOaTask,
|
||||
fetchTransferOaTask
|
||||
fetchTransferOaTask,
|
||||
fetchWithdrawOaInstance
|
||||
} from '../api/oa.js'
|
||||
import { prepareMobileFormSchema } from '../oa/schema-prep.js'
|
||||
import { statusText, statusTone } from '../oa/status-label.js'
|
||||
@@ -184,6 +209,7 @@ import OaPulseLoading from '../oa/components/OaPulseLoading.vue'
|
||||
import OaPersonSelect from '../oa/components/OaPersonSelect.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const loading = ref(true)
|
||||
const acting = ref(false)
|
||||
const detail = ref(null)
|
||||
@@ -255,6 +281,22 @@ const myPendingTask = computed(() => {
|
||||
|
||||
const canApprove = computed(() => !!myPendingTask.value && mode.value === 'approve')
|
||||
|
||||
/** 与 Web instances/detail:发起人 + 进行中 */
|
||||
const isInitiator = computed(() => {
|
||||
const uid = currentUserId.value
|
||||
if (!uid || !detail.value) return false
|
||||
return String(detail.value.initiatorUserId || '') === uid
|
||||
})
|
||||
|
||||
const canWithdraw = computed(
|
||||
() => isInitiator.value && String(detail.value?.status || '') === 'running'
|
||||
)
|
||||
|
||||
const canCopyRestart = computed(() => {
|
||||
if (!isInitiator.value || !detail.value) return false
|
||||
return ['returned', 'cancelled', 'rejected'].includes(String(detail.value.status || ''))
|
||||
})
|
||||
|
||||
/** 转交:排除自己 + 本节点已有 pending approval 的办理人 */
|
||||
const transferExcludeIds = computed(() => {
|
||||
const me = currentUserId.value
|
||||
@@ -321,6 +363,55 @@ function parseFormData(d) {
|
||||
return {}
|
||||
}
|
||||
|
||||
async function withdraw() {
|
||||
try {
|
||||
await showConfirmDialog({
|
||||
title: '提示',
|
||||
message: '确定撤销该审批吗?'
|
||||
})
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
const id = instanceId.value
|
||||
if (!id) return
|
||||
acting.value = true
|
||||
try {
|
||||
await fetchWithdrawOaInstance(id)
|
||||
showToast('已撤销')
|
||||
closeToMini({ action: 'oa-detail-withdraw', message: '已撤销' })
|
||||
} catch (e) {
|
||||
showToast(e?.message || '撤销失败')
|
||||
} finally {
|
||||
acting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function goCopyRestart() {
|
||||
const appId = String(detail.value?.appId || '').trim()
|
||||
const id = instanceId.value
|
||||
if (!appId || !id) {
|
||||
showToast('无法复制:缺少应用或实例信息')
|
||||
return
|
||||
}
|
||||
// 优先同 webview 内跳转发起页(带 fromInstanceId 预填)
|
||||
const q = { appId, fromInstanceId: id }
|
||||
const token = String(route.query.token || '').trim()
|
||||
if (token) q.token = token
|
||||
try {
|
||||
router.replace({ path: '/oa-start', query: q })
|
||||
return
|
||||
} catch (_) {
|
||||
/* fallback below */
|
||||
}
|
||||
// 离开 webview 栈时:宿主按 oa-detail-restart 再开发起页
|
||||
closeToMini({
|
||||
action: 'oa-detail-restart',
|
||||
appId,
|
||||
fromInstanceId: id,
|
||||
message: '复制并重新发起'
|
||||
})
|
||||
}
|
||||
|
||||
function openComment(kind) {
|
||||
if (!myPendingTask.value) return
|
||||
commentKind.value = kind
|
||||
@@ -505,6 +596,9 @@ onMounted(() => {
|
||||
.oa-detail__body--approve {
|
||||
padding-bottom: 128px;
|
||||
}
|
||||
.oa-detail__body--mine-act {
|
||||
padding-bottom: 120px;
|
||||
}
|
||||
.oa-detail__hd {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { showToast } from 'vant'
|
||||
import {
|
||||
fetchOaTemplateDetail,
|
||||
@@ -77,6 +77,7 @@ import {
|
||||
fetchOaInstanceDraftDetail,
|
||||
fetchSubmitOaInstanceDraft,
|
||||
fetchStartOaInstance,
|
||||
fetchOaInstanceDetail,
|
||||
fetchUserList,
|
||||
fetchCurrentUser,
|
||||
fetchCnHolidayRange
|
||||
@@ -109,6 +110,7 @@ import OaFlowPreview from '../oa/components/OaFlowPreview.vue'
|
||||
import OaPulseLoading from '../oa/components/OaPulseLoading.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const detailLoading = ref(true)
|
||||
const previewLoading = ref(false)
|
||||
const savingDraft = ref(false)
|
||||
@@ -612,6 +614,38 @@ async function loadByAppId(appId, { seedData = null, applyDefaults = true } = {}
|
||||
await loadFlowPreview()
|
||||
}
|
||||
|
||||
function takeRestartPrefillsFromSession() {
|
||||
try {
|
||||
const raw = sessionStorage.getItem('oa_restart_prefills')
|
||||
if (!raw) return null
|
||||
sessionStorage.removeItem('oa_restart_prefills')
|
||||
const parsed = JSON.parse(raw)
|
||||
if (!parsed || typeof parsed !== 'object') return null
|
||||
return parsed
|
||||
} catch (_) {
|
||||
try {
|
||||
sessionStorage.removeItem('oa_restart_prefills')
|
||||
} catch (__) {
|
||||
/* ignore */
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function parseInstanceFormData(d) {
|
||||
if (d?.formData && typeof d.formData === 'object') return { ...d.formData }
|
||||
if (d?.formDataJson && typeof d.formDataJson === 'object') return { ...d.formDataJson }
|
||||
return {}
|
||||
}
|
||||
|
||||
/** 加载成功后去掉 fromInstanceId,避免刷新/提交误当成编辑旧单 */
|
||||
function clearFromInstanceIdQuery() {
|
||||
const q = { ...route.query }
|
||||
if (q.fromInstanceId == null || q.fromInstanceId === '') return
|
||||
delete q.fromInstanceId
|
||||
router.replace({ path: '/oa-start', query: q }).catch(() => {})
|
||||
}
|
||||
|
||||
async function bootstrap() {
|
||||
detailLoading.value = true
|
||||
skipPreviewWatch = true
|
||||
@@ -619,6 +653,9 @@ async function bootstrap() {
|
||||
await loadCurrentUser()
|
||||
const appId = String(route.query.appId || '').trim()
|
||||
const draftId = String(route.query.draftId || '').trim()
|
||||
const fromInstanceId = String(route.query.fromInstanceId || '').trim()
|
||||
const sessionPrefills = takeRestartPrefillsFromSession()
|
||||
|
||||
if (draftId) {
|
||||
const d = await fetchOaInstanceDraftDetail(draftId)
|
||||
editingDraftId.value = draftId
|
||||
@@ -632,10 +669,40 @@ async function bootstrap() {
|
||||
d.formData && typeof d.formData === 'object' ? { ...d.formData } : {}
|
||||
// 草稿:保留已填值,仅补空的 owner 等默认
|
||||
await loadByAppId(aid, { seedData: seed, applyDefaults: true })
|
||||
} else if (appId) {
|
||||
initiatorSelectByNode.value = {}
|
||||
} else if (appId || fromInstanceId || sessionPrefills) {
|
||||
// 复制重开 / 普通发起:绝不带 editingDraftId,提交走新建实例
|
||||
editingDraftId.value = ''
|
||||
await loadByAppId(appId, { seedData: {}, applyDefaults: true })
|
||||
let seed = {}
|
||||
let aid = appId
|
||||
let initiatorSel = {}
|
||||
|
||||
if (sessionPrefills) {
|
||||
if (sessionPrefills.formData && typeof sessionPrefills.formData === 'object') {
|
||||
seed = { ...sessionPrefills.formData }
|
||||
}
|
||||
if (
|
||||
sessionPrefills.initiatorSelectByNode &&
|
||||
typeof sessionPrefills.initiatorSelectByNode === 'object'
|
||||
) {
|
||||
initiatorSel = { ...sessionPrefills.initiatorSelectByNode }
|
||||
}
|
||||
}
|
||||
|
||||
if (fromInstanceId) {
|
||||
try {
|
||||
const old = await fetchOaInstanceDetail(fromInstanceId)
|
||||
if (!aid) aid = String(old?.appId || '').trim()
|
||||
const copied = parseInstanceFormData(old)
|
||||
if (Object.keys(copied).length) seed = copied
|
||||
} catch (_) {
|
||||
/* 预填失败仍可手工填 */
|
||||
}
|
||||
}
|
||||
|
||||
if (!aid) throw new Error('缺少 appId')
|
||||
initiatorSelectByNode.value = initiatorSel
|
||||
await loadByAppId(aid, { seedData: seed, applyDefaults: true })
|
||||
clearFromInstanceIdQuery()
|
||||
} else {
|
||||
showToast('缺少 appId')
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user