import { ThemedText } from "@/components/themed-text"; import { ThemedView } from "@/components/themed-view"; import { IconSymbol } from "@/components/ui/icon-symbol"; import { Colors } from "@/constants/theme"; import { useAppState } from "@/context/AppStateContext"; import { useTheme } from "@/context/ThemeContext"; import { fetchSearch } from "@/lib/api"; import { getInitials, getLogoGradient } from "@/lib/avatar-utils"; import { SearchLeague, SearchPlayer, SearchTeam } from "@/types/api"; import AsyncStorage from "@react-native-async-storage/async-storage"; import { BlurView } from "expo-blur"; import { LinearGradient } from "expo-linear-gradient"; import { Stack, useRouter } from "expo-router"; import React, { useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { ActivityIndicator, Animated, FlatList, ScrollView, StyleSheet, TextInput, TouchableOpacity, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; type SearchType = "all" | "team" | "player" | "league"; interface SearchEntity { id: string; type: SearchType; name: string; meta: { country?: string; league?: string; season?: string; team?: string; pos?: string; }; } // 将 API 数据转换为 SearchEntity function convertLeagueToEntity(league: SearchLeague): SearchEntity { return { id: `league_${league.ID}`, type: "league", name: league.name, meta: { country: league.countryName, season: "", // API 中没有 season 字段 }, }; } function convertPlayerToEntity(player: SearchPlayer): SearchEntity { return { id: `player_${player.ID}`, type: "player", name: player.name, meta: { country: player.countryName, team: player.teamName, league: player.leagueName, pos: player.position, }, }; } function convertTeamToEntity(team: SearchTeam): SearchEntity { return { id: `team_${team.ID}`, type: "team", name: team.name, meta: { country: team.countryName, league: team.leagueName, }, }; } // 获取副标题文本 function getSubText(entity: SearchEntity): string { const { meta } = entity; const parts: string[] = []; if (entity.type === "league") { // 联赛:显示国家 if (meta.country) parts.push(meta.country); } else if (entity.type === "player") { // 球员:优先显示球队和位置,如果没有位置则显示联赛 if (meta.team) parts.push(meta.team); if (meta.pos) { parts.push(meta.pos); } else if (meta.league) { parts.push(meta.league); } } else if (entity.type === "team") { // 球队:显示国家和联赛 if (meta.country) parts.push(meta.country); if (meta.league) parts.push(meta.league); } return parts.filter(Boolean).join(" · ") || ""; } const RECENT_SEARCHES_KEY = "recent_searches"; export default function SearchScreen() { const router = useRouter(); const { theme } = useTheme(); const { t } = useTranslation(); const insets = useSafeAreaInsets(); const isDark = theme === "dark"; const textColor = isDark ? "#FFFFFF" : "#000000"; const cardBg = isDark ? "#1C1C1E" : "#FFFFFF"; const borderColor = isDark ? "#2C2C2E" : "rgba(0,0,0,0.1)"; const pageBg = isDark ? Colors.dark.background : "#f2f2f7"; const { state } = useAppState(); const [searchType, setSearchType] = useState("all"); const [query, setQuery] = useState(""); const [recentSearches, setRecentSearches] = useState([]); const [showDetailSheet, setShowDetailSheet] = useState(false); const [selectedEntity, setSelectedEntity] = useState(null); const [entities, setEntities] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [hasSearched, setHasSearched] = useState(false); const sheetAnim = useRef(new Animated.Value(0)).current; const searchTimeoutRef = useRef(null); useEffect(() => { const loadRecentSearches = async () => { try { const stored = await AsyncStorage.getItem(RECENT_SEARCHES_KEY); if (stored) { const parsed = JSON.parse(stored); if (Array.isArray(parsed)) { setRecentSearches(parsed); } } } catch (err) { console.error("Failed to load recent searches:", err); } }; loadRecentSearches(); }, []); useEffect(() => { if (showDetailSheet) { Animated.spring(sheetAnim, { toValue: 1, useNativeDriver: true, }).start(); } else { Animated.timing(sheetAnim, { toValue: 0, duration: 200, useNativeDriver: true, }).start(); } }, [showDetailSheet]); // 搜索 API 调用(带防抖) useEffect(() => { if (searchTimeoutRef.current) { clearTimeout(searchTimeoutRef.current); } if (!query.trim()) { setEntities([]); setError(null); setHasSearched(false); return; } searchTimeoutRef.current = setTimeout(async () => { try { setLoading(true); setError(null); const result = await fetchSearch( query.trim(), state.selectedSportId || undefined ); const allEntities: SearchEntity[] = [ ...result.leagues.map(convertLeagueToEntity), ...result.players.map(convertPlayerToEntity), ...result.teams.map(convertTeamToEntity), ]; setEntities(allEntities); setHasSearched(true); if (allEntities.length > 0) { const searchQuery = query.trim(); setRecentSearches((prev) => { const filtered = prev.filter((x) => x !== searchQuery); const newRecent = [searchQuery, ...filtered].slice(0, 8); AsyncStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(newRecent)).catch( (err) => console.error("Failed to save recent searches:", err) ); return newRecent; }); } } catch (err: any) { console.error("Search error:", err); setError(err?.message || t("search.error")); setEntities([]); setHasSearched(true); } finally { setLoading(false); } }, 500); // 500ms 防抖 return () => { if (searchTimeoutRef.current) { clearTimeout(searchTimeoutRef.current); } }; }, [query, state.selectedSportId]); const filteredEntities = entities.filter((e) => { if (searchType === "all") return true; if (e.type !== searchType) return false; return true; }); const handleSearchTypeChange = (type: SearchType) => { setSearchType(type); // 不再清空输入框,保持搜索结果 }; const handleRecentClick = (text: string) => { setQuery(text); }; const handleEntityPress = (entity: SearchEntity) => { setSelectedEntity(entity); setShowDetailSheet(true); }; const handleClearRecent = () => { setRecentSearches([]); AsyncStorage.removeItem(RECENT_SEARCHES_KEY).catch( (err) => console.error("Failed to clear recent searches:", err) ); }; const handleSaveToRecent = () => { if (selectedEntity) { const newRecent = [ selectedEntity.name, ...recentSearches.filter((x) => x !== selectedEntity.name), ].slice(0, 8); setRecentSearches(newRecent); AsyncStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(newRecent)).catch( (err) => console.error("Failed to save recent searches:", err) ); } }; const handleOpenDetail = () => { if (selectedEntity) { handleSaveToRecent(); // TODO: Navigate to detail page setShowDetailSheet(false); } }; const renderSearchBar = () => ( {query.length > 0 && ( setQuery("")}> )} ); const renderChips = () => ( {(["all", "team", "player", "league"] as SearchType[]).map((type) => ( handleSearchTypeChange(type)} > {t(`search.type.${type}`)} ))} ); const renderRecentSearches = () => { if (recentSearches.length === 0 || query.trim()) return null; return ( <> {t("search.recent")} {t("search.clear")} {recentSearches.map((item, index) => ( handleRecentClick(item)} > {item} {t(`search.type.${searchType}`)} ))} ); }; const renderEntityItem = ({ item }: { item: SearchEntity }) => { const gradient = getLogoGradient(item.name); const initials = getInitials(item.name); return ( handleEntityPress(item)} > {initials} {item.name} {getSubText(item)} {t(`search.type.${item.type}`)} ); }; const renderEmptyState = () => ( {t("search.no_results")} {t("search.no_results_hint")} ); const renderDetailSheet = () => { if (!selectedEntity) return null; const translateY = sheetAnim.interpolate({ inputRange: [0, 1], outputRange: [600, 0], }); return ( <> setShowDetailSheet(false)} > {selectedEntity.name} {t(`search.type.${selectedEntity.type}`)} ·{" "} {getSubText(selectedEntity)} setShowDetailSheet(false)} > {selectedEntity.type === "team" && ( <> {selectedEntity.meta.country && ( {t("search.detail.country")} {selectedEntity.meta.country} )} {selectedEntity.meta.league && ( {t("search.detail.league")} {selectedEntity.meta.league} )} )} {selectedEntity.type === "player" && ( <> {selectedEntity.meta.team && ( {t("search.detail.team")} {selectedEntity.meta.team} )} {selectedEntity.meta.pos && ( {t("search.detail.position")} {selectedEntity.meta.pos} )} {selectedEntity.meta.league && ( {t("search.detail.league")} {selectedEntity.meta.league} )} {selectedEntity.meta.country && ( {t("search.detail.country")} {selectedEntity.meta.country} )} )} {selectedEntity.type === "league" && ( <> {selectedEntity.meta.country && ( {t("search.detail.country")} {selectedEntity.meta.country} )} )} {t("search.detail.open")} {/* {t("search.detail.save")} */} ); }; return ( {renderSearchBar()} {renderChips()} {renderRecentSearches()} {query.trim() && ( {t("search.results")} {searchType !== "all" && ` · ${t(`search.type.${searchType}`)}`} {loading ? t("search.loading") : error ? t("search.error") : `${filteredEntities.length} ${t("search.found")}`} )} {query.trim() ? ( loading ? ( ) : error ? ( {error} {t("search.error_hint")} ) : hasSearched && filteredEntities.length === 0 ? ( renderEmptyState() ) : ( item.id} renderItem={renderEntityItem} contentContainerStyle={[ styles.listContent, { paddingBottom: insets.bottom + 20 }, ]} showsVerticalScrollIndicator={false} /> ) ) : null} {showDetailSheet && renderDetailSheet()} ); } const styles = StyleSheet.create({ container: { flex: 1, }, content: { flex: 1, }, contentContainer: { paddingHorizontal: 12, paddingTop: 8, }, listContent: { paddingHorizontal: 12, }, searchWrap: { marginBottom: 10, }, search: { flexDirection: "row", alignItems: "center", gap: 8, height: 46, paddingHorizontal: 12, borderRadius: 18, borderWidth: 1, overflow: "hidden", }, searchInput: { flex: 1, minWidth: 0, fontSize: 15, }, chips: { marginBottom: 8, }, chipsContent: { gap: 10, paddingBottom: 2, }, chip: { paddingHorizontal: 14, paddingVertical: 6, borderRadius: 999, borderWidth: 1, }, chipActive: {}, chipText: { fontSize: 12, fontWeight: "700", opacity: 0.8, }, chipTextActive: { opacity: 1, }, sectionHeader: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", marginTop: 12, marginBottom: 8, }, sectionTitle: { fontSize: 12, opacity: 0.8, fontWeight: "700", }, sectionAction: { fontSize: 12, opacity: 0.5, fontWeight: "600", }, recentRow: { marginBottom: 8, }, recentRowContent: { gap: 10, paddingBottom: 2, }, recentCard: { minWidth: 190, padding: 12, borderRadius: 16, borderWidth: 1, }, recentCardTitle: { fontSize: 12, fontWeight: "600", }, recentCardSub: { fontSize: 12, opacity: 0.5, marginTop: 4, fontWeight: "500", }, entityRow: { flexDirection: "row", alignItems: "center", gap: 12, padding: 12, borderRadius: 10, borderWidth: 1, marginBottom: 10, }, logoDot: { width: 34, height: 34, borderRadius: 8, alignItems: "center", justifyContent: "center", borderWidth: 1, }, logoText: { fontSize: 12, fontWeight: "700", color: "rgba(255, 255, 255, 0.92)", }, entityMeta: { flex: 1, minWidth: 0, }, entityName: { fontSize: 12, fontWeight: "600", }, entitySub: { fontSize: 12, opacity: 0.5, marginTop: 3, fontWeight: "500", }, entityBadge: { fontSize: 10, opacity: 0.9, fontWeight: "700", }, center: { flex: 1, justifyContent: "center", alignItems: "center", padding: 20, }, emptyState: { padding: 14, borderRadius: 20, borderWidth: 1, }, emptyTitle: { fontSize: 12, fontWeight: "600", }, emptySub: { marginTop: 6, opacity: 0.5, fontSize: 12, fontWeight: "500", lineHeight: 18, }, overlay: { ...StyleSheet.absoluteFillObject, zIndex: 80, }, sheet: { position: "absolute", left: 0, right: 0, bottom: 0, borderTopLeftRadius: 22, borderTopRightRadius: 22, borderWidth: 1, padding: 12, zIndex: 90, shadowColor: "#000", shadowOffset: { width: 0, height: -10 }, shadowOpacity: 0.3, shadowRadius: 30, elevation: 20, }, sheetHeader: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", gap: 10, marginBottom: 12, }, sheetTitleContainer: { flex: 1, minWidth: 0, }, sheetTitle: { fontSize: 12, fontWeight: "600", }, sheetSub: { fontSize: 12, opacity: 0.5, fontWeight: "500", marginTop: 2, }, kvContainer: { flexDirection: "row", flexWrap: "wrap", gap: 10, marginBottom: 12, }, kvItem: { flex: 1, minWidth: "45%", padding: 12, borderRadius: 18, borderWidth: 1, }, kvLabel: { fontSize: 12, opacity: 0.5, fontWeight: "700", }, kvValue: { marginTop: 6, fontSize: 12, fontWeight: "600", }, sheetActions: { flexDirection: "row", gap: 10, }, sheetBtn: { flex: 1, height: 36, paddingHorizontal: 12, borderRadius: 14, borderWidth: 1, alignItems: "center", justifyContent: "center", }, sheetBtnPrimary: {}, sheetBtnText: { fontSize: 12, fontWeight: "700", opacity: 0.9, }, sheetBtnTextPrimary: { opacity: 1, }, });