import { HomeHeader } from "@/components/home-header"; import { MatchCard } from "@/components/match-card"; import { MatchesByLeague } from "@/components/matches-by-league"; import { SelectionModal } from "@/components/selection-modal"; import { CalendarModal } from "@/components/simple-calendar"; import { ThemedText } from "@/components/themed-text"; import { ThemedView } from "@/components/themed-view"; import { IconSymbol } from "@/components/ui/icon-symbol"; import { Colors } from "@/constants/theme"; import { useAppState } from "@/context/AppStateContext"; import { useTheme } from "@/context/ThemeContext"; import { checkFavorite, fetchLeagues, fetchLiveScore, fetchSports, fetchTodayMatches, } from "@/lib/api"; import { storage } from "@/lib/storage"; import { League, Match, Sport } from "@/types/api"; import { useRouter } from "expo-router"; import React, { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { ActivityIndicator, FlatList, StyleSheet, TouchableOpacity, View, } from "react-native"; export default function HomeScreen() { const router = useRouter(); const { theme } = useTheme(); const { t } = useTranslation(); const isDark = theme === "dark"; const iconColor = isDark ? Colors.dark.icon : Colors.light.icon; const filterBg = isDark ? "#2C2C2E" : "#F2F2F7"; const { state, updateSportId, updateDate, updateLeagueKey, updateTimezone } = useAppState(); const [sports, setSports] = useState([]); const [leagues, setLeagues] = useState([]); const [matches, setMatches] = useState([]); const [loading, setLoading] = useState(true); const [loadingLeagues, setLoadingLeagues] = useState(false); const [now, setNow] = useState(() => new Date()); const [liveLeagueIdByMatchId, setLiveLeagueIdByMatchId] = useState< Record >({}); const [page, setPage] = useState(1); const [total, setTotal] = useState(0); const [loadingMore, setLoadingMore] = useState(false); const deviceTimeZone = useMemo(() => { try { console.log( "deviceTimeZone", Intl.DateTimeFormat().resolvedOptions().timeZone, ); return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"; } catch { return "UTC"; } }, []); // 初始化时同步设备时区到 Context useEffect(() => { updateTimezone(deviceTimeZone); }, [deviceTimeZone]); // Selection States - 从 Context 读取 const selectedSportId = state.selectedSportId; const selectedDate = state.selectedDate; const selectedLeagueKey = state.selectedLeagueKey; const [filterMode, setFilterMode] = useState<"time" | "league">("time"); // 时间或联赛模式 // Modal Visibilities const [showSportModal, setShowSportModal] = useState(false); const [showCalendarModal, setShowCalendarModal] = useState(false); // Load Sports and Leagues useEffect(() => { loadSports(); loadLeagues(); }, []); // 当前时间:每秒刷新(用于展示“现在时间/时区”) useEffect(() => { const id = setInterval(() => setNow(new Date()), 1000); return () => clearInterval(id); }, []); // Load Matches when sport or date changes useEffect(() => { if (selectedSportId !== null) { loadMatches(selectedSportId); } }, [selectedSportId, selectedDate]); useEffect(() => { setPage(1); setTotal(0); }, [selectedSportId, selectedDate, state.selectedLeagueKey]); const timezoneLabel = useMemo(() => { // 仅展示 UTC 偏移(不展示时区名) const offsetMin = -now.getTimezoneOffset(); const sign = offsetMin >= 0 ? "+" : "-"; const abs = Math.abs(offsetMin); const hh = String(Math.floor(abs / 60)).padStart(2, "0"); const mm = String(abs % 60).padStart(2, "0"); return `UTC${sign}${hh}`; }, [now]); const nowTimeText = useMemo(() => { const hh = String(now.getHours()).padStart(2, "0"); const mm = String(now.getMinutes()).padStart(2, "0"); const ss = String(now.getSeconds()).padStart(2, "0"); return `${hh}:${mm}`; }, [now]); // Load Leagues when sport changes useEffect(() => { if (selectedSportId !== null) { loadLeagues(); } }, [selectedSportId]); const loadSports = async () => { try { const apiList = await fetchSports(); // 创建8个运动的完整列表 const defaultSports: Sport[] = [ { id: 1, name: "football", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "", }, { id: 2, name: "basketball", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "", }, { id: 3, name: "tennis", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "", }, { id: 4, name: "cricket", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "", }, { id: 5, name: "baseball", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "", }, { id: 6, name: "badminton", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "", }, { id: 7, name: "snooker", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "", }, { id: 8, name: "volleyball", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "", }, ]; // 合并API返回的运动和默认列表 const sportsMap = new Map(); apiList.forEach((sport) => { sportsMap.set(sport.id, sport); }); // 补充默认运动到8个 defaultSports.forEach((sport) => { if (!sportsMap.has(sport.id)) { sportsMap.set(sport.id, sport); } }); const allSports = Array.from(sportsMap.values()) .sort((a, b) => a.id - b.id) .slice(0, 8); setSports(allSports); } catch (e) { console.error(e); // API失败时使用默认8个运动 const defaultSports: Sport[] = [ { id: 1, name: "football", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "", }, { id: 2, name: "basketball", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "", }, { id: 3, name: "tennis", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "", }, { id: 4, name: "cricket", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "", }, { id: 5, name: "baseball", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "", }, { id: 6, name: "badminton", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "", }, { id: 7, name: "snooker", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "", }, { id: 8, name: "volleyball", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "", }, ]; setSports(defaultSports); } }; // 加载联赛 const loadLeagues = async () => { try { if (selectedSportId !== null) { setLoadingLeagues(true); const res = await fetchLeagues({ sportId: selectedSportId }); setLeagues(res.list); } } catch (e) { console.error(e); } finally { setLoadingLeagues(false); } }; const loadMatches = async (sportId: number) => { setLoading(true); try { const res = await fetchTodayMatches({ sportId, date: selectedDate, timezone: deviceTimeZone, leagueKey: state.selectedLeagueKey || "", page: 1, pageSize: 50, }); setPage(1); setTotal(res.total); const normalizeDate = (d: Date) => { const year = d.getFullYear(); const month = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `${year}-${month}-${day}`; }; const todayStr = normalizeDate(new Date()); const selectedStr = normalizeDate(selectedDate); const shouldMergeLive = selectedStr === todayStr; let merged: Match[] = res.list.map((m) => ({ ...m, date: m.date || selectedStr, sportId: m.sportId ?? sportId, })); if (shouldMergeLive) { try { const liveData = await fetchLiveScore( sportId, state.selectedLeagueKey ? parseInt(state.selectedLeagueKey) : undefined, state.timezone || deviceTimeZone, ); const formatLiveTime = (status: string, fallback: string) => { const s = (status || "").trim(); if (!s) return (fallback || "").trim(); if (/^\d{1,3}$/.test(s)) return `${s}'`; return s; }; const map: Record = {}; const liveMatches: Match[] = (liveData || []).map((item) => { const id = item.event_key.toString(); map[id] = item.league_key; return { id, league: item.league_name, leagueName: item.league_name, leagueLogo: item.league_logo, time: formatLiveTime(item.event_status, item.event_time), date: item.event_date, home: item.event_home_team, away: item.event_away_team, homeTeamName: item.event_home_team, awayTeamName: item.event_away_team, homeTeamLogo: item.home_team_logo, awayTeamLogo: item.away_team_logo, scoreText: item.event_halftime_result || "0 - 0", fav: false, sportId: sportId, isLive: true, leagueKey: item.league_key, }; }); setLiveLeagueIdByMatchId(map); // Merge by id: live takes precedence const byId = new Map(); liveMatches.forEach((m) => byId.set(m.id, m)); merged.forEach((m) => { if (!byId.has(m.id)) byId.set(m.id, m); }); merged = Array.from(byId.values()); } catch (e) { // Live merge failure should not block base list console.warn("Fetch live score failed:", e); } } else { setLiveLeagueIdByMatchId({}); } const token = await storage.getAccessToken(); let listWithFavStatus = merged; if (token) { listWithFavStatus = await Promise.all( merged.map(async (m) => { try { const favRes = await checkFavorite("match", m.id); return { ...m, fav: favRes.isFavorite }; } catch (error) { console.error(`Check favorite failed for match ${m.id}:`, error); return m; } }), ); } // 收藏置顶,其次直播置顶 const sortedList = [...listWithFavStatus].sort((a, b) => { if (a.fav !== b.fav) return a.fav ? -1 : 1; const aLive = !!a.isLive; const bLive = !!b.isLive; if (aLive !== bLive) return aLive ? -1 : 1; return 0; }); setMatches(sortedList); } catch (e) { console.error(e); } finally { setLoading(false); } }; const loadMoreMatches = async () => { if (loadingMore || loading) return; if (!selectedSportId) return; if (matches.length >= total) return; setLoadingMore(true); try { const nextPage = page + 1; const res = await fetchTodayMatches({ sportId: selectedSportId, date: selectedDate, timezone: deviceTimeZone, leagueKey: state.selectedLeagueKey || "", page: nextPage, pageSize: 50, }); setPage(nextPage); setTotal(res.total); setMatches((prev) => { const byId = new Map(); prev.forEach((m) => byId.set(m.id, m)); res.list.forEach((m) => { if (!byId.has(m.id)) byId.set(m.id, m); }); return Array.from(byId.values()); }); } catch (e) { console.error(e); } finally { setLoadingMore(false); } }; const handleFavoriteToggle = (matchId: string, isFav: boolean) => { setMatches((prev) => { const updated = prev.map((m) => m.id === matchId ? { ...m, fav: isFav } : m, ); return [...updated].sort((a, b) => { if (a.fav === b.fav) return 0; return a.fav ? -1 : 1; }); }); }; const currentSport = sports.find((s) => s.id === selectedSportId); // 获取当前运动的国际化名称 const getSportName = (sport: Sport | undefined): string => { if (!sport) return t("home.select_sport"); const sportKeyMap: { [key: number]: string } = { 1: "football", 2: "basketball", 3: "tennis", 4: "cricket", 5: "baseball", 6: "badminton", 7: "snooker", 8: "volleyball", }; const sportKey = sportKeyMap[sport.id] || sport.name.toLowerCase(); return t(`sports.${sportKey}`, { defaultValue: sport.name }); }; const handleLeagueSelect = (leagueKey: string) => { updateLeagueKey(leagueKey); console.log("Selected league:", leagueKey); }; // 缓存运动选项,避免每次渲染都重新计算 const sportOptions = useMemo(() => { return sports.map((s) => { const sportKeyMap: { [key: number]: string } = { 1: "football", 2: "basketball", 3: "tennis", 4: "cricket", 5: "baseball", 6: "badminton", 7: "snooker", 8: "volleyball", }; const sportKey = sportKeyMap[s.id] || s.name.toLowerCase(); return { id: s.id, label: s.name, value: s.id, icon: s.icon, }; }); }, [sports]); const renderHeader = () => ( {/* Time/League Filter Toggle */} setFilterMode(filterMode === "time" ? "league" : "time")} > {filterMode === "time" ? t("home.time") : t("home.league")} {/* Sport Selector */} setShowSportModal(true)} > {getSportName(currentSport)} {/* Date Selector */} setShowCalendarModal(true)} > {selectedDate.getDate()} {timezoneLabel} {nowTimeText} ); return ( {renderHeader()} {loading ? ( {t("home.loading")} ) : filterMode === "time" ? ( item.id} renderItem={({ item }) => ( )} onEndReached={loadMoreMatches} onEndReachedThreshold={0.4} ListFooterComponent={ loadingMore ? ( ) : null } contentContainerStyle={styles.listContent} ListEmptyComponent={ {t("home.no_matches")} } /> ) : ( )} {/* Modals - 条件渲染,只在可见时渲染 */} {showSportModal && ( setShowSportModal(false)} title={t("home.select_sport")} options={sportOptions} selectedValue={selectedSportId} onSelect={updateSportId} /> )} {showCalendarModal && ( setShowCalendarModal(false)} selectedDate={selectedDate} onSelectDate={updateDate} /> )} ); } const styles = StyleSheet.create({ container: { flex: 1, }, center: { flex: 1, justifyContent: "center", alignItems: "center", paddingTop: 50, }, filterContainer: { flexDirection: "row", paddingHorizontal: 16, paddingVertical: 12, gap: 12, }, filterBtn: { flex: 1, height: 44, // Increased from 36 flexDirection: "column", // Stacked logic for Date, or Row for others justifyContent: "center", alignItems: "center", borderRadius: 8, // Rounded corners // iOS shadow shadowColor: "#000", shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.1, shadowRadius: 2, // Android elevation elevation: 2, }, mainFilterBtn: { flex: 2, // Wider for sport flexDirection: "row", gap: 8, }, filterText: { fontSize: 14, fontWeight: "500", }, dateDayText: { fontSize: 16, lineHeight: 16, fontWeight: "bold", }, dateMonthText: { fontSize: 12, lineHeight: 12, opacity: 0.6, }, listContent: { padding: 16, paddingTop: 8, }, footer: { paddingVertical: 16, alignItems: "center", justifyContent: "center", }, });