feat: 七月迭代公共状态与异步任务交互
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m59s

This commit is contained in:
luo
2026-07-24 11:48:02 +08:00
parent 15122e225a
commit a0c44ff538
14 changed files with 677 additions and 63 deletions

View File

@@ -0,0 +1,178 @@
import {
computed,
onBeforeUnmount,
onMounted,
ref,
shallowRef,
type Ref,
type ShallowRef
} from 'vue'
import { isAsyncTaskActive, isAsyncTaskTerminal, type AsyncTaskProgress } from '@/types/api'
const POLL_DELAYS = [2000, 3000, 5000]
const MAX_POLL_DELAY = 10000
export interface AsyncTaskPollingOptions<T extends AsyncTaskProgress> {
storageKey: string
fetchTask: (taskId: number) => Promise<T>
isForbidden?: (error: unknown) => boolean
autoRestore?: boolean
}
export interface AsyncTaskPollingState<T extends AsyncTaskProgress> {
taskId: Ref<number | null>
task: ShallowRef<T | null>
loading: Ref<boolean>
forbidden: Ref<boolean>
error: Ref<string | null>
isActive: Readonly<Ref<boolean>>
start: (taskId: number) => Promise<void>
retry: () => Promise<void>
stop: () => void
clear: () => void
}
const readStoredTaskId = (storageKey: string): number | null => {
try {
const value = Number(localStorage.getItem(storageKey))
return Number.isInteger(value) && value > 0 ? value : null
} catch {
return null
}
}
const writeStoredTaskId = (storageKey: string, taskId: number | null) => {
try {
if (taskId) localStorage.setItem(storageKey, String(taskId))
else localStorage.removeItem(storageKey)
} catch {
// Storage may be unavailable in private browsing or restricted webviews.
}
}
const getErrorMessage = (error: any) =>
error?.response?.data?.msg || error?.message || '获取导出任务详情失败'
export function useAsyncTaskPolling<T extends AsyncTaskProgress>(
options: AsyncTaskPollingOptions<T>
): AsyncTaskPollingState<T> {
const taskId = ref<number | null>(readStoredTaskId(options.storageKey))
const task = shallowRef<T | null>(null)
const loading = ref(false)
const forbidden = ref(false)
const error = ref<string | null>(null)
const isActive = computed(() => isAsyncTaskActive(task.value?.status))
let timer: number | undefined
let pollIndex = 0
let requestInFlight = false
const clearTimer = () => {
if (timer !== undefined) {
window.clearTimeout(timer)
timer = undefined
}
}
const stop = () => {
clearTimer()
}
const clear = () => {
stop()
taskId.value = null
task.value = null
error.value = null
forbidden.value = false
writeStoredTaskId(options.storageKey, null)
}
const schedule = () => {
clearTimer()
if (document.hidden || !isActive.value || !taskId.value) return
const delay = POLL_DELAYS[pollIndex] ?? MAX_POLL_DELAY
pollIndex += 1
timer = window.setTimeout(() => {
timer = undefined
void refresh()
}, delay)
}
const refresh = async () => {
if (!taskId.value || requestInFlight || document.hidden) return
requestInFlight = true
loading.value = true
forbidden.value = false
error.value = null
try {
const nextTask = await options.fetchTask(taskId.value)
task.value = nextTask
if (isAsyncTaskTerminal(nextTask.status)) {
stop()
writeStoredTaskId(options.storageKey, null)
} else {
schedule()
}
} catch (requestError) {
if (options.isForbidden?.(requestError) ?? false) {
forbidden.value = true
stop()
} else {
error.value = getErrorMessage(requestError)
stop()
}
} finally {
loading.value = false
requestInFlight = false
}
}
const start = async (nextTaskId: number) => {
if (!Number.isInteger(nextTaskId) || nextTaskId <= 0) return
taskId.value = nextTaskId
task.value = null
pollIndex = 0
writeStoredTaskId(options.storageKey, nextTaskId)
await refresh()
}
const retry = async () => {
if (!taskId.value) return
await refresh()
}
const handleVisibilityChange = () => {
if (document.hidden) {
stop()
return
}
if (taskId.value && isActive.value) {
pollIndex = 0
void refresh()
}
}
onMounted(() => {
document.addEventListener('visibilitychange', handleVisibilityChange)
if (options.autoRestore !== false && taskId.value) void refresh()
})
onBeforeUnmount(() => {
stop()
document.removeEventListener('visibilitychange', handleVisibilityChange)
})
return {
taskId,
task: task as ShallowRef<T | null>,
loading,
forbidden,
error,
isActive,
start,
retry,
stop,
clear
}
}