diff --git a/frontend/src/composables/usePolling.ts b/frontend/src/composables/usePolling.ts index e922131..bee684c 100644 --- a/frontend/src/composables/usePolling.ts +++ b/frontend/src/composables/usePolling.ts @@ -1,20 +1,82 @@ -import { useIntervalFn } from '@vueuse/core' +import { onBeforeUnmount, onMounted, ref } from 'vue' + +interface PollingOptions { + /** 启动后是否立即执行一次,默认立即执行。 */ + immediate?: boolean + /** 页面不可见时是否暂停,默认暂停。 */ + pauseWhenHidden?: boolean + /** 轮询函数抛错时的可选回调。 */ + onError?: (error: unknown) => void +} /** * 轮询 composable - * 封装 useIntervalFn,自动在组件卸载时清理 + * 串行轮询:上一轮完成后才安排下一轮,避免慢请求重叠和旧响应覆盖。 + * 页面进入后台时自动暂停,恢复可见后立即刷新;组件卸载时自动清理。 */ -export function usePolling(fn: () => void | Promise, interval = 5000, immediate = true) { - const { pause, resume } = useIntervalFn(fn, interval, { immediate }) +export function usePolling( + fn: () => void | Promise, + interval: number | (() => number) = 5000, + options: PollingOptions = {}, +) { + const { immediate = true, pauseWhenHidden = true, onError } = options + const isActive = ref(false) + const isRunning = ref(false) + let timer: ReturnType | null = null + + function clearTimer() { + if (!timer) return + clearTimeout(timer) + timer = null + } + + function schedule() { + clearTimer() + if (!isActive.value) return + if (pauseWhenHidden && document.hidden) return + const delay = typeof interval === 'function' ? interval() : interval + timer = setTimeout(() => void run(), Math.max(0, delay)) + } + + async function run() { + clearTimer() + if (!isActive.value || isRunning.value) return + if (pauseWhenHidden && document.hidden) return + + isRunning.value = true + try { + await fn() + } catch (error) { + onError?.(error) + } finally { + isRunning.value = false + schedule() + } + } function stop() { - pause() + isActive.value = false + clearTimer() } function start() { - if (immediate) fn() - resume() + if (isActive.value) return + isActive.value = true + if (immediate) void run() + else schedule() } - return { start, stop, pause, resume } + function handleVisibilityChange() { + if (!pauseWhenHidden || !isActive.value) return + if (document.hidden) clearTimer() + else void run() + } + + onMounted(() => document.addEventListener('visibilitychange', handleVisibilityChange)) + onBeforeUnmount(() => { + stop() + document.removeEventListener('visibilitychange', handleVisibilityChange) + }) + + return { start, stop, run, isActive, isRunning } }