交锋往绩
This commit is contained in:
@@ -5,6 +5,7 @@ import { FootballScoreTable } from "@/components/match-detail/football/football-
|
||||
import { GoalsCard } from "@/components/match-detail/football/goals-card";
|
||||
import { LineupsCard } from "@/components/match-detail/football/lineups-card";
|
||||
import { SubstitutesCard } from "@/components/match-detail/football/substitutes-card";
|
||||
import { H2H } from "@/components/match-detail/h2h";
|
||||
import { LeagueInfo } from "@/components/match-detail/league-info";
|
||||
import { MatchInfoCard } from "@/components/match-detail/match-info-card";
|
||||
import { MatchTabs } from "@/components/match-detail/match-tabs";
|
||||
@@ -187,13 +188,7 @@ export default function MatchDetailScreen() {
|
||||
};
|
||||
return <OddsCard sportId={sportId} match={matchForOdds} isDark={isDark} />;
|
||||
case "h2h":
|
||||
return (
|
||||
<View style={styles.emptyContent}>
|
||||
<ThemedText style={styles.emptyText}>
|
||||
{t("detail.empty_h2h")}
|
||||
</ThemedText>
|
||||
</View>
|
||||
);
|
||||
return <H2H data={data} isDark={isDark} />;
|
||||
case "chat":
|
||||
return (
|
||||
<View style={styles.emptyContent}>
|
||||
|
||||
377
components/match-detail/h2h.tsx
Normal file
377
components/match-detail/h2h.tsx
Normal file
@@ -0,0 +1,377 @@
|
||||
import { ThemedText } from "@/components/themed-text";
|
||||
import { fetchH2H } from "@/lib/api";
|
||||
import { H2HData, H2HMatch, MatchDetailData } from "@/types/api";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
ActivityIndicator,
|
||||
StyleSheet,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from "react-native";
|
||||
|
||||
interface H2HProps {
|
||||
data: MatchDetailData;
|
||||
isDark: boolean;
|
||||
}
|
||||
|
||||
export function H2H({ data, isDark }: H2HProps) {
|
||||
const { t } = useTranslation();
|
||||
const { match } = data;
|
||||
const [h2hData, setH2hData] = useState<H2HData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [activeSection, setActiveSection] = useState<"h2h" | "first" | "second">("h2h");
|
||||
|
||||
const bgColor = isDark ? "#1C1C1E" : "#FFF";
|
||||
const borderColor = isDark ? "rgba(150,150,150,0.2)" : "rgba(150,150,150,0.2)";
|
||||
|
||||
// 颜色常量配置
|
||||
const colors = {
|
||||
bg: isDark ? "#000000" : "#F8F8F8",
|
||||
card: bgColor,
|
||||
border: borderColor,
|
||||
textMain: isDark ? "#FFFFFF" : "#000000",
|
||||
textSecondary: isDark ? "#8E8E93" : "#8E8E93",
|
||||
win: "#4CAF50",
|
||||
loss: "#F44336",
|
||||
draw: "#2196F3",
|
||||
scoreHighlight: "#FFB800",
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadH2H();
|
||||
}, []);
|
||||
|
||||
const loadH2H = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const sportId = match.sportId;
|
||||
const options: any = {};
|
||||
|
||||
if (sportId === 3) {
|
||||
options.firstPlayerId = parseInt(match.homeTeamKey);
|
||||
options.secondPlayerId = parseInt(match.awayTeamKey);
|
||||
} else {
|
||||
options.firstTeamId = parseInt(match.homeTeamKey);
|
||||
options.secondTeamId = parseInt(match.awayTeamKey);
|
||||
}
|
||||
|
||||
const result = await fetchH2H(sportId, options);
|
||||
setH2hData(result);
|
||||
} catch (err: any) {
|
||||
setError(err?.message || t("detail.h2h.error"));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 核心逻辑:计算胜负状态
|
||||
const getMatchResult = (item: H2HMatch) => {
|
||||
const homeScore = parseInt(item.event_final_result?.split("-")[0] || "0");
|
||||
const awayScore = parseInt(item.event_final_result?.split("-")[1] || "0");
|
||||
|
||||
// 确定当前视角球队名称
|
||||
const perspectiveTeam = activeSection === "first" ? match.eventHomeTeam :
|
||||
activeSection === "second" ? match.eventAwayTeam : null;
|
||||
|
||||
if (!perspectiveTeam) return { label: "", color: "transparent" };
|
||||
|
||||
const isHome = item.event_home_team === perspectiveTeam;
|
||||
if (homeScore === awayScore) return { label: "D", color: colors.draw };
|
||||
|
||||
const isWin = isHome ? homeScore > awayScore : awayScore > homeScore;
|
||||
return isWin ? { label: "W", color: colors.win } : { label: "L", color: colors.loss };
|
||||
};
|
||||
|
||||
const renderMatchItem = ({ item, index }: { item: H2HMatch; index: number }) => {
|
||||
const homeScore = parseInt(item.event_final_result?.split("-")[0] || "0");
|
||||
const awayScore = parseInt(item.event_final_result?.split("-")[1] || "0");
|
||||
const { label, color } = getMatchResult(item);
|
||||
const matchYear = item.event_date?.split("-")[0] || "";
|
||||
|
||||
return (
|
||||
<View
|
||||
key={item.event_key ?? index}
|
||||
style={[styles.matchRow, { borderBottomColor: borderColor }]}
|
||||
>
|
||||
{/* 左侧:日期状态 */}
|
||||
<View style={styles.leftCol}>
|
||||
<ThemedText style={styles.yearText}>{matchYear}</ThemedText>
|
||||
<ThemedText style={styles.ftText}>FT</ThemedText>
|
||||
</View>
|
||||
|
||||
{/* 中间:球队对阵 */}
|
||||
<View style={styles.centerCol}>
|
||||
<View style={styles.teamLine}>
|
||||
<ThemedText
|
||||
style={[
|
||||
styles.teamName,
|
||||
homeScore > awayScore && [
|
||||
styles.boldText,
|
||||
{ color: isDark ? "#FFF" : "#000" }
|
||||
]
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{item.event_home_team}
|
||||
</ThemedText>
|
||||
</View>
|
||||
<View style={styles.teamLine}>
|
||||
<ThemedText
|
||||
style={[
|
||||
styles.teamName,
|
||||
awayScore > homeScore && [
|
||||
styles.boldText,
|
||||
{ color: isDark ? "#FFF" : "#000" }
|
||||
]
|
||||
]}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{item.event_away_team}
|
||||
</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
{/* 右侧:比分与胜负角标 */}
|
||||
<View style={styles.rightCol}>
|
||||
<View style={styles.scoreContainer}>
|
||||
<ThemedText style={[styles.scoreText, homeScore > awayScore && { color: colors.scoreHighlight }]}>
|
||||
{homeScore}
|
||||
</ThemedText>
|
||||
<ThemedText style={[styles.scoreText, awayScore > homeScore && { color: colors.scoreHighlight }]}>
|
||||
{awayScore}
|
||||
</ThemedText>
|
||||
</View>
|
||||
{label !== "" && (
|
||||
<View style={[styles.resultBadge, { backgroundColor: color }]}>
|
||||
<ThemedText style={styles.resultBadgeText}>{label}</ThemedText>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
// 状态处理 (Loading, Error, Empty) 保持原样但应用新背景色
|
||||
if (loading) {
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: colors.card }]}>
|
||||
<View style={styles.loadingContainer}>
|
||||
<ActivityIndicator size="small" color={isDark ? "#FFF" : "#000"} />
|
||||
<ThemedText style={styles.loadingText}>
|
||||
{t("detail.h2h.loading")}
|
||||
</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: colors.card }]}>
|
||||
<View style={styles.errorContainer}>
|
||||
<ThemedText style={styles.errorText}>{error}</ThemedText>
|
||||
<TouchableOpacity
|
||||
style={[styles.retryButton, { backgroundColor: isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.05)" }]}
|
||||
onPress={loadH2H}
|
||||
>
|
||||
<ThemedText style={styles.retryText}>
|
||||
{t("detail.h2h.retry")}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
if (!h2hData) {
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: colors.card }]}>
|
||||
<View style={styles.emptyContainer}>
|
||||
<ThemedText style={styles.emptyText}>
|
||||
{t("detail.h2h.no_data")}
|
||||
</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const currentData = activeSection === "h2h" ? h2hData?.H2H :
|
||||
activeSection === "first" ? h2hData?.firstTeamResults : h2hData?.secondTeamResults;
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: colors.card }]}>
|
||||
{/* 选项卡 */}
|
||||
<View style={[styles.tabs, { borderBottomColor: borderColor }]}>
|
||||
{[
|
||||
{ id: "h2h", label: t("detail.h2h.h2h") },
|
||||
{ id: "first", label: match.eventHomeTeam },
|
||||
{ id: "second", label: match.eventAwayTeam }
|
||||
].map((tab) => (
|
||||
<TouchableOpacity
|
||||
key={tab.id}
|
||||
style={[styles.tab, activeSection === tab.id && styles.tabActive, { borderBottomColor: activeSection === tab.id ? colors.scoreHighlight : 'transparent' }]}
|
||||
onPress={() => setActiveSection(tab.id as any)}
|
||||
>
|
||||
<ThemedText style={[styles.tabText, activeSection === tab.id && styles.tabTextActive]}>
|
||||
{tab.label}
|
||||
</ThemedText>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{/* 列表内容 */}
|
||||
<View style={styles.listContent}>
|
||||
{currentData && currentData.length > 0 ? (
|
||||
currentData.map((item, index) => renderMatchItem({ item, index }))
|
||||
) : (
|
||||
<ThemedText style={styles.emptyText}>
|
||||
{t("detail.h2h.no_data")}
|
||||
</ThemedText>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
margin: 16,
|
||||
marginTop: 0,
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
elevation: 2,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.1,
|
||||
shadowRadius: 4,
|
||||
},
|
||||
loadingContainer: {
|
||||
padding: 40,
|
||||
alignItems: "center",
|
||||
gap: 12,
|
||||
},
|
||||
loadingText: {
|
||||
fontSize: 14,
|
||||
opacity: 0.6,
|
||||
},
|
||||
errorContainer: {
|
||||
padding: 40,
|
||||
alignItems: "center",
|
||||
gap: 16,
|
||||
},
|
||||
errorText: {
|
||||
fontSize: 14,
|
||||
opacity: 0.7,
|
||||
textAlign: "center",
|
||||
},
|
||||
retryButton: {
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 10,
|
||||
borderRadius: 8,
|
||||
},
|
||||
retryText: {
|
||||
fontSize: 13,
|
||||
fontWeight: "600",
|
||||
},
|
||||
emptyContainer: {
|
||||
padding: 40,
|
||||
alignItems: "center",
|
||||
},
|
||||
tabs: {
|
||||
flexDirection: "row",
|
||||
marginBottom: 12,
|
||||
paddingBottom: 12,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
},
|
||||
tab: {
|
||||
paddingVertical: 12,
|
||||
marginRight: 20,
|
||||
borderBottomWidth: 2,
|
||||
},
|
||||
tabActive: {},
|
||||
tabText: {
|
||||
fontSize: 14,
|
||||
opacity: 0.6,
|
||||
},
|
||||
tabTextActive: {
|
||||
fontWeight: "bold",
|
||||
opacity: 1,
|
||||
},
|
||||
listContent: {
|
||||
paddingBottom: 20,
|
||||
},
|
||||
matchRow: {
|
||||
flexDirection: "row",
|
||||
paddingVertical: 12,
|
||||
borderBottomWidth: StyleSheet.hairlineWidth,
|
||||
alignItems: "center",
|
||||
},
|
||||
leftCol: {
|
||||
width: 50,
|
||||
alignItems: "flex-start",
|
||||
},
|
||||
yearText: {
|
||||
fontSize: 12,
|
||||
color: "#8E8E93",
|
||||
},
|
||||
ftText: {
|
||||
fontSize: 11,
|
||||
fontWeight: "bold",
|
||||
marginTop: 2,
|
||||
color: "#666",
|
||||
},
|
||||
centerCol: {
|
||||
flex: 1,
|
||||
paddingLeft: 4,
|
||||
},
|
||||
teamLine: {
|
||||
height: 24,
|
||||
justifyContent: "center",
|
||||
},
|
||||
teamName: {
|
||||
fontSize: 14,
|
||||
color: "#BBB",
|
||||
},
|
||||
boldText: {
|
||||
fontWeight: "600",
|
||||
},
|
||||
rightCol: {
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
},
|
||||
scoreContainer: {
|
||||
backgroundColor: "#1C1C1E",
|
||||
borderRadius: 6,
|
||||
paddingHorizontal: 10,
|
||||
paddingVertical: 4,
|
||||
minWidth: 40,
|
||||
alignItems: "center",
|
||||
marginRight: 10,
|
||||
},
|
||||
scoreText: {
|
||||
fontSize: 14,
|
||||
fontWeight: "bold",
|
||||
lineHeight: 20,
|
||||
color: "#FFF",
|
||||
},
|
||||
resultBadge: {
|
||||
width: 22,
|
||||
height: 38,
|
||||
borderRadius: 4,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
resultBadgeText: {
|
||||
color: "#FFF",
|
||||
fontSize: 13,
|
||||
fontWeight: "bold",
|
||||
},
|
||||
emptyText: {
|
||||
textAlign: "center",
|
||||
marginTop: 40,
|
||||
opacity: 0.5,
|
||||
},
|
||||
});
|
||||
@@ -70,6 +70,13 @@
|
||||
"empty_odds": "No odds data",
|
||||
"empty_h2h": "No H2H data",
|
||||
"empty_chat": "Chat is not available",
|
||||
"h2h": {
|
||||
"h2h": "Head-to-Head",
|
||||
"loading": "Loading...",
|
||||
"error": "Failed to load",
|
||||
"retry": "Retry",
|
||||
"no_data": "No data available"
|
||||
},
|
||||
"odds_card": {
|
||||
"title": "Match Odds",
|
||||
"disclaimer": "18+. Please gamble responsibly. Odds are subject to change."
|
||||
|
||||
@@ -70,6 +70,13 @@
|
||||
"empty_odds": "暂无赔率数据",
|
||||
"empty_h2h": "暂无交锋数据",
|
||||
"empty_chat": "聊天功能暂未开启",
|
||||
"h2h": {
|
||||
"h2h": "历史对战",
|
||||
"loading": "加载中...",
|
||||
"error": "加载失败",
|
||||
"retry": "重试",
|
||||
"no_data": "暂无数据"
|
||||
},
|
||||
"odds_card": {
|
||||
"title": "比赛赔率",
|
||||
"disclaimer": "18+. 请负责任地赌博。赔率可能会变动。"
|
||||
|
||||
36
types/api.ts
36
types/api.ts
@@ -404,23 +404,25 @@ export interface SearchResult {
|
||||
|
||||
// H2H 历史对战比赛项
|
||||
export interface H2HMatch {
|
||||
awayTeamKey: string;
|
||||
countryName: string;
|
||||
eventAwayTeam: string;
|
||||
eventCountryKey: string;
|
||||
eventDate: string;
|
||||
eventFinalResult: string;
|
||||
eventHalftimeResult: string;
|
||||
eventHomeTeam: string;
|
||||
eventKey: string;
|
||||
eventLive: string;
|
||||
eventStatus: string;
|
||||
eventTime: string;
|
||||
homeTeamKey: string;
|
||||
leagueKey: string;
|
||||
leagueName: string;
|
||||
leagueRound: string;
|
||||
leagueSeason: string;
|
||||
away_team_key: number;
|
||||
away_team_logo?: string;
|
||||
country_name: string;
|
||||
event_away_team: string;
|
||||
event_country_key: number;
|
||||
event_date: string;
|
||||
event_final_result: string;
|
||||
event_halftime_result: string;
|
||||
event_home_team: string;
|
||||
event_key: number;
|
||||
event_live: string;
|
||||
event_status: string;
|
||||
event_time: string;
|
||||
home_team_key: number;
|
||||
home_team_logo?: string;
|
||||
league_key: number;
|
||||
league_name: string;
|
||||
league_round: string;
|
||||
league_season: string;
|
||||
}
|
||||
|
||||
// H2H 历史对战数据
|
||||
|
||||
Reference in New Issue
Block a user