From d83ca038c7d0cb6415fac90b6c0d3b57dc7a52b1 Mon Sep 17 00:00:00 2001 From: xianyi Date: Wed, 14 Jan 2026 14:32:59 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=86=E7=A6=BB=E8=B6=B3=E7=90=83=E4=BA=8B?= =?UTF-8?q?=E4=BB=B6=E5=8D=A1=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/match-detail/[id].tsx | 40 +++- .../match-detail/football/cards-card.tsx | 151 ++++++++++++ .../match-detail/football/football-events.tsx | 84 ++++++- .../match-detail/football/goals-card.tsx | 164 +++++++++++++ .../match-detail/football/lineups-card.tsx | 142 +++++++++++ .../football/substitutes-card.tsx | 222 ++++++++++++++++++ 6 files changed, 787 insertions(+), 16 deletions(-) create mode 100644 components/match-detail/football/cards-card.tsx create mode 100644 components/match-detail/football/goals-card.tsx create mode 100644 components/match-detail/football/lineups-card.tsx create mode 100644 components/match-detail/football/substitutes-card.tsx diff --git a/app/match-detail/[id].tsx b/app/match-detail/[id].tsx index f8b3230..c9e9784 100644 --- a/app/match-detail/[id].tsx +++ b/app/match-detail/[id].tsx @@ -1,6 +1,9 @@ import { BasketballScoreTable } from "@/components/match-detail/basketball/basketball-score-table"; -import { FootballEvents } from "@/components/match-detail/football/football-events"; +import { CardsCard } from "@/components/match-detail/football/cards-card"; import { FootballScoreTable } from "@/components/match-detail/football/football-score-table"; +import { GoalsCard } from "@/components/match-detail/football/goals-card"; +import { LineupsCard } from "@/components/match-detail/football/lineups-card"; +import { SubstitutesCard } from "@/components/match-detail/football/substitutes-card"; import { LeagueInfo } from "@/components/match-detail/league-info"; import { MatchInfoCard } from "@/components/match-detail/match-info-card"; import { MatchTabs } from "@/components/match-detail/match-tabs"; @@ -73,6 +76,9 @@ export default function MatchDetailScreen() { const result = await fetchMatchDetail(id as string); setData(result); console.log("首发阵容" , result.match.players?.away_team); + console.log("红黄牌", result.events); + + } catch (err: any) { setError(err.message || t("detail.fetch_failed")); @@ -107,12 +113,11 @@ export default function MatchDetailScreen() { case "info": // 根据 sportId 显示不同的详情组件 if (sportId === 1) { - // 足球:显示 FootballScoreTable (半场/全场)、MatchInfoCard 和 FootballEvents + // 足球:显示 FootballScoreTable (半场/全场) 和 MatchInfoCard return ( <> - ); } else if (sportId === 2) { @@ -129,18 +134,31 @@ export default function MatchDetailScreen() { <> - ); } case "stats": - return ( - - - {t("detail.empty_stats")} - - - ); + // 统计数据:显示进球、红黄牌、换人、首发阵容(分开显示) + if (sportId === 1) { + // 足球:分别显示各个卡片 + return ( + <> + + + + + + ); + } else { + // 其他运动暂时显示空状态 + return ( + + + {t("detail.empty_stats")} + + + ); + } case "odds": return ( diff --git a/components/match-detail/football/cards-card.tsx b/components/match-detail/football/cards-card.tsx new file mode 100644 index 0000000..2033a1f --- /dev/null +++ b/components/match-detail/football/cards-card.tsx @@ -0,0 +1,151 @@ +import { ThemedText } from "@/components/themed-text"; +import { CardEvent, MatchDetailData } from "@/types/api"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { StyleSheet, View } from "react-native"; + +interface CardsCardProps { + data: MatchDetailData; + isDark: boolean; +} + +export function CardsCard({ data, isDark }: CardsCardProps) { + const { t } = useTranslation(); + const bgColor = isDark ? "#1C1C1E" : "#FFF"; + const borderColor = isDark ? "#2C2C2E" : "#EEE"; + + // events 可能在 data.events 或 match.events 中 + const events = (data as any).events || (data.match as any).events; + if (!events || typeof events !== "object") { + return null; + } + + // 处理数组格式的 events + let eventsObj = events; + if (Array.isArray(events) && events.length > 0) { + eventsObj = events[0]; + } + + const cards = eventsObj?.cards || []; + const filteredCards = cards.filter((card: CardEvent) => card.home_fault || card.away_fault); + + if (filteredCards.length === 0) { + return null; + } + + const renderCardEvent = (card: CardEvent, index: number) => { + const isHome = !!card.home_fault; + const cardType = card.card?.toLowerCase() || ""; + const isRed = cardType.includes("red"); + const isYellow = cardType.includes("yellow"); + const cardColor = isRed ? "#F44336" : isYellow ? "#FFC107" : "#9E9E9E"; + const playerName = card.home_fault || card.away_fault || ""; + + return ( + + + + {card.time ? `${card.time}'` : ""} + + + + + + {playerName} + + {card.card && ( + + + {isRed ? t("detail.events.red_card") : isYellow ? t("detail.events.yellow_card") : card.card} + + + )} + + + ); + }; + + return ( + + + {t("detail.events.cards")} + + + {filteredCards.map((card: CardEvent, index: number) => renderCardEvent(card, index))} + + + ); +} + +const styles = StyleSheet.create({ + container: { + margin: 16, + marginTop: 0, + borderRadius: 12, + padding: 16, + borderWidth: 1, + elevation: 2, + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + }, + title: { + fontSize: 14, + fontWeight: "600", + marginBottom: 12, + opacity: 0.7, + }, + content: { + gap: 0, + }, + eventRow: { + flexDirection: "row", + alignItems: "center", + paddingVertical: 8, + borderBottomWidth: StyleSheet.hairlineWidth, + }, + eventTime: { + width: 40, + alignItems: "center", + }, + timeText: { + fontSize: 12, + fontWeight: "500", + opacity: 0.6, + }, + eventContent: { + flex: 1, + flexDirection: "row", + alignItems: "center", + gap: 8, + paddingLeft: 12, + }, + homeEvent: { + justifyContent: "flex-start", + }, + awayEvent: { + justifyContent: "flex-end", + flexDirection: "row-reverse", + }, + eventText: { + fontSize: 13, + fontWeight: "500", + flex: 1, + }, + cardIcon: { + width: 12, + height: 16, + borderRadius: 2, + }, + cardTypeText: { + fontSize: 10, + fontWeight: "600", + textTransform: "uppercase", + }, + cardBadge: { + paddingHorizontal: 6, + paddingVertical: 2, + borderRadius: 4, + }, +}); diff --git a/components/match-detail/football/football-events.tsx b/components/match-detail/football/football-events.tsx index 2d520db..ef04aca 100644 --- a/components/match-detail/football/football-events.tsx +++ b/components/match-detail/football/football-events.tsx @@ -124,11 +124,85 @@ export function FootballEvents({ data, isDark }: FootballEventsProps) { }; const renderSubstituteEvent = (sub: SubstituteEvent, index: number) => { - const isHome = !!sub.home_scorer || !!sub.home_assist; - // 换人数据可能包含:home_scorer (换入), away_scorer (换出) 或相反 - // 也可能有单独的字段如 home_assist, away_assist 等 - const playerIn = sub.home_scorer || sub.home_assist || sub.away_scorer || ""; - const playerOut = sub.away_scorer || sub.away_assist || sub.home_scorer_out || sub.away_scorer_out || ""; + // 辅助函数:从字段中提取字符串值(可能是字符串、数组或对象) + const extractPlayerName = (value: any): string => { + if (!value) return ""; + + // 如果是字符串,直接返回 + if (typeof value === "string") return value; + + // 如果是数组,取第一个元素 + if (Array.isArray(value)) { + if (value.length === 0) return ""; + const firstItem = value[0]; + // 如果数组元素是对象,提取 in 或 out + if (typeof firstItem === "object" && firstItem !== null) { + return String(firstItem.in || firstItem.out || firstItem.player || ""); + } + return String(firstItem); + } + + // 如果是对象,提取 in 或 out 字段 + if (typeof value === "object" && value !== null) { + return String(value.in || value.out || value.player || ""); + } + + return String(value); + }; + + // 提取换入和换出球员名称 + let playerIn = ""; + let playerOut = ""; + let isHome = false; + + // 优先处理 home_scorer(可能是对象 {in, out} 或数组) + const homeScorerData = (sub as any).home_scorer; + if (homeScorerData) { + isHome = true; + if (typeof homeScorerData === "object" && !Array.isArray(homeScorerData) && "in" in homeScorerData) { + // 对象格式:{in: "球员名", out: "球员名", in_id, out_id} + playerIn = String(homeScorerData.in || ""); + playerOut = String(homeScorerData.out || ""); + } else if (Array.isArray(homeScorerData) && homeScorerData.length > 0) { + // 数组格式:取第一个元素 + const firstItem = homeScorerData[0]; + if (typeof firstItem === "object" && firstItem !== null && "in" in firstItem) { + playerIn = String(firstItem.in || ""); + playerOut = String(firstItem.out || ""); + } else { + playerIn = extractPlayerName(firstItem); + } + } else { + playerIn = extractPlayerName(homeScorerData); + } + } + + // 处理 away_scorer(可能是对象 {in, out} 或数组) + const awayScorerData = (sub as any).away_scorer; + if (awayScorerData && !playerIn) { + isHome = false; + if (typeof awayScorerData === "object" && !Array.isArray(awayScorerData) && "in" in awayScorerData) { + // 对象格式:{in: "球员名", out: "球员名", in_id, out_id} + playerIn = String(awayScorerData.in || ""); + playerOut = String(awayScorerData.out || ""); + } else if (Array.isArray(awayScorerData) && awayScorerData.length > 0) { + // 数组格式:取第一个元素 + const firstItem = awayScorerData[0]; + if (typeof firstItem === "object" && firstItem !== null && "in" in firstItem) { + playerIn = String(firstItem.in || ""); + playerOut = String(firstItem.out || ""); + } else { + playerIn = extractPlayerName(firstItem); + } + } else { + playerIn = extractPlayerName(awayScorerData); + } + } + + // 如果还没有提取到换出球员,尝试从其他字段获取 + if (!playerOut) { + playerOut = extractPlayerName(sub.home_scorer_out || sub.away_scorer_out || sub.home_assist || sub.away_assist); + } return ( diff --git a/components/match-detail/football/goals-card.tsx b/components/match-detail/football/goals-card.tsx new file mode 100644 index 0000000..12c02c0 --- /dev/null +++ b/components/match-detail/football/goals-card.tsx @@ -0,0 +1,164 @@ +import { ThemedText } from "@/components/themed-text"; +import { IconSymbol } from "@/components/ui/icon-symbol"; +import { GoalEvent, MatchDetailData } from "@/types/api"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { StyleSheet, View } from "react-native"; + +interface GoalsCardProps { + data: MatchDetailData; + isDark: boolean; +} + +export function GoalsCard({ data, isDark }: GoalsCardProps) { + const { t } = useTranslation(); + const bgColor = isDark ? "#1C1C1E" : "#FFF"; + const borderColor = isDark ? "#2C2C2E" : "#EEE"; + + // events 可能在 data.events 或 match.events 中 + const events = (data as any).events || (data.match as any).events; + if (!events || typeof events !== "object") { + return null; + } + + // 处理数组格式的 events + let eventsObj = events; + if (Array.isArray(events) && events.length > 0) { + eventsObj = events[0]; + } + + const goalscorers = eventsObj?.goalscorers || []; + const filteredGoals = goalscorers.filter((goal: GoalEvent) => goal.home_scorer || goal.away_scorer); + + if (filteredGoals.length === 0) { + return null; + } + + const renderGoalEvent = (goal: GoalEvent, index: number) => { + const isHome = !!goal.home_scorer; + const scorerRaw = goal.home_scorer || goal.away_scorer || ""; + const isPenalty = scorerRaw.includes("(pen.)"); + const playerName = scorerRaw.replace("(pen.)", "").trim(); + + return ( + + + + {goal.time ? `${goal.time}'` : ""} + + + + + + {playerName && ( + + {playerName} + + )} + {isPenalty && ( + + P + + )} + + {goal.score && ( + {goal.score} + )} + + + ); + }; + + return ( + + + {t("detail.events.goals")} + + + {filteredGoals.map((goal: GoalEvent, index: number) => renderGoalEvent(goal, index))} + + + ); +} + +const styles = StyleSheet.create({ + container: { + margin: 16, + marginTop: 16, + borderRadius: 12, + padding: 16, + borderWidth: 1, + elevation: 2, + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + }, + title: { + fontSize: 14, + fontWeight: "600", + marginBottom: 12, + opacity: 0.7, + }, + content: { + gap: 0, + }, + eventRow: { + flexDirection: "row", + alignItems: "center", + paddingVertical: 8, + borderBottomWidth: StyleSheet.hairlineWidth, + }, + eventTime: { + width: 40, + alignItems: "center", + }, + timeText: { + fontSize: 12, + fontWeight: "500", + opacity: 0.6, + }, + eventContent: { + flex: 1, + flexDirection: "row", + alignItems: "center", + gap: 8, + paddingLeft: 12, + }, + homeEvent: { + justifyContent: "flex-start", + }, + awayEvent: { + justifyContent: "flex-end", + flexDirection: "row-reverse", + }, + eventText: { + fontSize: 13, + fontWeight: "500", + flex: 1, + }, + scoreText: { + fontSize: 12, + fontWeight: "600", + opacity: 0.7, + }, + goalContent: { + flex: 1, + flexDirection: "row", + alignItems: "center", + gap: 6, + }, + penaltyBadge: { + width: 18, + height: 18, + borderRadius: 9, + backgroundColor: "#FF9800", + alignItems: "center", + justifyContent: "center", + }, + penaltyText: { + fontSize: 10, + fontWeight: "700", + color: "#FFF", + }, +}); diff --git a/components/match-detail/football/lineups-card.tsx b/components/match-detail/football/lineups-card.tsx new file mode 100644 index 0000000..675a142 --- /dev/null +++ b/components/match-detail/football/lineups-card.tsx @@ -0,0 +1,142 @@ +import { ThemedText } from "@/components/themed-text"; +import { MatchDetailData, Player } from "@/types/api"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { StyleSheet, View } from "react-native"; + +interface LineupsCardProps { + data: MatchDetailData; + isDark: boolean; +} + +export function LineupsCard({ data, isDark }: LineupsCardProps) { + const { t } = useTranslation(); + const { match } = data; + const bgColor = isDark ? "#1C1C1E" : "#FFF"; + const borderColor = isDark ? "#2C2C2E" : "#EEE"; + + const players = (match as any).players || null; + + if (!players || (!players.home_team && !players.away_team)) { + return null; + } + + const hasLineups = + (players.home_team && Array.isArray(players.home_team) && players.home_team.length > 0) || + (players.away_team && Array.isArray(players.away_team) && players.away_team.length > 0); + + if (!hasLineups) { + return null; + } + + return ( + + + {t("detail.events.lineups")} + + + {players.home_team && Array.isArray(players.home_team) && players.home_team.length > 0 && ( + + + {match.eventHomeTeam} + + {players.home_team.slice(0, 11).map((player: Player, idx: number) => ( + + + {player.player_number || player.number || idx + 1} + + + {player.player_name || player.player || player.name || ""} + + {player.position && ( + + {player.position} + + )} + + ))} + + )} + {players.away_team && Array.isArray(players.away_team) && players.away_team.length > 0 && ( + + + {match.eventAwayTeam} + + {players.away_team.slice(0, 11).map((player: Player, idx: number) => ( + + + {player.player_number || player.number || idx + 1} + + + {player.player_name || player.player || player.name || ""} + + {player.position && ( + + {player.position} + + )} + + ))} + + )} + + + ); +} + +const styles = StyleSheet.create({ + container: { + margin: 16, + marginTop: 0, + borderRadius: 12, + padding: 16, + borderWidth: 1, + elevation: 2, + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + }, + title: { + fontSize: 14, + fontWeight: "600", + marginBottom: 12, + opacity: 0.7, + }, + content: { + gap: 0, + }, + teamLineup: { + marginTop: 8, + }, + teamName: { + fontSize: 13, + fontWeight: "600", + marginBottom: 8, + opacity: 0.8, + }, + playerRow: { + flexDirection: "row", + alignItems: "center", + paddingVertical: 4, + gap: 8, + }, + playerNumber: { + fontSize: 12, + fontWeight: "600", + width: 24, + textAlign: "center", + opacity: 0.6, + }, + playerName: { + fontSize: 13, + fontWeight: "500", + flex: 1, + }, + playerPosition: { + fontSize: 11, + fontWeight: "500", + opacity: 0.5, + textTransform: "uppercase", + }, +}); diff --git a/components/match-detail/football/substitutes-card.tsx b/components/match-detail/football/substitutes-card.tsx new file mode 100644 index 0000000..281efaa --- /dev/null +++ b/components/match-detail/football/substitutes-card.tsx @@ -0,0 +1,222 @@ +import { ThemedText } from "@/components/themed-text"; +import { IconSymbol } from "@/components/ui/icon-symbol"; +import { MatchDetailData, SubstituteEvent } from "@/types/api"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { StyleSheet, View } from "react-native"; + +interface SubstitutesCardProps { + data: MatchDetailData; + isDark: boolean; +} + +export function SubstitutesCard({ data, isDark }: SubstitutesCardProps) { + const { t } = useTranslation(); + const bgColor = isDark ? "#1C1C1E" : "#FFF"; + const borderColor = isDark ? "#2C2C2E" : "#EEE"; + + // events 可能在 data.events 或 match.events 中 + const events = (data as any).events || (data.match as any).events; + if (!events || typeof events !== "object") { + return null; + } + + // 处理数组格式的 events + let eventsObj = events; + if (Array.isArray(events) && events.length > 0) { + eventsObj = events[0]; + } + + const substitutes = eventsObj?.substitutes || []; + const filteredSubstitutes = substitutes.filter( + (sub: SubstituteEvent) => sub.home_scorer || sub.away_scorer || sub.home_assist || sub.away_assist + ); + + if (filteredSubstitutes.length === 0) { + return null; + } + + const extractPlayerName = (value: any): string => { + if (!value) return ""; + if (typeof value === "string") return value; + if (Array.isArray(value)) { + if (value.length === 0) return ""; + const firstItem = value[0]; + if (typeof firstItem === "object" && firstItem !== null) { + return String(firstItem.in || firstItem.out || firstItem.player || ""); + } + return String(firstItem); + } + if (typeof value === "object" && value !== null) { + return String(value.in || value.out || value.player || ""); + } + return String(value); + }; + + const renderSubstituteEvent = (sub: SubstituteEvent, index: number) => { + let playerIn = ""; + let playerOut = ""; + let isHome = false; + + const homeScorerData = (sub as any).home_scorer; + if (homeScorerData) { + isHome = true; + if (typeof homeScorerData === "object" && !Array.isArray(homeScorerData) && "in" in homeScorerData) { + playerIn = String(homeScorerData.in || ""); + playerOut = String(homeScorerData.out || ""); + } else if (Array.isArray(homeScorerData) && homeScorerData.length > 0) { + const firstItem = homeScorerData[0]; + if (typeof firstItem === "object" && firstItem !== null && "in" in firstItem) { + playerIn = String(firstItem.in || ""); + playerOut = String(firstItem.out || ""); + } else { + playerIn = extractPlayerName(firstItem); + } + } else { + playerIn = extractPlayerName(homeScorerData); + } + } + + const awayScorerData = (sub as any).away_scorer; + if (awayScorerData && !playerIn) { + isHome = false; + if (typeof awayScorerData === "object" && !Array.isArray(awayScorerData) && "in" in awayScorerData) { + playerIn = String(awayScorerData.in || ""); + playerOut = String(awayScorerData.out || ""); + } else if (Array.isArray(awayScorerData) && awayScorerData.length > 0) { + const firstItem = awayScorerData[0]; + if (typeof firstItem === "object" && firstItem !== null && "in" in firstItem) { + playerIn = String(firstItem.in || ""); + playerOut = String(firstItem.out || ""); + } else { + playerIn = extractPlayerName(firstItem); + } + } else { + playerIn = extractPlayerName(awayScorerData); + } + } + + if (!playerOut) { + playerOut = extractPlayerName(sub.home_scorer_out || sub.away_scorer_out || sub.home_assist || sub.away_assist); + } + + return ( + + + + {sub.time ? `${sub.time}'` : ""} + + + + + + {playerOut ? ( + <> + + {playerIn} + + + + {playerOut} + + + ) : ( + + {playerIn} + + )} + + + + ); + }; + + return ( + + + {t("detail.events.substitutes")} + + + {filteredSubstitutes.map((sub: SubstituteEvent, index: number) => renderSubstituteEvent(sub, index))} + + + ); +} + +const styles = StyleSheet.create({ + container: { + margin: 16, + marginTop: 0, + borderRadius: 12, + padding: 16, + borderWidth: 1, + elevation: 2, + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.1, + shadowRadius: 4, + }, + title: { + fontSize: 14, + fontWeight: "600", + marginBottom: 12, + opacity: 0.7, + }, + content: { + gap: 0, + }, + eventRow: { + flexDirection: "row", + alignItems: "center", + paddingVertical: 8, + borderBottomWidth: StyleSheet.hairlineWidth, + }, + eventTime: { + width: 40, + alignItems: "center", + }, + timeText: { + fontSize: 12, + fontWeight: "500", + opacity: 0.6, + }, + eventContent: { + flex: 1, + flexDirection: "row", + alignItems: "center", + gap: 8, + paddingLeft: 12, + }, + homeEvent: { + justifyContent: "flex-start", + }, + awayEvent: { + justifyContent: "flex-end", + flexDirection: "row-reverse", + }, + eventText: { + fontSize: 13, + fontWeight: "500", + flex: 1, + }, + substituteContent: { + flex: 1, + flexDirection: "row", + alignItems: "center", + gap: 6, + }, + substituteIn: { + fontSize: 13, + fontWeight: "500", + }, + substituteArrow: { + fontSize: 12, + opacity: 0.5, + }, + substituteOut: { + fontSize: 13, + fontWeight: "500", + opacity: 0.6, + textDecorationLine: "line-through", + }, +});