Files
2026-01-23 16:57:06 +08:00

394 lines
11 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { EmptyPlaceholder } from "@/components/empty-placeholder";
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<string, string> = {
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 = () => (
<View style={[styles.headerCell, { height: HEADER_HEIGHT, borderBottomColor: borderColor }]}>
<ThemedText style={[styles.headerText, { color: secondaryText }]}>
Player
</ThemedText>
<IconSymbol
name="information-circle-outline"
size={14}
color={secondaryText}
style={{ marginLeft: 4 }}
/>
</View>
);
// 2. 左侧列:球员信息行
const renderLeftPlayerRow = (player: any) => {
const playerName = player.player || "";
const playerPosition = player.player_position || "";
const gradient = getLogoGradient(playerName);
const initials = getInitials(playerName);
return (
<View style={[styles.playerRow, { height: ROW_HEIGHT, borderBottomColor: borderColor }]}>
<View style={styles.avatarContainer}>
<LinearGradient
colors={[gradient.color1, gradient.color2]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
style={styles.avatarGradient}
>
<ThemedText style={styles.avatarText}>{initials}</ThemedText>
</LinearGradient>
</View>
<View style={styles.playerInfo}>
<ThemedText
style={[styles.playerName, { color: textColor }]}
numberOfLines={1}
>
{playerName}
</ThemedText>
{playerPosition && (
<ThemedText style={[styles.playerNumPos, { color: secondaryText }]}>
{playerPosition}
</ThemedText>
)}
</View>
</View>
);
};
// 3. 右上角:数据表头
const renderRightHeader = () => (
<View style={[styles.dataHeaderRow, { height: HEADER_HEIGHT, borderBottomColor: borderColor }]}>
{columns.map((col) => (
<View key={col.key} style={{ width: col.width, alignItems: "center" }}>
<ThemedText style={[styles.headerText, { color: secondaryText }]}>
{col.label}
</ThemedText>
</View>
))}
</View>
);
// 4. 右侧列:数据行
const renderRightDataRow = (player: any) => (
<View style={[styles.dataRow, { height: ROW_HEIGHT, borderBottomColor: borderColor }]}>
{columns.map((col) => (
<View key={col.key} style={{ width: col.width, alignItems: "center" }}>
<ThemedText style={[styles.statText, { color: textColor }]}>
{getPlayerStat(player, col.key)}
</ThemedText>
</View>
))}
</View>
);
// 队名/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");
return (
<View style={[styles.container, { backgroundColor: bgColor }]}>
{/* 顶部控制栏 */}
<View style={styles.controlsHeader}>
<View>
<ThemedText style={[styles.controlTitle, { color: textColor }]}>
</ThemedText>
<ThemedText style={[styles.controlSubtitle, { color: secondaryText }]}>
</ThemedText>
</View>
<Switch
value={isDetailed}
onValueChange={setIsDetailed}
trackColor={{ false: isDark ? "#3e3e3e" : "#E5E5E5", true: "#FF9500" }}
thumbColor={"#fff"}
/>
</View>
{/* 队伍切换 Tab */}
<View style={[styles.teamTabsContainer, { backgroundColor: isDark ? "#2C2C2E" : "#F5F5F5" }]}>
<TouchableOpacity
style={[
styles.teamTab,
activeTab === "home" && { backgroundColor: isDark ? "#1C1C1E" : "#FFF" },
{ borderTopLeftRadius: 8, borderBottomLeftRadius: 8 },
]}
onPress={() => setActiveTab("home")}
>
{hasHomeLogo ? (
<Image source={{ uri: homeTeamLogo }} style={styles.teamTabLogo} contentFit="contain" />
) : (
<EmptyPlaceholder type="team" size={24} />
)}
</TouchableOpacity>
<TouchableOpacity
style={[
styles.teamTab,
activeTab === "away" && { backgroundColor: isDark ? "#1C1C1E" : "#FFF" },
{ borderTopRightRadius: 8, borderBottomRightRadius: 8 },
]}
onPress={() => setActiveTab("away")}
>
{hasAwayLogo ? (
<Image source={{ uri: awayTeamLogo }} style={styles.teamTabLogo} contentFit="contain" />
) : (
<EmptyPlaceholder type="team" size={24} />
)}
</TouchableOpacity>
</View>
{/* 核心表格区域Vertical ScrollView 包裹整个表格 */}
<ScrollView
style={[styles.tableContainer, { borderColor }]}
showsVerticalScrollIndicator={false}
>
<View style={{ flexDirection: "row" }}>
{/* 左侧区域:固定不横向滚动 */}
<View style={{ width: LEFT_COLUMN_WIDTH, borderRightWidth: StyleSheet.hairlineWidth, borderColor }}>
{/* Header */}
{renderLeftHeader()}
{/* Players List */}
<View>
{currentPlayers?.map((player, index) => (
<View key={`left-${player.player_id || index}`}>
{renderLeftPlayerRow(player)}
</View>
))}
</View>
</View>
{/* 右侧区域:整体横向滚动 */}
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
<View>
{/* Header */}
{renderRightHeader()}
{/* Data List */}
<View>
{currentPlayers?.map((player, index) => (
<View key={`right-${player.player_id || index}`}>
{renderRightDataRow(player)}
</View>
))}
</View>
</View>
</ScrollView>
</View>
{/* Empty State */}
{(!currentPlayers || currentPlayers.length === 0) && (
<View style={styles.emptyState}>
<ThemedText style={{ color: secondaryText }}></ThemedText>
</View>
)}
</ScrollView>
</View>
);
}
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",
},
});