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",