优化联赛选择模态框性能
This commit is contained in:
@@ -10,7 +10,7 @@ import { Colors } from "@/constants/theme";
|
|||||||
import { useTheme } from "@/context/ThemeContext";
|
import { useTheme } from "@/context/ThemeContext";
|
||||||
import { fetchLeagues, fetchSports, fetchTodayMatches } from "@/lib/api";
|
import { fetchLeagues, fetchSports, fetchTodayMatches } from "@/lib/api";
|
||||||
import { League, Match, Sport } from "@/types/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 { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
@@ -168,6 +168,29 @@ export default function HomeScreen() {
|
|||||||
console.log("Selected league:", 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 = () => (
|
const renderHeader = () => (
|
||||||
<View style={styles.filterContainer}>
|
<View style={styles.filterContainer}>
|
||||||
{/* Time/League Filter Toggle */}
|
{/* Time/League Filter Toggle */}
|
||||||
@@ -265,49 +288,37 @@ export default function HomeScreen() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Modals */}
|
{/* Modals - 条件渲染,只在可见时渲染 */}
|
||||||
<SelectionModal
|
{showSportModal && (
|
||||||
visible={showSportModal}
|
<SelectionModal
|
||||||
onClose={() => setShowSportModal(false)}
|
visible={showSportModal}
|
||||||
title={t("home.select_sport")}
|
onClose={() => setShowSportModal(false)}
|
||||||
options={sports.map((s) => {
|
title={t("home.select_sport")}
|
||||||
const sportKeyMap: { [key: number]: string } = {
|
options={sportOptions}
|
||||||
1: "football",
|
selectedValue={selectedSportId}
|
||||||
2: "basketball",
|
onSelect={setSelectedSportId}
|
||||||
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}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<CalendarModal
|
{showCalendarModal && (
|
||||||
visible={showCalendarModal}
|
<CalendarModal
|
||||||
onClose={() => setShowCalendarModal(false)}
|
visible={showCalendarModal}
|
||||||
selectedDate={selectedDate}
|
onClose={() => setShowCalendarModal(false)}
|
||||||
onSelectDate={setSelectedDate}
|
selectedDate={selectedDate}
|
||||||
/>
|
onSelectDate={setSelectedDate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
<LeagueModal
|
{showLeagueModal && (
|
||||||
visible={showLeagueModal}
|
<LeagueModal
|
||||||
onClose={() => setShowLeagueModal(false)}
|
visible={showLeagueModal}
|
||||||
leagues={leagues}
|
onClose={() => setShowLeagueModal(false)}
|
||||||
selectedLeagueKey={selectedLeagueKey}
|
leagues={leagues}
|
||||||
loading={loadingLeagues}
|
selectedLeagueKey={selectedLeagueKey}
|
||||||
onSelect={handleLeagueSelect}
|
loading={loadingLeagues}
|
||||||
/>
|
onSelect={handleLeagueSelect}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</ThemedView>
|
</ThemedView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import { Colors } from "@/constants/theme";
|
|||||||
import { useTheme } from "@/context/ThemeContext";
|
import { useTheme } from "@/context/ThemeContext";
|
||||||
import { League } from "@/types/api";
|
import { League } from "@/types/api";
|
||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import React from "react";
|
import React, { useMemo } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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";
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
|
|
||||||
interface LeagueModalProps {
|
interface LeagueModalProps {
|
||||||
@@ -31,6 +31,104 @@ export function LeagueModal({
|
|||||||
const isDark = theme === "dark";
|
const isDark = theme === "dark";
|
||||||
const bg = isDark ? "#1C1C1E" : "#FFFFFF";
|
const bg = isDark ? "#1C1C1E" : "#FFFFFF";
|
||||||
const text = isDark ? "#FFFFFF" : "#000000";
|
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 (
|
||||||
|
<Pressable
|
||||||
|
style={[styles.option, { backgroundColor: optionBg }]}
|
||||||
|
onPress={() => {
|
||||||
|
onSelect(league.key);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 左侧图标 */}
|
||||||
|
<View style={[styles.iconContainer, { backgroundColor: iconBg }]}>
|
||||||
|
{league.logo ? (
|
||||||
|
<Image
|
||||||
|
source={{ uri: league.logo }}
|
||||||
|
style={styles.leagueLogo}
|
||||||
|
contentFit="contain"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<IconSymbol name="trophy-outline" size={24} color={text} />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 中间内容 */}
|
||||||
|
<View style={styles.content}>
|
||||||
|
<ThemedText
|
||||||
|
style={[
|
||||||
|
styles.leagueName,
|
||||||
|
{ fontWeight: isSelected ? "600" : "400" },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{league.name}
|
||||||
|
</ThemedText>
|
||||||
|
{league.countryName && (
|
||||||
|
<ThemedText style={styles.countryName}>
|
||||||
|
{league.countryName}
|
||||||
|
</ThemedText>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 右侧指示点 */}
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.indicator,
|
||||||
|
{
|
||||||
|
backgroundColor: isSelected
|
||||||
|
? Colors.light.tint
|
||||||
|
: isDark
|
||||||
|
? "#666666"
|
||||||
|
: "#CCCCCC",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderContent = () => {
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<View style={styles.loadingContainer}>
|
||||||
|
<ActivityIndicator size="large" color={Colors[theme].tint} />
|
||||||
|
<ThemedText style={styles.loadingText}>
|
||||||
|
{t("home.loading")}
|
||||||
|
</ThemedText>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (leagues.length === 0) {
|
||||||
|
return (
|
||||||
|
<View style={styles.emptyContainer}>
|
||||||
|
<ThemedText style={styles.emptyText}>
|
||||||
|
{t("home.no_leagues")}
|
||||||
|
</ThemedText>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FlatList
|
||||||
|
data={leagues}
|
||||||
|
renderItem={renderLeagueItem}
|
||||||
|
keyExtractor={(item) => item.id.toString()}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
contentContainerStyle={styles.listContent}
|
||||||
|
removeClippedSubviews={true}
|
||||||
|
maxToRenderPerBatch={10}
|
||||||
|
updateCellsBatchingPeriod={50}
|
||||||
|
windowSize={10}
|
||||||
|
initialNumToRender={10}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -53,86 +151,7 @@ export function LeagueModal({
|
|||||||
<IconSymbol name="close" size={24} color={text} />
|
<IconSymbol name="close" size={24} color={text} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
|
{renderContent()}
|
||||||
{loading ? (
|
|
||||||
<View style={styles.loadingContainer}>
|
|
||||||
<ActivityIndicator size="large" color={Colors[theme].tint} />
|
|
||||||
<ThemedText style={styles.loadingText}>
|
|
||||||
{t("home.loading")}
|
|
||||||
</ThemedText>
|
|
||||||
</View>
|
|
||||||
) : leagues.length === 0 ? (
|
|
||||||
<View style={styles.emptyContainer}>
|
|
||||||
<ThemedText style={styles.emptyText}>
|
|
||||||
{t("home.no_leagues")}
|
|
||||||
</ThemedText>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
leagues.map((league) => {
|
|
||||||
const isSelected = selectedLeagueKey === league.key;
|
|
||||||
const optionBg = isDark ? "#2C2C2E" : "#F5F5F5";
|
|
||||||
const iconBg = isDark ? "#3A3A3C" : "#E5E5EA";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Pressable
|
|
||||||
key={league.id}
|
|
||||||
style={[
|
|
||||||
styles.option,
|
|
||||||
{ backgroundColor: optionBg },
|
|
||||||
]}
|
|
||||||
onPress={() => {
|
|
||||||
onSelect(league.key);
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* 左侧图标 */}
|
|
||||||
<View style={[styles.iconContainer, { backgroundColor: iconBg }]}>
|
|
||||||
{league.logo ? (
|
|
||||||
<Image
|
|
||||||
source={{ uri: league.logo }}
|
|
||||||
style={styles.leagueLogo}
|
|
||||||
contentFit="contain"
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<IconSymbol name="trophy-outline" size={24} color={text} />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 中间内容 */}
|
|
||||||
<View style={styles.content}>
|
|
||||||
<ThemedText
|
|
||||||
style={[
|
|
||||||
styles.leagueName,
|
|
||||||
{ fontWeight: isSelected ? "600" : "400" },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{league.name}
|
|
||||||
</ThemedText>
|
|
||||||
{league.countryName && (
|
|
||||||
<ThemedText style={styles.countryName}>
|
|
||||||
{league.countryName}
|
|
||||||
</ThemedText>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 右侧指示点 */}
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.indicator,
|
|
||||||
{
|
|
||||||
backgroundColor: isSelected
|
|
||||||
? Colors.light.tint
|
|
||||||
: isDark
|
|
||||||
? "#666666"
|
|
||||||
: "#CCCCCC",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
)}
|
|
||||||
</ScrollView>
|
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -166,8 +185,8 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
},
|
},
|
||||||
scrollView: {
|
listContent: {
|
||||||
maxHeight: 400,
|
paddingBottom: 20,
|
||||||
},
|
},
|
||||||
loadingContainer: {
|
loadingContainer: {
|
||||||
padding: 40,
|
padding: 40,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useTheme } from "@/context/ThemeContext";
|
|||||||
import { Image } from "expo-image";
|
import { Image } from "expo-image";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
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";
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
|
|
||||||
interface SelectionOption {
|
interface SelectionOption {
|
||||||
@@ -119,6 +119,65 @@ export function SelectionModal({
|
|||||||
const isDark = theme === "dark";
|
const isDark = theme === "dark";
|
||||||
const bg = isDark ? "#1C1C1E" : "#FFFFFF";
|
const bg = isDark ? "#1C1C1E" : "#FFFFFF";
|
||||||
const text = isDark ? "#FFFFFF" : "#000000";
|
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 (
|
||||||
|
<Pressable
|
||||||
|
style={[styles.option, { backgroundColor: optionBg }]}
|
||||||
|
onPress={() => {
|
||||||
|
onSelect(opt.value);
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* 左侧图标 */}
|
||||||
|
<View style={[styles.iconContainer, { backgroundColor: iconBg }]}>
|
||||||
|
<Image
|
||||||
|
source={getSportIconSource(opt.label, isSelected, sportId)}
|
||||||
|
style={styles.iconImage}
|
||||||
|
contentFit="contain"
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 中间内容 */}
|
||||||
|
<View style={styles.content}>
|
||||||
|
<ThemedText
|
||||||
|
style={[
|
||||||
|
styles.optionLabel,
|
||||||
|
{ fontWeight: isSelected ? "600" : "400" },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
{sportName}
|
||||||
|
</ThemedText>
|
||||||
|
<ThemedText style={styles.statusText}>
|
||||||
|
{isSelected ? t("selection.selected") : t("selection.click_to_toggle")}
|
||||||
|
</ThemedText>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* 右侧指示点 */}
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.indicator,
|
||||||
|
{
|
||||||
|
backgroundColor: isSelected
|
||||||
|
? Colors.light.tint
|
||||||
|
: isDark
|
||||||
|
? "#666666"
|
||||||
|
: "#CCCCCC",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -141,70 +200,18 @@ export function SelectionModal({
|
|||||||
<IconSymbol name="close" size={24} color={text} />
|
<IconSymbol name="close" size={24} color={text} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
|
<FlatList
|
||||||
{options.map((opt) => {
|
data={options}
|
||||||
const isSelected = selectedValue === opt.value;
|
renderItem={renderOption}
|
||||||
const optionBg = isDark ? "#2C2C2E" : "#F5F5F5";
|
keyExtractor={(item) => item.id.toString()}
|
||||||
const iconBg = isDark ? "#3A3A3C" : "#E5E5EA";
|
showsVerticalScrollIndicator={false}
|
||||||
|
contentContainerStyle={styles.listContent}
|
||||||
// 获取运动键用于i18n和图标
|
removeClippedSubviews={true}
|
||||||
const sportId = typeof opt.id === "number" ? opt.id : undefined;
|
maxToRenderPerBatch={10}
|
||||||
const sportKey = sportId ? getSportKeyById(sportId) : getSportIconName(opt.label, sportId);
|
updateCellsBatchingPeriod={50}
|
||||||
const sportName = t(`sports.${sportKey}`, { defaultValue: opt.label });
|
windowSize={10}
|
||||||
|
initialNumToRender={10}
|
||||||
return (
|
/>
|
||||||
<Pressable
|
|
||||||
key={opt.id}
|
|
||||||
style={[
|
|
||||||
styles.option,
|
|
||||||
{ backgroundColor: optionBg },
|
|
||||||
]}
|
|
||||||
onPress={() => {
|
|
||||||
onSelect(opt.value);
|
|
||||||
onClose();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{/* 左侧图标 */}
|
|
||||||
<View style={[styles.iconContainer, { backgroundColor: iconBg }]}>
|
|
||||||
<Image
|
|
||||||
source={getSportIconSource(opt.label, isSelected, sportId)}
|
|
||||||
style={styles.iconImage}
|
|
||||||
contentFit="contain"
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 中间内容 */}
|
|
||||||
<View style={styles.content}>
|
|
||||||
<ThemedText
|
|
||||||
style={[
|
|
||||||
styles.optionLabel,
|
|
||||||
{ fontWeight: isSelected ? "600" : "400" },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{sportName}
|
|
||||||
</ThemedText>
|
|
||||||
<ThemedText style={styles.statusText}>
|
|
||||||
{isSelected ? t("selection.selected") : t("selection.click_to_toggle")}
|
|
||||||
</ThemedText>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 右侧指示点 */}
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
styles.indicator,
|
|
||||||
{
|
|
||||||
backgroundColor: isSelected
|
|
||||||
? Colors.light.tint
|
|
||||||
: isDark
|
|
||||||
? "#666666"
|
|
||||||
: "#CCCCCC",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ScrollView>
|
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
</View>
|
</View>
|
||||||
</Modal>
|
</Modal>
|
||||||
@@ -238,8 +245,8 @@ const styles = StyleSheet.create({
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
},
|
},
|
||||||
scrollView: {
|
listContent: {
|
||||||
maxHeight: 400,
|
paddingBottom: 20,
|
||||||
},
|
},
|
||||||
option: {
|
option: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
|
|||||||
Reference in New Issue
Block a user