From 3e9cc032179fd3242cdefdcfd0ebd55698e22422 Mon Sep 17 00:00:00 2001 From: xianyi Date: Thu, 22 Jan 2026 17:29:44 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E7=AF=AE=E7=90=83=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E6=95=B0=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[]; From 6dc1170d1dd3e89303f15c413024ec7688f4915b Mon Sep 17 00:00:00 2001 From: xianyi Date: Thu, 22 Jan 2026 18:05:04 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E7=AF=AE=E7=90=83=E6=95=B4=E4=BD=93?= =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E6=95=B0=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/live-detail/[id].tsx | 1 + app/match-detail/[id].tsx | 9 +- .../basketball/basketball-overall-stats.tsx | 285 ++++++++++++++++++ components/match-detail/match-tabs.tsx | 7 +- i18n/locales/en.json | 7 + i18n/locales/hi.json | 7 + i18n/locales/id.json | 7 + i18n/locales/ms.json | 7 + i18n/locales/th.json | 7 + i18n/locales/vi.json | 7 + i18n/locales/zh.json | 7 + 11 files changed, 347 insertions(+), 4 deletions(-) create mode 100644 components/match-detail/basketball/basketball-overall-stats.tsx diff --git a/app/live-detail/[id].tsx b/app/live-detail/[id].tsx index 6219dcd..e661e01 100644 --- a/app/live-detail/[id].tsx +++ b/app/live-detail/[id].tsx @@ -69,6 +69,7 @@ export default function LiveDetailScreen() { if (liveData && Array.isArray(liveData)) { // Find the specific match const found = liveData.find((m) => m.event_key.toString() === id); + console.log("found", JSON.stringify(found, null, 2)); if (found) { setMatch(found); } diff --git a/app/match-detail/[id].tsx b/app/match-detail/[id].tsx index e509f1a..62b7dcf 100644 --- a/app/match-detail/[id].tsx +++ b/app/match-detail/[id].tsx @@ -1,4 +1,5 @@ import { OddsCard } from "@/components/live-detail/odds-card"; +import { BasketballOverallStats } from "@/components/match-detail/basketball/basketball-overall-stats"; 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"; @@ -60,7 +61,7 @@ export default function MatchDetailScreen() { validTabs = ["info", "stats", "odds", "h2h", "chat"]; } else if (sportId === 2) { // 篮球 - validTabs = ["info", "stats", "h2h", "chat"]; + validTabs = ["info", "stats", "overall", "h2h", "chat"]; } else if (sportId === 3) { // 网球 validTabs = ["info", "chat"]; @@ -82,8 +83,8 @@ export default function MatchDetailScreen() { setError(null); const result = await fetchMatchDetail(id as string); setData(result); - console.log("首发阵容", result.match.players?.away_team); - console.log("红黄牌", result.events); + // console.log("首发阵容", result.match.players?.away_team); + // console.log("红黄牌", result.events); @@ -177,6 +178,8 @@ export default function MatchDetailScreen() { ); } + case "overall": + return ; case "odds": // 将 MatchDetailData.match 转换为 LiveScoreMatch 格式 const matchForOdds = { diff --git a/components/match-detail/basketball/basketball-overall-stats.tsx b/components/match-detail/basketball/basketball-overall-stats.tsx new file mode 100644 index 0000000..40c2c3b --- /dev/null +++ b/components/match-detail/basketball/basketball-overall-stats.tsx @@ -0,0 +1,285 @@ +import { ThemedText } from "@/components/themed-text"; +import { MatchDetailData } from "@/types/api"; +import { LinearGradient } from "expo-linear-gradient"; +import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { StyleSheet, View } from "react-native"; + +interface BasketballOverallStatsProps { + data: MatchDetailData; + isDark: boolean; +} + +type StatItem = { + type: string; + home: string | number; + away: string | number; +}; + +function toNumber(v: unknown): number { + if (typeof v === "number") return Number.isFinite(v) ? v : 0; + if (typeof v === "string") { + const cleaned = v.trim().replace("%", ""); + const n = Number(cleaned); + return Number.isFinite(n) ? n : 0; + } + return 0; +} + +export function BasketballOverallStats({ data, isDark }: BasketballOverallStatsProps) { + const { t } = useTranslation(); + + useEffect(() => { + console.log("=== Basketball Overall Stats Loaded ==="); + console.log(JSON.stringify(data.match.stats, null, 2)); + }, [data.match.stats]); + + const stats = (data.match.stats || []) as StatItem[]; + + const getStatLabel = (type: string): string => { + const key = `detail.overall_stats.${type.toLowerCase().replace(/\s+/g, "_")}`; + const translated = t(key); + return translated !== key ? translated : type; + }; + + if (!Array.isArray(stats) || stats.length === 0) { + return ( + + + {t("detail.empty_stats")} + + + ); + } + + return ( + + {/* Card */} + + {stats.map((item, index) => { + const home = toNumber(item.home); + const away = toNumber(item.away); + const maxV = Math.max(home, away, 1); + + const homeLeading = home >= away; + const awayLeading = away > home; + + return ( + + + + + + {getStatLabel(item.type)} + + + + + + + + ); + })} + + + ); +} + +function ValuePill({ + text, + variant, + isDark, +}: { + text: string; + variant: "home" | "away" | "awayLeading"; + isDark: boolean; +}) { + const common = { + start: { x: 0, y: 0 }, + end: { x: 1, y: 0 }, + style: styles.pill, + } as const; + + // 蓝色:主队/默认;金色:客队领先 + const blue = isDark + ? ["#0B2A73", "#0E4BFF"] + : ["#1D4ED8", "#2563EB"]; + const gold = isDark + ? ["#3A2A00", "#C08B00"] + : ["#B45309", "#D97706"]; + + const colors = + variant === "awayLeading" + ? gold + : blue; + + return ( + + {text} + + ); +} + +function CompareBar({ + home, + away, + maxV, + isDark, + homeLeading, + awayLeading, +}: { + home: number; + away: number; + maxV: number; + isDark: boolean; + homeLeading: boolean; + awayLeading: boolean; +}) { + const SIDE_MAX = 150; + const GAP = 10; + + const homeW = Math.max(2, Math.round((home / maxV) * SIDE_MAX)); + const awayW = Math.max(2, Math.round((away / maxV) * SIDE_MAX)); + + const track = isDark ? "rgba(255,255,255,0.10)" : "rgba(0,0,0,0.10)"; + const muted = isDark ? "rgba(255,255,255,0.18)" : "rgba(0,0,0,0.18)"; + const blue = "#1F5BFF"; + const gold = "#C08B00"; + + const homeColor = homeLeading ? blue : muted; + const awayColor = awayLeading ? gold : muted; + + return ( + + + + + + + + + + + ); +} + +/* ----------------- Styles ----------------- */ + +const styles = StyleSheet.create({ + wrap: { + paddingHorizontal: 14, + paddingTop: 12, + paddingBottom: 16, + }, + card: { + borderRadius: 18, + paddingVertical: 6, + overflow: "hidden", + }, + + row: { + paddingHorizontal: 14, + paddingVertical: 14, + }, + rowHeader: { + flexDirection: "row", + alignItems: "center", + }, + + pill: { + minWidth: 54, + height: 30, + borderRadius: 16, + alignItems: "center", + justifyContent: "center", + paddingHorizontal: 10, + }, + pillText: { + color: "#FFFFFF", + fontSize: 14, + fontWeight: "800", + letterSpacing: 0.3, + }, + + title: { + flex: 1, + textAlign: "center", + fontSize: 15, + fontWeight: "700", + paddingHorizontal: 10, + }, + + barWrap: { + alignSelf: "center", + flexDirection: "row", + alignItems: "center", + marginTop: 10, + height: 8, + position: "relative", + }, + barTrack: { + position: "absolute", + left: 0, + right: 0, + height: 6, + borderRadius: 999, + }, + barSide: { + height: 8, + flexDirection: "row", + alignItems: "center", + }, + barFill: { + height: 6, + borderRadius: 999, + }, + + empty: { + padding: 40, + alignItems: "center", + }, + emptyText: { + fontSize: 14, + opacity: 0.85, + }, +}); diff --git a/components/match-detail/match-tabs.tsx b/components/match-detail/match-tabs.tsx index db29fa8..498c228 100644 --- a/components/match-detail/match-tabs.tsx +++ b/components/match-detail/match-tabs.tsx @@ -63,7 +63,7 @@ export function MatchTabs({ }, ]; } else if (sportId === 2) { - // 篮球: 详情、统计数据、交锋往绩、聊天 + // 篮球: 详情、统计数据、整体统计、交锋往绩、聊天 return [ { id: "info", @@ -75,6 +75,11 @@ export function MatchTabs({ label: t("detail.tabs.stats"), icon: "stats-chart-outline", }, + { + id: "overall", + label: t("detail.tabs.overall"), + icon: "bar-chart-outline", + }, { id: "h2h", label: t("detail.tabs.h2h"), diff --git a/i18n/locales/en.json b/i18n/locales/en.json index fe0d285..6b821dd 100644 --- a/i18n/locales/en.json +++ b/i18n/locales/en.json @@ -92,6 +92,7 @@ "fetch_failed": "Failed to fetch details", "not_found": "Match data not found", "tabs": { + "overall": "Overall", "info": "Details", "stats": "Statistics", "odds": "Odds", @@ -166,6 +167,12 @@ "lineups_coach": "Coach", "lineups_subs": "Substitutes", "lineups_missing": "Missing players" + }, + "overall_stats": { + "total_assists": "Assists", + "total_blocks": "Blocks", + "total_steals": "Steals", + "total_turnovers": "Turnovers" } }, "selection": { diff --git a/i18n/locales/hi.json b/i18n/locales/hi.json index 1cd85fe..6d145a1 100644 --- a/i18n/locales/hi.json +++ b/i18n/locales/hi.json @@ -88,6 +88,7 @@ "fetch_failed": "विवरण प्राप्त करने में विफल", "not_found": "मैच डेटा नहीं मिला", "tabs": { + "overall": "कुल", "info": "विवरण", "stats": "आँकड़े", "odds": "ऑड्स", @@ -162,6 +163,12 @@ "lineups_coach": "कोच", "lineups_subs": "सब्स्टीट्यूट", "lineups_missing": "अनुपस्थित खिलाड़ी" + }, + "overall_stats": { + "total_assists": "असिस्ट", + "total_blocks": "ब्लॉक", + "total_steals": "स्टील", + "total_turnovers": "टर्नओवर" } }, "selection": { diff --git a/i18n/locales/id.json b/i18n/locales/id.json index 05fdc9c..3a93703 100644 --- a/i18n/locales/id.json +++ b/i18n/locales/id.json @@ -88,6 +88,7 @@ "fetch_failed": "Gagal mengambil data", "not_found": "Data pertandingan tidak ditemukan", "tabs": { + "overall": "Keseluruhan", "info": "Detail", "stats": "Statistik", "odds": "Odds", @@ -162,6 +163,12 @@ "lineups_coach": "Pelatih", "lineups_subs": "Cadangan", "lineups_missing": "Pemain Absen" + }, + "overall_stats": { + "total_assists": "Asisten", + "total_blocks": "Blok", + "total_steals": "Steal", + "total_turnovers": "Turnover" } }, "selection": { diff --git a/i18n/locales/ms.json b/i18n/locales/ms.json index fb7df10..69784a4 100644 --- a/i18n/locales/ms.json +++ b/i18n/locales/ms.json @@ -88,6 +88,7 @@ "fetch_failed": "Gagal mendapatkan maklumat", "not_found": "Data perlawanan tidak ditemui", "tabs": { + "overall": "Keseluruhan", "info": "Maklumat", "stats": "Statistik", "odds": "Odds", @@ -162,6 +163,12 @@ "lineups_coach": "Jurulatih", "lineups_subs": "Pemain Simpanan", "lineups_missing": "Pemain Tidak Tersenarai" + }, + "overall_stats": { + "total_assists": "Bantuan", + "total_blocks": "Sekatan", + "total_steals": "Curi", + "total_turnovers": "Pusingan" } }, "selection": { diff --git a/i18n/locales/th.json b/i18n/locales/th.json index 7b16919..eb02df7 100644 --- a/i18n/locales/th.json +++ b/i18n/locales/th.json @@ -88,6 +88,7 @@ "fetch_failed": "ไม่สามารถดึงข้อมูลได้", "not_found": "ไม่พบข้อมูลการแข่งขัน", "tabs": { + "overall": "ทั้งหมด", "info": "รายละเอียด", "stats": "สถิติ", "odds": "อัตราต่อรอง", @@ -162,6 +163,12 @@ "lineups_coach": "โค้ช", "lineups_subs": "ตัวสำรอง", "lineups_missing": "ผู้เล่นที่ขาดหาย" + }, + "overall_stats": { + "total_assists": "แอสซิสต์", + "total_blocks": "บล็อก", + "total_steals": "สตีล", + "total_turnovers": "เทิร์นโอเวอร์" } }, "selection": { diff --git a/i18n/locales/vi.json b/i18n/locales/vi.json index e79cbbb..fcc9642 100644 --- a/i18n/locales/vi.json +++ b/i18n/locales/vi.json @@ -88,6 +88,7 @@ "fetch_failed": "Không thể tải dữ liệu", "not_found": "Không tìm thấy dữ liệu trận đấu", "tabs": { + "overall": "Tổng", "info": "Thông tin", "stats": "Thống kê", "odds": "Tỷ lệ cược", @@ -162,6 +163,12 @@ "lineups_coach": "Huấn luyện viên", "lineups_subs": "Dự bị", "lineups_missing": "Cầu thủ vắng mặt" + }, + "overall_stats": { + "total_assists": "Kiến tạo", + "total_blocks": "Chặn bóng", + "total_steals": "Cướp bóng", + "total_turnovers": "Mất bóng" } }, "selection": { diff --git a/i18n/locales/zh.json b/i18n/locales/zh.json index 478afcf..1f7018d 100644 --- a/i18n/locales/zh.json +++ b/i18n/locales/zh.json @@ -92,6 +92,7 @@ "fetch_failed": "获取详情失败", "not_found": "未找到比赛数据", "tabs": { + "overall": "全部", "info": "详情", "stats": "统计数据", "odds": "赔率", @@ -166,6 +167,12 @@ "lineups_coach": "主教练", "lineups_subs": "替补球员", "lineups_missing": "缺席球员" + }, + "overall_stats": { + "total_assists": "助攻", + "total_blocks": "盖帽", + "total_steals": "抢断", + "total_turnovers": "失误" } }, "selection": { From 91a25c5e6b216d3d0b48ae20d78b2ee4f8b4d7fa Mon Sep 17 00:00:00 2001 From: xianyi Date: Thu, 22 Jan 2026 18:27:09 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E7=AF=AE=E7=90=83live=E8=AF=A6=E6=83=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(tabs)/index.tsx | 4 +- app/live-detail/[id].tsx | 105 +++++++++++++++++++-- components/live-detail/live-match-tabs.tsx | 52 +++++++--- types/api.ts | 71 +++++++++++++- 4 files changed, 205 insertions(+), 27 deletions(-) diff --git a/app/(tabs)/index.tsx b/app/(tabs)/index.tsx index 0c7cf9d..b6d010b 100644 --- a/app/(tabs)/index.tsx +++ b/app/(tabs)/index.tsx @@ -388,8 +388,8 @@ export default function HomeScreen() { away: item.event_away_team, homeTeamName: item.event_home_team, awayTeamName: item.event_away_team, - homeTeamLogo: item.home_team_logo, - awayTeamLogo: item.away_team_logo, + homeTeamLogo: item.home_team_logo || "", + awayTeamLogo: item.away_team_logo || "", scoreText: item.event_halftime_result || "0 - 0", fav: false, sportId: sportId, diff --git a/app/live-detail/[id].tsx b/app/live-detail/[id].tsx index e661e01..e5d9ed9 100644 --- a/app/live-detail/[id].tsx +++ b/app/live-detail/[id].tsx @@ -5,14 +5,17 @@ import { LiveScoreHeader } from "@/components/live-detail/live-score-header"; import { OddsCard } from "@/components/live-detail/odds-card"; import { OtherInfoCard } from "@/components/live-detail/other-info-card"; import { StatsCard } from "@/components/live-detail/stats-card"; +import { BasketballOverallStats } from "@/components/match-detail/basketball/basketball-overall-stats"; +import { BasketballScoreTable } from "@/components/match-detail/basketball/basketball-score-table"; +import { BasketballStats } from "@/components/match-detail/basketball/basketball-stats"; import { ThemedText } from "@/components/themed-text"; import { ThemedView } from "@/components/themed-view"; import { Colors } from "@/constants/theme"; import { useTheme } from "@/context/ThemeContext"; import { fetchLiveScore } from "@/lib/api"; -import { LiveScoreMatch } from "@/types/api"; +import { LiveScoreMatch, MatchDetailData } from "@/types/api"; import { useLocalSearchParams } from "expo-router"; -import React, { useEffect, useState } from "react"; +import React, { useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { ActivityIndicator, @@ -36,11 +39,72 @@ export default function LiveDetailScreen() { const [loading, setLoading] = useState(true); const [match, setMatch] = useState(null); - const [activeTab, setActiveTab] = useState("detail"); // Default to detail to show all data + const [activeTab, setActiveTab] = useState("detail"); + + const convertToMatchDetailData = useMemo((): MatchDetailData | null => { + if (!match) return null; + + return { + events: [], + match: { + ID: 0, + CreatedAt: "", + UpdatedAt: "", + DeletedAt: null, + eventKey: match.event_key.toString(), + eventDate: match.event_date, + eventTime: match.event_time, + eventHomeTeam: match.event_home_team, + homeTeamKey: match.home_team_key.toString(), + homeTeamLogo: match.home_team_logo || "", + eventAwayTeam: match.event_away_team, + awayTeamKey: match.away_team_key.toString(), + awayTeamLogo: match.away_team_logo || "", + eventHalftimeResult: match.event_halftime_result || "", + eventFinalResult: match.event_final_result, + eventFtResult: "", + eventPenaltyResult: "", + eventStatus: match.event_status, + countryName: match.country_name, + leagueName: match.league_name, + leagueKey: match.league_key.toString(), + leagueRound: match.league_round, + leagueSeason: match.league_season, + eventLive: match.event_live, + eventStadium: "", + eventReferee: "", + eventCountryKey: match.event_country_key?.toString() || "", + leagueLogo: match.league_logo || "", + countryLogo: match.country_logo || "", + eventHomeFormation: "", + eventAwayFormation: "", + fkStageKey: "", + stageName: "", + leagueGroup: "", + sportId: parseInt(sport_id || "1"), + eventQuarter: match.event_quarter || "", + eventSet: "", + eventType: "", + eventToss: "", + eventManOfMatch: "", + scores: match.scores, + stats: match.statistics, + players: match.player_statistics ? { + home_team: (match.player_statistics.home_team || []).map(p => ({ + ...p, + player_oncourt: p.player_oncourt || undefined, + })), + away_team: (match.player_statistics.away_team || []).map(p => ({ + ...p, + player_oncourt: p.player_oncourt || undefined, + })), + } : undefined, + }, + }; + }, [match, sport_id]); useEffect(() => { loadLiveDetail(); - // 设置每 15 秒更新一次直播比分 const timer = setInterval(() => { refreshLiveDetail(); }, 15000); @@ -60,14 +124,12 @@ export default function LiveDetailScreen() { const refreshLiveDetail = async () => { try { - // Fetch live scores for the league const sportId = parseInt(sport_id || "1"); const leagueId = parseInt(league_id || "0"); const liveData = await fetchLiveScore(sportId, leagueId); if (liveData && Array.isArray(liveData)) { - // Find the specific match const found = liveData.find((m) => m.event_key.toString() === id); console.log("found", JSON.stringify(found, null, 2)); if (found) { @@ -99,7 +161,37 @@ export default function LiveDetailScreen() { } const renderTabContent = () => { + if (!match) return null; + const numericSportId = parseInt(sport_id || "1"); + + if (numericSportId === 2 && convertToMatchDetailData) { + switch (activeTab) { + case "stats": + return ; + case "overall": + return ; + case "odds": + return ( + + ); + case "detail": + return ( + <> + + + ); + default: + return ( + + + {t("detail.empty_stats")} + + + ); + } + } + switch (activeTab) { case "stats": return ; @@ -140,6 +232,7 @@ export default function LiveDetailScreen() { activeTab={activeTab} onTabChange={setActiveTab} isDark={isDark} + sportId={parseInt(sport_id || "1")} /> {renderTabContent()} diff --git a/components/live-detail/live-match-tabs.tsx b/components/live-detail/live-match-tabs.tsx index e3eccf5..c4e9359 100644 --- a/components/live-detail/live-match-tabs.tsx +++ b/components/live-detail/live-match-tabs.tsx @@ -8,31 +8,53 @@ interface LiveMatchTabsProps { activeTab: string; onTabChange: (tab: string) => void; isDark: boolean; + sportId?: number; } export function LiveMatchTabs({ activeTab, onTabChange, isDark, + sportId = 1, }: LiveMatchTabsProps) { const { t } = useTranslation(); const containerBg = isDark ? "#121212" : "#F5F5F5"; - const tabs = [ - { - id: "detail", - label: t("detail.tabs.info"), - icon: "document-text-outline", - }, - { id: "stats", label: t("detail.tabs.stats"), icon: "stats-chart-outline" }, - { id: "odds", label: t("detail.tabs.odds"), icon: "cash-outline" }, - { id: "lineup", label: t("detail.tabs.lineup"), icon: "shirt-outline" }, - { - id: "analysis", - label: t("detail.tabs.analysis"), - icon: "pie-chart-outline", - }, - ]; + const getTabs = () => { + if (sportId === 2) { + return [ + { + id: "detail", + label: t("detail.tabs.info"), + icon: "document-text-outline", + }, + { id: "stats", label: t("detail.tabs.stats"), icon: "stats-chart-outline" }, + { + id: "overall", + label: t("detail.tabs.overall"), + icon: "bar-chart-outline", + }, + { id: "odds", label: t("detail.tabs.odds"), icon: "cash-outline" }, + ]; + } + return [ + { + id: "detail", + label: t("detail.tabs.info"), + icon: "document-text-outline", + }, + { id: "stats", label: t("detail.tabs.stats"), icon: "stats-chart-outline" }, + { id: "odds", label: t("detail.tabs.odds"), icon: "cash-outline" }, + { id: "lineup", label: t("detail.tabs.lineup"), icon: "shirt-outline" }, + { + id: "analysis", + label: t("detail.tabs.analysis"), + icon: "pie-chart-outline", + }, + ]; + }; + + const tabs = getTabs(); return ( diff --git a/types/api.ts b/types/api.ts index c31bfe2..f8a84d3 100644 --- a/types/api.ts +++ b/types/api.ts @@ -52,10 +52,10 @@ export interface LiveScoreMatch { event_time: string; event_home_team: string; home_team_key: number; - home_team_logo: string; + home_team_logo: string | null; event_away_team: string; away_team_key: number; - away_team_logo: string; + away_team_logo: string | null; event_final_result: string; event_halftime_result: string; event_status: string; @@ -68,6 +68,7 @@ export interface LiveScoreMatch { country_name: string; country_logo: string; event_country_key: number; + event_quarter?: string; // Tennis specific event_first_player?: string; event_first_player_logo?: string; @@ -75,7 +76,12 @@ export interface LiveScoreMatch { event_second_player_logo?: string; event_serve?: string; pointbypoint?: any[]; - scores?: any[]; // LiveScoreMatch uses array for scores, Match uses string + 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 }>; + }; goalscorers?: { time: string; home_scorer: string; @@ -112,7 +118,64 @@ export interface LiveScoreMatch { info_time: string; score: string; }[]; - lineups?: unknown; + lineups?: { + home_team?: { + starting_lineups?: Array<{ player: string; player_id: number }>; + substitutes?: Array<{ player: string; player_id: number }>; + }; + away_team?: { + starting_lineups?: Array<{ player: string; player_id: number }>; + substitutes?: Array<{ player: string; player_id: number }>; + }; + }; + player_statistics?: { + home_team?: Array<{ + player: string; + player_id: number; + player_position?: string; + player_minutes?: string; + player_points?: string; + player_total_rebounds?: string; + player_offence_rebounds?: string; + player_defense_rebounds?: string; + player_assists?: string; + player_steals?: string; + player_blocks?: string; + player_turnovers?: string; + player_personal_fouls?: string; + player_plus_minus?: string; + player_field_goals_made?: string; + player_field_goals_attempts?: string; + player_threepoint_goals_made?: string; + player_threepoint_goals_attempts?: string; + player_freethrows_goals_made?: string; + player_freethrows_goals_attempts?: string; + player_oncourt?: string | null; + }>; + away_team?: Array<{ + player: string; + player_id: number; + player_position?: string; + player_minutes?: string; + player_points?: string; + player_total_rebounds?: string; + player_offence_rebounds?: string; + player_defense_rebounds?: string; + player_assists?: string; + player_steals?: string; + player_blocks?: string; + player_turnovers?: string; + player_personal_fouls?: string; + player_plus_minus?: string; + player_field_goals_made?: string; + player_field_goals_attempts?: string; + player_threepoint_goals_made?: string; + player_threepoint_goals_attempts?: string; + player_freethrows_goals_made?: string; + player_freethrows_goals_attempts?: string; + player_oncourt?: string | null; + }>; + }; statistics?: { type: string; home: string;