"use client"; import { useEffect, useRef, useState } from "react"; export interface RankedArtist { id: string; no: string; name: string; enName: string; avatar: string | null; portrait: string | null; voteCount: number; currentRank: number | null; rank: number; } export interface RankingData { list: RankedArtist[]; top3: RankedArtist[]; top12: RankedArtist[]; candidates: RankedArtist[]; generatedAt: string; } interface UseRankingOptions { /** 轮询间隔(毫秒),默认 30s */ pollInterval?: number; /** 是否启用轮询,默认 true */ enabled?: boolean; } interface UseRankingResult { data: RankingData | null; loading: boolean; error: string | null; /** 上次更新时间(用于 "Live · 5s ago" 显示) */ lastUpdated: Date | null; /** 手动刷新 */ refresh: () => void; } /** * 实时排名 Hook · 客户端轮询 /api/ranking * * 用法: * const { data, lastUpdated } = useRanking({ pollInterval: 30_000 }); * * 当页面隐藏时自动暂停轮询,可见时恢复(节省流量)。 */ export function useRanking(options: UseRankingOptions = {}): UseRankingResult { const { pollInterval = 30_000, enabled = true } = options; const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [lastUpdated, setLastUpdated] = useState(null); const [tick, setTick] = useState(0); const aborterRef = useRef(null); useEffect(() => { if (!enabled) return; let cancelled = false; aborterRef.current?.abort(); const aborter = new AbortController(); aborterRef.current = aborter; const fetchData = async () => { try { const res = await fetch("/api/ranking", { signal: aborter.signal, cache: "no-store", }); const body = await res.json(); if (cancelled) return; if (!body.ok) throw new Error(body.error?.message ?? "fetch failed"); setData(body.data as RankingData); setLastUpdated(new Date()); setError(null); } catch (e) { if (cancelled || aborter.signal.aborted) return; if (e instanceof Error && e.name === "AbortError") return; setError(e instanceof Error ? e.message : "未知错误"); } finally { if (!cancelled) setLoading(false); } }; fetchData(); return () => { cancelled = true; aborter.abort(); }; }, [enabled, tick]); // 定时器:可见性感知 useEffect(() => { if (!enabled) return; let timerId: ReturnType | null = null; const start = () => { if (timerId) return; timerId = setInterval(() => setTick((t) => t + 1), pollInterval); }; const stop = () => { if (timerId) { clearInterval(timerId); timerId = null; } }; const onVisibility = () => { if (document.hidden) stop(); else { // 页面可见时立即拉一次最新数据 setTick((t) => t + 1); start(); } }; start(); document.addEventListener("visibilitychange", onVisibility); return () => { stop(); document.removeEventListener("visibilitychange", onVisibility); }; }, [enabled, pollInterval]); return { data, loading, error, lastUpdated, refresh: () => setTick((t) => t + 1), }; }