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) => (