import { ThemedText } from "@/components/themed-text"; import { IconSymbol } from "@/components/ui/icon-symbol"; import { Colors } from "@/constants/theme"; import { useAppState } from "@/context/AppStateContext"; import { useTheme } from "@/context/ThemeContext"; import { addFavorite, fetchLiveScore, fetchOdds, removeFavorite, } from "@/lib/api"; import { LiveScoreMatch, Match, OddsItem } from "@/types/api"; import { Image } from "expo-image"; import { LinearGradient } from "expo-linear-gradient"; import { useRouter } from "expo-router"; import React, { useEffect, useState } from "react"; import { Pressable, StyleSheet, TouchableOpacity, View } from "react-native"; interface MatchCardProps { match: Match; onPress?: (match: Match) => void; onFavoriteToggle?: (matchId: string, isFav: boolean) => void; } export function MatchCard({ match, onPress, onFavoriteToggle, }: MatchCardProps) { const router = useRouter(); const { theme } = useTheme(); const { state } = useAppState(); const [isFav, setIsFav] = useState(match.fav); const [loading, setLoading] = useState(false); const [odds, setOdds] = useState(match.odds || []); const [liveDetail, setLiveDetail] = useState(null); // console.log("MatchCard render:", JSON.stringify(match)); const oddsSettings = state.oddsSettings; const cardsSettings = state.cardsSettings; const cornerSettings = state.cornerSettings; useEffect(() => { if ( oddsSettings.enabled && oddsSettings.selectedBookmakers.length > 0 && match.isLive && !match.odds ) { fetchOdds(match.sportId || 1, parseInt(match.id)) .then((res) => { const matchOdds = res[match.id]?.data || []; setOdds(matchOdds); }) .catch((err) => console.log("Fetch match card odds error:", err)); } }, [ oddsSettings.enabled, oddsSettings.selectedBookmakers, match.id, match.odds, match.isLive, match.sportId, ]); // Fetch live score detail for cards and corners info useEffect(() => { if ( (cardsSettings.enabled || cornerSettings.enabled) && isLive && match.leagueKey ) { fetchLiveScore(match.sportId || 1, Number(match.leagueKey)) .then((matches) => { const detail = matches.find((m) => String(m.event_key) === match.id); if (detail) { setLiveDetail(detail); } }) .catch((err) => console.log("Fetch live detail for cards error:", err)); } }, [ cardsSettings.enabled, cornerSettings.enabled, match.id, match.leagueKey, match.sportId, match.isLive, ]); // 当外部传入的 match.fav 改变时,更新内部状态 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 ? "#2C2C2E" : "rgba(0,0,0,0.06)"; const scoreBorder = isDark ? "rgba(255,255,255,0.18)" : "rgba(230, 230, 230, 0.28)"; const scoreBg = isDark ? "rgba(255,255,255,0.04)" : "rgba(255,255,255,0.6)"; const oddBadgeBg = isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.03)"; const isLive = React.useMemo(() => { return !!match.isLive; }, [match.isLive]); // Tennis logic const isTennis = match.sportId === 3 || match.sport === "tennis"; const homeName = isTennis ? liveDetail?.event_first_player || match.eventFirstPlayer : liveDetail?.event_home_team || match.homeTeamName || match.home; const awayName = isTennis ? liveDetail?.event_second_player || match.eventSecondPlayer : liveDetail?.event_away_team || match.awayTeamName || match.away; const homeLogo = isTennis ? liveDetail?.event_first_player_logo || match.eventFirstPlayerLogo : liveDetail?.home_team_logo || match.homeTeamLogo; const awayLogo = isTennis ? liveDetail?.event_second_player_logo || match.eventSecondPlayerLogo : liveDetail?.away_team_logo || match.awayTeamLogo; const tennisScores = React.useMemo(() => { // 优先使用 liveDetail 中的 scores if (isTennis && liveDetail?.scores && Array.isArray(liveDetail.scores)) { return liveDetail.scores; } if (!isTennis || !match.scores) return []; try { const parsed = JSON.parse(match.scores); return Array.isArray(parsed) ? parsed : []; } catch { return []; } }, [match.scores, isTennis, liveDetail]); 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.leagueName || 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.leagueName, match.league]); const scoreParts = React.useMemo(() => { if (isTennis) { if (tennisScores.length > 0) { const last = tennisScores[tennisScores.length - 1]; const h = parseInt(last.score_first || "0"); const a = parseInt(last.score_second || "0"); return { home: last.score_first || "0", away: last.score_second || "0", hasScore: true, homeLead: h > a, awayLead: a > h, }; } // 如果没有分数(如未开始),显示 0-0 或 - return { home: "0", away: "0", hasScore: true, // 保持 ScoreBox 显示,以便显示 0-0 homeLead: false, awayLead: false, }; } const s = (match.scoreText || "").trim(); const m = s.match(/(\d+)\s*[-:]\s*(\d+)/); if (m) { const h = parseInt(m[1]); const a = parseInt(m[2]); return { home: m[1], away: m[2], hasScore: true, homeLead: h > a, awayLead: a > h, }; } if (s && s !== "-" && s !== "0 - 0") { return { home: s, away: "", hasScore: true, homeLead: false, awayLead: false, }; } if (s === "0 - 0" || s === "0-0") { return { home: "0", away: "0", hasScore: true, homeLead: false, awayLead: false, }; } return { home: "", away: "", hasScore: false, homeLead: false, awayLead: false, }; }, [match.scoreText]); const cardsCount = React.useMemo(() => { if (!liveDetail?.cards || !cardsSettings.enabled) { return { homeYellow: 0, homeRed: 0, awayYellow: 0, awayRed: 0 }; } let homeYellow = 0, homeRed = 0, awayYellow = 0, awayRed = 0; liveDetail.cards.forEach((card) => { const cardType = (card.card || "").toLowerCase(); const isYellow = cardType.includes("yellow"); const isRed = cardType.includes("red"); if (!isYellow && !isRed) return; const info = (card.info || "").toLowerCase(); const sideFromInfo = info.includes("home") ? "home" : info.includes("away") ? "away" : null; const sideFromFault = card.home_fault ? "home" : card.away_fault ? "away" : null; const side = sideFromInfo || sideFromFault; if (!side) return; if (side === "home") { if (isYellow) homeYellow++; if (isRed) homeRed++; } else { if (isYellow) awayYellow++; if (isRed) awayRed++; } }); return { homeYellow, homeRed, awayYellow, awayRed }; }, [liveDetail, cardsSettings.enabled]); const extraStats = React.useMemo(() => { if (!liveDetail?.statistics || !cornerSettings.enabled) { return { home: "", away: "" }; } // Cast statistics to any[] to avoid union type issues when accessing 'home'/'away' properties const stats = liveDetail.statistics as any[]; const corners = stats.find((s) => s.type === "Corners"); if (corners) { return { home: corners.home, away: corners.away }; } const dangerousAttacks = stats.find((s) => s.type === "Dangerous Attacks"); if (dangerousAttacks) { return { home: dangerousAttacks.home, away: dangerousAttacks.away }; } return { home: "", away: "" }; }, [liveDetail, cornerSettings.enabled]); const handlePress = () => { if (onPress) { onPress(match); } else { if (isLive) { router.push({ pathname: "/live-detail/[id]", params: { id: match.id, league_id: match.leagueId?.toString() || "", sport_id: match.sportId?.toString() || "", }, }); } else { router.push(`/match-detail/${match.id}`); } } }; const toggleFavorite = async () => { if (loading) return; setLoading(true); const newFavState = !isFav; try { if (newFavState) { await addFavorite({ matchId: parseInt(match.id), type: "match", typeId: match.id, notify: true, }); } else { await removeFavorite({ type: "match", typeId: match.id, }); } setIsFav(newFavState); if (onFavoriteToggle) { onFavoriteToggle(match.id, newFavState); } } catch (error) { console.error("Toggle favorite error:", error); } finally { setLoading(false); } }; const renderTennisSetScores = (isHome: boolean) => { // 显示除最后一盘之外的盘分 (当前盘分在右侧大框显示) // 如果只有一盘,则这里不显示任何内容 if (tennisScores.length <= 1) return null; const prevSets = tennisScores.slice(0, tennisScores.length - 1); return ( {prevSets.map((s: any, i: number) => ( {isHome ? s.score_first : s.score_second} ))} ); }; const renderOddsRow = (bookmakerName: string, isHighlight: boolean) => { if (isTennis) return renderTennisSetScores(isHighlight); // Reuse isHighlight param as 'isHome' (true for first player logic) if (!oddsSettings.enabled || !bookmakerName) return null; const item = odds.find((o) => o.odd_bookmakers === bookmakerName); // 如果没有找到该博彩公司的赔率数据,直接返回 null,不进行占位 if (!item) return null; const val1 = item?.odd_1 || item?.ah0_1 || "-"; const val2 = item?.odd_x || "0" || "-"; const val3 = item?.odd_2 || item?.ah0_2 || "-"; return ( {val1} {val2} {val3} ); }; const renderCardsInline = (yellow: number, red: number) => { if (!cardsSettings.enabled || !liveDetail) return null; if (yellow <= 0 && red <= 0) return null; return ( {yellow > 0 && ( {yellow} )} {red > 0 && ( {red} )} ); }; return ( [ styles.card, { backgroundColor: cardBg, borderColor, opacity: pressed ? 0.7 : 1 }, ]} > {isLive && ( )} {/* Left: League short + time */} {leagueShort} {timeLabel} {/* Middle: Teams & Odds Row Integration */} {homeName} {!isTennis && renderCardsInline(cardsCount.homeYellow, cardsCount.homeRed)} {renderOddsRow(oddsSettings.selectedBookmakers[0], true)} {awayName} {!isTennis && renderCardsInline(cardsCount.awayYellow, cardsCount.awayRed)} {renderOddsRow(oddsSettings.selectedBookmakers[1], false)} {/* Right: Score box + extra stats + favorite */} {scoreParts.hasScore ? ( {scoreParts.home} {scoreParts.away} ) : ( )} {extraStats.home !== "" || extraStats.away !== "" ? ( {extraStats.home} {extraStats.away} ) : ( )} { e.stopPropagation(); toggleFavorite(); }} disabled={loading} hitSlop={{ top: 12, bottom: 12, left: 12, right: 12 }} > ); } const styles = StyleSheet.create({ card: { height: 78, paddingHorizontal: 8, marginBottom: 8, borderRadius: 14, borderWidth: 1, justifyContent: "center", overflow: "hidden", shadowColor: "#000", shadowOffset: { width: 0, height: 1 }, shadowOpacity: 0.05, shadowRadius: 2, elevation: 2, }, row: { flexDirection: "row", alignItems: "center", }, left: { width: 33, alignItems: "flex-start", justifyContent: "center", gap: 4, }, leagueShortText: { fontSize: 12, fontWeight: "700", opacity: 0.8, }, timeText: { fontSize: 12, opacity: 0.5, }, timeTextLive: { color: "#FF3B30", opacity: 1, fontWeight: "800", }, middle: { flex: 1, justifyContent: "center", gap: 10, marginHorizontal: 8, }, contentRow: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", }, teamInfo: { flexDirection: "row", alignItems: "center", flex: 1, minWidth: 0, marginRight: 8, }, teamLogo: { width: 18, height: 18, }, teamLogoPlaceholder: { width: 18, height: 18, justifyContent: "center", alignItems: "center", }, teamName: { fontSize: 12, fontWeight: "600", marginLeft: 6, flexShrink: 1, minWidth: 0, }, bookmakerOddsRow: { width: 98, flexDirection: "row", gap: 4, justifyContent: "flex-end", }, oddBadge: { paddingHorizontal: 5, borderRadius: 8, minWidth: 30, alignItems: "center", justifyContent: "center", }, oddText: { fontSize: 10, fontWeight: "700", opacity: 0.9, }, oddTextHighlight: { color: "#FF9500", }, right: { flexDirection: "row", alignItems: "center", }, scoreContainer: { width: 35, alignItems: "center", }, extraStatsContainer: { width: 18, alignItems: "center", marginHorizontal: 4, }, favoriteContainer: { width: 25, alignItems: "center", }, scoreBox: { width: 35, height: 55, borderRadius: 8, borderWidth: 0.3, alignItems: "stretch", justifyContent: "center", overflow: "hidden", }, scoreHalf: { flex: 1, alignItems: "center", justifyContent: "center", }, scoreHalfLead: { backgroundColor: "rgba(255, 149, 0, 0.08)", }, scoreBoxText: { fontSize: 15, fontWeight: "900", }, scoreTextLead: { color: "#ff9500", }, scoreDivider: { width: "100%", height: 1, backgroundColor: "rgba(0,0,0,0.06)", }, scoreBoxPlaceholder: { width: 35, height: 48, }, cardsInline: { flexDirection: "row", alignItems: "center", gap: 6, marginLeft: 8, flexShrink: 0, }, cardBadge: { minWidth: 10, height: 15, borderRadius: 3, alignItems: "center", justifyContent: "center", paddingHorizontal: 4, }, cardBadgeYellow: { backgroundColor: "#FFC400", }, cardBadgeRed: { backgroundColor: "#FF3B30", }, extraStatsColumn: { alignItems: "center", justifyContent: "space-between", height: 55, paddingVertical: 2, }, extraStatText: { fontSize: 11, fontWeight: "500", opacity: 0.4, }, cardBadgeText: { fontSize: 8, fontWeight: "900", lineHeight: 10, color: "#fff", }, cardBadgeTextDark: { color: "#000", }, tennisScoresRow: { flexDirection: "row", gap: 12, marginRight: 4, alignItems: "center", }, tennisScoreText: { fontSize: 14, fontWeight: "500", minWidth: 14, textAlign: "center", }, });