From c522f2ba010398d79c2e8827ecf86d0f4d61f161 Mon Sep 17 00:00:00 2001 From: xianyi Date: Mon, 19 Jan 2026 14:50:47 +0800 Subject: [PATCH] =?UTF-8?q?=E8=81=94=E8=B5=9B=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(tabs)/index.tsx | 23 +-- components/match-card-league.tsx | 260 +++++++++++++++++++++--------- components/matches-by-league.tsx | 268 +++++++++++++++++++++++++++++++ types/api.ts | 26 ++- 4 files changed, 481 insertions(+), 96 deletions(-) create mode 100644 components/matches-by-league.tsx diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index de983e6..3f70b18 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -1,6 +1,6 @@ import { HomeHeader } from "@/components/home-header"; import { MatchCard } from "@/components/match-card"; -import { MatchCardLeague } from "@/components/match-card-league"; +import { MatchesByLeague } from "@/components/matches-by-league"; import { SelectionModal } from "@/components/selection-modal"; import { CalendarModal } from "@/components/simple-calendar"; import { ThemedText } from "@/components/themed-text"; @@ -341,7 +341,7 @@ export default function HomeScreen() { let merged: Match[] = list.map((m) => ({ ...m, date: (m as any).date || selectedStr, - sport: (m as any).sport ?? sportId, + sportId: (m as any).sportId ?? sportId, })); if (shouldMergeLive) { @@ -374,7 +374,7 @@ export default function HomeScreen() { away: item.event_away_team, scoreText: item.event_final_result || "0 - 0", fav: false, - sport: sportId, + sportId: sportId, isLive: true, }; }); @@ -556,17 +556,13 @@ export default function HomeScreen() { {t("home.loading")} - ) : ( + ) : filterMode === "time" ? ( item.id} - renderItem={({ item }) => - filterMode === "time" ? ( - - ) : ( - - ) - } + renderItem={({ item }) => ( + + )} contentContainerStyle={styles.listContent} ListEmptyComponent={ @@ -574,6 +570,11 @@ export default function HomeScreen() { } /> + ) : ( + )} {/* Modals - 条件渲染,只在可见时渲染 */} diff --git a/components/match-card-league.tsx b/components/match-card-league.tsx index d655d79..88eb1f7 100644 --- a/components/match-card-league.tsx +++ b/components/match-card-league.tsx @@ -1,12 +1,11 @@ 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"; import { useRouter } from "expo-router"; import React, { useState } from "react"; -import { Pressable, StyleSheet, TouchableOpacity, View } from "react-native"; +import { Image, Pressable, StyleSheet, TouchableOpacity, View } from "react-native"; interface MatchCardLeagueProps { match: Match; @@ -24,15 +23,17 @@ export function MatchCardLeague({ const [isFav, setIsFav] = useState(match.fav); const [loading, setLoading] = useState(false); - // 当外部传入的 match.fav 改变时,更新内部状态 React.useEffect(() => { setIsFav(match.fav); }, [match.fav]); const isDark = theme === "dark"; - const iconColor = isDark ? Colors.dark.icon : Colors.light.icon; + // 截图中的卡片背景通常非常深,接近纯黑 const cardBg = isDark ? "#1C1C1E" : "#FFFFFF"; - const borderColor = isDark ? "#38383A" : "#E5E5EA"; + const textColor = isDark ? "#FFFFFF" : "#000000"; + // 赢家的高亮颜色 (截图中的橙黄色) + const winnerColor = "#FF9500"; + const loserColor = isDark ? "#FFFFFF" : "#000000"; const handlePress = () => { if (onPress) { @@ -71,109 +72,210 @@ export function MatchCardLeague({ } }; + // --- 数据解析与样式逻辑 --- + + // 假设 match 对象中有 homeScore 和 awayScore (数字或字符串) + // 如果 API 只有 "1 - 0" 这种 scoreText,你需要在这里拆分 + // 这里为了演示,假设 match 对象已经扩展了这些字段,或者我们从 scoreText 简单解析 + let homeScore = 0; + let awayScore = 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; + + // 判断文字颜色和背景样式 + let homeColor = loserColor; + let awayColor = loserColor; + let homeScoreBg = "#2C2C2E"; // 默认深灰背景 + let awayScoreBg = "#2C2C2E"; + let homeBorderColor = "transparent"; + let awayBorderColor = "transparent"; + + if (homeScore > awayScore) { + homeColor = "#000000"; // 赢家黑色文字 + homeScoreBg = winnerColor; // 金色背景 + homeBorderColor = winnerColor; + } else if (awayScore > homeScore) { + awayColor = "#000000"; // 赢家黑色文字 + awayScoreBg = winnerColor; // 金色背景 + awayBorderColor = winnerColor; + } + // 如果相等,保持默认深灰背景 + return ( [ styles.card, - { backgroundColor: cardBg, borderColor, opacity: pressed ? 0.7 : 1 }, + { backgroundColor: cardBg, opacity: pressed ? 0.8 : 1 }, ]} > - - - - {match.league} - - - {match.time} - - - - - {match.home} vs2 {match.away} + {/* 1. 左侧:时间/状态 */} + + {/* 如果有状态字段 match.meta (如 'FT', 'AET'), 优先显示,否则显示时间 */} + + {(match.meta || match.time || "").toUpperCase()} - - - {match.scoreText} + + + {/* 2. 中间:球队信息 (上下排布) */} + + {/* 主队行 */} + + + + {match.home || match.homeTeamName} + + + + {/* 客队行 */} + + + + {match.away || match.awayTeamName} - { - e.stopPropagation(); - toggleFavorite(); - }} - disabled={loading} - hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }} - > - - - {match.meta && ( - {match.meta} - )} + {/* 3. 右侧:比分与铃铛 */} + + {/* 比分列 (上下排布) */} + + + + {homeScore} + + + + + {awayScore} + + + + + {/* 收藏按钮 */} + { + e.stopPropagation(); + toggleFavorite(); + }} + disabled={loading} + hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} + style={styles.favoriteButton} + > + + + ); } const styles = StyleSheet.create({ card: { - padding: 12, - marginBottom: 12, + flexDirection: "row", + paddingVertical: 14, + paddingHorizontal: 16, + marginHorizontal: 0, + marginBottom: 0, borderRadius: 12, - borderWidth: 1, - }, - header: { - flexDirection: "row", alignItems: "center", - marginBottom: 8, + minHeight: 68, }, - leagueBadge: { - paddingHorizontal: 8, - paddingVertical: 4, - borderRadius: 4, - marginRight: 8, - maxWidth: "70%", + // 左侧时间列 + leftColumn: { + width: 52, + justifyContent: "center", + alignItems: "flex-start", + marginRight: 12, }, - leagueText: { + statusText: { fontSize: 12, - opacity: 0.7, + color: "#8E8E93", // 次要文本颜色 (Grey) + fontWeight: "600", }, - timeText: { - fontSize: 12, - opacity: 0.5, - }, - teamsContainer: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - marginBottom: 4, - }, - teamsText: { - fontSize: 16, + // 中间球队列 + teamsColumn: { flex: 1, - marginRight: 8, + justifyContent: "center", + paddingRight: 8, }, - scoreContainer: { + teamRow: { flexDirection: "row", alignItems: "center", - gap: 12, + }, + teamLogo: { + width: 22, + height: 22, + borderRadius: 11, // 圆形图标 + marginRight: 10, + backgroundColor: "#3A3A3C", // 图片加载占位 + }, + teamName: { + fontSize: 16, + fontWeight: "500", + flex: 1, + }, + // 右侧整体包装 + rightWrapper: { + flexDirection: "row", + alignItems: "center", + }, + // 比分列 + scoresColumn: { + alignItems: "flex-end", // 数字右对齐 + justifyContent: "center", + marginRight: 12, + }, + scoreBox: { + minWidth: 32, + height: 28, + borderRadius: 6, + justifyContent: "center", + alignItems: "center", + paddingHorizontal: 8, }, scoreText: { fontSize: 16, + fontWeight: "700", + lineHeight: 20, }, - metaText: { - fontSize: 12, - opacity: 0.5, - marginTop: 4, - }, -}); + // 收藏按钮 + favoriteButton: { + paddingHorizontal: 4, + paddingVertical: 4, + justifyContent: 'center', + alignItems: 'center', + } +}); \ No newline at end of file diff --git a/components/matches-by-league.tsx b/components/matches-by-league.tsx new file mode 100644 index 0000000..8b3d760 --- /dev/null +++ b/components/matches-by-league.tsx @@ -0,0 +1,268 @@ +import { MatchCardLeague } from "@/components/match-card-league"; +import { ThemedText } from "@/components/themed-text"; +import { useTheme } from "@/context/ThemeContext"; +import { Match } from "@/types/api"; +import React, { useState } from "react"; +import { + Image, + LayoutAnimation, + Platform, + ScrollView, + StyleSheet, + TouchableOpacity, + UIManager, + View, +} from "react-native"; + +// 开启 Android 上的 LayoutAnimation +if ( + Platform.OS === "android" && + UIManager.setLayoutAnimationEnabledExperimental +) { + UIManager.setLayoutAnimationEnabledExperimental(true); +} + +interface MatchesByLeagueProps { + matches: Match[]; + onFavoriteToggle?: (matchId: string, isFav: boolean) => void; + /** + * 是否支持折叠收起 + * @default true + */ + enableCollapsible?: boolean; +} + + +export function MatchesByLeague({ + matches, + onFavoriteToggle, + enableCollapsible = true, +}: MatchesByLeagueProps) { + const { theme } = useTheme(); + const isDark = theme === "dark"; + + // 数据分组逻辑 + const matchesByLeague = React.useMemo(() => { + const grouped: Record = {}; + matches.forEach((match) => { + const league = match.league || match.leagueName || "其他"; + if (!grouped[league]) { + grouped[league] = []; + } + grouped[league].push(match); + }); + return grouped; + }, [matches]); + + const leagueNames = Object.keys(matchesByLeague); + + // 状态:记录哪些联赛被折叠了 (Key 为联赛名称),默认全部收起 + const [collapsedSections, setCollapsedSections] = useState< + Record + >({}); + + // 当联赛列表变化时,确保所有联赛默认都是收起状态 + React.useEffect(() => { + setCollapsedSections((prev) => { + const updated: Record = {}; + leagueNames.forEach((name) => { + updated[name] = prev[name] !== undefined ? prev[name] : true; + }); + return updated; + }); + }, [leagueNames.join(",")]); + + // 切换折叠状态 + const toggleCollapse = (leagueName: string) => { + if (!enableCollapsible) return; + + LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); + setCollapsedSections((prev) => ({ + ...prev, + [leagueName]: !prev[leagueName], + })); + }; + + if (leagueNames.length === 0) { + return ( + + 暂无比赛 + + ); + } + + return ( + + {leagueNames.map((leagueName) => { + const leagueMatches = matchesByLeague[leagueName]; + // 取该组第一场比赛的数据作为头部信息的来源(图标、国家等) + const firstMatch = leagueMatches[0]; + const isCollapsed = collapsedSections[leagueName]; + + return ( + + {/* 联赛头部 */} + toggleCollapse(leagueName)} + style={styles.leagueHeaderWrapper} + > + + {/* 联赛 Logo */} + + + + {/* 联赛名称 */} + {leagueName} + + {/* 国家信息行 */} + + + + {firstMatch.countryName || (firstMatch as any).countryName || "International"} + + + + + + + {/* 比赛数量 */} + + {leagueMatches.length} + + + {/* 折叠箭头 (仅当支持折叠时显示) */} + {enableCollapsible && ( + + {isCollapsed ? "⌄" : "⌃"} + + )} + + + + {/* 比赛列表内容 (根据状态显示/隐藏) */} + {!isCollapsed && ( + + {leagueMatches.map((match, index) => ( + + + + ))} + + )} + + ); + })} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + leagueSection: { + marginBottom: 16, + }, + leagueHeaderWrapper: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingHorizontal: 16, + paddingVertical: 12, + backgroundColor: "transparent", + }, + leagueHeaderLeft: { + flexDirection: "row", + alignItems: "center", + flex: 1, + }, + leagueLogo: { + width: 36, + height: 36, + borderRadius: 6, + marginRight: 12, + backgroundColor: "#3A3A3C", + }, + leagueInfoText: { + justifyContent: "center", + }, + leagueTitle: { + fontSize: 17, + fontWeight: "600", + color: "#FFFFFF", + marginBottom: 3, + }, + countryRow: { + flexDirection: "row", + alignItems: "center", + }, + countryFlag: { + width: 16, + height: 12, + marginRight: 5, + borderRadius: 2, + }, + countryName: { + fontSize: 13, + color: "#8E8E93", + fontWeight: "500", + }, + leagueHeaderRight: { + flexDirection: "row", + alignItems: "center", + gap: 8, + }, + matchCount: { + fontSize: 15, + color: "#8E8E93", + fontWeight: "600", + }, + chevron: { + fontSize: 16, + color: "#8E8E93", + fontWeight: '600', + }, + matchListContainer: { + backgroundColor: "#1C1C1E", + borderRadius: 12, + marginHorizontal: 16, + overflow: "hidden", + }, + matchCardWrapper: { + // 卡片包装器 + }, + matchCardDivider: { + borderBottomWidth: 0.5, + borderBottomColor: "#3A3A3C", + }, + emptyContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + paddingTop: 100, + }, +}); \ No newline at end of file diff --git a/types/api.ts b/types/api.ts index 7a81388..3fab9c2 100644 --- a/types/api.ts +++ b/types/api.ts @@ -12,12 +12,26 @@ export interface Match { id: string; league: string; time: string; - date: string; + date?: string; home: string; away: string; scoreText: string; fav: boolean; - // sport: string; + sport?: string; + homeTeamName?: string; + homeTeamKey?: string; + homeTeamLogo?: string; + awayTeamName?: string; + awayTeamKey?: string; + awayTeamLogo?: string; + leagueName?: string; + leagueKey?: string; + leagueLogo?: string; + countryName?: string; + countryKey?: string; + countryLogo?: string; + events?: string; + players?: string; sportId?: number; leagueId?: number; isLive?: boolean; @@ -73,11 +87,11 @@ export interface LiveScoreMatch { substitutes?: { time: string; home_scorer: - | { in: string; out: string; in_id: number; out_id: number } - | any[]; + | { in: string; out: string; in_id: number; out_id: number } + | any[]; away_scorer: - | { in: string; out: string; in_id: number; out_id: number } - | any[]; + | { in: string; out: string; in_id: number; out_id: number } + | any[]; info: string; info_time: string; score: string;