diff --git a/app/search.tsx b/app/search.tsx index c64916e..844da78 100644 --- a/app/search.tsx +++ b/app/search.tsx @@ -1,13 +1,19 @@ 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 { 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, @@ -18,7 +24,7 @@ import { } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -type SearchType = "team" | "player" | "league"; +type SearchType = "all" | "team" | "player" | "league"; interface SearchEntity { id: string; @@ -33,22 +39,44 @@ interface SearchEntity { }; } -// 模拟数据 -const ENTITIES: SearchEntity[] = [ - { id: "t_rm", type: "team", name: "Real Madrid", meta: { country: "Spain", league: "LaLiga", season: "2025/26" } }, - { id: "t_fcb", type: "team", name: "FC Barcelona", meta: { country: "Spain", league: "LaLiga", season: "2025/26" } }, - { id: "t_mci", type: "team", name: "Manchester City", meta: { country: "England", league: "Premier League", season: "2025/26" } }, - { id: "t_liv", type: "team", name: "Liverpool", meta: { country: "England", league: "Premier League", season: "2025/26" } }, - { id: "t_ars", type: "team", name: "Arsenal", meta: { country: "England", league: "Premier League", season: "2025/26" } }, - { id: "p_mbappe", type: "player", name: "Kylian Mbappé", meta: { country: "France", team: "Real Madrid", league: "LaLiga", pos: "FW", season: "2025/26" } }, - { id: "p_haaland", type: "player", name: "Erling Haaland", meta: { country: "Norway", team: "Manchester City", league: "Premier League", pos: "FW", season: "2025/26" } }, - { id: "p_salah", type: "player", name: "Mohamed Salah", meta: { country: "Egypt", team: "Liverpool", league: "Premier League", pos: "FW", season: "2025/26" } }, - { id: "p_curry", type: "player", name: "Stephen Curry", meta: { country: "USA", team: "Golden State Warriors", league: "NBA", pos: "G", season: "2025/26" } }, - { id: "l_epl", type: "league", name: "Premier League", meta: { country: "England", season: "2025/26" } }, - { id: "l_laliga", type: "league", name: "LaLiga", meta: { country: "Spain", season: "2025/26" } }, - { id: "l_ucl", type: "league", name: "UEFA Champions League", meta: { country: "Europe", season: "2025/26" } }, - { id: "l_nba", type: "league", name: "NBA", meta: { country: "USA", season: "2025/26" } }, -]; +// 将 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 getInitials(name: string): string { @@ -113,33 +141,66 @@ function getLogoGradient(name: string): { color1: string; color2: string } { // 获取副标题文本 function getSubText(entity: SearchEntity): string { const { meta } = entity; + const parts: string[] = []; + if (entity.type === "league") { - return `${meta.country || ""} · ${meta.season || ""}`; + // 联赛:显示国家 + 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); } - if (entity.type === "player") { - return `${meta.team || ""} · ${meta.league || ""}`; - } - return `${meta.country || ""} · ${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 { state } = useAppState(); - const [searchType, setSearchType] = useState("team"); + const [searchType, setSearchType] = useState("all"); const [query, setQuery] = useState(""); - const [recentSearches, setRecentSearches] = useState([ - "Real Madrid", - "Premier League", - "Curry", - ]); + 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 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) { @@ -156,18 +217,70 @@ export default function SearchScreen() { } }, [showDetailSheet]); - const filteredEntities = ENTITIES.filter((e) => { + // 搜索 API 调用(带防抖) + useEffect(() => { + if (searchTimeoutRef.current) { + clearTimeout(searchTimeoutRef.current); + } + + if (!query.trim()) { + setEntities([]); + setError(null); + 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); + 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([]); + } 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; - if (!query.trim()) return true; - const q = query.toLowerCase(); - return ( - e.name.toLowerCase().includes(q) || getSubText(e).toLowerCase().includes(q) - ); + return true; }); const handleSearchTypeChange = (type: SearchType) => { setSearchType(type); - setQuery(""); + // 不再清空输入框,保持搜索结果 }; const handleRecentClick = (text: string) => { @@ -181,6 +294,9 @@ export default function SearchScreen() { const handleClearRecent = () => { setRecentSearches([]); + AsyncStorage.removeItem(RECENT_SEARCHES_KEY).catch( + (err) => console.error("Failed to clear recent searches:", err) + ); }; const handleSaveToRecent = () => { @@ -188,8 +304,11 @@ export default function SearchScreen() { const newRecent = [ selectedEntity.name, ...recentSearches.filter((x) => x !== selectedEntity.name), - ].slice(0, 10); + ].slice(0, 8); setRecentSearches(newRecent); + AsyncStorage.setItem(RECENT_SEARCHES_KEY, JSON.stringify(newRecent)).catch( + (err) => console.error("Failed to save recent searches:", err) + ); } }; @@ -242,7 +361,7 @@ export default function SearchScreen() { style={styles.chips} contentContainerStyle={styles.chipsContent} > - {(["team", "player", "league"] as SearchType[]).map((type) => ( + {(["all", "team", "player", "league"] as SearchType[]).map((type) => ( {selectedEntity.type === "team" && ( <> - - - {t("search.detail.country")} - - - {selectedEntity.meta.country || "-"} - - - - - {t("search.detail.league")} - - - {selectedEntity.meta.league || "-"} - - - - - {t("search.detail.season")} - - - {selectedEntity.meta.season || "-"} - - + {selectedEntity.meta.country && ( + + + {t("search.detail.country")} + + + {selectedEntity.meta.country} + + + )} + {selectedEntity.meta.league && ( + + + {t("search.detail.league")} + + + {selectedEntity.meta.league} + + + )} )} {selectedEntity.type === "player" && ( <> - - - {t("search.detail.team")} - - - {selectedEntity.meta.team || "-"} - - - - - {t("search.detail.league")} - - - {selectedEntity.meta.league || "-"} - - - - - {t("search.detail.position")} - - - {selectedEntity.meta.pos || "-"} - - - - - {t("search.detail.country")} - - - {selectedEntity.meta.country || "-"} - - + {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" && ( <> - - - {t("search.detail.country")} - - - {selectedEntity.meta.country || "-"} - - - - - {t("search.detail.season")} - - - {selectedEntity.meta.season || "-"} - - + {selectedEntity.meta.country && ( + + + {t("search.detail.country")} + + + {selectedEntity.meta.country} + + + )} )} @@ -684,7 +777,7 @@ export default function SearchScreen() { {t("search.detail.open")} - {t("search.detail.save")} - + */} @@ -762,26 +855,6 @@ export default function SearchScreen() { {t("search.subtitle")} - - - @@ -790,29 +863,49 @@ export default function SearchScreen() { {renderChips()} {renderRecentSearches()} - - - {t("search.results")} · {t(`search.type.${searchType}`)} - - - {query.trim() - ? `${filteredEntities.length} ${t("search.found")}` - : t("search.tap_to_open")} - - + {query.trim() && ( + + + {t("search.results")} + {searchType !== "all" && ` · ${t(`search.type.${searchType}`)}`} + + + {loading + ? t("search.loading") + : error + ? t("search.error") + : `${filteredEntities.length} ${t("search.found")}`} + + + )} - item.id} - renderItem={renderEntityItem} - ListEmptyComponent={() => renderEmptyState()} - contentContainerStyle={[ - styles.listContent, - { paddingBottom: insets.bottom + 20 }, - ]} - showsVerticalScrollIndicator={false} - /> + {query.trim() ? ( + loading ? ( + + + + ) : error ? ( + + {error} + + {t("search.error_hint")} + + + ) : ( + item.id} + renderItem={renderEntityItem} + ListEmptyComponent={() => renderEmptyState()} + contentContainerStyle={[ + styles.listContent, + { paddingBottom: insets.bottom + 20 }, + ]} + showsVerticalScrollIndicator={false} + /> + ) + ) : null} {showDetailSheet && renderDetailSheet()} @@ -984,6 +1077,12 @@ const styles = StyleSheet.create({ opacity: 0.52, fontWeight: "900", }, + center: { + flex: 1, + justifyContent: "center", + alignItems: "center", + padding: 20, + }, emptyState: { padding: 14, borderRadius: 20, diff --git a/i18n/locales/en.json b/i18n/locales/en.json index f8fe0bd..a0e0d41 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -133,7 +133,13 @@ "tap_to_open": "Tap to open", "no_results": "No results", "no_results_hint": "Try another keyword or switch category", + "loading": "Loading", + "error": "Error", + "error_hint": "Search failed, please try again", + "start_searching": "Start searching", + "start_searching_hint": "Enter keywords to search for teams, players or leagues", "type": { + "all": "All", "team": "Team", "player": "Player", "league": "League" diff --git a/i18n/locales/zh.json b/i18n/locales/zh.json index 26ce400..e0e512d 100644 --- a/i18n/locales/zh.json +++ b/i18n/locales/zh.json @@ -133,7 +133,13 @@ "tap_to_open": "点击打开", "no_results": "无结果", "no_results_hint": "尝试其他关键词或切换分类", + "loading": "加载中", + "error": "错误", + "error_hint": "搜索失败,请重试", + "start_searching": "开始搜索", + "start_searching_hint": "输入关键词搜索球队、球员或联赛", "type": { + "all": "全部", "team": "球队", "player": "球员", "league": "联赛"