Files
physical-expo/components/selection-modal.tsx
2026-01-14 10:45:22 +08:00

289 lines
8.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { ThemedText } from "@/components/themed-text";
import { IconSymbol } from "@/components/ui/icon-symbol";
import { Colors } from "@/constants/theme";
import { useTheme } from "@/context/ThemeContext";
import { Image } from "expo-image";
import React from "react";
import { useTranslation } from "react-i18next";
import { FlatList, Modal, Pressable, StyleSheet, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
interface SelectionOption {
id: number | string;
label: string;
value: any;
icon?: string; // 图标名称
}
// 运动ID到运动键的映射支持8种运动
// 根据API文档1:足球, 2:篮球, 3:网球, 4:板球
// 扩展为8种1:足球, 2:篮球, 3:网球, 4:板球, 5:棒球, 6:羽毛球, 7:斯诺克, 8:排球
const getSportKeyById = (id: number): string => {
const sportMap: { [key: number]: string } = {
1: "football",
2: "basketball",
3: "tennis",
4: "cricket",
5: "baseball",
6: "badminton",
7: "snooker",
8: "volleyball",
};
return sportMap[id] || "football";
};
// 运动名称到图标文件名的映射支持8种运动
const getSportIconName = (label: string, id?: number): string => {
// 优先使用ID
if (id !== undefined) {
return getSportKeyById(id);
}
const labelLower = label.toLowerCase();
if (labelLower.includes("足球") || labelLower.includes("football") || labelLower.includes("soccer")) {
return "football";
}
if (labelLower.includes("篮球") || labelLower.includes("basketball")) {
return "basketball";
}
if (labelLower.includes("板球") || labelLower.includes("cricket")) {
return "cricket";
}
if (labelLower.includes("网球") || labelLower.includes("tennis")) {
return "tennis";
}
if (labelLower.includes("棒球") || labelLower.includes("baseball")) {
return "baseball";
}
if (labelLower.includes("羽毛球") || labelLower.includes("badminton")) {
return "badminton";
}
if (labelLower.includes("斯诺克") || labelLower.includes("snooker")) {
return "snooker";
}
if (labelLower.includes("排球") || labelLower.includes("volleyball")) {
return "volleyball";
}
// 默认图标
return "football";
};
// 获取图标资源
const getSportIconSource = (label: string, isSelected: boolean, id?: number) => {
const iconName = getSportIconName(label, id);
const state = isSelected ? "active" : "inactive";
// 使用 require 动态导入图标8种运动
const iconMap: { [key: string]: any } = {
"football_active": require("@/assets/balls/football_active.svg"),
"football_inactive": require("@/assets/balls/football_inactive.svg"),
"basketball_active": require("@/assets/balls/basketball_active.svg"),
"basketball_inactive": require("@/assets/balls/basketball_inactive.svg"),
"cricket_active": require("@/assets/balls/cricket_active.svg"),
"cricket_inactive": require("@/assets/balls/cricket_inactive.svg"),
"tennis_active": require("@/assets/balls/tennis_active.svg"),
"tennis_inactive": require("@/assets/balls/tennis_inactive.svg"),
"baseball_active": require("@/assets/balls/baseball_active.svg"),
"baseball_inactive": require("@/assets/balls/baseball_inactive.svg"),
"badminton_active": require("@/assets/balls/badminton_active.svg"),
"badminton_inactive": require("@/assets/balls/badminton_inactive.svg"),
"snooker_active": require("@/assets/balls/snooker_active.svg"),
"snooker_inactive": require("@/assets/balls/snooker_inactive.svg"),
"volleyball_active": require("@/assets/balls/football_active.svg"), // 使用football作为默认因为volleyball图标不存在
"volleyball_inactive": require("@/assets/balls/football_inactive.svg"),
};
return iconMap[`${iconName}_${state}`] || iconMap["football_inactive"];
};
interface SelectionModalProps {
visible: boolean;
onClose: () => void;
title: string;
options: SelectionOption[];
onSelect: (value: any) => void;
selectedValue: any;
}
export function SelectionModal({
visible,
onClose,
title,
options,
onSelect,
selectedValue,
}: SelectionModalProps) {
const { theme } = useTheme();
const { t } = useTranslation();
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
visible={visible}
transparent
animationType="slide"
onRequestClose={onClose}
>
<Pressable style={styles.overlay} onPress={onClose}>
<View />
</Pressable>
<View style={[styles.sheet, { backgroundColor: bg }]}>
<SafeAreaView edges={["bottom"]}>
<View style={styles.header}>
<ThemedText type="subtitle" style={styles.title}>
{title}
</ThemedText>
<Pressable onPress={onClose} style={styles.closeButton}>
<IconSymbol name="close" size={24} color={text} />
</Pressable>
</View>
<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>
);
}
const styles = StyleSheet.create({
overlay: {
flex: 1,
backgroundColor: "rgba(0,0,0,0.5)",
},
sheet: {
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
padding: 20,
height: "55%",
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 20,
},
title: {
fontSize: 18,
fontWeight: "600",
},
closeButton: {
width: 32,
height: 32,
alignItems: "center",
justifyContent: "center",
},
listContent: {
paddingBottom: 20,
},
option: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 16,
paddingHorizontal: 16,
marginBottom: 12,
borderRadius: 12,
},
iconContainer: {
width: 48,
height: 48,
borderRadius: 24,
alignItems: "center",
justifyContent: "center",
marginRight: 12,
},
iconImage: {
width: 32,
height: 32,
},
content: {
flex: 1,
},
optionLabel: {
fontSize: 16,
marginBottom: 4,
},
statusText: {
fontSize: 12,
opacity: 0.6,
},
indicator: {
width: 8,
height: 8,
borderRadius: 4,
marginLeft: 12,
},
});