diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 963e58f..e9fad51 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -10,7 +10,7 @@ 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 React, { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { ActivityIndicator, @@ -168,6 +168,29 @@ export default function HomeScreen() { 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 */} @@ -265,49 +288,37 @@ export default function HomeScreen() { /> )} - {/* 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} - /> + {/* Modals - 条件渲染,只在可见时渲染 */} + {showSportModal && ( + setShowSportModal(false)} + title={t("home.select_sport")} + options={sportOptions} + selectedValue={selectedSportId} + onSelect={setSelectedSportId} + /> + )} - setShowCalendarModal(false)} - selectedDate={selectedDate} - onSelectDate={setSelectedDate} - /> + {showCalendarModal && ( + setShowCalendarModal(false)} + selectedDate={selectedDate} + onSelectDate={setSelectedDate} + /> + )} - setShowLeagueModal(false)} - leagues={leagues} - selectedLeagueKey={selectedLeagueKey} - loading={loadingLeagues} - onSelect={handleLeagueSelect} - /> + {showLeagueModal && ( + setShowLeagueModal(false)} + leagues={leagues} + selectedLeagueKey={selectedLeagueKey} + loading={loadingLeagues} + onSelect={handleLeagueSelect} + /> + )} ); } diff --git a/components/league-modal.tsx b/components/league-modal.tsx index 25a5e51..2e113cf 100644 --- a/components/league-modal.tsx +++ b/components/league-modal.tsx @@ -4,9 +4,9 @@ import { Colors } from "@/constants/theme"; import { useTheme } from "@/context/ThemeContext"; import { League } from "@/types/api"; import { Image } from "expo-image"; -import React from "react"; +import React, { useMemo } from "react"; import { useTranslation } from "react-i18next"; -import { ActivityIndicator, Modal, Pressable, ScrollView, StyleSheet, View } from "react-native"; +import { ActivityIndicator, FlatList, Modal, Pressable, StyleSheet, View } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; interface LeagueModalProps { @@ -31,6 +31,104 @@ export function LeagueModal({ const isDark = theme === "dark"; const bg = isDark ? "#1C1C1E" : "#FFFFFF"; const text = isDark ? "#FFFFFF" : "#000000"; + const optionBg = isDark ? "#2C2C2E" : "#F5F5F5"; + const iconBg = isDark ? "#3A3A3C" : "#E5E5EA"; + + const renderLeagueItem = ({ item: league }: { item: League }) => { + const isSelected = selectedLeagueKey === league.key; + + return ( + { + onSelect(league.key); + onClose(); + }} + > + {/* 左侧图标 */} + + {league.logo ? ( + + ) : ( + + )} + + + {/* 中间内容 */} + + + {league.name} + + {league.countryName && ( + + {league.countryName} + + )} + + + {/* 右侧指示点 */} + + + ); + }; + + const renderContent = () => { + if (loading) { + return ( + + + + {t("home.loading")} + + + ); + } + + if (leagues.length === 0) { + return ( + + + {t("home.no_leagues")} + + + ); + } + + return ( + item.id.toString()} + showsVerticalScrollIndicator={false} + contentContainerStyle={styles.listContent} + removeClippedSubviews={true} + maxToRenderPerBatch={10} + updateCellsBatchingPeriod={50} + windowSize={10} + initialNumToRender={10} + /> + ); + }; return ( - - {loading ? ( - - - - {t("home.loading")} - - - ) : leagues.length === 0 ? ( - - - {t("home.no_leagues")} - - - ) : ( - leagues.map((league) => { - const isSelected = selectedLeagueKey === league.key; - const optionBg = isDark ? "#2C2C2E" : "#F5F5F5"; - const iconBg = isDark ? "#3A3A3C" : "#E5E5EA"; - - return ( - { - onSelect(league.key); - onClose(); - }} - > - {/* 左侧图标 */} - - {league.logo ? ( - - ) : ( - - )} - - - {/* 中间内容 */} - - - {league.name} - - {league.countryName && ( - - {league.countryName} - - )} - - - {/* 右侧指示点 */} - - - ); - }) - )} - + {renderContent()} @@ -166,8 +185,8 @@ const styles = StyleSheet.create({ alignItems: "center", justifyContent: "center", }, - scrollView: { - maxHeight: 400, + listContent: { + paddingBottom: 20, }, loadingContainer: { padding: 40, diff --git a/components/selection-modal.tsx b/components/selection-modal.tsx index 940c2cd..4f112af 100644 --- a/components/selection-modal.tsx +++ b/components/selection-modal.tsx @@ -5,7 +5,7 @@ import { useTheme } from "@/context/ThemeContext"; import { Image } from "expo-image"; import React from "react"; import { useTranslation } from "react-i18next"; -import { Modal, Pressable, ScrollView, StyleSheet, View } from "react-native"; +import { FlatList, Modal, Pressable, StyleSheet, View } from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; interface SelectionOption { @@ -119,6 +119,65 @@ export function SelectionModal({ const isDark = theme === "dark"; const bg = isDark ? "#1C1C1E" : "#FFFFFF"; const text = isDark ? "#FFFFFF" : "#000000"; + const optionBg = isDark ? "#2C2C2E" : "#F5F5F5"; + const iconBg = isDark ? "#3A3A3C" : "#E5E5EA"; + + const renderOption = ({ item: opt }: { item: SelectionOption }) => { + const isSelected = selectedValue === opt.value; + + // 获取运动键用于i18n和图标 + const sportId = typeof opt.id === "number" ? opt.id : undefined; + const sportKey = sportId ? getSportKeyById(sportId) : getSportIconName(opt.label, sportId); + const sportName = t(`sports.${sportKey}`, { defaultValue: opt.label }); + + return ( + { + onSelect(opt.value); + onClose(); + }} + > + {/* 左侧图标 */} + + + + + {/* 中间内容 */} + + + {sportName} + + + {isSelected ? t("selection.selected") : t("selection.click_to_toggle")} + + + + {/* 右侧指示点 */} + + + ); + }; return ( - - {options.map((opt) => { - const isSelected = selectedValue === opt.value; - const optionBg = isDark ? "#2C2C2E" : "#F5F5F5"; - const iconBg = isDark ? "#3A3A3C" : "#E5E5EA"; - - // 获取运动键用于i18n和图标 - const sportId = typeof opt.id === "number" ? opt.id : undefined; - const sportKey = sportId ? getSportKeyById(sportId) : getSportIconName(opt.label, sportId); - const sportName = t(`sports.${sportKey}`, { defaultValue: opt.label }); - - return ( - { - onSelect(opt.value); - onClose(); - }} - > - {/* 左侧图标 */} - - - - - {/* 中间内容 */} - - - {sportName} - - - {isSelected ? t("selection.selected") : t("selection.click_to_toggle")} - - - - {/* 右侧指示点 */} - - - ); - })} - + item.id.toString()} + showsVerticalScrollIndicator={false} + contentContainerStyle={styles.listContent} + removeClippedSubviews={true} + maxToRenderPerBatch={10} + updateCellsBatchingPeriod={50} + windowSize={10} + initialNumToRender={10} + /> @@ -238,8 +245,8 @@ const styles = StyleSheet.create({ alignItems: "center", justifyContent: "center", }, - scrollView: { - maxHeight: 400, + listContent: { + paddingBottom: 20, }, option: { flexDirection: "row",