优化联赛选择模态框性能
This commit is contained in:
@@ -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 (
|
||||
<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 (
|
||||
<Modal
|
||||
@@ -53,86 +151,7 @@ export function LeagueModal({
|
||||
<IconSymbol name="close" size={24} color={text} />
|
||||
</Pressable>
|
||||
</View>
|
||||
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
|
||||
{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>
|
||||
{renderContent()}
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
</Modal>
|
||||
@@ -166,8 +185,8 @@ const styles = StyleSheet.create({
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
scrollView: {
|
||||
maxHeight: 400,
|
||||
listContent: {
|
||||
paddingBottom: 20,
|
||||
},
|
||||
loadingContainer: {
|
||||
padding: 40,
|
||||
|
||||
@@ -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 (
|
||||
<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 (
|
||||
<Modal
|
||||
@@ -141,70 +200,18 @@ export function SelectionModal({
|
||||
<IconSymbol name="close" size={24} color={text} />
|
||||
</Pressable>
|
||||
</View>
|
||||
<ScrollView style={styles.scrollView} showsVerticalScrollIndicator={false}>
|
||||
{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 (
|
||||
<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>
|
||||
<FlatList
|
||||
data={options}
|
||||
renderItem={renderOption}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
showsVerticalScrollIndicator={false}
|
||||
contentContainerStyle={styles.listContent}
|
||||
removeClippedSubviews={true}
|
||||
maxToRenderPerBatch={10}
|
||||
updateCellsBatchingPeriod={50}
|
||||
windowSize={10}
|
||||
initialNumToRender={10}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
</View>
|
||||
</Modal>
|
||||
@@ -238,8 +245,8 @@ const styles = StyleSheet.create({
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
scrollView: {
|
||||
maxHeight: 400,
|
||||
listContent: {
|
||||
paddingBottom: 20,
|
||||
},
|
||||
option: {
|
||||
flexDirection: "row",
|
||||
|
||||
Reference in New Issue
Block a user