Files
physical-expo/components/match-detail/football/football-events.tsx
2026-01-14 14:32:59 +08:00

477 lines
15 KiB
TypeScript
Raw 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 { ThemedText } from "@/components/themed-text";
import { IconSymbol } from "@/components/ui/icon-symbol";
import { CardEvent, GoalEvent, MatchDetailData, Player, SubstituteEvent } from "@/types/api";
import React from "react";
import { useTranslation } from "react-i18next";
import { StyleSheet, View } from "react-native";
interface FootballEventsProps {
data: MatchDetailData;
isDark: boolean;
}
export function FootballEvents({ data, isDark }: FootballEventsProps) {
const { t } = useTranslation();
const { match } = data;
const bgColor = isDark ? "#1C1C1E" : "#FFF";
const borderColor = isDark ? "#2C2C2E" : "#EEE";
// events 可能在 data.events 或 match.events 中
const events = (data as any).events || (match as any).events;
// 调试:打印 events 数据
if (__DEV__) {
console.log("FootballEvents - events data:", events);
}
if (!events || typeof events !== "object") {
if (__DEV__) {
console.log("FootballEvents - No events data found");
}
return null;
}
// 处理数组格式的 events如果 events 是数组)
let eventsObj = events;
if (Array.isArray(events) && events.length > 0) {
// 如果 events 是数组,取第一个元素
eventsObj = events[0];
}
const goalscorers = eventsObj?.goalscorers || [];
const cards = eventsObj?.cards || [];
const substitutes = eventsObj?.substitutes || [];
const players = (match as any).players || null;
if (__DEV__) {
console.log("FootballEvents - goalscorers:", goalscorers.length, "cards:", cards.length, "substitutes:", substitutes.length);
}
const hasEvents = goalscorers.length > 0 || cards.length > 0 || substitutes.length > 0;
if (!hasEvents) {
if (__DEV__) {
console.log("FootballEvents - No events to display");
}
return null;
}
const renderGoalEvent = (goal: GoalEvent, index: number) => {
const isHome = !!goal.home_scorer;
const scorerRaw = goal.home_scorer || goal.away_scorer || "";
const isPenalty = scorerRaw.includes("(pen.)");
const playerName = scorerRaw.replace("(pen.)", "").trim();
return (
<View key={index} style={[styles.eventRow, { borderBottomColor: borderColor }]}>
<View style={styles.eventTime}>
<ThemedText style={styles.timeText}>
{goal.time ? `${goal.time}'` : ""}
</ThemedText>
</View>
<View style={[styles.eventContent, isHome ? styles.homeEvent : styles.awayEvent]}>
<IconSymbol name="football" size={16} color={isDark ? "#4CAF50" : "#2E7D32"} />
<View style={styles.goalContent}>
{playerName && (
<ThemedText style={styles.eventText} numberOfLines={1}>
{playerName}
</ThemedText>
)}
{isPenalty && (
<View style={styles.penaltyBadge}>
<ThemedText style={styles.penaltyText}>P</ThemedText>
</View>
)}
</View>
{goal.score && (
<ThemedText style={styles.scoreText}>{goal.score}</ThemedText>
)}
</View>
</View>
);
};
const renderCardEvent = (card: CardEvent, index: number) => {
const isHome = !!card.home_fault;
const cardType = card.card?.toLowerCase() || "";
const isRed = cardType.includes("red");
const isYellow = cardType.includes("yellow");
const cardColor = isRed ? "#F44336" : isYellow ? "#FFC107" : "#9E9E9E";
const playerName = card.home_fault || card.away_fault || "";
return (
<View key={`card-${index}`} style={[styles.eventRow, { borderBottomColor: borderColor }]}>
<View style={styles.eventTime}>
<ThemedText style={styles.timeText}>
{card.time ? `${card.time}'` : ""}
</ThemedText>
</View>
<View style={[styles.eventContent, isHome ? styles.homeEvent : styles.awayEvent]}>
<View style={[styles.cardIcon, { backgroundColor: cardColor }]} />
<ThemedText style={styles.eventText} numberOfLines={1}>
{playerName}
</ThemedText>
{card.card && (
<View style={[styles.cardBadge, { backgroundColor: cardColor + "20" }]}>
<ThemedText style={[styles.cardTypeText, { color: cardColor }]}>
{isRed ? t("detail.events.red_card") : isYellow ? t("detail.events.yellow_card") : card.card}
</ThemedText>
</View>
)}
</View>
</View>
);
};
const renderSubstituteEvent = (sub: SubstituteEvent, index: number) => {
// 辅助函数:从字段中提取字符串值(可能是字符串、数组或对象)
const extractPlayerName = (value: any): string => {
if (!value) return "";
// 如果是字符串,直接返回
if (typeof value === "string") return value;
// 如果是数组,取第一个元素
if (Array.isArray(value)) {
if (value.length === 0) return "";
const firstItem = value[0];
// 如果数组元素是对象,提取 in 或 out
if (typeof firstItem === "object" && firstItem !== null) {
return String(firstItem.in || firstItem.out || firstItem.player || "");
}
return String(firstItem);
}
// 如果是对象,提取 in 或 out 字段
if (typeof value === "object" && value !== null) {
return String(value.in || value.out || value.player || "");
}
return String(value);
};
// 提取换入和换出球员名称
let playerIn = "";
let playerOut = "";
let isHome = false;
// 优先处理 home_scorer可能是对象 {in, out} 或数组)
const homeScorerData = (sub as any).home_scorer;
if (homeScorerData) {
isHome = true;
if (typeof homeScorerData === "object" && !Array.isArray(homeScorerData) && "in" in homeScorerData) {
// 对象格式:{in: "球员名", out: "球员名", in_id, out_id}
playerIn = String(homeScorerData.in || "");
playerOut = String(homeScorerData.out || "");
} else if (Array.isArray(homeScorerData) && homeScorerData.length > 0) {
// 数组格式:取第一个元素
const firstItem = homeScorerData[0];
if (typeof firstItem === "object" && firstItem !== null && "in" in firstItem) {
playerIn = String(firstItem.in || "");
playerOut = String(firstItem.out || "");
} else {
playerIn = extractPlayerName(firstItem);
}
} else {
playerIn = extractPlayerName(homeScorerData);
}
}
// 处理 away_scorer可能是对象 {in, out} 或数组)
const awayScorerData = (sub as any).away_scorer;
if (awayScorerData && !playerIn) {
isHome = false;
if (typeof awayScorerData === "object" && !Array.isArray(awayScorerData) && "in" in awayScorerData) {
// 对象格式:{in: "球员名", out: "球员名", in_id, out_id}
playerIn = String(awayScorerData.in || "");
playerOut = String(awayScorerData.out || "");
} else if (Array.isArray(awayScorerData) && awayScorerData.length > 0) {
// 数组格式:取第一个元素
const firstItem = awayScorerData[0];
if (typeof firstItem === "object" && firstItem !== null && "in" in firstItem) {
playerIn = String(firstItem.in || "");
playerOut = String(firstItem.out || "");
} else {
playerIn = extractPlayerName(firstItem);
}
} else {
playerIn = extractPlayerName(awayScorerData);
}
}
// 如果还没有提取到换出球员,尝试从其他字段获取
if (!playerOut) {
playerOut = extractPlayerName(sub.home_scorer_out || sub.away_scorer_out || sub.home_assist || sub.away_assist);
}
return (
<View key={`sub-${index}`} style={[styles.eventRow, { borderBottomColor: borderColor }]}>
<View style={styles.eventTime}>
<ThemedText style={styles.timeText}>
{sub.time ? `${sub.time}'` : ""}
</ThemedText>
</View>
<View style={[styles.eventContent, isHome ? styles.homeEvent : styles.awayEvent]}>
<IconSymbol name="swap-horizontal" size={16} color={isDark ? "#2196F3" : "#1976D2"} />
<View style={styles.substituteContent}>
{playerOut ? (
<>
<ThemedText style={[styles.substituteIn, { color: isDark ? "#4CAF50" : "#2E7D32" }]} numberOfLines={1}>
{playerIn}
</ThemedText>
<ThemedText style={styles.substituteArrow}></ThemedText>
<ThemedText style={styles.substituteOut} numberOfLines={1}>
{playerOut}
</ThemedText>
</>
) : (
<ThemedText style={styles.eventText} numberOfLines={1}>
{playerIn}
</ThemedText>
)}
</View>
</View>
</View>
);
};
return (
<View style={[styles.container, { backgroundColor: bgColor, borderColor }]}>
{goalscorers.length > 0 && (
<View style={styles.section}>
<ThemedText style={styles.sectionTitle}>
{t("detail.events.goals")}
</ThemedText>
{goalscorers
.filter((goal: GoalEvent) => goal.home_scorer || goal.away_scorer)
.map((goal: GoalEvent, index: number) => renderGoalEvent(goal, index))}
</View>
)}
{cards.length > 0 && (
<View style={styles.section}>
<ThemedText style={styles.sectionTitle}>
{t("detail.events.cards")}
</ThemedText>
{cards
.filter((card: CardEvent) => card.home_fault || card.away_fault)
.map((card: CardEvent, index: number) => renderCardEvent(card, index))}
</View>
)}
{substitutes.length > 0 && (
<View style={styles.section}>
<ThemedText style={styles.sectionTitle}>
{t("detail.events.substitutes")}
</ThemedText>
{substitutes
.filter((sub: SubstituteEvent) => sub.home_scorer || sub.away_scorer || sub.home_assist || sub.away_assist)
.map((sub: SubstituteEvent, index: number) => renderSubstituteEvent(sub, index))}
</View>
)}
{players && (players.home_team || players.away_team) && (
<View style={styles.section}>
<ThemedText style={styles.sectionTitle}>
{t("detail.events.lineups")}
</ThemedText>
{players.home_team && Array.isArray(players.home_team) && players.home_team.length > 0 && (
<View style={styles.teamLineup}>
<ThemedText style={styles.teamName}>
{match.eventHomeTeam}
</ThemedText>
{players.home_team.slice(0, 11).map((player: Player, idx: number) => (
<View key={idx} style={styles.playerRow}>
<ThemedText style={styles.playerNumber}>
{player.player_number || player.number || idx + 1}
</ThemedText>
<ThemedText style={styles.playerName} numberOfLines={1}>
{player.player_name || player.player || player.name || ""}
</ThemedText>
{player.position && (
<ThemedText style={styles.playerPosition}>
{player.position}
</ThemedText>
)}
</View>
))}
</View>
)}
{players.away_team && Array.isArray(players.away_team) && players.away_team.length > 0 && (
<View style={[styles.teamLineup, { marginTop: 16 }]}>
<ThemedText style={styles.teamName}>
{match.eventAwayTeam}
</ThemedText>
{players.away_team.slice(0, 11).map((player: Player, idx: number) => (
<View key={idx} style={styles.playerRow}>
<ThemedText style={styles.playerNumber}>
{player.player_number || player.number || idx + 1}
</ThemedText>
<ThemedText style={styles.playerName} numberOfLines={1}>
{player.player_name || player.player || player.name || ""}
</ThemedText>
{player.position && (
<ThemedText style={styles.playerPosition}>
{player.position}
</ThemedText>
)}
</View>
))}
</View>
)}
</View>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
margin: 16,
marginTop: 0,
borderRadius: 12,
padding: 16,
borderWidth: 1,
elevation: 2,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
section: {
marginBottom: 20,
},
sectionTitle: {
fontSize: 14,
fontWeight: "600",
marginBottom: 12,
opacity: 0.7,
},
eventRow: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 8,
borderBottomWidth: StyleSheet.hairlineWidth,
},
eventTime: {
width: 40,
alignItems: "center",
},
timeText: {
fontSize: 12,
fontWeight: "500",
opacity: 0.6,
},
eventContent: {
flex: 1,
flexDirection: "row",
alignItems: "center",
gap: 8,
paddingLeft: 12,
},
homeEvent: {
justifyContent: "flex-start",
},
awayEvent: {
justifyContent: "flex-end",
flexDirection: "row-reverse",
},
eventText: {
fontSize: 13,
fontWeight: "500",
flex: 1,
},
scoreText: {
fontSize: 12,
fontWeight: "600",
opacity: 0.7,
},
cardIcon: {
width: 12,
height: 16,
borderRadius: 2,
},
cardTypeText: {
fontSize: 10,
fontWeight: "600",
textTransform: "uppercase",
},
cardBadge: {
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 4,
},
goalContent: {
flex: 1,
flexDirection: "row",
alignItems: "center",
gap: 6,
},
penaltyBadge: {
width: 18,
height: 18,
borderRadius: 9,
backgroundColor: "#FF9800",
alignItems: "center",
justifyContent: "center",
},
penaltyText: {
fontSize: 10,
fontWeight: "700",
color: "#FFF",
},
substituteContent: {
flex: 1,
flexDirection: "row",
alignItems: "center",
gap: 6,
},
substituteIn: {
fontSize: 13,
fontWeight: "500",
},
substituteArrow: {
fontSize: 12,
opacity: 0.5,
},
substituteOut: {
fontSize: 13,
fontWeight: "500",
opacity: 0.6,
textDecorationLine: "line-through",
},
teamLineup: {
marginTop: 8,
},
teamName: {
fontSize: 13,
fontWeight: "600",
marginBottom: 8,
opacity: 0.8,
},
playerRow: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 4,
gap: 8,
},
playerNumber: {
fontSize: 12,
fontWeight: "600",
width: 24,
textAlign: "center",
opacity: 0.6,
},
playerName: {
fontSize: 13,
fontWeight: "500",
flex: 1,
},
playerPosition: {
fontSize: 11,
fontWeight: "500",
opacity: 0.5,
textTransform: "uppercase",
},
});