diff --git a/assets/down-light.svg b/assets/down-light.svg new file mode 100644 index 0000000..e7ccf0a --- /dev/null +++ b/assets/down-light.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/down.svg b/assets/down.svg new file mode 100644 index 0000000..9d5ee64 --- /dev/null +++ b/assets/down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/components/match-card-league.tsx b/components/match-card-league.tsx index 0c04c42..4180a86 100644 --- a/components/match-card-league.tsx +++ b/components/match-card-league.tsx @@ -1,5 +1,6 @@ import { ThemedText } from "@/components/themed-text"; import { IconSymbol } from "@/components/ui/icon-symbol"; +import { Colors } from "@/constants/theme"; import { useTheme } from "@/context/ThemeContext"; import { addFavorite, removeFavorite } from "@/lib/api"; import { Match } from "@/types/api"; @@ -13,6 +14,14 @@ interface MatchCardLeagueProps { onFavoriteToggle?: (matchId: string, isFav: boolean) => void; } +const WINNER_BORDER = "#D4A84B"; +const WINNER_BG_LIGHT = "#FFF8E7"; +const WINNER_BG_DARK = "#3D3422"; +const NEUTRAL_BG_LIGHT = "#F5F5F5"; +const NEUTRAL_BG_DARK = "#2C2C2E"; +const RED_CARD_COLOR = "#C53030"; +const YELLOW_CARD_COLOR = "#F59E0B"; + export function MatchCardLeague({ match, onPress, @@ -28,12 +37,14 @@ export function MatchCardLeague({ }, [match.fav]); const isDark = theme === "dark"; - // 截图中的卡片背景通常非常深,接近纯黑 - const cardBg = isDark ? "#1C1C1E" : "#FFFFFF"; - const textColor = isDark ? "#FFFFFF" : "#000000"; - // 赢家的高亮颜色 (截图中的橙黄色) - const winnerColor = "#FF9500"; - const loserColor = isDark ? "#FFFFFF" : "#000000"; + const textColor = isDark ? Colors.dark.text : Colors.light.text; + const secondaryText = isDark ? "#8E8E93" : "#6B7280"; + const scoreBorder = isDark ? "rgba(255,255,255,0.18)" : "rgba(0,0,0,0.12)"; + const scoreBg = isDark ? "rgba(255,255,255,0.04)" : "rgba(255,255,255,0.6)"; + + const isLive = React.useMemo(() => { + return !!match.isLive; + }, [match.isLive]); const handlePress = () => { if (onPress) { @@ -72,116 +83,117 @@ export function MatchCardLeague({ } }; - // --- 数据解析与样式逻辑 --- + const scoreParts = React.useMemo(() => { + const s = (match.scoreText || "").trim(); + const m = s.match(/(\d+)\s*[-:]\s*(\d+)/); + if (m) return { home: m[1], away: m[2], hasScore: true }; + if (s && s !== "-" && s !== "0 - 0") + return { home: s, away: "", hasScore: true }; + if (s === "0 - 0") return { home: "0", away: "0", hasScore: true }; + return { home: "", away: "", hasScore: false }; + }, [match.scoreText]); - // 假设 match 对象中有 homeScore 和 awayScore (数字或字符串) - // 如果 API 只有 "1 - 0" 这种 scoreText,你需要在这里拆分 - // 这里为了演示,假设 match 对象已经扩展了这些字段,或者我们从 scoreText 简单解析 - let homeScore = 0; - let awayScore = 0; + const { homeRedCards, awayRedCards, homeYellowCards, awayYellowCards } = React.useMemo(() => { + let homeRed = (match as any).homeRedCards || 0; + let awayRed = (match as any).awayRedCards || 0; + let homeYellow = (match as any).homeYellowCards || 0; + let awayYellow = (match as any).awayYellowCards || 0; - // 简单的解析逻辑 demo (根据你的实际数据结构调整) - if (match.scoreText && match.scoreText.includes("-")) { - const parts = match.scoreText.split("-"); - homeScore = parseInt(parts[0].trim()) || 0; - awayScore = parseInt(parts[1].trim()) || 0; - } - // 如果 match 对象里直接有 match.homeScore 最好: - // homeScore = match.homeScore; - // awayScore = match.awayScore; + const matchStats = (match as any).stats; + if (matchStats) { + try { + const stats = typeof matchStats === 'string' ? JSON.parse(matchStats) : matchStats; + const redCardsStat = stats.find((s: any) => s.type === "Red Cards"); + const yellowCardsStat = stats.find((s: any) => s.type === "Yellow Cards"); - // 判断文字颜色和背景样式 - let homeColor = loserColor; - let awayColor = loserColor; - let homeScoreBg = "#2C2C2E"; // 默认深灰背景 - let awayScoreBg = "#2C2C2E"; - let homeBorderColor = "transparent"; - let awayBorderColor = "transparent"; + if (redCardsStat) { + homeRed = parseInt(redCardsStat.home) || 0; + awayRed = parseInt(redCardsStat.away) || 0; + } + if (yellowCardsStat) { + homeYellow = parseInt(yellowCardsStat.home) || 0; + awayYellow = parseInt(yellowCardsStat.away) || 0; + } + } catch (e) { + console.error("Parse stats error:", e); + } + } - if (homeScore > awayScore) { - homeColor = "#000000"; // 赢家黑色文字 - homeScoreBg = winnerColor; // 金色背景 - homeBorderColor = winnerColor; - } else if (awayScore > homeScore) { - awayColor = "#000000"; // 赢家黑色文字 - awayScoreBg = winnerColor; // 金色背景 - awayBorderColor = winnerColor; - } - // 如果相等,保持默认深灰背景 + return { homeRedCards: homeRed, awayRedCards: awayRed, homeYellowCards: homeYellow, awayYellowCards: awayYellow }; + }, [(match as any).stats, (match as any).homeRedCards, (match as any).awayRedCards]); + + const cardBg = isDark ? "#1C1C1E" : "#F5F5F5"; return ( [ styles.card, - { backgroundColor: cardBg, opacity: pressed ? 0.8 : 1 }, + { + backgroundColor: cardBg, + opacity: pressed ? 0.7 : 1 + }, ]} > - {/* 1. 左侧:时间/状态 */} - {/* 如果有状态字段 match.meta (如 'FT', 'AET'), 优先显示,否则显示时间 */} - + {(match.time || "").toUpperCase()} - {/* 2. 中间:球队信息 (上下排布) */} - {/* 主队行 */} - - {match.home || match.homeTeamName} - + + + {match.home || match.homeTeamName} + + {homeYellowCards > 0 && } + {homeRedCards > 0 && } + - {/* 客队行 */} - + - - {match.away || match.awayTeamName} - + + + {match.away || match.awayTeamName} + + {awayYellowCards > 0 && } + {awayRedCards > 0 && } + - {/* 3. 右侧:比分与铃铛 */} - {/* 比分列 (上下排布) */} - - - - {homeScore} + {scoreParts.hasScore ? ( + + + {scoreParts.home} + + + + {scoreParts.away} - - - {awayScore} - - - + ) : ( + + )} - {/* 收藏按钮 */} { e.stopPropagation(); @@ -194,7 +206,7 @@ export function MatchCardLeague({ @@ -205,76 +217,87 @@ export function MatchCardLeague({ const styles = StyleSheet.create({ card: { flexDirection: "row", - paddingVertical: 14, + paddingVertical: 16, paddingHorizontal: 16, - marginHorizontal: 0, - marginBottom: 0, - borderRadius: 12, alignItems: "center", - minHeight: 68, + borderRadius: 12, + marginBottom: 8, }, - // 左侧时间列 leftColumn: { - width: 52, + width: 48, justifyContent: "center", - alignItems: "flex-start", - marginRight: 12, + alignItems: "center", + marginRight: 8, }, statusText: { - fontSize: 12, - color: "#8E8E93", // 次要文本颜色 (Grey) + fontSize: 14, fontWeight: "600", }, - // 中间球队列 teamsColumn: { flex: 1, justifyContent: "center", - paddingRight: 8, + paddingRight: 12, }, teamRow: { flexDirection: "row", alignItems: "center", }, teamLogo: { - width: 22, - height: 22, - borderRadius: 11, // 圆形图标 - marginRight: 10, - backgroundColor: "#3A3A3C", // 图片加载占位 + width: 24, + height: 24, + borderRadius: 14, + marginRight: 12, + backgroundColor: "#E5E5E5", }, - teamName: { - fontSize: 16, - fontWeight: "500", + teamNameContainer: { + flexDirection: "row", + alignItems: "center", flex: 1, }, - // 右侧整体包装 + teamName: { + fontSize: 15, + fontWeight: "500", + }, + cardBadge: { + width: 14, + height: 18, + borderRadius: 2, + marginLeft: 8, + }, + redCard: { + backgroundColor: RED_CARD_COLOR, + }, + yellowCard: { + backgroundColor: YELLOW_CARD_COLOR, + }, rightWrapper: { flexDirection: "row", alignItems: "center", - }, - // 比分列 - scoresColumn: { - alignItems: "flex-end", // 数字右对齐 - justifyContent: "center", - marginRight: 12, + gap: 6, }, scoreBox: { - minWidth: 32, - height: 28, - borderRadius: 6, - justifyContent: "center", + width: 36, + height: 54, + borderRadius: 8, + borderWidth: 1.5, alignItems: "center", - paddingHorizontal: 8, + justifyContent: "center", }, - scoreText: { - fontSize: 16, - fontWeight: "700", - lineHeight: 20, + scoreBoxText: { + fontSize: 20, + fontWeight: "900", + }, + scoreDivider: { + width: "60%", + height: 1, + marginVertical: 1, + }, + scoreBoxPlaceholder: { + width: 36, + height: 54, }, - // 收藏按钮 favoriteButton: { - paddingHorizontal: 4, - paddingVertical: 4, + padding: 4, justifyContent: 'center', alignItems: 'center', } diff --git a/components/matches-by-league.tsx b/components/matches-by-league.tsx index 509f4b2..30c0210 100644 --- a/components/matches-by-league.tsx +++ b/components/matches-by-league.tsx @@ -1,11 +1,13 @@ import { MatchCardLeague } from "@/components/match-card-league"; import { ThemedText } from "@/components/themed-text"; +import { Colors } from "@/constants/theme"; import { useTheme } from "@/context/ThemeContext"; import { fetchLeagues, fetchTodayMatches } from "@/lib/api"; import { League, Match } from "@/types/api"; +import { Image } from "expo-image"; import React, { useState } from "react"; import { - Image, + ActivityIndicator, LayoutAnimation, Platform, ScrollView, @@ -14,6 +16,11 @@ import { UIManager, View, } from "react-native"; +import Animated, { + useAnimatedStyle, + useSharedValue, + withTiming, +} from "react-native-reanimated"; // 开启 Android 上的 LayoutAnimation if ( @@ -41,6 +48,34 @@ export function MatchesByLeague({ const { theme } = useTheme(); const isDark = theme === "dark"; + function ChevronIcon({ isCollapsed, isDark }: { isCollapsed: boolean; isDark: boolean }) { + const rotation = useSharedValue(isCollapsed ? 0 : 180); + + React.useEffect(() => { + rotation.value = withTiming(isCollapsed ? 0 : 180, { duration: 200 }); + }, [isCollapsed, rotation]); + + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ rotate: `${rotation.value}deg` }], + })); + + return ( + + + + + + ); + } + const [leagues, setLeagues] = useState([]); const [collapsed, setCollapsed] = useState>({}); const [matchesByLeagueKey, setMatchesByLeagueKey] = useState< @@ -49,6 +84,7 @@ export function MatchesByLeague({ const [loadingLeagueKey, setLoadingLeagueKey] = useState< Record >({}); + const [loadingLeagues, setLoadingLeagues] = useState(true); const dateStr = React.useMemo(() => { const year = date.getFullYear(); @@ -59,6 +95,7 @@ export function MatchesByLeague({ React.useEffect(() => { let mounted = true; + setLoadingLeagues(true); fetchLeagues({ sportId, date: dateStr, @@ -79,8 +116,12 @@ export function MatchesByLeague({ }); setMatchesByLeagueKey({}); setLoadingLeagueKey({}); + setLoadingLeagues(false); }) - .catch(() => { }); + .catch(() => { + if (!mounted) return; + setLoadingLeagues(false); + }); return () => { mounted = false; }; @@ -105,12 +146,25 @@ export function MatchesByLeague({ page: 1, pageSize: 50, }); + console.log("choose", res); setMatchesByLeagueKey((prev) => ({ ...prev, [leagueKey]: res.list })); } finally { setLoadingLeagueKey((prev) => ({ ...prev, [leagueKey]: false })); } }; + if (loadingLeagues) { + return ( + + + 加载中... + + ); + } + if (leagues.length === 0) { return ( @@ -119,13 +173,18 @@ export function MatchesByLeague({ ); } + const cardBg = isDark ? "#1C1C1E" : "#FFFFFF"; + const dividerColor = isDark ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.06)"; + const skeletonBg = isDark ? "#2C2C2E" : "#E5E5E5"; + const headerBg = isDark ? "#1C1C1E" : "#FBFBFB"; + return ( {leagues.map((league) => { const isCollapsed = collapsed[league.key] !== false; @@ -141,103 +200,81 @@ export function MatchesByLeague({ toggleCollapse(league.key); } }} - style={styles.leagueHeaderWrapper} + style={[styles.leagueHeaderWrapper, { backgroundColor: headerBg }]} > - {league.name} + + {league.name} + - + {league.countryName || "International"} - - - {league.matchCount} - - - {enableCollapsible && league.matchCount > 0 && ( - - {isCollapsed ? "⌄" : "⌃"} - - )} - + {enableCollapsible && league.matchCount > 0 && ( + + )} {!isCollapsed && ( - + {isLoading ? ( - <> - {[0, 1, 2].map((i) => ( - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + - ))} - + + + + + + + + ) : ( leagueMatches.map((match, index) => (