/** * useMatchesList: 赛程列表数据获取(筛选/游标分页/进行中比赛)。 * * D3: 从 Matches.tsx 拆出。竞态防护语义不变 —— 递增序号只认最后一次请求; * loadMore 不自增序号(切换筛选才自增,翻页跟随当前序列)。 */ import { useCallback, useEffect, useRef, useState } from 'react' import { http } from '../../../lib/http' import type { Match } from '../types' interface UseMatchesListOptions { /** 共享 error state(拆分前列表与预测共用同一个 error,行为保持一致) */ onError: (msg: string | null) => void } export function useMatchesList({ onError }: UseMatchesListOptions) { const [league, setLeague] = useState('E0') const [status, setStatus] = useState('scheduled') const [matches, setMatches] = useState([]) const [nextCursor, setNextCursor] = useState(null) const [loadingMore, setLoadingMore] = useState(false) const [loading, setLoading] = useState(false) const [showAllUpcoming, setShowAllUpcoming] = useState(false) // 默认仅展示未来 3 天;true 展开全部 const [liveMatches, setLiveMatches] = useState([]) // 进行中比赛(顶部独立区块) // 请求竞态防护:切换联赛/状态很快时,先发的慢请求可能后返回, // 把旧结果覆盖到新筛选上。用递增序号只认最后一次请求的响应。 const loadSeq = useRef(0) const load = useCallback(async () => { const seq = ++loadSeq.current setLoading(true) setLoadingMore(false) setShowAllUpcoming(false) // 切换筛选重置为「未来 3 天」视图 onError(null) try { const params = new URLSearchParams({ league, status, limit: '50' }) const data = await http.get<{ items: Match[]; next_cursor: string | null }>(`/matches?${params}`) if (seq !== loadSeq.current) return // 已有更新的请求,丢弃本次结果 setMatches(data.items) setNextCursor(data.next_cursor ?? null) } catch (e) { if (seq !== loadSeq.current) return onError(e instanceof Error ? e.message : String(e)) } finally { if (seq === loadSeq.current) setLoading(false) } }, [league, status, onError]) // 加载下一页(游标分页) const loadMore = async () => { if (!nextCursor || loadingMore) return const seq = loadSeq.current // 不做自增:切换筛选会自增,这里只跟随当前序列 setLoadingMore(true) try { const params = new URLSearchParams({ league, status, limit: '50', cursor: nextCursor }) const data = await http.get<{ items: Match[]; next_cursor: string | null }>(`/matches?${params}`) if (seq !== loadSeq.current) return setMatches(prev => [...prev, ...data.items]) setNextCursor(data.next_cursor ?? null) } catch (e) { if (seq !== loadSeq.current) return onError(e instanceof Error ? e.message : String(e)) } finally { if (seq === loadSeq.current) setLoadingMore(false) } } // 加载进行中比赛(顶部独立区块) const loadLive = useCallback(async () => { try { const params = new URLSearchParams({ league, status: 'in_play', limit: '20' }) const data = await http.get<{ items: Match[] }>(`/matches?${params}`) setLiveMatches(data.items ?? []) } catch { /* ignore:进行中非核心功能 */ } }, [league]) useEffect(() => { load(); loadLive() }, [load, loadLive]) return { league, setLeague, status, setStatus, matches, nextCursor, loading, loadingMore, showAllUpcoming, setShowAllUpcoming, liveMatches, load, loadMore, } }