From c7c6585c7c56a67aee2118a0bf5f254ecb715b04 Mon Sep 17 00:00:00 2001 From: xianyi Date: Tue, 13 Jan 2026 16:46:16 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=AF=A6=E6=83=85=E6=98=BE?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/match-detail/[id].tsx | 7 +- .../match-detail/football/football-events.tsx | 402 ++++++++++++++++++ .../football/football-score-table.tsx | 50 ++- components/match-detail/match-info-card.tsx | 49 ++- constants/api.ts | 2 +- i18n/locales/en.json | 18 +- i18n/locales/zh.json | 18 +- types/api.ts | 49 +++ 8 files changed, 564 insertions(+), 31 deletions(-) create mode 100644 components/match-detail/football/football-events.tsx diff --git a/app/match-detail/[id].tsx b/app/match-detail/[id].tsx index 65a2cbc..f8b3230 100644 --- a/app/match-detail/[id].tsx +++ b/app/match-detail/[id].tsx @@ -1,4 +1,5 @@ import { BasketballScoreTable } from "@/components/match-detail/basketball/basketball-score-table"; +import { FootballEvents } from "@/components/match-detail/football/football-events"; import { FootballScoreTable } from "@/components/match-detail/football/football-score-table"; import { LeagueInfo } from "@/components/match-detail/league-info"; import { MatchInfoCard } from "@/components/match-detail/match-info-card"; @@ -71,6 +72,8 @@ export default function MatchDetailScreen() { setError(null); const result = await fetchMatchDetail(id as string); setData(result); + console.log("首发阵容" , result.match.players?.away_team); + } catch (err: any) { setError(err.message || t("detail.fetch_failed")); } finally { @@ -104,11 +107,12 @@ export default function MatchDetailScreen() { case "info": // 根据 sportId 显示不同的详情组件 if (sportId === 1) { - // 足球:显示 FootballScoreTable (半场/全场) 和 MatchInfoCard + // 足球:显示 FootballScoreTable (半场/全场)、MatchInfoCard 和 FootballEvents return ( <> + ); } else if (sportId === 2) { @@ -125,6 +129,7 @@ export default function MatchDetailScreen() { <> + ); } diff --git a/components/match-detail/football/football-events.tsx b/components/match-detail/football/football-events.tsx new file mode 100644 index 0000000..2d520db --- /dev/null +++ b/components/match-detail/football/football-events.tsx @@ -0,0 +1,402 @@ +import { ThemedText } from "@/components/themed-text"; +import { IconSymbol } from "@/components/ui/icon-symbol"; +import { CardEvent, GoalEvent, MatchDetailData, Player, SubstituteEvent } from "@/types/api"; +import React from "react"; +import { useTranslation } from "react-i18next"; +import { StyleSheet, View } from "react-native"; + +interface FootballEventsProps { + data: MatchDetailData; + isDark: boolean; +} + +export function FootballEvents({ data, isDark }: FootballEventsProps) { + const { t } = useTranslation(); + const { match } = data; + const bgColor = isDark ? "#1C1C1E" : "#FFF"; + const borderColor = isDark ? "#2C2C2E" : "#EEE"; + + // events 可能在 data.events 或 match.events 中 + const events = (data as any).events || (match as any).events; + + // 调试:打印 events 数据 + if (__DEV__) { + console.log("FootballEvents - events data:", events); + } + + if (!events || typeof events !== "object") { + if (__DEV__) { + console.log("FootballEvents - No events data found"); + } + return null; + } + + // 处理数组格式的 events(如果 events 是数组) + let eventsObj = events; + if (Array.isArray(events) && events.length > 0) { + // 如果 events 是数组,取第一个元素 + eventsObj = events[0]; + } + + const goalscorers = eventsObj?.goalscorers || []; + const cards = eventsObj?.cards || []; + const substitutes = eventsObj?.substitutes || []; + const players = (match as any).players || null; + + if (__DEV__) { + console.log("FootballEvents - goalscorers:", goalscorers.length, "cards:", cards.length, "substitutes:", substitutes.length); + } + + const hasEvents = goalscorers.length > 0 || cards.length > 0 || substitutes.length > 0; + + if (!hasEvents) { + if (__DEV__) { + console.log("FootballEvents - No events to display"); + } + 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} + )} + + + ); + }; + + 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} + + + )} + + + ); + }; + + 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 || ""; + + return ( + + + + {sub.time ? `${sub.time}'` : ""} + + + + + + {playerOut ? ( + <> + + {playerIn} + + + + {playerOut} + + + ) : ( + + {playerIn} + + )} + + + + ); + }; + + return ( + + {goalscorers.length > 0 && ( + + + {t("detail.events.goals")} + + {goalscorers + .filter((goal: GoalEvent) => goal.home_scorer || goal.away_scorer) + .map((goal: GoalEvent, index: number) => renderGoalEvent(goal, index))} + + )} + + {cards.length > 0 && ( + + + {t("detail.events.cards")} + + {cards + .filter((card: CardEvent) => card.home_fault || card.away_fault) + .map((card: CardEvent, index: number) => renderCardEvent(card, index))} + + )} + + {substitutes.length > 0 && ( + + + {t("detail.events.substitutes")} + + {substitutes + .filter((sub: SubstituteEvent) => sub.home_scorer || sub.away_scorer || sub.home_assist || sub.away_assist) + .map((sub: SubstituteEvent, index: number) => renderSubstituteEvent(sub, index))} + + )} + + {players && (players.home_team || players.away_team) && ( + + + {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, + }, + section: { + marginBottom: 20, + }, + sectionTitle: { + fontSize: 14, + fontWeight: "600", + marginBottom: 12, + opacity: 0.7, + }, + 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, + }, + cardIcon: { + width: 12, + height: 16, + borderRadius: 2, + }, + cardTypeText: { + fontSize: 10, + fontWeight: "600", + textTransform: "uppercase", + }, + cardBadge: { + paddingHorizontal: 6, + paddingVertical: 2, + borderRadius: 4, + }, + 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", + }, + 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", + }, + 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/football-score-table.tsx b/components/match-detail/football/football-score-table.tsx index e64f8a0..a3714a2 100644 --- a/components/match-detail/football/football-score-table.tsx +++ b/components/match-detail/football/football-score-table.tsx @@ -9,10 +9,11 @@ interface FootballScoreTableProps { isDark: boolean; } -// 解析足球比分字符串,例如 "2-1" 或 "1-0" +// 解析足球比分字符串,例如 "2 - 1" 或 "1-0" function parseFootballScore(scoreString: string): number[] { - if (!scoreString || scoreString === "-") return [0, 0]; - const parts = scoreString.split("-"); + if (!scoreString || scoreString === "-" || scoreString.trim() === "") return [0, 0]; + // 处理 "2 - 1" 或 "2-1" 格式 + const parts = scoreString.replace(/\s+/g, "").split("-"); if (parts.length === 2) { return [parseInt(parts[0]) || 0, parseInt(parts[1]) || 0]; } @@ -25,27 +26,42 @@ export function FootballScoreTable({ data, isDark }: FootballScoreTableProps) { const bgColor = isDark ? "#1C1C1E" : "#FFF"; const headerTextColor = isDark ? "#666" : "#999"; - // 解析足球比分 + // 解析各种比分 const finalScore = parseFootballScore(match.eventFinalResult || "-"); const halftimeScore = parseFootballScore(match.eventHalftimeResult || "-"); + const ftScore = parseFootballScore(match.eventFtResult || "-"); + const penaltyScore = parseFootballScore(match.eventPenaltyResult || "-"); - const headers = [ - t("detail.score_table.team"), - t("detail.score_table.total"), - t("detail.score_table.halftime"), - ]; + // 判断是否有加时赛或点球 + const hasExtraTime = ftScore[0] !== finalScore[0] || ftScore[1] !== finalScore[1]; + const hasPenalty = penaltyScore[0] > 0 || penaltyScore[1] > 0; + + // 动态生成表头 + const headers = [t("detail.score_table.team")]; + if (hasPenalty) { + headers.push(t("detail.score_table.penalty")); + } + if (hasExtraTime) { + headers.push(t("detail.score_table.extra_time")); + } + headers.push(t("detail.score_table.full_time")); + headers.push(t("detail.score_table.halftime")); const rows = [ { logo: match.homeTeamLogo, name: match.eventHomeTeam, - total: finalScore[0], + penalty: hasPenalty ? penaltyScore[0] : null, + extraTime: hasExtraTime ? finalScore[0] : null, + fullTime: hasExtraTime ? ftScore[0] : finalScore[0], halftime: halftimeScore[0], }, { logo: match.awayTeamLogo, name: match.eventAwayTeam, - total: finalScore[1], + penalty: hasPenalty ? penaltyScore[1] : null, + extraTime: hasExtraTime ? finalScore[1] : null, + fullTime: hasExtraTime ? ftScore[1] : finalScore[1], halftime: halftimeScore[1], }, ]; @@ -78,7 +94,17 @@ export function FootballScoreTable({ data, isDark }: FootballScoreTableProps) { {row.name} - {row.total} + {hasPenalty && ( + + {row.penalty !== null ? row.penalty : "-"} + + )} + {hasExtraTime && ( + + {row.extraTime !== null ? row.extraTime : "-"} + + )} + {row.fullTime} {row.halftime} ))} diff --git a/components/match-detail/match-info-card.tsx b/components/match-detail/match-info-card.tsx index 3623915..2c36ca3 100644 --- a/components/match-detail/match-info-card.tsx +++ b/components/match-detail/match-info-card.tsx @@ -13,6 +13,17 @@ export function MatchInfoCard({ data, isDark }: MatchInfoCardProps) { const { t } = useTranslation(); const { match } = data; const bgColor = isDark ? "#1C1C1E" : "#FFF"; + const labelColor = isDark ? "#999" : "#666"; + const valueColor = isDark ? "#FFF" : "#000"; + + const infoItems = [ + { label: t("detail.info_card.country"), value: match.countryName }, + { label: t("detail.info_card.league"), value: match.leagueName }, + { label: t("detail.info_card.stage"), value: match.stageName }, + { label: t("detail.info_card.stadium"), value: match.eventStadium }, + { label: t("detail.info_card.referee"), value: match.eventReferee }, + { label: t("detail.info_card.date"), value: `${match.eventDate} ${match.eventTime}` }, + ].filter((item) => item.value && item.value.trim() !== ""); return ( @@ -21,10 +32,16 @@ export function MatchInfoCard({ data, isDark }: MatchInfoCardProps) { - {match.leagueName} - - {match.eventDate} • {match.eventTime} - + {infoItems.map((item, index) => ( + + + {item.label} + + + {item.value} + + + ))} ); @@ -49,14 +66,24 @@ const styles = StyleSheet.create({ marginBottom: 16, }, content: { - gap: 4, + gap: 12, }, - leagueName: { - fontSize: 16, - fontWeight: "600", + infoRow: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "flex-start", + gap: 12, }, - dateTime: { - fontSize: 14, - opacity: 0.6, + label: { + fontSize: 13, + fontWeight: "500", + opacity: 0.7, + minWidth: 80, + }, + value: { + fontSize: 13, + fontWeight: "500", + flex: 1, + textAlign: "right", }, }); diff --git a/constants/api.ts b/constants/api.ts index 5c93bb8..8c502a5 100644 --- a/constants/api.ts +++ b/constants/api.ts @@ -1,6 +1,6 @@ export const API_CONFIG = { BASE_URL: "http://192.168.1.111:8000", // Update this with your machine IP if running on device ex: http://192.168.1.5:8000 - TIMEOUT: 10000, + TIMEOUT: 100000, }; export const API_ENDPOINTS = { diff --git a/i18n/locales/en.json b/i18n/locales/en.json index 00819f6..d3d92da 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -48,18 +48,30 @@ "league": "League", "stage": "Stage", "stadium": "Stadium", - "referee": "Referee" + "referee": "Referee", + "date": "Date & Time" }, "score_table": { "team": "Team", "total": "Full Time", - "halftime": "Half Time" + "halftime": "Half Time", + "full_time": "90'", + "extra_time": "ET", + "penalty": "Pen" }, "halftime": "Half: {{score}}", "empty_stats": "No statistics data", "empty_odds": "No odds data", "empty_h2h": "No H2H data", - "empty_chat": "Chat is not available" + "empty_chat": "Chat is not available", + "events": { + "goals": "Goals", + "cards": "Cards", + "substitutes": "Substitutes", + "lineups": "Lineups", + "red_card": "RED", + "yellow_card": "YELLOW" + } }, "selection": { "selected": "Selected", diff --git a/i18n/locales/zh.json b/i18n/locales/zh.json index ca26b6d..fa50ff9 100644 --- a/i18n/locales/zh.json +++ b/i18n/locales/zh.json @@ -48,18 +48,30 @@ "league": "赛事", "stage": "阶段", "stadium": "场馆", - "referee": "裁判" + "referee": "裁判", + "date": "日期时间" }, "score_table": { "team": "球队", "total": "全场", - "halftime": "半场" + "halftime": "半场", + "full_time": "90分钟", + "extra_time": "加时", + "penalty": "点球" }, "halftime": "半场: {{score}}", "empty_stats": "暂无统计数据", "empty_odds": "暂无赔率数据", "empty_h2h": "暂无交锋数据", - "empty_chat": "聊天功能暂未开启" + "empty_chat": "聊天功能暂未开启", + "events": { + "goals": "进球", + "cards": "红黄牌", + "substitutes": "换人", + "lineups": "首发阵容", + "red_card": "红牌", + "yellow_card": "黄牌" + } }, "selection": { "selected": "已选择", diff --git a/types/api.ts b/types/api.ts index 7f57b21..7e40e64 100644 --- a/types/api.ts +++ b/types/api.ts @@ -30,6 +30,50 @@ export interface ApiListResponse { total: number; } +export interface GoalEvent { + time: string; + home_scorer?: string; + away_scorer?: string; + score?: string; + comment?: string; +} + +export interface CardEvent { + time: string; + home_fault?: string; + away_fault?: string; + card?: string; + comment?: string; +} + +export interface SubstituteEvent { + time: string; + home_scorer?: string; + away_scorer?: string; + home_assist?: string; + away_assist?: string; + home_scorer_out?: string; + away_scorer_out?: string; + comment?: string; +} + +export interface MatchEvents { + goalscorers: GoalEvent[]; + cards: CardEvent[]; + substitutes: SubstituteEvent[]; +} + +export interface Player { + // AllSports API 常见字段 + player?: string; // 有些接口用 player + player_name?: string; // 有些接口用 player_name + name?: string; // 兜底 + player_number?: number | string; + number?: number | string; + player_type?: string; // 位置/角色,例如 Goalkeepers + position?: string; // 兜底位置字段 +} + export interface MatchDetailData { events: any[]; match: { @@ -73,5 +117,10 @@ export interface MatchDetailData { eventType: string; eventToss: string; eventManOfMatch: string; + events?: MatchEvents; + players?: { + home_team?: Player[]; + away_team?: Player[]; + }; }; }