添加网球详情
This commit is contained in:
332
components/live-detail/tennis-power-graph.tsx
Normal file
332
components/live-detail/tennis-power-graph.tsx
Normal file
@@ -0,0 +1,332 @@
|
||||
import { ThemedText } from "@/components/themed-text";
|
||||
import { ThemedView } from "@/components/themed-view";
|
||||
import { LiveScoreMatch } from "@/types/api";
|
||||
import React, { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Image, ScrollView, StyleSheet, View } from "react-native";
|
||||
|
||||
interface TennisPowerGraphProps {
|
||||
match: LiveScoreMatch;
|
||||
isDark: boolean;
|
||||
}
|
||||
|
||||
export function TennisPowerGraph({ match, isDark }: TennisPowerGraphProps) {
|
||||
const { t } = useTranslation();
|
||||
const pointData = match.pointbypoint;
|
||||
|
||||
if (!pointData || pointData.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Grouped Data by Game
|
||||
// Structure: { gameIndex: number, score: string, set: string, points: {winner: 1|2}[] }
|
||||
const gameData = useMemo(() => {
|
||||
return pointData.map((game) => {
|
||||
const points = game.points || [];
|
||||
const bars: { winner: 1 | 2 }[] = [];
|
||||
let prevP1Score = 0;
|
||||
let prevP2Score = 0;
|
||||
|
||||
const parseScore = (s: string) => {
|
||||
if (s === "A") return 45;
|
||||
return parseInt(s) || 0;
|
||||
};
|
||||
|
||||
points.forEach((point: { score: string }) => {
|
||||
const parts = point.score.split("-").map((s) => s.trim());
|
||||
if (parts.length !== 2) return;
|
||||
const p1 = parseScore(parts[0]);
|
||||
const p2 = parseScore(parts[1]);
|
||||
|
||||
let winner: 1 | 2 | null = null;
|
||||
if (p1 > prevP1Score && p2 === prevP2Score) winner = 1;
|
||||
else if (p2 > prevP2Score && p1 === prevP1Score) winner = 2;
|
||||
else if (p1 === 45 && prevP1Score === 40) winner = 1;
|
||||
else if (p2 === 45 && prevP2Score === 40) winner = 2;
|
||||
else if (p1 === 40 && prevP1Score === 45) winner = 2;
|
||||
else if (p2 === 40 && prevP2Score === 45) winner = 1;
|
||||
else if (
|
||||
points.length === 1 &&
|
||||
prevP1Score === 0 &&
|
||||
prevP2Score === 0
|
||||
) {
|
||||
if (p1 > 0) winner = 1;
|
||||
if (p2 > 0) winner = 2;
|
||||
} else if (p1 > prevP1Score || (p2 < prevP2Score && prevP2Score === 45))
|
||||
winner = 1;
|
||||
else if (p2 > prevP2Score || (p1 < prevP1Score && prevP1Score === 45))
|
||||
winner = 2;
|
||||
|
||||
if (winner) {
|
||||
bars.push({ winner });
|
||||
}
|
||||
prevP1Score = p1;
|
||||
prevP2Score = p2;
|
||||
});
|
||||
|
||||
// Find if this game ended a set? Or just carry current set score/game score.
|
||||
// `game.score` is e.g. "1 - 0" (Set Score or Game Score within set?)
|
||||
// Usually `pointbypoint` item has `score` field which is the score in Games for that Set.
|
||||
|
||||
return {
|
||||
gameIndex: parseInt(game.number_game),
|
||||
setNumber: game.set_number, // "Set 1"
|
||||
score: game.score, // "6 - 4" or "1 - 0"
|
||||
serveWinner: game.serve_winner, // "First Player" or "Second Player"
|
||||
bars,
|
||||
};
|
||||
});
|
||||
}, [pointData]);
|
||||
|
||||
if (gameData.length === 0) return null;
|
||||
|
||||
// Render logic
|
||||
// Left side: Avatars
|
||||
// Right side: ScrollView of Game Blocks
|
||||
|
||||
// Avatars
|
||||
const p1Logo = match.event_first_player_logo;
|
||||
const p2Logo = match.event_second_player_logo;
|
||||
|
||||
return (
|
||||
<ThemedView
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
backgroundColor: isDark ? "#1E1E20" : "#FFF",
|
||||
borderRadius: 12,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
elevation: 3,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<ThemedText style={[styles.title, { color: isDark ? "#FFF" : "#000" }]}>
|
||||
{t("detail.power_graph")}
|
||||
</ThemedText>
|
||||
|
||||
<View style={styles.contentRow}>
|
||||
{/* Sidebar - Avatars */}
|
||||
<View style={styles.sidebar}>
|
||||
<View style={{ flex: 1 }} /> {/* Spacer top */}
|
||||
<View style={styles.avatarContainer}>
|
||||
{p1Logo ? (
|
||||
<Image source={{ uri: p1Logo }} style={styles.avatar} />
|
||||
) : (
|
||||
<View style={[styles.avatar, { backgroundColor: "#ccc" }]} />
|
||||
)}
|
||||
</View>
|
||||
<View style={{ height: 10 }} />{" "}
|
||||
{/* Gap corresponding to center line if needed, but styling is easier with even spacing */}
|
||||
<View style={styles.avatarContainer}>
|
||||
{p2Logo ? (
|
||||
<Image source={{ uri: p2Logo }} style={styles.avatar} />
|
||||
) : (
|
||||
<View style={[styles.avatar, { backgroundColor: "#ccc" }]} />
|
||||
)}
|
||||
</View>
|
||||
<View style={{ flex: 1 }} /> {/* Spacer bottom */}
|
||||
</View>
|
||||
|
||||
{/* Scrollable Graph */}
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
style={styles.graphScroll}
|
||||
>
|
||||
{gameData.map((game, i) => {
|
||||
// Updated to match screenshot: Split background (Blue top, Yellow bottom)
|
||||
const topBg = isDark ? "rgba(33, 150, 243, 0.15)" : "#E3F2FD";
|
||||
const botBg = isDark ? "rgba(255, 193, 7, 0.15)" : "#FFF8E1";
|
||||
|
||||
return (
|
||||
<View key={i} style={styles.gameBlock}>
|
||||
{/* Top Score Label if needed */}
|
||||
<View style={styles.gameTopLabel} />
|
||||
|
||||
{/* Bars Area */}
|
||||
<View style={styles.barsArea}>
|
||||
{/* Backgrounds */}
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: "50%",
|
||||
backgroundColor: topBg,
|
||||
}}
|
||||
/>
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: "50%",
|
||||
backgroundColor: botBg,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Center Line */}
|
||||
<View
|
||||
style={[
|
||||
styles.centerLine,
|
||||
{ backgroundColor: "#FFF", height: 2, zIndex: 1 },
|
||||
]}
|
||||
/>
|
||||
|
||||
{game.bars.map((bar, bi) => (
|
||||
<View key={bi} style={[styles.barColumn, { zIndex: 2 }]}>
|
||||
{/* P1 Bar (Top) */}
|
||||
<View
|
||||
style={[
|
||||
styles.barSegment,
|
||||
{
|
||||
height: 24,
|
||||
justifyContent: "flex-end",
|
||||
},
|
||||
]}
|
||||
>
|
||||
{bar.winner === 1 && (
|
||||
<View
|
||||
style={[
|
||||
styles.activeBar,
|
||||
{
|
||||
backgroundColor: "#2196F3",
|
||||
height:
|
||||
bi === game.bars.length - 1 ? "100%" : "60%",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* P2 Bar (Bottom) */}
|
||||
<View
|
||||
style={[
|
||||
styles.barSegment,
|
||||
{
|
||||
height: 24,
|
||||
justifyContent: "flex-start",
|
||||
},
|
||||
]}
|
||||
>
|
||||
{bar.winner === 2 && (
|
||||
<View
|
||||
style={[
|
||||
styles.activeBar,
|
||||
{
|
||||
backgroundColor: "#FFC107",
|
||||
height:
|
||||
bi === game.bars.length - 1 ? "100%" : "60%",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* Bottom Game Number */}
|
||||
<View style={styles.gameBottomLabel}>
|
||||
<ThemedText style={styles.gameNumberText}>
|
||||
{game.gameIndex}
|
||||
</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</ThemedView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
margin: 16,
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
},
|
||||
title: {
|
||||
fontSize: 16,
|
||||
fontWeight: "bold",
|
||||
marginBottom: 20,
|
||||
},
|
||||
contentRow: {
|
||||
flexDirection: "row",
|
||||
height: 120, // Total height for graph area
|
||||
},
|
||||
sidebar: {
|
||||
width: 40,
|
||||
alignItems: "center",
|
||||
justifyContent: "center", // Center avatars vertically relative to bars
|
||||
paddingBottom: 20, // Adjust for game number row
|
||||
},
|
||||
avatarContainer: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 16,
|
||||
overflow: "hidden",
|
||||
borderWidth: 1,
|
||||
borderColor: "#333",
|
||||
},
|
||||
avatar: {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
},
|
||||
graphScroll: {
|
||||
flex: 1,
|
||||
marginLeft: 8,
|
||||
},
|
||||
gameBlock: {
|
||||
paddingHorizontal: 4,
|
||||
marginRight: 2,
|
||||
minWidth: 40,
|
||||
borderRadius: 4,
|
||||
},
|
||||
gameTopLabel: {
|
||||
height: 20,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
barsArea: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
height: 60, // Central bar area
|
||||
position: "relative",
|
||||
justifyContent: "center",
|
||||
},
|
||||
centerLine: {
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: 1,
|
||||
top: 30, // Middle of 60
|
||||
},
|
||||
barColumn: {
|
||||
width: 6,
|
||||
marginHorizontal: 1,
|
||||
height: "100%",
|
||||
justifyContent: "center",
|
||||
},
|
||||
barSegment: {
|
||||
width: "100%",
|
||||
},
|
||||
activeBar: {
|
||||
width: "100%",
|
||||
borderRadius: 2,
|
||||
},
|
||||
gameBottomLabel: {
|
||||
height: 20,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
gameNumberText: {
|
||||
fontSize: 10,
|
||||
opacity: 0.6,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user