From 3e9cc032179fd3242cdefdcfd0ebd55698e22422 Mon Sep 17 00:00:00 2001 From: xianyi Date: Thu, 22 Jan 2026 17:29:44 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AF=AE=E7=90=83=E7=BB=9F=E8=AE=A1=E6=95=B0?= =?UTF-8?q?=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/match-detail/[id].tsx | 6 +- .../basketball/basketball-score-table.tsx | 137 ++++-- .../basketball/basketball-stats.tsx | 407 ++++++++++++++++++ components/match-detail/match-tabs.tsx | 25 +- types/api.ts | 32 +- 5 files changed, 561 insertions(+), 46 deletions(-) create mode 100644 components/match-detail/basketball/basketball-stats.tsx diff --git a/app/match-detail/[id].tsx b/app/match-detail/[id].tsx index c7b9d6c..e509f1a 100644 --- a/app/match-detail/[id].tsx +++ b/app/match-detail/[id].tsx @@ -1,5 +1,6 @@ import { OddsCard } from "@/components/live-detail/odds-card"; import { BasketballScoreTable } from "@/components/match-detail/basketball/basketball-score-table"; +import { BasketballStats } from "@/components/match-detail/basketball/basketball-stats"; 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"; @@ -59,7 +60,7 @@ export default function MatchDetailScreen() { validTabs = ["info", "stats", "odds", "h2h", "chat"]; } else if (sportId === 2) { // 篮球 - validTabs = ["info", "h2h", "chat"]; + validTabs = ["info", "stats", "h2h", "chat"]; } else if (sportId === 3) { // 网球 validTabs = ["info", "chat"]; @@ -163,6 +164,9 @@ export default function MatchDetailScreen() { ); + } else if (sportId === 2) { + // 篮球:显示统计数据 + return ; } else { // 其他运动暂时显示空状态 return ( diff --git a/components/match-detail/basketball/basketball-score-table.tsx b/components/match-detail/basketball/basketball-score-table.tsx index 6de77ee..85c87e0 100644 --- a/components/match-detail/basketball/basketball-score-table.tsx +++ b/components/match-detail/basketball/basketball-score-table.tsx @@ -16,22 +16,81 @@ export function BasketballScoreTable({ const { t } = useTranslation(); const { match } = data; const bgColor = isDark ? "#1C1C1E" : "#FFF"; + const borderColor = isDark ? "#2C2C2E" : "rgba(0,0,0,0.06)"; const headerTextColor = isDark ? "#666" : "#999"; + const textColor = isDark ? "#FFF" : "#000"; - // 解析比分 - 篮球通常是 4 节 - const parseScore = (scoreString: string) => { - if (!scoreString || scoreString === "-") return [0, 0, 0, 0, 0]; - // 假设格式可能是 "25-20,30-28,22-25,28-26" 或类似 - // 这里简化处理,实际需要根据 API 返回格式解析 - return [0, 0, 0, 0, 0]; // total, q1, q2, q3, q4 + // 解析篮球比分 - 从 scores 对象获取四节比分 + const parseBasketballScores = () => { + const scores = match.scores as any; + if (!scores || typeof scores !== "object") { + // 如果没有 scores 对象,尝试从 eventFinalResult 解析 + const finalResult = match.eventFinalResult || "-"; + const matchResult = finalResult.match(/(\d+)\s*[-–]\s*(\d+)/); + if (matchResult) { + return { + home: { + total: parseInt(matchResult[1], 10), + q1: 0, + q2: 0, + q3: 0, + q4: 0, + }, + away: { + total: parseInt(matchResult[2], 10), + q1: 0, + q2: 0, + q3: 0, + q4: 0, + }, + }; + } + return { + home: { total: 0, q1: 0, q2: 0, q3: 0, q4: 0 }, + away: { total: 0, q1: 0, q2: 0, q3: 0, q4: 0 }, + }; + } + + const q1 = scores["1stQuarter"]?.[0] || { score_home: "0", score_away: "0" }; + const q2 = scores["2ndQuarter"]?.[0] || { score_home: "0", score_away: "0" }; + const q3 = scores["3rdQuarter"]?.[0] || { score_home: "0", score_away: "0" }; + const q4 = scores["4thQuarter"]?.[0] || { score_home: "0", score_away: "0" }; + + const homeQ1 = parseInt(q1.score_home || "0", 10); + const homeQ2 = parseInt(q2.score_home || "0", 10); + const homeQ3 = parseInt(q3.score_home || "0", 10); + const homeQ4 = parseInt(q4.score_home || "0", 10); + const homeTotal = homeQ1 + homeQ2 + homeQ3 + homeQ4; + + const awayQ1 = parseInt(q1.score_away || "0", 10); + const awayQ2 = parseInt(q2.score_away || "0", 10); + const awayQ3 = parseInt(q3.score_away || "0", 10); + const awayQ4 = parseInt(q4.score_away || "0", 10); + const awayTotal = awayQ1 + awayQ2 + awayQ3 + awayQ4; + + return { + home: { + total: homeTotal, + q1: homeQ1, + q2: homeQ2, + q3: homeQ3, + q4: homeQ4, + }, + away: { + total: awayTotal, + q1: awayQ1, + q2: awayQ2, + q3: awayQ3, + q4: awayQ4, + }, + }; }; - const homeScores = parseScore(match.eventFinalResult || "-"); - const awayScores = parseScore(match.eventFinalResult || "-"); + const scoreData = parseBasketballScores(); const headers = [ - t("detail.score_table.team"), - t("detail.score_table.total"), + t("detail.score_table.team") || "Team", + t("detail.score_table.total") || "Total", "1", "2", "3", @@ -42,26 +101,26 @@ export function BasketballScoreTable({ { logo: match.homeTeamLogo, name: match.eventHomeTeam, - total: homeScores[0], - q1: homeScores[1], - q2: homeScores[2], - q3: homeScores[3], - q4: homeScores[4], + total: scoreData.home.total, + q1: scoreData.home.q1, + q2: scoreData.home.q2, + q3: scoreData.home.q3, + q4: scoreData.home.q4, }, { logo: match.awayTeamLogo, name: match.eventAwayTeam, - total: awayScores[0], - q1: awayScores[1], - q2: awayScores[2], - q3: awayScores[3], - q4: awayScores[4], + total: scoreData.away.total, + q1: scoreData.away.q1, + q2: scoreData.away.q2, + q3: scoreData.away.q3, + q4: scoreData.away.q4, }, ]; return ( - - + + {headers.map((h, i) => ( {rows.map((row, idx) => ( - + - - + {row.logo && row.logo.trim() !== "" && !row.logo.includes("placehold") ? ( + + ) : null} + {row.name} - + {row.total} - {row.q1} - {row.q2} - {row.q3} - {row.q4} + {row.q1} + {row.q2} + {row.q3} + {row.q4} ))} @@ -103,20 +164,16 @@ export function BasketballScoreTable({ const styles = StyleSheet.create({ container: { marginHorizontal: 16, - marginBottom: 16, + marginTop: 12, borderRadius: 16, padding: 20, - elevation: 2, - shadowColor: "#000", - shadowOffset: { width: 0, height: 2 }, - shadowOpacity: 0.05, - shadowRadius: 8, + borderWidth: 1, + overflow: "hidden", }, header: { flexDirection: "row", paddingBottom: 16, borderBottomWidth: 1, - borderBottomColor: "rgba(150,150,150,0.1)", }, headerText: { fontSize: 12, @@ -128,10 +185,6 @@ const styles = StyleSheet.create({ alignItems: "center", paddingVertical: 14, }, - rowBorder: { - borderBottomWidth: 1, - borderBottomColor: "rgba(150,150,150,0.1)", - }, teamCell: { flex: 3, flexDirection: "row", @@ -146,7 +199,7 @@ const styles = StyleSheet.create({ teamName: { flex: 1, fontSize: 14, - fontWeight: "700", + fontWeight: "600", }, cellText: { flex: 1, diff --git a/components/match-detail/basketball/basketball-stats.tsx b/components/match-detail/basketball/basketball-stats.tsx new file mode 100644 index 0000000..8dfc759 --- /dev/null +++ b/components/match-detail/basketball/basketball-stats.tsx @@ -0,0 +1,407 @@ +import { ThemedText } from "@/components/themed-text"; +import { IconSymbol } from "@/components/ui/icon-symbol"; +import { getInitials, getLogoGradient } from "@/lib/avatar-utils"; +import { MatchDetailData } from "@/types/api"; +import { Image } from "expo-image"; +import { LinearGradient } from "expo-linear-gradient"; +import React, { useMemo, useState } from "react"; +import { + ScrollView, + StyleSheet, + Switch, + TouchableOpacity, + View +} from "react-native"; + +interface BasketballStatsProps { + data: MatchDetailData; + isDark: boolean; +} + +// 定义固定高度以保证左右对齐 +const HEADER_HEIGHT = 40; +const ROW_HEIGHT = 60; +const LEFT_COLUMN_WIDTH = 150; + +const COLUMNS_BASIC = [ + { key: "pts", label: "PTS", width: 50 }, + { key: "reb", label: "REB", width: 50 }, + { key: "ast", label: "AST", width: 50 }, +]; + +const COLUMNS_DETAILED = [ + { key: "min", label: "MIN", width: 50 }, + { key: "pts", label: "PTS", width: 50 }, + { key: "reb", label: "REB", width: 50 }, + { key: "ast", label: "AST", width: 50 }, + { key: "blk", label: "BLK", width: 50 }, + { key: "stl", label: "STL", width: 50 }, + { key: "to", label: "TO", width: 50 }, + { key: "pf", label: "PF", width: 50 }, +]; + +export function BasketballStats({ data, isDark }: BasketballStatsProps) { + const { match } = data; + const [activeTab, setActiveTab] = useState<"home" | "away">("home"); + const [isDetailed, setIsDetailed] = useState(false); + + // 获取当前展示的球员列表 + const currentPlayers = useMemo(() => { + if (!match.players) return []; + return activeTab === "home" + ? match.players.home_team + : match.players.away_team; + }, [match.players, activeTab]); + + const columns = isDetailed ? COLUMNS_DETAILED : COLUMNS_BASIC; + + const getPlayerStat = (player: any, statKey: string): string => { + const statMap: Record = { + pts: player.player_points || "0", + reb: player.player_total_rebounds || "0", + ast: player.player_assists || "0", + min: player.player_minutes || "0", + blk: player.player_blocks || "0", + stl: player.player_steals || "0", + to: player.player_turnovers || "0", + pf: player.player_personal_fouls || "0", + }; + const value = statMap[statKey] || "0"; + return value === "-" || value === "" ? "0" : value; + }; + + const bgColor = isDark ? "#1C1C1E" : "#FFF"; + const borderColor = isDark ? "#2C2C2E" : "rgba(0,0,0,0.06)"; + const textColor = isDark ? "#FFF" : "#000"; + const secondaryText = isDark ? "#8E8E93" : "#6B7280"; + + // --- 渲染组件 --- + + // 1. 左上角:Player 表头 + const renderLeftHeader = () => ( + + + Player + + + + ); + + // 2. 左侧列:球员信息行 + const renderLeftPlayerRow = (player: any) => { + const playerName = player.player || ""; + const playerPosition = player.player_position || ""; + const gradient = getLogoGradient(playerName); + const initials = getInitials(playerName); + + return ( + + + + {initials} + + + + + {playerName} + + {playerPosition && ( + + {playerPosition} + + )} + + + ); + }; + + // 3. 右上角:数据表头 + const renderRightHeader = () => ( + + {columns.map((col) => ( + + + {col.label} + + + ))} + + ); + + // 4. 右侧列:数据行 + const renderRightDataRow = (player: any) => ( + + {columns.map((col) => ( + + + {getPlayerStat(player, col.key)} + + + ))} + + ); + + // 队名/Logo逻辑 + const homeTeamName = match.eventHomeTeam || ""; + const awayTeamName = match.eventAwayTeam || ""; + const homeTeamLogo = match.homeTeamLogo || ""; + const awayTeamLogo = match.awayTeamLogo || ""; + const hasHomeLogo = homeTeamLogo && homeTeamLogo.trim() !== "" && !homeTeamLogo.includes("placehold"); + const hasAwayLogo = awayTeamLogo && awayTeamLogo.trim() !== "" && !awayTeamLogo.includes("placehold"); + const homeGradient = getLogoGradient(homeTeamName); + const awayGradient = getLogoGradient(awayTeamName); + const homeInitials = getInitials(homeTeamName); + const awayInitials = getInitials(awayTeamName); + + return ( + + {/* 顶部控制栏 */} + + + + 统计外观 + + + 详细视图 + + + + + + {/* 队伍切换 Tab */} + + setActiveTab("home")} + > + {hasHomeLogo ? ( + + ) : ( + + {homeInitials} + + )} + + setActiveTab("away")} + > + {hasAwayLogo ? ( + + ) : ( + + {awayInitials} + + )} + + + + {/* 核心表格区域:Vertical ScrollView 包裹整个表格 */} + + + + {/* 左侧区域:固定不横向滚动 */} + + {/* Header */} + {renderLeftHeader()} + {/* Players List */} + + {currentPlayers?.map((player, index) => ( + + {renderLeftPlayerRow(player)} + + ))} + + + + {/* 右侧区域:整体横向滚动 */} + + + {/* Header */} + {renderRightHeader()} + {/* Data List */} + + {currentPlayers?.map((player, index) => ( + + {renderRightDataRow(player)} + + ))} + + + + + + + {/* Empty State */} + {(!currentPlayers || currentPlayers.length === 0) && ( + + 暂无球员数据 + + )} + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + paddingTop: 12, + }, + controlsHeader: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingHorizontal: 16, + marginBottom: 12, + }, + controlTitle: { + fontSize: 14, + fontWeight: "600", + }, + controlSubtitle: { + fontSize: 12, + marginTop: 2, + }, + teamTabsContainer: { + flexDirection: "row", + marginHorizontal: 16, + marginBottom: 12, + borderRadius: 12, + height: 44, + borderWidth: 1, + overflow: "hidden", + }, + teamTab: { + flex: 1, + justifyContent: "center", + alignItems: "center", + }, + teamTabLogo: { + width: 24, + height: 24, + }, + teamTabLogoGradient: { + width: 24, + height: 24, + borderRadius: 12, + alignItems: "center", + justifyContent: "center", + }, + teamTabLogoText: { + fontSize: 10, + fontWeight: "700", + color: "rgba(255, 255, 255, 0.92)", + }, + tableContainer: { + marginHorizontal: 16, + marginBottom: 20, + borderRadius: 16, + borderWidth: 1, + overflow: "hidden", // 确保圆角生效 + }, + // 左侧样式 + headerCell: { + flexDirection: "row", + alignItems: "center", + borderBottomWidth: StyleSheet.hairlineWidth, + paddingHorizontal: 12, + width: "100%", + }, + playerRow: { + flexDirection: "row", + alignItems: "center", + borderBottomWidth: StyleSheet.hairlineWidth, + paddingHorizontal: 8, + width: "100%", + }, + // 右侧样式 + dataHeaderRow: { + flexDirection: "row", + alignItems: "center", + borderBottomWidth: StyleSheet.hairlineWidth, + paddingHorizontal: 16, // 给右侧表头一些左右padding,美观 + }, + dataRow: { + flexDirection: "row", + alignItems: "center", + borderBottomWidth: StyleSheet.hairlineWidth, + paddingHorizontal: 16, + }, + // 通用文本 + headerText: { + fontSize: 12, + fontWeight: "700", + }, + statText: { + fontSize: 14, + fontWeight: "500", + }, + // 球员头像与信息 + avatarContainer: { + marginRight: 8, + }, + avatarGradient: { + width: 32, + height: 32, + borderRadius: 16, + alignItems: "center", + justifyContent: "center", + }, + avatarText: { + fontSize: 12, + fontWeight: "700", + color: "rgba(255, 255, 255, 0.92)", + }, + playerInfo: { + flex: 1, + justifyContent: "center", + }, + playerName: { + fontSize: 13, + fontWeight: "600", + marginBottom: 2, + }, + playerNumPos: { + fontSize: 11, + }, + emptyState: { + padding: 40, + alignItems: "center", + }, +}); \ No newline at end of file diff --git a/components/match-detail/match-tabs.tsx b/components/match-detail/match-tabs.tsx index bcb1928..db29fa8 100644 --- a/components/match-detail/match-tabs.tsx +++ b/components/match-detail/match-tabs.tsx @@ -63,8 +63,29 @@ export function MatchTabs({ }, ]; } else if (sportId === 2) { - // 篮球 - return commonTabs; + // 篮球: 详情、统计数据、交锋往绩、聊天 + return [ + { + id: "info", + label: t("detail.tabs.info"), + icon: "document-text-outline", + }, + { + id: "stats", + label: t("detail.tabs.stats"), + icon: "stats-chart-outline", + }, + { + id: "h2h", + label: t("detail.tabs.h2h"), + icon: "swap-horizontal-outline", + }, + { + id: "chat", + label: t("detail.tabs.chat"), + icon: "chatbubbles-outline", + }, + ]; } else if (sportId === 3) { // 网球: 详情、聊天 return [ diff --git a/types/api.ts b/types/api.ts index 1ffb97d..c31bfe2 100644 --- a/types/api.ts +++ b/types/api.ts @@ -213,6 +213,26 @@ export interface Player { number?: number | string; player_type?: string; // 位置/角色,例如 Goalkeepers position?: string; // 兜底位置字段 + // 篮球相关字段 + player_id?: number; + player_points?: string; + player_assists?: string; + player_minutes?: string; + player_blocks?: string; + player_steals?: string; + player_turnovers?: string; + player_plus_minus?: string; + player_personal_fouls?: string; + player_total_rebounds?: string; + player_defense_rebounds?: string; + player_offence_rebounds?: string; + player_field_goals_made?: string; + player_field_goals_attempts?: string; + player_freethrows_goals_made?: string; + player_freethrows_goals_attempts?: string; + player_threepoint_goals_made?: string; + player_threepoint_goals_attempts?: string; + player_oncourt?: string; } export interface UpcomingMatch { @@ -312,7 +332,17 @@ export interface MatchDetailData { firstPlayerKey?: string; secondPlayerKey?: string; eventGameResult?: string; - scores?: any[]; + scores?: any[] | { + "1stQuarter"?: Array<{ score_home: string; score_away: string }>; + "2ndQuarter"?: Array<{ score_home: string; score_away: string }>; + "3rdQuarter"?: Array<{ score_home: string; score_away: string }>; + "4thQuarter"?: Array<{ score_home: string; score_away: string }>; + }; + stats?: Array<{ + type: string; + home: string; + away: string; + }>; events?: MatchEvents; players?: { home_team?: Player[];