篮球整体统计数据

This commit is contained in:
xianyi
2026-01-22 18:05:04 +08:00
parent 3e9cc03217
commit 6dc1170d1d
11 changed files with 347 additions and 4 deletions

View File

@@ -69,6 +69,7 @@ export default function LiveDetailScreen() {
if (liveData && Array.isArray(liveData)) { if (liveData && Array.isArray(liveData)) {
// Find the specific match // Find the specific match
const found = liveData.find((m) => m.event_key.toString() === id); const found = liveData.find((m) => m.event_key.toString() === id);
console.log("found", JSON.stringify(found, null, 2));
if (found) { if (found) {
setMatch(found); setMatch(found);
} }

View File

@@ -1,4 +1,5 @@
import { OddsCard } from "@/components/live-detail/odds-card"; 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 { BasketballScoreTable } from "@/components/match-detail/basketball/basketball-score-table";
import { BasketballStats } from "@/components/match-detail/basketball/basketball-stats"; import { BasketballStats } from "@/components/match-detail/basketball/basketball-stats";
import { CardsCard } from "@/components/match-detail/football/cards-card"; import { CardsCard } from "@/components/match-detail/football/cards-card";
@@ -60,7 +61,7 @@ export default function MatchDetailScreen() {
validTabs = ["info", "stats", "odds", "h2h", "chat"]; validTabs = ["info", "stats", "odds", "h2h", "chat"];
} else if (sportId === 2) { } else if (sportId === 2) {
// 篮球 // 篮球
validTabs = ["info", "stats", "h2h", "chat"]; validTabs = ["info", "stats", "overall", "h2h", "chat"];
} else if (sportId === 3) { } else if (sportId === 3) {
// 网球 // 网球
validTabs = ["info", "chat"]; validTabs = ["info", "chat"];
@@ -82,8 +83,8 @@ export default function MatchDetailScreen() {
setError(null); setError(null);
const result = await fetchMatchDetail(id as string); const result = await fetchMatchDetail(id as string);
setData(result); setData(result);
console.log("首发阵容", result.match.players?.away_team); // console.log("首发阵容", result.match.players?.away_team);
console.log("红黄牌", result.events); // console.log("红黄牌", result.events);
@@ -177,6 +178,8 @@ export default function MatchDetailScreen() {
</View> </View>
); );
} }
case "overall":
return <BasketballOverallStats data={data} isDark={isDark} />;
case "odds": case "odds":
// 将 MatchDetailData.match 转换为 LiveScoreMatch 格式 // 将 MatchDetailData.match 转换为 LiveScoreMatch 格式
const matchForOdds = { const matchForOdds = {

View File

@@ -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 (
<View style={styles.empty}>
<ThemedText style={[styles.emptyText, { color: isDark ? "#A1A1AA" : "#6B7280" }]}>
{t("detail.empty_stats")}
</ThemedText>
</View>
);
}
return (
<View
style={[
styles.wrap,
{ backgroundColor: isDark ? "#151517" : "#F3F4F6" },
]}
>
{/* Card */}
<View
style={[
styles.card,
{ backgroundColor: isDark ? "#1C1C1E" : "#FFFFFF" },
]}
>
{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 (
<View
key={`${item.type}-${index}`}
style={[
styles.row,
index !== stats.length - 1 && {
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.06)",
},
]}
>
<View style={styles.rowHeader}>
<ValuePill
text={`${home}`}
variant="home"
isDark={isDark}
/>
<ThemedText
style={[
styles.title,
{ color: isDark ? "#EAEAF0" : "#111827" },
]}
numberOfLines={1}
>
{getStatLabel(item.type)}
</ThemedText>
<ValuePill
text={`${away}`}
variant={awayLeading ? "awayLeading" : "away"}
isDark={isDark}
/>
</View>
<CompareBar
home={home}
away={away}
maxV={maxV}
isDark={isDark}
homeLeading={homeLeading}
awayLeading={awayLeading}
/>
</View>
);
})}
</View>
</View>
);
}
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 (
<LinearGradient colors={[...colors] as [string, string]} {...common}>
<ThemedText style={styles.pillText}>{text}</ThemedText>
</LinearGradient>
);
}
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 (
<View style={[styles.barWrap, { width: SIDE_MAX * 2 + GAP }]}>
<View style={[styles.barTrack, { backgroundColor: track }]} />
<View style={[styles.barSide, { width: SIDE_MAX, justifyContent: "flex-end" }]}>
<View style={[styles.barFill, { width: homeW, backgroundColor: homeColor }]} />
</View>
<View style={{ width: GAP }} />
<View style={[styles.barSide, { width: SIDE_MAX, justifyContent: "flex-start" }]}>
<View style={[styles.barFill, { width: awayW, backgroundColor: awayColor }]} />
</View>
</View>
);
}
/* ----------------- 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,
},
});

View File

@@ -63,7 +63,7 @@ export function MatchTabs({
}, },
]; ];
} else if (sportId === 2) { } else if (sportId === 2) {
// 篮球: 详情、统计数据、交锋往绩、聊天 // 篮球: 详情、统计数据、整体统计、交锋往绩、聊天
return [ return [
{ {
id: "info", id: "info",
@@ -75,6 +75,11 @@ export function MatchTabs({
label: t("detail.tabs.stats"), label: t("detail.tabs.stats"),
icon: "stats-chart-outline", icon: "stats-chart-outline",
}, },
{
id: "overall",
label: t("detail.tabs.overall"),
icon: "bar-chart-outline",
},
{ {
id: "h2h", id: "h2h",
label: t("detail.tabs.h2h"), label: t("detail.tabs.h2h"),

View File

@@ -92,6 +92,7 @@
"fetch_failed": "Failed to fetch details", "fetch_failed": "Failed to fetch details",
"not_found": "Match data not found", "not_found": "Match data not found",
"tabs": { "tabs": {
"overall": "Overall",
"info": "Details", "info": "Details",
"stats": "Statistics", "stats": "Statistics",
"odds": "Odds", "odds": "Odds",
@@ -166,6 +167,12 @@
"lineups_coach": "Coach", "lineups_coach": "Coach",
"lineups_subs": "Substitutes", "lineups_subs": "Substitutes",
"lineups_missing": "Missing players" "lineups_missing": "Missing players"
},
"overall_stats": {
"total_assists": "Assists",
"total_blocks": "Blocks",
"total_steals": "Steals",
"total_turnovers": "Turnovers"
} }
}, },
"selection": { "selection": {

View File

@@ -88,6 +88,7 @@
"fetch_failed": "विवरण प्राप्त करने में विफल", "fetch_failed": "विवरण प्राप्त करने में विफल",
"not_found": "मैच डेटा नहीं मिला", "not_found": "मैच डेटा नहीं मिला",
"tabs": { "tabs": {
"overall": "कुल",
"info": "विवरण", "info": "विवरण",
"stats": "आँकड़े", "stats": "आँकड़े",
"odds": "ऑड्स", "odds": "ऑड्स",
@@ -162,6 +163,12 @@
"lineups_coach": "कोच", "lineups_coach": "कोच",
"lineups_subs": "सब्स्टीट्यूट", "lineups_subs": "सब्स्टीट्यूट",
"lineups_missing": "अनुपस्थित खिलाड़ी" "lineups_missing": "अनुपस्थित खिलाड़ी"
},
"overall_stats": {
"total_assists": "असिस्ट",
"total_blocks": "ब्लॉक",
"total_steals": "स्टील",
"total_turnovers": "टर्नओवर"
} }
}, },
"selection": { "selection": {

View File

@@ -88,6 +88,7 @@
"fetch_failed": "Gagal mengambil data", "fetch_failed": "Gagal mengambil data",
"not_found": "Data pertandingan tidak ditemukan", "not_found": "Data pertandingan tidak ditemukan",
"tabs": { "tabs": {
"overall": "Keseluruhan",
"info": "Detail", "info": "Detail",
"stats": "Statistik", "stats": "Statistik",
"odds": "Odds", "odds": "Odds",
@@ -162,6 +163,12 @@
"lineups_coach": "Pelatih", "lineups_coach": "Pelatih",
"lineups_subs": "Cadangan", "lineups_subs": "Cadangan",
"lineups_missing": "Pemain Absen" "lineups_missing": "Pemain Absen"
},
"overall_stats": {
"total_assists": "Asisten",
"total_blocks": "Blok",
"total_steals": "Steal",
"total_turnovers": "Turnover"
} }
}, },
"selection": { "selection": {

View File

@@ -88,6 +88,7 @@
"fetch_failed": "Gagal mendapatkan maklumat", "fetch_failed": "Gagal mendapatkan maklumat",
"not_found": "Data perlawanan tidak ditemui", "not_found": "Data perlawanan tidak ditemui",
"tabs": { "tabs": {
"overall": "Keseluruhan",
"info": "Maklumat", "info": "Maklumat",
"stats": "Statistik", "stats": "Statistik",
"odds": "Odds", "odds": "Odds",
@@ -162,6 +163,12 @@
"lineups_coach": "Jurulatih", "lineups_coach": "Jurulatih",
"lineups_subs": "Pemain Simpanan", "lineups_subs": "Pemain Simpanan",
"lineups_missing": "Pemain Tidak Tersenarai" "lineups_missing": "Pemain Tidak Tersenarai"
},
"overall_stats": {
"total_assists": "Bantuan",
"total_blocks": "Sekatan",
"total_steals": "Curi",
"total_turnovers": "Pusingan"
} }
}, },
"selection": { "selection": {

View File

@@ -88,6 +88,7 @@
"fetch_failed": "ไม่สามารถดึงข้อมูลได้", "fetch_failed": "ไม่สามารถดึงข้อมูลได้",
"not_found": "ไม่พบข้อมูลการแข่งขัน", "not_found": "ไม่พบข้อมูลการแข่งขัน",
"tabs": { "tabs": {
"overall": "ทั้งหมด",
"info": "รายละเอียด", "info": "รายละเอียด",
"stats": "สถิติ", "stats": "สถิติ",
"odds": "อัตราต่อรอง", "odds": "อัตราต่อรอง",
@@ -162,6 +163,12 @@
"lineups_coach": "โค้ช", "lineups_coach": "โค้ช",
"lineups_subs": "ตัวสำรอง", "lineups_subs": "ตัวสำรอง",
"lineups_missing": "ผู้เล่นที่ขาดหาย" "lineups_missing": "ผู้เล่นที่ขาดหาย"
},
"overall_stats": {
"total_assists": "แอสซิสต์",
"total_blocks": "บล็อก",
"total_steals": "สตีล",
"total_turnovers": "เทิร์นโอเวอร์"
} }
}, },
"selection": { "selection": {

View File

@@ -88,6 +88,7 @@
"fetch_failed": "Không thể tải dữ liệu", "fetch_failed": "Không thể tải dữ liệu",
"not_found": "Không tìm thấy dữ liệu trận đấu", "not_found": "Không tìm thấy dữ liệu trận đấu",
"tabs": { "tabs": {
"overall": "Tổng",
"info": "Thông tin", "info": "Thông tin",
"stats": "Thống kê", "stats": "Thống kê",
"odds": "Tỷ lệ cược", "odds": "Tỷ lệ cược",
@@ -162,6 +163,12 @@
"lineups_coach": "Huấn luyện viên", "lineups_coach": "Huấn luyện viên",
"lineups_subs": "Dự bị", "lineups_subs": "Dự bị",
"lineups_missing": "Cầu thủ vắng mặt" "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": { "selection": {

View File

@@ -92,6 +92,7 @@
"fetch_failed": "获取详情失败", "fetch_failed": "获取详情失败",
"not_found": "未找到比赛数据", "not_found": "未找到比赛数据",
"tabs": { "tabs": {
"overall": "全部",
"info": "详情", "info": "详情",
"stats": "统计数据", "stats": "统计数据",
"odds": "赔率", "odds": "赔率",
@@ -166,6 +167,12 @@
"lineups_coach": "主教练", "lineups_coach": "主教练",
"lineups_subs": "替补球员", "lineups_subs": "替补球员",
"lineups_missing": "缺席球员" "lineups_missing": "缺席球员"
},
"overall_stats": {
"total_assists": "助攻",
"total_blocks": "盖帽",
"total_steals": "抢断",
"total_turnovers": "失误"
} }
}, },
"selection": { "selection": {