feat: 七月迭代公共状态与异步任务交互
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m59s
All checks were successful
构建并部署前端到测试环境 / build-and-deploy (push) Successful in 5m59s
This commit is contained in:
178
src/composables/useAsyncTaskPolling.ts
Normal file
178
src/composables/useAsyncTaskPolling.ts
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user