diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index f4080fd..4e0dd0c 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -13,11 +13,13 @@ import { useTheme } from "@/context/ThemeContext"; import { checkFavorite, fetchLeagues, + fetchLiveScore, fetchSports, fetchTodayMatches, } from "@/lib/api"; import { storage } from "@/lib/storage"; import { League, Match, Sport } from "@/types/api"; +import { useRouter } from "expo-router"; import React, { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { @@ -29,6 +31,7 @@ import { } from "react-native"; export default function HomeScreen() { + const router = useRouter(); const { theme } = useTheme(); const { t } = useTranslation(); const isDark = theme === "dark"; @@ -44,12 +47,15 @@ export default function HomeScreen() { const [loading, setLoading] = useState(true); const [loadingLeagues, setLoadingLeagues] = useState(false); const [now, setNow] = useState(() => new Date()); + const [liveLeagueIdByMatchId, setLiveLeagueIdByMatchId] = useState< + Record + >({}); const deviceTimeZone = useMemo(() => { try { console.log( "deviceTimeZone", - Intl.DateTimeFormat().resolvedOptions().timeZone + Intl.DateTimeFormat().resolvedOptions().timeZone, ); return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC"; } catch { @@ -315,16 +321,90 @@ export default function HomeScreen() { const list = await fetchTodayMatches( sportId, selectedDate, - deviceTimeZone + deviceTimeZone, ); + //将isLive全改为true + list.forEach((m) => { + (m as Match).isLive = true; + }); + + const normalizeDate = (d: Date) => { + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, "0"); + const day = String(d.getDate()).padStart(2, "0"); + return `${year}-${month}-${day}`; + }; + + const todayStr = normalizeDate(new Date()); + const selectedStr = normalizeDate(selectedDate); + const shouldMergeLive = selectedStr === todayStr; + + let merged: Match[] = list.map((m) => ({ + ...m, + date: (m as any).date || selectedStr, + sport: (m as any).sport ?? sportId, + })); + + if (shouldMergeLive) { + try { + const liveData = await fetchLiveScore( + sportId, + state.selectedLeagueKey + ? parseInt(state.selectedLeagueKey) + : undefined, + state.timezone || deviceTimeZone, + ); + + const formatLiveTime = (status: string, fallback: string) => { + const s = (status || "").trim(); + if (!s) return (fallback || "").trim(); + if (/^\d{1,3}$/.test(s)) return `${s}'`; + return s; + }; + + const map: Record = {}; + const liveMatches: Match[] = (liveData || []).map((item) => { + const id = item.event_key.toString(); + map[id] = item.league_key; + return { + id, + league: item.league_name, + time: formatLiveTime(item.event_status, item.event_time), + date: item.event_date, + home: item.event_home_team, + away: item.event_away_team, + scoreText: item.event_final_result || "0 - 0", + fav: false, + sport: sportId, + isLive: true, + }; + }); + + setLiveLeagueIdByMatchId(map); + + // Merge by id: live takes precedence + const byId = new Map(); + liveMatches.forEach((m) => byId.set(m.id, m)); + merged.forEach((m) => { + if (!byId.has(m.id)) byId.set(m.id, m); + }); + merged = Array.from(byId.values()); + } catch (e) { + // Live merge failure should not block base list + console.warn("Fetch live score failed:", e); + } + } else { + setLiveLeagueIdByMatchId({}); + } + const token = await storage.getAccessToken(); - let listWithFavStatus = list; + let listWithFavStatus = merged; if (token) { // 直接传递 match.id 查询是否收藏,并更新列表状态 listWithFavStatus = await Promise.all( - list.map(async (m) => { + merged.map(async (m) => { try { // 查询比赛是否已被收藏 const favRes = await checkFavorite("match", m.id); @@ -333,15 +413,26 @@ export default function HomeScreen() { console.error(`Check favorite failed for match ${m.id}:`, error); return m; } - }) + }), ); } - // 将收藏的比赛置顶 + const isLiveRow = (m: Match) => { + const t = (m.time || "").trim(); + return ( + /\d+'$/.test(t) || /^\d{1,3}$/.test(t) || /\b(ht|half)\b/i.test(t) + ); + }; + + // 收藏置顶,其次直播置顶 const sortedList = [...listWithFavStatus].sort((a, b) => { - if (a.fav === b.fav) return 0; - return a.fav ? -1 : 1; + if (a.fav !== b.fav) return a.fav ? -1 : 1; + const aLive = isLiveRow(a); + const bLive = isLiveRow(b); + if (aLive !== bLive) return aLive ? -1 : 1; + return 0; }); + setMatches(sortedList); } catch (e) { console.error(e); @@ -353,7 +444,7 @@ export default function HomeScreen() { const handleFavoriteToggle = (matchId: string, isFav: boolean) => { setMatches((prev) => { const updated = prev.map((m) => - m.id === matchId ? { ...m, fav: isFav } : m + m.id === matchId ? { ...m, fav: isFav } : m, ); return [...updated].sort((a, b) => { if (a.fav === b.fav) return 0; diff --git a/app/(tabs)/live.tsx b/app/(tabs)/live.tsx index 5fbd11f..5601f13 100644 --- a/app/(tabs)/live.tsx +++ b/app/(tabs)/live.tsx @@ -65,6 +65,8 @@ export default function LiveScreen() { leagueId: item.league_key, sportId: state.selectedSportId ?? undefined, isLive: true, + date: item.event_date, + // sport: item.sport_name, })); const token = await storage.getAccessToken(); diff --git a/components/match-card.tsx b/components/match-card.tsx index 18ca36e..e6f22d6 100644 --- a/components/match-card.tsx +++ b/components/match-card.tsx @@ -4,6 +4,7 @@ import { Colors } from "@/constants/theme"; import { useTheme } from "@/context/ThemeContext"; import { addFavorite, removeFavorite } from "@/lib/api"; import { Match } from "@/types/api"; +import { LinearGradient } from "expo-linear-gradient"; import { useRouter } from "expo-router"; import React, { useState } from "react"; import { Pressable, StyleSheet, TouchableOpacity, View } from "react-native"; @@ -23,6 +24,7 @@ export function MatchCard({ const { theme } = useTheme(); const [isFav, setIsFav] = useState(match.fav); const [loading, setLoading] = useState(false); + // console.log("MatchCard render:", JSON.stringify(match)); // 当外部传入的 match.fav 改变时,更新内部状态 React.useEffect(() => { @@ -32,7 +34,35 @@ export function MatchCard({ const isDark = theme === "dark"; const iconColor = isDark ? Colors.dark.icon : Colors.light.icon; const cardBg = isDark ? "#1C1C1E" : "#FFFFFF"; - const borderColor = isDark ? "#38383A" : "#E5E5EA"; + const borderColor = isDark ? "#2C2C2E" : "rgba(0,0,0,0.06)"; + 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 timeLabel = React.useMemo(() => { + const raw = (match.time || "").trim(); + if (!raw) return ""; + if (/^\d{1,3}$/.test(raw)) return `${raw}'`; + return raw; + }, [match.time]); + + const leagueShort = React.useMemo(() => { + const league = (match.league || "").trim(); + if (!league) return "--"; + const first = league.split(/[^A-Za-z0-9]+/).filter(Boolean)[0] || league; + return first.slice(0, 3).toUpperCase(); + }, [match.league]); + + 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 !== "-") return { home: s, away: "", hasScore: true }; + return { home: "", away: "", hasScore: false }; + }, [match.scoreText]); const handlePress = () => { if (onPress) { @@ -79,28 +109,72 @@ export function MatchCard({ { backgroundColor: cardBg, borderColor, opacity: pressed ? 0.7 : 1 }, ]} > - - - - {match.league} + {isLive && ( + + )} + + {/* Left: League short + time */} + + + {leagueShort} + + + {timeLabel} - {match.time} - - - - {match.home} vs {match.away} - - - - {match.scoreText} + {/* Middle: Teams */} + + + {match.home} + + {match.away} + + + + {/* Right: Score box + favorite */} + + {scoreParts.hasScore ? ( + + + {scoreParts.home} + + + {scoreParts.away} + + + ) : ( + + )} + { e.stopPropagation(); @@ -111,69 +185,90 @@ export function MatchCard({ > - - {match.meta && ( - {match.meta} - )} ); } const styles = StyleSheet.create({ card: { - padding: 12, + height: 78, + paddingHorizontal: 14, marginBottom: 12, - borderRadius: 12, + borderRadius: 14, borderWidth: 1, + justifyContent: "center", + overflow: "hidden", + // iOS shadow + shadowColor: "#000", + shadowOffset: { width: 0, height: 0.5 }, + shadowOpacity: 0.03, + shadowRadius: 2, + // Android elevation + elevation: 1, }, - header: { - flexDirection: "row", - alignItems: "center", - marginBottom: 8, - }, - leagueBadge: { - paddingHorizontal: 8, - paddingVertical: 4, - borderRadius: 4, - marginRight: 8, - maxWidth: "70%", - }, - leagueText: { - fontSize: 12, - opacity: 0.7, - }, - timeText: { - fontSize: 12, - opacity: 0.5, - }, - teamsContainer: { - flexDirection: "row", - justifyContent: "space-between", - alignItems: "center", - marginBottom: 4, - }, - teamsText: { - fontSize: 16, - flex: 1, - marginRight: 8, - }, - scoreContainer: { + row: { flexDirection: "row", alignItems: "center", gap: 12, }, - scoreText: { - fontSize: 16, + left: { + width: 52, + alignItems: "center", + justifyContent: "center", + gap: 6, }, - metaText: { + leagueShortText: { fontSize: 12, - opacity: 0.5, - marginTop: 4, + fontWeight: "700", + opacity: 0.85, + }, + timeText: { + fontSize: 12, + opacity: 0.55, + }, + timeTextLive: { + color: "#FF3B30", + opacity: 1, + }, + middle: { + flex: 1, + justifyContent: "center", + gap: 6, + minWidth: 0, + }, + teamLine: { + fontSize: 16, + lineHeight: 18, + }, + right: { + flexDirection: "row", + alignItems: "center", + gap: 10, + }, + scoreBox: { + width: 44, + height: 56, + borderRadius: 10, + borderWidth: 1, + borderColor: "rgba(0,0,0,0.12)", + backgroundColor: "rgba(255,255,255,0.6)", + alignItems: "center", + justifyContent: "center", + gap: 6, + }, + scoreBoxText: { + fontSize: 16, + fontWeight: "700", + opacity: 0.9, + }, + scoreBoxPlaceholder: { + width: 44, + height: 56, }, }); diff --git a/types/api.ts b/types/api.ts index 917a2f4..7a81388 100644 --- a/types/api.ts +++ b/types/api.ts @@ -12,14 +12,16 @@ export interface Match { id: string; league: string; time: string; + date: string; home: string; away: string; - meta?: string; scoreText: string; fav: boolean; - leagueId?: number; + // sport: string; sportId?: number; + leagueId?: number; isLive?: boolean; + meta?: string; } export interface LiveScoreMatch {