diff --git a/app/search.tsx b/app/search.tsx index 844da78..ece5b1c 100644 --- a/app/search.tsx +++ b/app/search.tsx @@ -5,6 +5,7 @@ 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"; @@ -78,71 +79,12 @@ function convertTeamToEntity(team: SearchTeam): SearchEntity { }; } -// 生成首字母 -function getInitials(name: string): string { - const s = name.trim(); - if (!s) return "SN"; - const clean = s.replace(/[^a-zA-Z0-9]+/g, " ").trim(); - const parts = clean ? clean.split(/\s+/) : [s]; - const a = (parts[0] || s).charAt(0) || "S"; - const b = (parts[1] || "").charAt(0) || ((parts[0] || s).charAt(1) || "N"); - return (a + b).toUpperCase(); -} - -// 生成颜色哈希 -function hashString(str: string): number { - let hash = 2166136261; - for (let i = 0; i < str.length; i++) { - hash ^= str.charCodeAt(i); - hash = (hash * 16777619) >>> 0; - } - return hash >>> 0; -} - -// HSL 转 RGB -function hslToRgb(h: number, s: number, l: number): string { - s /= 100; - l /= 100; - const c = (1 - Math.abs(2 * l - 1)) * s; - const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); - const m = l - c / 2; - let r = 0, g = 0, b = 0; - - if (0 <= h && h < 60) { - r = c; g = x; b = 0; - } else if (60 <= h && h < 120) { - r = x; g = c; b = 0; - } else if (120 <= h && h < 180) { - r = 0; g = c; b = x; - } else if (180 <= h && h < 240) { - r = 0; g = x; b = c; - } else if (240 <= h && h < 300) { - r = x; g = 0; b = c; - } else if (300 <= h && h < 360) { - r = c; g = 0; b = x; - } - r = Math.round((r + m) * 255); - g = Math.round((g + m) * 255); - b = Math.round((b + m) * 255); - return `rgb(${r}, ${g}, ${b})`; -} - -// 生成渐变颜色 -function getLogoGradient(name: string): { color1: string; color2: string } { - const h = hashString(name); - const hue = h % 360; - const hue2 = (hue + 36 + (h % 24)) % 360; - return { - color1: hslToRgb(hue, 85, 58), - color2: hslToRgb(hue2, 85, 48), - }; -} // 获取副标题文本 function getSubText(entity: SearchEntity): string { const { meta } = entity; const parts: string[] = []; - + if (entity.type === "league") { // 联赛:显示国家 if (meta.country) parts.push(meta.country); @@ -159,7 +101,7 @@ function getSubText(entity: SearchEntity): string { if (meta.country) parts.push(meta.country); if (meta.league) parts.push(meta.league); } - + return parts.filter(Boolean).join(" · ") || ""; } @@ -171,6 +113,8 @@ export default function SearchScreen() { const { t } = useTranslation(); const insets = useSafeAreaInsets(); const isDark = theme === "dark"; + const cardBg = isDark ? "#1C1C1E" : "#FFFFFF"; + const borderColor = isDark ? "#2C2C2E" : "rgba(0,0,0,0.06)"; const { state } = useAppState(); const [searchType, setSearchType] = useState("all"); @@ -181,6 +125,7 @@ export default function SearchScreen() { 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); @@ -226,6 +171,7 @@ export default function SearchScreen() { if (!query.trim()) { setEntities([]); setError(null); + setHasSearched(false); return; } @@ -245,6 +191,7 @@ export default function SearchScreen() { ]; setEntities(allEntities); + setHasSearched(true); if (allEntities.length > 0) { const searchQuery = query.trim(); setRecentSearches((prev) => { @@ -260,6 +207,7 @@ export default function SearchScreen() { console.error("Search error:", err); setError(err?.message || t("search.error")); setEntities([]); + setHasSearched(true); } finally { setLoading(false); } @@ -328,12 +276,8 @@ export default function SearchScreen() { style={[ styles.search, { - backgroundColor: isDark - ? "rgba(0, 0, 0, 0.28)" - : "rgba(255, 255, 255, 0.8)", - borderColor: isDark - ? "rgba(255, 255, 255, 0.12)" - : "rgba(0, 0, 0, 0.1)", + backgroundColor: cardBg, + borderColor: borderColor, }, ]} > @@ -368,20 +312,8 @@ export default function SearchScreen() { styles.chip, searchType === type && styles.chipActive, { - backgroundColor: isDark - ? searchType === type - ? "rgba(255, 255, 255, 0.12)" - : "rgba(255, 255, 255, 0.06)" - : searchType === type - ? "rgba(0, 0, 0, 0.08)" - : "rgba(0, 0, 0, 0.04)", - borderColor: isDark - ? searchType === type - ? "rgba(255, 255, 255, 0.14)" - : "rgba(255, 255, 255, 0.10)" - : searchType === type - ? "rgba(0, 0, 0, 0.12)" - : "rgba(0, 0, 0, 0.08)", + backgroundColor: cardBg, + borderColor: borderColor, }, ]} onPress={() => handleSearchTypeChange(type)} @@ -426,12 +358,8 @@ export default function SearchScreen() { style={[ styles.recentCard, { - backgroundColor: isDark - ? "rgba(255, 255, 255, 0.05)" - : "rgba(0, 0, 0, 0.04)", - borderColor: isDark - ? "rgba(255, 255, 255, 0.10)" - : "rgba(0, 0, 0, 0.08)", + backgroundColor: cardBg, + borderColor: borderColor, }, ]} onPress={() => handleRecentClick(item)} @@ -458,12 +386,8 @@ export default function SearchScreen() { style={[ styles.entityRow, { - backgroundColor: isDark - ? "rgba(255, 255, 255, 0.06)" - : "rgba(0, 0, 0, 0.04)", - borderColor: isDark - ? "rgba(255, 255, 255, 0.10)" - : "rgba(0, 0, 0, 0.08)", + backgroundColor: cardBg, + borderColor: borderColor, }, ]} onPress={() => handleEntityPress(item)} @@ -503,12 +427,8 @@ export default function SearchScreen() { style={[ styles.emptyState, { - backgroundColor: isDark - ? "rgba(255, 255, 255, 0.05)" - : "rgba(0, 0, 0, 0.04)", - borderColor: isDark - ? "rgba(255, 255, 255, 0.10)" - : "rgba(0, 0, 0, 0.08)", + backgroundColor: cardBg, + borderColor: borderColor, }, ]} > @@ -547,12 +467,8 @@ export default function SearchScreen() { styles.sheet, { transform: [{ translateY }], - backgroundColor: isDark - ? "rgba(18, 20, 26, 0.96)" - : "rgba(255, 255, 255, 0.96)", - borderColor: isDark - ? "rgba(255, 255, 255, 0.12)" - : "rgba(0, 0, 0, 0.1)", + backgroundColor: cardBg, + borderColor: borderColor, }, { paddingBottom: insets.bottom + 14 }, ]} @@ -571,12 +487,8 @@ export default function SearchScreen() { style={[ styles.iconBtn, { - backgroundColor: isDark - ? "rgba(255, 255, 255, 0.06)" - : "rgba(0, 0, 0, 0.06)", - borderColor: isDark - ? "rgba(255, 255, 255, 0.10)" - : "rgba(0, 0, 0, 0.10)", + backgroundColor: cardBg, + borderColor: borderColor, }, ]} onPress={() => setShowDetailSheet(false)} @@ -597,12 +509,8 @@ export default function SearchScreen() { style={[ styles.kvItem, { - backgroundColor: isDark - ? "rgba(255, 255, 255, 0.05)" - : "rgba(0, 0, 0, 0.04)", - borderColor: isDark - ? "rgba(255, 255, 255, 0.10)" - : "rgba(0, 0, 0, 0.08)", + backgroundColor: cardBg, + borderColor: borderColor, }, ]} > @@ -619,12 +527,8 @@ export default function SearchScreen() { style={[ styles.kvItem, { - backgroundColor: isDark - ? "rgba(255, 255, 255, 0.05)" - : "rgba(0, 0, 0, 0.04)", - borderColor: isDark - ? "rgba(255, 255, 255, 0.10)" - : "rgba(0, 0, 0, 0.08)", + backgroundColor: cardBg, + borderColor: borderColor, }, ]} > @@ -645,12 +549,8 @@ export default function SearchScreen() { style={[ styles.kvItem, { - backgroundColor: isDark - ? "rgba(255, 255, 255, 0.05)" - : "rgba(0, 0, 0, 0.04)", - borderColor: isDark - ? "rgba(255, 255, 255, 0.10)" - : "rgba(0, 0, 0, 0.08)", + backgroundColor: cardBg, + borderColor: borderColor, }, ]} > @@ -667,12 +567,8 @@ export default function SearchScreen() { style={[ styles.kvItem, { - backgroundColor: isDark - ? "rgba(255, 255, 255, 0.05)" - : "rgba(0, 0, 0, 0.04)", - borderColor: isDark - ? "rgba(255, 255, 255, 0.10)" - : "rgba(0, 0, 0, 0.08)", + backgroundColor: cardBg, + borderColor: borderColor, }, ]} > @@ -689,12 +585,8 @@ export default function SearchScreen() { style={[ styles.kvItem, { - backgroundColor: isDark - ? "rgba(255, 255, 255, 0.05)" - : "rgba(0, 0, 0, 0.04)", - borderColor: isDark - ? "rgba(255, 255, 255, 0.10)" - : "rgba(0, 0, 0, 0.08)", + backgroundColor: cardBg, + borderColor: borderColor, }, ]} > @@ -711,12 +603,8 @@ export default function SearchScreen() { style={[ styles.kvItem, { - backgroundColor: isDark - ? "rgba(255, 255, 255, 0.05)" - : "rgba(0, 0, 0, 0.04)", - borderColor: isDark - ? "rgba(255, 255, 255, 0.10)" - : "rgba(0, 0, 0, 0.08)", + backgroundColor: cardBg, + borderColor: borderColor, }, ]} > @@ -737,12 +625,8 @@ export default function SearchScreen() { style={[ styles.kvItem, { - backgroundColor: isDark - ? "rgba(255, 255, 255, 0.05)" - : "rgba(0, 0, 0, 0.04)", - borderColor: isDark - ? "rgba(255, 255, 255, 0.10)" - : "rgba(0, 0, 0, 0.08)", + backgroundColor: cardBg, + borderColor: borderColor, }, ]} > @@ -763,12 +647,8 @@ export default function SearchScreen() { style={[ styles.sheetBtn, { - backgroundColor: isDark - ? "rgba(0, 0, 0, 0.14)" - : "rgba(0, 0, 0, 0.06)", - borderColor: isDark - ? "rgba(255, 255, 255, 0.12)" - : "rgba(0, 0, 0, 0.1)", + backgroundColor: cardBg, + borderColor: borderColor, }, ]} onPress={handleOpenDetail} @@ -822,9 +702,7 @@ export default function SearchScreen() { style={[ styles.nav, { - backgroundColor: isDark - ? "rgba(7, 8, 11, 0.95)" - : "rgba(255, 255, 255, 0.95)", + backgroundColor: cardBg, }, { paddingTop: insets.top + 10 }, ]} @@ -833,12 +711,8 @@ export default function SearchScreen() { style={[ styles.iconBtn, { - backgroundColor: isDark - ? "rgba(255, 255, 255, 0.06)" - : "rgba(0, 0, 0, 0.06)", - borderColor: isDark - ? "rgba(255, 255, 255, 0.10)" - : "rgba(0, 0, 0, 0.10)", + backgroundColor: cardBg, + borderColor: borderColor, }, ]} onPress={() => router.back()} @@ -873,8 +747,8 @@ export default function SearchScreen() { {loading ? t("search.loading") : error - ? t("search.error") - : `${filteredEntities.length} ${t("search.found")}`} + ? t("search.error") + : `${filteredEntities.length} ${t("search.found")}`} )} @@ -892,12 +766,13 @@ export default function SearchScreen() { {t("search.error_hint")} + ) : hasSearched && filteredEntities.length === 0 ? ( + renderEmptyState() ) : ( item.id} renderItem={renderEntityItem} - ListEmptyComponent={() => renderEmptyState()} contentContainerStyle={[ styles.listContent, { paddingBottom: insets.bottom + 20 }, @@ -942,13 +817,13 @@ const styles = StyleSheet.create({ }, brandTitle: { fontSize: 18, - fontWeight: "900", + fontWeight: "700", letterSpacing: 0.2, }, brandSub: { fontSize: 12, - opacity: 0.42, - fontWeight: "800", + opacity: 0.5, + fontWeight: "600", }, content: { flex: 1, @@ -993,9 +868,9 @@ const styles = StyleSheet.create({ }, chipActive: {}, chipText: { - fontSize: 14, - fontWeight: "900", - opacity: 0.78, + fontSize: 12, + fontWeight: "700", + opacity: 0.8, }, chipTextActive: { opacity: 1, @@ -1008,14 +883,14 @@ const styles = StyleSheet.create({ marginBottom: 8, }, sectionTitle: { - fontSize: 13, - opacity: 0.55, - fontWeight: "900", + fontSize: 12, + opacity: 0.8, + fontWeight: "700", }, sectionAction: { fontSize: 12, - opacity: 0.42, - fontWeight: "800", + opacity: 0.5, + fontWeight: "600", }, recentRow: { marginBottom: 8, @@ -1031,12 +906,14 @@ const styles = StyleSheet.create({ borderWidth: 1, }, recentCardTitle: { - fontWeight: "900", + fontSize: 12, + fontWeight: "600", }, recentCardSub: { fontSize: 12, - opacity: 0.55, + opacity: 0.5, marginTop: 4, + fontWeight: "500", }, entityRow: { flexDirection: "row", @@ -1057,7 +934,7 @@ const styles = StyleSheet.create({ }, logoText: { fontSize: 12, - fontWeight: "900", + fontWeight: "700", color: "rgba(255, 255, 255, 0.92)", }, entityMeta: { @@ -1065,17 +942,19 @@ const styles = StyleSheet.create({ minWidth: 0, }, entityName: { - fontWeight: "900", + fontSize: 12, + fontWeight: "600", }, entitySub: { fontSize: 12, - opacity: 0.55, + opacity: 0.5, marginTop: 3, + fontWeight: "500", }, entityBadge: { - fontSize: 12, - opacity: 0.52, - fontWeight: "900", + fontSize: 10, + opacity: 0.9, + fontWeight: "700", }, center: { flex: 1, @@ -1089,13 +968,14 @@ const styles = StyleSheet.create({ borderWidth: 1, }, emptyTitle: { - fontWeight: "900", + fontSize: 12, + fontWeight: "600", }, emptySub: { marginTop: 6, - opacity: 0.6, + opacity: 0.5, fontSize: 12, - fontWeight: "800", + fontWeight: "500", lineHeight: 18, }, overlay: { @@ -1130,12 +1010,13 @@ const styles = StyleSheet.create({ minWidth: 0, }, sheetTitle: { - fontWeight: "900", + fontSize: 12, + fontWeight: "600", }, sheetSub: { fontSize: 12, - opacity: 0.52, - fontWeight: "800", + opacity: 0.5, + fontWeight: "500", marginTop: 2, }, kvContainer: { @@ -1153,12 +1034,13 @@ const styles = StyleSheet.create({ }, kvLabel: { fontSize: 12, - opacity: 0.52, - fontWeight: "900", + opacity: 0.5, + fontWeight: "700", }, kvValue: { marginTop: 6, - fontWeight: "900", + fontSize: 12, + fontWeight: "600", }, sheetActions: { flexDirection: "row", @@ -1175,9 +1057,9 @@ const styles = StyleSheet.create({ }, sheetBtnPrimary: {}, sheetBtnText: { - fontSize: 14, - fontWeight: "900", - opacity: 0.86, + fontSize: 12, + fontWeight: "700", + opacity: 0.9, }, sheetBtnTextPrimary: { opacity: 1, diff --git a/components/match-card-league.tsx b/components/match-card-league.tsx index 395eee2..b1ca1d8 100644 --- a/components/match-card-league.tsx +++ b/components/match-card-league.tsx @@ -4,7 +4,9 @@ import { Colors } from "@/constants/theme"; import { useAppState } from "@/context/AppStateContext"; import { useTheme } from "@/context/ThemeContext"; import { addFavorite, removeFavorite } from "@/lib/api"; +import { getInitials, getLogoGradient } from "@/lib/avatar-utils"; import { Match } from "@/types/api"; +import { LinearGradient } from "expo-linear-gradient"; import { useRouter } from "expo-router"; import React, { useState } from "react"; import { Image, Pressable, StyleSheet, TouchableOpacity, View } from "react-native"; @@ -179,14 +181,31 @@ export function MatchCardLeague({ - + {(() => { + const teamName = isTennis ? (match as any).eventFirstPlayer : (match.home || match.homeTeamName); + const logoUri = isTennis + ? (match as any).eventFirstPlayerLogo + : ((match as any).homeLogo || match.homeTeamLogo); + const hasLogo = logoUri && logoUri.trim() !== "" && !logoUri.includes("placehold"); + const gradient = getLogoGradient(teamName || ""); + const initials = getInitials(teamName || ""); + + return hasLogo ? ( + + ) : ( + + {initials} + + ); + })()} {isTennis ? (match as any).eventFirstPlayer : (match.home || match.homeTeamName)} @@ -197,14 +216,31 @@ export function MatchCardLeague({ - + {(() => { + const teamName = isTennis ? (match as any).eventSecondPlayer : (match.away || match.awayTeamName); + const logoUri = isTennis + ? (match as any).eventSecondPlayerLogo + : ((match as any).awayLogo || match.awayTeamLogo); + const hasLogo = logoUri && logoUri.trim() !== "" && !logoUri.includes("placehold"); + const gradient = getLogoGradient(teamName || ""); + const initials = getInitials(teamName || ""); + + return hasLogo ? ( + + ) : ( + + {initials} + + ); + })()} {isTennis ? (match as any).eventSecondPlayer : (match.away || match.awayTeamName)} @@ -310,13 +346,26 @@ const styles = StyleSheet.create({ marginRight: 12, backgroundColor: "#E5E5E5", }, + teamLogoGradient: { + width: 24, + height: 24, + borderRadius: 14, + marginRight: 12, + alignItems: "center", + justifyContent: "center", + }, + teamLogoText: { + fontSize: 9, + fontWeight: "700", + color: "rgba(255, 255, 255, 0.92)", + }, teamNameContainer: { flexDirection: "row", alignItems: "center", flex: 1, }, teamName: { - fontSize: 15, + fontSize: 13, fontWeight: "500", }, cardBadge: { diff --git a/components/matches-by-league.tsx b/components/matches-by-league.tsx index 168a723..83a2d0d 100644 --- a/components/matches-by-league.tsx +++ b/components/matches-by-league.tsx @@ -3,8 +3,10 @@ import { ThemedText } from "@/components/themed-text"; import { Colors } from "@/constants/theme"; import { useTheme } from "@/context/ThemeContext"; import { fetchLeagues, fetchTodayMatches } from "@/lib/api"; +import { getInitials, getLogoGradient } from "@/lib/avatar-utils"; import { League, Match } from "@/types/api"; import { Image } from "expo-image"; +import { LinearGradient } from "expo-linear-gradient"; import React, { useState } from "react"; import { ActivityIndicator, @@ -208,12 +210,27 @@ export function MatchesByLeague({ style={[styles.leagueHeaderWrapper, { backgroundColor: headerBg }]} > - + {(() => { + const hasLogo = league.logo && league.logo.trim() !== "" && !league.logo.includes("placehold"); + const gradient = getLogoGradient(league.name || ""); + const initials = getInitials(league.name || ""); + + return hasLogo ? ( + + ) : ( + + {initials} + + ); + })()} >> 0; + } + return hash >>> 0; +} + +function hslToRgb(h: number, s: number, l: number): string { + s /= 100; + l /= 100; + const c = (1 - Math.abs(2 * l - 1)) * s; + const x = c * (1 - Math.abs(((h / 60) % 2) - 1)); + const m = l - c / 2; + let r = 0, g = 0, b = 0; + + if (0 <= h && h < 60) { + r = c; g = x; b = 0; + } else if (60 <= h && h < 120) { + r = x; g = c; b = 0; + } else if (120 <= h && h < 180) { + r = 0; g = c; b = x; + } else if (180 <= h && h < 240) { + r = 0; g = x; b = c; + } else if (240 <= h && h < 300) { + r = x; g = 0; b = c; + } else if (300 <= h && h < 360) { + r = c; g = 0; b = x; + } + r = Math.round((r + m) * 255); + g = Math.round((g + m) * 255); + b = Math.round((b + m) * 255); + return `rgb(${r}, ${g}, ${b})`; +} + +export function getLogoGradient(name: string): { color1: string; color2: string } { + const h = hashString(name); + const hue = h % 360; + const hue2 = (hue + 36 + (h % 24)) % 360; + return { + color1: hslToRgb(hue, 85, 58), + color2: hslToRgb(hue2, 85, 48), + }; +}