diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 2b802e2..963e58f 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -31,6 +31,7 @@ export default function HomeScreen() { const [leagues, setLeagues] = useState([]); const [matches, setMatches] = useState([]); const [loading, setLoading] = useState(true); + const [loadingLeagues, setLoadingLeagues] = useState(false); // Selection States // 默认足球 @@ -119,11 +120,14 @@ export default function HomeScreen() { const loadLeagues = async () => { try { if (selectedSportId !== null) { + setLoadingLeagues(true); const list = await fetchLeagues(selectedSportId, ""); setLeagues(list); } } catch (e) { console.error(e); + } finally { + setLoadingLeagues(false); } }; @@ -213,7 +217,17 @@ export default function HomeScreen() { ) : ( setShowLeagueModal(true)} + onPress={() => { + // 立即显示弹窗 + setShowLeagueModal(true); + // 如果联赛列表为空,立即设置loading状态并加载 + if (selectedSportId !== null) { + if (leagues.length === 0) { + setLoadingLeagues(true); + } + loadLeagues(); + } + }} > @@ -291,6 +305,7 @@ export default function HomeScreen() { onClose={() => setShowLeagueModal(false)} leagues={leagues} selectedLeagueKey={selectedLeagueKey} + loading={loadingLeagues} onSelect={handleLeagueSelect} /> diff --git a/app/(tabs)/live.tsx b/app/(tabs)/live.tsx index a27240a..2b802e2 100644 --- a/app/(tabs)/live.tsx +++ b/app/(tabs)/live.tsx @@ -1,11 +1,298 @@ +import { HomeHeader } from "@/components/home-header"; +import { LeagueModal } from "@/components/league-modal"; +import { MatchCard } from "@/components/match-card"; +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 { StyleSheet } from "react-native"; +import { IconSymbol } from "@/components/ui/icon-symbol"; +import { Colors } from "@/constants/theme"; +import { useTheme } from "@/context/ThemeContext"; +import { fetchLeagues, fetchSports, fetchTodayMatches } from "@/lib/api"; +import { League, Match, Sport } from "@/types/api"; +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { + ActivityIndicator, + FlatList, + StyleSheet, + TouchableOpacity, + View, +} from "react-native"; + +export default function HomeScreen() { + 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 [sports, setSports] = useState([]); + const [leagues, setLeagues] = useState([]); + const [matches, setMatches] = useState([]); + const [loading, setLoading] = useState(true); + + // Selection States + // 默认足球 + const [selectedSportId, setSelectedSportId] = useState(1); + const [selectedDate, setSelectedDate] = useState(new Date()); + const [selectedLeagueKey, setSelectedLeagueKey] = useState(null); + const [filterMode, setFilterMode] = useState<"time" | "league">("time"); // 时间或联赛模式 + + // Modal Visibilities + const [showSportModal, setShowSportModal] = useState(false); + const [showCalendarModal, setShowCalendarModal] = useState(false); + const [showLeagueModal, setShowLeagueModal] = useState(false); + + // Load Sports and Leagues + useEffect(() => { + loadSports(); + loadLeagues(); + }, []); + + // Load Matches when sport or date changes + useEffect(() => { + if (selectedSportId !== null) { + loadMatches(selectedSportId); + } + }, [selectedSportId, selectedDate]); + + // 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); + setSelectedSportId(1); + } + }; + + // 加载联赛 + const loadLeagues = async () => { + try { + if (selectedSportId !== null) { + const list = await fetchLeagues(selectedSportId, ""); + setLeagues(list); + } + } catch (e) { + console.error(e); + } + }; + + const loadMatches = async (sportId: number) => { + setLoading(true); + try { + // Pass selectedDate if API supported it + const list = await fetchTodayMatches(sportId); + setMatches(list); + } catch (e) { + console.error(e); + } finally { + setLoading(false); + } + }; + + 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) => { + setSelectedLeagueKey(leagueKey); + console.log("Selected league:", leagueKey); + }; + + 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/League Selector */} + {filterMode === "time" ? ( + setShowCalendarModal(true)} + > + + {selectedDate.getDate()} + + + {selectedDate.getHours()}: + {selectedDate.getMinutes().toString().padStart(2, "0")} + + + ) : ( + setShowLeagueModal(true)} + > + + + {selectedLeagueKey + ? leagues.find(l => l.key === selectedLeagueKey)?.name || t("home.select_league") + : t("home.select_league")} + + + )} + + ); -export default function LiveScreen() { return ( - Live Streams + + + {renderHeader()} + + {loading ? ( + + + {t("home.loading")} + + ) : ( + item.id} + renderItem={({ item }) => } + contentContainerStyle={styles.listContent} + ListEmptyComponent={ + + {t("home.no_matches")} + + } + /> + )} + + {/* Modals */} + setShowSportModal(false)} + title={t("home.select_sport")} + options={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, + }; + })} + selectedValue={selectedSportId} + onSelect={setSelectedSportId} + /> + + setShowCalendarModal(false)} + selectedDate={selectedDate} + onSelectDate={setSelectedDate} + /> + + setShowLeagueModal(false)} + leagues={leagues} + selectedLeagueKey={selectedLeagueKey} + onSelect={handleLeagueSelect} + /> ); } @@ -13,7 +300,53 @@ export default function LiveScreen() { const styles = StyleSheet.create({ container: { flex: 1, - alignItems: "center", + }, + 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, + fontWeight: "bold", + }, + dateMonthText: { + fontSize: 10, + opacity: 0.6, + }, + listContent: { + padding: 16, + paddingTop: 8, }, }); diff --git a/components/league-modal.tsx b/components/league-modal.tsx index f6af4ad..25a5e51 100644 --- a/components/league-modal.tsx +++ b/components/league-modal.tsx @@ -6,7 +6,7 @@ import { League } from "@/types/api"; import { Image } from "expo-image"; import React from "react"; import { useTranslation } from "react-i18next"; -import { Modal, Pressable, ScrollView, StyleSheet, View } from "react-native"; +import { ActivityIndicator, Modal, Pressable, ScrollView, StyleSheet, View } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; interface LeagueModalProps { @@ -14,6 +14,7 @@ interface LeagueModalProps { onClose: () => void; leagues: League[]; selectedLeagueKey: string | null; + loading?: boolean; onSelect: (leagueKey: string) => void; } @@ -22,6 +23,7 @@ export function LeagueModal({ onClose, leagues, selectedLeagueKey, + loading = false, onSelect, }: LeagueModalProps) { const { theme } = useTheme(); @@ -52,7 +54,14 @@ export function LeagueModal({ - {leagues.length === 0 ? ( + {loading ? ( + + + + {t("home.loading")} + + + ) : leagues.length === 0 ? ( {t("home.no_leagues")} @@ -160,6 +169,16 @@ const styles = StyleSheet.create({ scrollView: { maxHeight: 400, }, + loadingContainer: { + padding: 40, + alignItems: "center", + justifyContent: "center", + }, + loadingText: { + marginTop: 12, + fontSize: 14, + opacity: 0.6, + }, emptyContainer: { padding: 40, alignItems: "center",