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;