diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 240915c..c18ef58 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -622,7 +622,9 @@ export default function HomeScreen() { /> ) : ( )} diff --git a/components/match-card-league.tsx b/components/match-card-league.tsx index 88eb1f7..0c04c42 100644 --- a/components/match-card-league.tsx +++ b/components/match-card-league.tsx @@ -121,7 +121,7 @@ export function MatchCardLeague({ {/* 如果有状态字段 match.meta (如 'FT', 'AET'), 优先显示,否则显示时间 */} - {(match.meta || match.time || "").toUpperCase()} + {(match.time || "").toUpperCase()} diff --git a/components/matches-by-league.tsx b/components/matches-by-league.tsx index 8b3d760..509f4b2 100644 --- a/components/matches-by-league.tsx +++ b/components/matches-by-league.tsx @@ -1,7 +1,8 @@ import { MatchCardLeague } from "@/components/match-card-league"; import { ThemedText } from "@/components/themed-text"; import { useTheme } from "@/context/ThemeContext"; -import { Match } from "@/types/api"; +import { fetchLeagues, fetchTodayMatches } from "@/lib/api"; +import { League, Match } from "@/types/api"; import React, { useState } from "react"; import { Image, @@ -23,70 +24,97 @@ if ( } interface MatchesByLeagueProps { - matches: Match[]; + sportId: number; + date: Date; + timezone: string; onFavoriteToggle?: (matchId: string, isFav: boolean) => void; - /** - * 是否支持折叠收起 - * @default true - */ enableCollapsible?: boolean; } - export function MatchesByLeague({ - matches, + sportId, + date, + timezone, onFavoriteToggle, enableCollapsible = true, }: MatchesByLeagueProps) { const { theme } = useTheme(); const isDark = theme === "dark"; - // 数据分组逻辑 - const matchesByLeague = React.useMemo(() => { - const grouped: Record = {}; - matches.forEach((match) => { - const league = match.league || match.leagueName || "其他"; - if (!grouped[league]) { - grouped[league] = []; - } - grouped[league].push(match); - }); - return grouped; - }, [matches]); - - const leagueNames = Object.keys(matchesByLeague); - - // 状态:记录哪些联赛被折叠了 (Key 为联赛名称),默认全部收起 - const [collapsedSections, setCollapsedSections] = useState< + const [leagues, setLeagues] = useState([]); + const [collapsed, setCollapsed] = useState>({}); + const [matchesByLeagueKey, setMatchesByLeagueKey] = useState< + Record + >({}); + const [loadingLeagueKey, setLoadingLeagueKey] = useState< Record >({}); - // 当联赛列表变化时,确保所有联赛默认都是收起状态 + const dateStr = React.useMemo(() => { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; + }, [date]); + React.useEffect(() => { - setCollapsedSections((prev) => { - const updated: Record = {}; - leagueNames.forEach((name) => { - updated[name] = prev[name] !== undefined ? prev[name] : true; - }); - return updated; - }); - }, [leagueNames.join(",")]); + let mounted = true; + fetchLeagues({ + sportId, + date: dateStr, + page: 1, + pageSize: 200, + sortBy: "matchCount", + sortOrder: "desc", + }) + .then((res) => { + if (!mounted) return; + setLeagues(res.list); + setCollapsed((prev) => { + const next: Record = {}; + res.list.forEach((l) => { + next[l.key] = prev[l.key] !== undefined ? prev[l.key] : true; + }); + return next; + }); + setMatchesByLeagueKey({}); + setLoadingLeagueKey({}); + }) + .catch(() => { }); + return () => { + mounted = false; + }; + }, [sportId, dateStr]); - // 切换折叠状态 - const toggleCollapse = (leagueName: string) => { + const toggleCollapse = async (leagueKey: string) => { if (!enableCollapsible) return; - LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); - setCollapsedSections((prev) => ({ - ...prev, - [leagueName]: !prev[leagueName], - })); + setCollapsed((prev) => ({ ...prev, [leagueKey]: !prev[leagueKey] })); + + const nextCollapsed = !collapsed[leagueKey]; + if (nextCollapsed) return; + if (matchesByLeagueKey[leagueKey]) return; + + setLoadingLeagueKey((prev) => ({ ...prev, [leagueKey]: true })); + try { + const res = await fetchTodayMatches({ + sportId, + date: dateStr, + timezone, + leagueKey, + page: 1, + pageSize: 50, + }); + setMatchesByLeagueKey((prev) => ({ ...prev, [leagueKey]: res.list })); + } finally { + setLoadingLeagueKey((prev) => ({ ...prev, [leagueKey]: false })); + } }; - if (leagueNames.length === 0) { + if (leagues.length === 0) { return ( - 暂无比赛 + 暂无联赛 ); } @@ -99,54 +127,51 @@ export function MatchesByLeague({ ]} contentContainerStyle={{ paddingBottom: 40 }} > - {leagueNames.map((leagueName) => { - const leagueMatches = matchesByLeague[leagueName]; - // 取该组第一场比赛的数据作为头部信息的来源(图标、国家等) - const firstMatch = leagueMatches[0]; - const isCollapsed = collapsedSections[leagueName]; + {leagues.map((league) => { + const isCollapsed = collapsed[league.key] !== false; + const leagueMatches = matchesByLeagueKey[league.key] || []; + const isLoading = !!loadingLeagueKey[league.key]; return ( - - {/* 联赛头部 */} + toggleCollapse(leagueName)} + activeOpacity={enableCollapsible && league.matchCount > 0 ? 0.7 : 1} + onPress={() => { + if (enableCollapsible && league.matchCount > 0) { + toggleCollapse(league.key); + } + }} style={styles.leagueHeaderWrapper} > - {/* 联赛 Logo */} - {/* 联赛名称 */} - {leagueName} + {league.name} - {/* 国家信息行 */} - {firstMatch.countryName || (firstMatch as any).countryName || "International"} + {league.countryName || "International"} - {/* 比赛数量 */} - {leagueMatches.length} + {league.matchCount} - {/* 折叠箭头 (仅当支持折叠时显示) */} - {enableCollapsible && ( + {enableCollapsible && league.matchCount > 0 && ( {isCollapsed ? "⌄" : "⌃"} @@ -154,23 +179,74 @@ export function MatchesByLeague({ - {/* 比赛列表内容 (根据状态显示/隐藏) */} {!isCollapsed && ( - {leagueMatches.map((match, index) => ( - - - - ))} + {isLoading ? ( + <> + {[0, 1, 2].map((i) => ( + + + + + + + + + + + + + + + + + + + + + + + + + ))} + + ) : ( + leagueMatches.map((match, index) => ( + + + + )) + )} )} @@ -252,13 +328,67 @@ const styles = StyleSheet.create({ marginHorizontal: 16, overflow: "hidden", }, + // 布局与 MatchCardLeague 保持一致,便于骨架对齐 + leftColumn: { + width: 50, + justifyContent: "center", + alignItems: "flex-start", + marginRight: 8, + }, + teamsColumn: { + flex: 1, + justifyContent: "center", + paddingRight: 8, + }, + rightWrapper: { + flexDirection: "row", + alignItems: "center", + }, matchCardWrapper: { - // 卡片包装器 }, matchCardDivider: { borderBottomWidth: 0.5, borderBottomColor: "#3A3A3C", }, + favoriteButton: { + paddingLeft: 12, + justifyContent: "center", + alignItems: "center", + }, + skeletonRow: { + flexDirection: "row", + alignItems: "center", + paddingHorizontal: 16, + paddingVertical: 14, + }, + skeletonLine: { + height: 8, + borderRadius: 4, + backgroundColor: "#2C2C2E", + }, + skeletonAvatar: { + width: 20, + height: 20, + borderRadius: 10, + backgroundColor: "#2C2C2E", + marginRight: 10, + }, + skeletonTeamRow: { + flexDirection: "row", + alignItems: "center", + }, + skeletonScoreBox: { + width: 24, + height: 32, + borderRadius: 8, + backgroundColor: "#2C2C2E", + }, + skeletonCircle: { + width: 16, + height: 16, + borderRadius: 8, + backgroundColor: "#2C2C2E", + }, emptyContainer: { flex: 1, justifyContent: "center", diff --git a/types/api.ts b/types/api.ts index 94c127d..aac8012 100644 --- a/types/api.ts +++ b/types/api.ts @@ -88,11 +88,11 @@ export interface LiveScoreMatch { substitutes?: { time: string; home_scorer: - | { in: string; out: string; in_id: number; out_id: number } - | any[]; + | { in: string; out: string; in_id: number; out_id: number } + | any[]; away_scorer: - | { in: string; out: string; in_id: number; out_id: number } - | any[]; + | { in: string; out: string; in_id: number; out_id: number } + | any[]; info: string; info_time: string; score: string; @@ -153,7 +153,7 @@ export interface League { isActive: boolean; createdAt: string; updatedAt: string; - matchCount?: number; + matchCount: number; } export interface GoalEvent {