实现详情页

This commit is contained in:
yuchenglong
2026-01-13 09:26:13 +08:00
parent bb6c21496f
commit 9c16586994
13 changed files with 902 additions and 21 deletions

View File

@@ -1,14 +1,18 @@
import { DarkTheme, DefaultTheme, ThemeProvider as NavigationThemeProvider } from '@react-navigation/native';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import 'react-native-reanimated';
import {
DarkTheme,
DefaultTheme,
ThemeProvider as NavigationThemeProvider,
} from "@react-navigation/native";
import { Stack } from "expo-router";
import { StatusBar } from "expo-status-bar";
import "react-native-reanimated";
import { useColorScheme } from '@/hooks/use-color-scheme';
import { ThemeProvider } from '@/context/ThemeContext';
import '@/i18n'; // Initialize i18n
import { ThemeProvider } from "@/context/ThemeContext";
import { useColorScheme } from "@/hooks/use-color-scheme";
import "@/i18n"; // Initialize i18n
export const unstable_settings = {
anchor: '(tabs)',
anchor: "(tabs)",
};
export default function RootLayout() {
@@ -23,12 +27,25 @@ function RootLayoutNav() {
const colorScheme = useColorScheme();
return (
<NavigationThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<NavigationThemeProvider
value={colorScheme === "dark" ? DarkTheme : DefaultTheme}
>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
<Stack.Screen
name="modal"
options={{ presentation: "modal", title: "Modal" }}
/>
<Stack.Screen
name="match-detail/[id]"
options={{
animation: "slide_from_right",
headerShown: true,
headerTitle: "",
}}
/>
</Stack>
<StatusBar style={colorScheme === 'dark' ? 'light' : 'dark'} />
<StatusBar style={colorScheme === "dark" ? "light" : "dark"} />
</NavigationThemeProvider>
);
}

160
app/match-detail/[id].tsx Normal file
View File

@@ -0,0 +1,160 @@
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";
import { ScoreHeader } from "@/components/match-detail/score-header";
import { ScoreTable } from "@/components/match-detail/score-table";
import { ThemedText } from "@/components/themed-text";
import { ThemedView } from "@/components/themed-view";
import { Colors } from "@/constants/theme";
import { useTheme } from "@/context/ThemeContext";
import { fetchMatchDetail } from "@/lib/api";
import { MatchDetailData } from "@/types/api";
import { Stack, useLocalSearchParams, useRouter } from "expo-router";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import {
ActivityIndicator,
ScrollView,
StyleSheet,
TouchableOpacity,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
export default function MatchDetailScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const router = useRouter();
const { theme } = useTheme();
const { t } = useTranslation();
const insets = useSafeAreaInsets();
const isDark = theme === "dark";
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [data, setData] = useState<MatchDetailData | null>(null);
const [activeTab, setActiveTab] = useState("info");
useEffect(() => {
if (id) {
loadMatchDetail();
}
}, [id]);
const loadMatchDetail = async () => {
try {
setLoading(true);
setError(null);
const result = await fetchMatchDetail(id as string);
setData(result);
} catch (err: any) {
setError(err.message || t("detail.fetch_failed"));
} finally {
setLoading(false);
}
};
if (loading) {
return (
<ThemedView style={styles.center}>
<ActivityIndicator size="large" color={Colors[theme].tint} />
</ThemedView>
);
}
if (error || !data) {
return (
<ThemedView style={styles.center}>
<ThemedText>{error || t("detail.not_found")}</ThemedText>
<TouchableOpacity style={styles.retryButton} onPress={loadMatchDetail}>
<ThemedText style={styles.retryText}>{t("detail.retry")}</ThemedText>
</TouchableOpacity>
</ThemedView>
);
}
const renderTabContent = () => {
switch (activeTab) {
case "info":
return (
<>
<ScoreTable data={data} isDark={isDark} />
<MatchInfoCard data={data} isDark={isDark} />
</>
);
case "h2h":
return (
<View style={styles.emptyContent}>
<ThemedText style={styles.emptyText}>
{t("detail.empty_h2h")}
</ThemedText>
</View>
);
case "chat":
return (
<View style={styles.emptyContent}>
<ThemedText style={styles.emptyText}>
{t("detail.empty_chat")}
</ThemedText>
</View>
);
default:
return null;
}
};
return (
<ThemedView style={styles.container}>
<Stack.Screen options={{ headerShown: false }} />
<ScrollView
bounces={false}
showsVerticalScrollIndicator={false}
contentContainerStyle={{ paddingBottom: insets.bottom + 20 }}
>
<ScoreHeader data={data} isDark={isDark} topInset={insets.top} />
<LeagueInfo data={data} isDark={isDark} />
<MatchTabs
activeTab={activeTab}
onTabChange={setActiveTab}
isDark={isDark}
/>
{renderTabContent()}
</ScrollView>
</ThemedView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
center: {
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 20,
},
errorText: {
fontSize: 16,
marginBottom: 20,
textAlign: "center",
},
retryButton: {
paddingHorizontal: 20,
paddingVertical: 10,
backgroundColor: "#007AFF",
borderRadius: 8,
},
retryText: {
color: "#FFF",
fontWeight: "600",
},
emptyContent: {
padding: 50,
alignItems: "center",
},
emptyText: {
opacity: 0.5,
},
});

View File

@@ -3,22 +3,34 @@ import { IconSymbol } from "@/components/ui/icon-symbol";
import { Colors } from "@/constants/theme";
import { useTheme } from "@/context/ThemeContext";
import { Match } from "@/types/api";
import { useRouter } from "expo-router";
import React from "react";
import { StyleSheet, View } from "react-native";
import { Pressable, StyleSheet, View } from "react-native";
interface MatchCardProps {
match: Match;
}
export function MatchCard({ match }: MatchCardProps) {
const router = useRouter();
const { theme } = useTheme();
const isDark = theme === "dark";
const iconColor = isDark ? Colors.dark.icon : Colors.light.icon;
const cardBg = isDark ? "#1C1C1E" : "#FFFFFF";
const borderColor = isDark ? "#38383A" : "#E5E5EA";
const handlePress = () => {
router.push(`/match-detail/${match.id}`);
};
return (
<View style={[styles.card, { backgroundColor: cardBg, borderColor }]}>
<Pressable
onPress={handlePress}
style={({ pressed }) => [
styles.card,
{ backgroundColor: cardBg, borderColor, opacity: pressed ? 0.7 : 1 },
]}
>
<View style={styles.header}>
<View
style={[
@@ -52,7 +64,7 @@ export function MatchCard({ match }: MatchCardProps) {
{match.meta && (
<ThemedText style={styles.metaText}>{match.meta}</ThemedText>
)}
</View>
</Pressable>
);
}

View File

@@ -0,0 +1,84 @@
import { ThemedText } from "@/components/themed-text";
import { IconSymbol } from "@/components/ui/icon-symbol";
import { MatchDetailData } from "@/types/api";
import React from "react";
import { Image, StyleSheet, TouchableOpacity, View } from "react-native";
interface LeagueInfoProps {
data: MatchDetailData;
isDark: boolean;
}
export function LeagueInfo({ data, isDark }: LeagueInfoProps) {
const { match } = data;
const bgColor = isDark ? "#1C1C1E" : "#FFF";
const borderColor = isDark ? "#2C2C2E" : "#EEE";
return (
<TouchableOpacity
style={[
styles.container,
{ backgroundColor: bgColor, borderBottomColor: borderColor },
]}
activeOpacity={0.7}
>
<View style={styles.left}>
{match.leagueLogo ? (
<Image source={{ uri: match.leagueLogo }} style={styles.leagueLogo} />
) : (
<View
style={[
styles.fallbackLogo,
{ backgroundColor: isDark ? "#333" : "#F0F0F0" },
]}
>
<IconSymbol
name="trophy-outline"
size={14}
color={isDark ? "#AAA" : "#888"}
/>
</View>
)}
<ThemedText style={styles.leagueName}>{match.leagueName}</ThemedText>
</View>
<IconSymbol
name="chevron-forward"
size={16}
color={isDark ? "#666" : "#CCC"}
/>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
container: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: 16,
paddingVertical: 12,
borderBottomWidth: 1,
},
left: {
flexDirection: "row",
alignItems: "center",
},
leagueLogo: {
width: 24,
height: 24,
resizeMode: "contain",
marginRight: 8,
},
fallbackLogo: {
width: 24,
height: 24,
borderRadius: 12,
justifyContent: "center",
alignItems: "center",
marginRight: 8,
},
leagueName: {
fontSize: 14,
fontWeight: "500",
},
});

View File

@@ -0,0 +1,62 @@
import { ThemedText } from "@/components/themed-text";
import { MatchDetailData } from "@/types/api";
import React from "react";
import { useTranslation } from "react-i18next";
import { StyleSheet, View } from "react-native";
interface MatchInfoCardProps {
data: MatchDetailData;
isDark: boolean;
}
export function MatchInfoCard({ data, isDark }: MatchInfoCardProps) {
const { t } = useTranslation();
const { match } = data;
const bgColor = isDark ? "#1C1C1E" : "#FFF";
return (
<View style={[styles.container, { backgroundColor: bgColor }]}>
<ThemedText style={styles.title}>
{t("detail.info_card.title")}
</ThemedText>
<View style={styles.content}>
<ThemedText style={styles.leagueName}>{match.leagueName}</ThemedText>
<ThemedText style={styles.dateTime}>
{match.eventDate} {match.eventTime}
</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,
},
title: {
fontSize: 14,
fontWeight: "600",
color: "#666",
marginBottom: 16,
},
content: {
gap: 4,
},
leagueName: {
fontSize: 16,
fontWeight: "600",
},
dateTime: {
fontSize: 14,
opacity: 0.6,
},
});

View File

@@ -0,0 +1,92 @@
import { ThemedText } from "@/components/themed-text";
import React from "react";
import { useTranslation } from "react-i18next";
import { ScrollView, StyleSheet, TouchableOpacity, View } from "react-native";
interface MatchTabsProps {
activeTab: string;
onTabChange: (tab: string) => void;
isDark: boolean;
}
export function MatchTabs({ activeTab, onTabChange, isDark }: MatchTabsProps) {
const { t } = useTranslation();
const containerBg = isDark ? "#121212" : "#F8F8F8";
const tabs = [
{ id: "info", label: t("detail.tabs.info") },
{ id: "h2h", label: t("detail.tabs.h2h") },
{ id: "chat", label: t("detail.tabs.chat") },
];
return (
<View style={[styles.container, { backgroundColor: containerBg }]}>
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.scrollContent}
>
{tabs.map((tab) => {
const isActive = activeTab === tab.id;
return (
<TouchableOpacity
key={tab.id}
onPress={() => onTabChange(tab.id)}
style={[
styles.tabBtn,
isActive && styles.activeTabBtn,
isActive && {
backgroundColor: isDark ? "rgba(255,255,255,0.05)" : "#FFF",
},
]}
>
<ThemedText
style={[
styles.tabText,
isActive && styles.activeTabText,
isActive && { color: "#FF9800" },
]}
>
{tab.label}
</ThemedText>
</TouchableOpacity>
);
})}
</ScrollView>
</View>
);
}
const styles = StyleSheet.create({
container: {
paddingVertical: 12,
},
scrollContent: {
paddingHorizontal: 16,
gap: 12,
},
tabBtn: {
paddingHorizontal: 20,
paddingVertical: 8,
borderRadius: 20,
borderWidth: 1,
borderColor: "transparent",
},
activeTabBtn: {
borderColor: "rgba(255,152,0,0.3)",
// Shadow for iOS/Android if needed
elevation: 2,
shadowColor: "#000",
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
tabText: {
fontSize: 14,
fontWeight: "500",
opacity: 0.6,
},
activeTabText: {
opacity: 1,
},
});

View File

@@ -0,0 +1,196 @@
import { ThemedText } from "@/components/themed-text";
import { IconSymbol } from "@/components/ui/icon-symbol";
import { MatchDetailData } from "@/types/api";
import { LinearGradient } from "expo-linear-gradient";
import { useRouter } from "expo-router";
import React from "react";
import { useTranslation } from "react-i18next";
import { Image, StyleSheet, TouchableOpacity, View } from "react-native";
interface ScoreHeaderProps {
data: MatchDetailData;
isDark: boolean;
topInset: number;
}
export function ScoreHeader({ data, isDark, topInset }: ScoreHeaderProps) {
const router = useRouter();
const { t } = useTranslation();
const { match } = data;
return (
<LinearGradient
colors={["#1A2138", "#2A3C5B"]}
style={[styles.container, { paddingTop: Math.max(topInset, 20) }]}
>
{/* Top Bar */}
<View style={styles.topBar}>
<View style={styles.leftContainer}>
<TouchableOpacity
onPress={() => router.back()}
style={styles.iconBtn}
>
<IconSymbol name="chevron-back" size={24} color="#FFF" />
</TouchableOpacity>
</View>
<View style={styles.statusContainer}>
<ThemedText style={styles.statusText}>
{match.eventStatus || t("detail.pending")}
</ThemedText>
</View>
<View style={styles.rightContainer}>
<TouchableOpacity style={styles.iconBtn}>
<IconSymbol name="notifications-outline" size={22} color="#FFF" />
</TouchableOpacity>
<TouchableOpacity style={styles.iconBtn}>
<IconSymbol name="star-outline" size={22} color="#FFF" />
</TouchableOpacity>
</View>
</View>
{/* Score Section */}
<View style={styles.scoreRow}>
<View style={styles.teamInfo}>
<View style={styles.logoContainer}>
<Image
source={{ uri: match.homeTeamLogo }}
style={styles.teamLogo}
/>
</View>
<ThemedText style={styles.teamName} numberOfLines={2}>
{match.eventHomeTeam}
</ThemedText>
</View>
<View style={styles.centerScore}>
<View style={styles.scoreBox}>
<ThemedText style={styles.scoreValue}>
{match.eventFinalResult && match.eventFinalResult !== ""
? match.eventFinalResult
: "0 - 0"}
</ThemedText>
</View>
<TouchableOpacity style={styles.fieldBtn}>
<IconSymbol name="football-outline" size={16} color="#FFF" />
</TouchableOpacity>
</View>
<View style={styles.teamInfo}>
<View style={styles.logoContainer}>
<Image
source={{ uri: match.awayTeamLogo }}
style={styles.teamLogo}
/>
</View>
<ThemedText style={styles.teamName} numberOfLines={2}>
{match.eventAwayTeam}
</ThemedText>
</View>
</View>
</LinearGradient>
);
}
const styles = StyleSheet.create({
container: {
paddingBottom: 30,
paddingHorizontal: 16,
},
topBar: {
flexDirection: "row",
alignItems: "center",
marginBottom: 20,
minHeight: 40,
},
leftContainer: {
flex: 1,
flexDirection: "row",
alignItems: "center",
},
statusContainer: {
flex: 2,
justifyContent: "center",
alignItems: "center",
},
rightContainer: {
flex: 1,
flexDirection: "row",
justifyContent: "flex-end",
alignItems: "center",
gap: 12,
},
statusText: {
color: "#FFF",
fontSize: 16,
fontWeight: "600",
opacity: 0.8,
},
rightIcons: {
flexDirection: "row",
gap: 12,
},
iconBtn: {
width: 36,
height: 36,
justifyContent: "center",
alignItems: "center",
},
scoreRow: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
paddingHorizontal: 10,
},
teamInfo: {
flex: 1.2,
alignItems: "center",
},
logoContainer: {
width: 64,
height: 64,
borderRadius: 32,
backgroundColor: "rgba(255,255,255,0.1)",
justifyContent: "center",
alignItems: "center",
marginBottom: 8,
overflow: "hidden",
},
teamLogo: {
width: 48,
height: 48,
resizeMode: "contain",
},
teamName: {
color: "#FFF",
fontSize: 14,
fontWeight: "600",
textAlign: "center",
},
centerScore: {
flex: 1,
alignItems: "center",
},
scoreBox: {
backgroundColor: "#FFF",
borderRadius: 12,
paddingHorizontal: 16,
paddingVertical: 8,
marginBottom: 12,
},
scoreValue: {
color: "#000",
fontSize: 32,
fontWeight: "bold",
letterSpacing: 2,
},
fieldBtn: {
backgroundColor: "#0055FF",
width: 80,
height: 28,
borderRadius: 14,
justifyContent: "center",
alignItems: "center",
},
});

View File

@@ -0,0 +1,132 @@
import { ThemedText } from "@/components/themed-text";
import { MatchDetailData } from "@/types/api";
import React from "react";
import { useTranslation } from "react-i18next";
import { Image, StyleSheet, View } from "react-native";
interface ScoreTableProps {
data: MatchDetailData;
isDark: boolean;
}
export function ScoreTable({ data, isDark }: ScoreTableProps) {
const { t } = useTranslation();
const { match } = data;
const bgColor = isDark ? "#1C1C1E" : "#FFF";
const headerTextColor = isDark ? "#666" : "#999";
// Mock quarters for demo purposes as seen in screenshot
// In real app, these would come from the API (e.g. match.eventQuarter or specific fields)
const headers = [
t("detail.score_table.team"),
t("detail.score_table.total"),
"1",
"2",
"3",
"4",
];
const rows = [
{
logo: match.homeTeamLogo,
name: match.eventHomeTeam,
total: 0,
q1: 0,
q2: 0,
q3: 0,
q4: 0,
},
{
logo: match.awayTeamLogo,
name: match.eventAwayTeam,
total: 0,
q1: 0,
q2: 0,
q3: 0,
q4: 0,
},
];
return (
<View style={[styles.container, { backgroundColor: bgColor }]}>
<View style={styles.header}>
{headers.map((h, i) => (
<ThemedText
key={h}
style={[
styles.headerText,
{
color: headerTextColor,
flex: i === 0 ? 3 : 1,
textAlign: i === 0 ? "left" : "center",
},
]}
>
{h}
</ThemedText>
))}
</View>
{rows.map((row, idx) => (
<View key={idx} style={[styles.row, idx === 0 && styles.rowBorder]}>
<View style={styles.teamCell}>
<Image source={{ uri: row.logo }} style={styles.teamLogo} />
</View>
<ThemedText style={styles.cellText}>{row.total}</ThemedText>
<ThemedText style={styles.cellText}>{row.q1}</ThemedText>
<ThemedText style={styles.cellText}>{row.q2}</ThemedText>
<ThemedText style={styles.cellText}>{row.q3}</ThemedText>
<ThemedText style={styles.cellText}>{row.q4}</ThemedText>
</View>
))}
</View>
);
}
const styles = StyleSheet.create({
container: {
margin: 16,
borderRadius: 12,
padding: 16,
// Shadow
elevation: 2,
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
header: {
flexDirection: "row",
paddingBottom: 12,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: "rgba(150,150,150,0.2)",
},
headerText: {
fontSize: 12,
fontWeight: "500",
},
row: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 12,
},
rowBorder: {
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: "rgba(150,150,150,0.1)",
},
teamCell: {
flex: 3,
flexDirection: "row",
alignItems: "center",
},
teamLogo: {
width: 24,
height: 24,
resizeMode: "contain",
},
cellText: {
flex: 1,
textAlign: "center",
fontSize: 14,
fontWeight: "500",
},
});

View File

@@ -1,9 +1,10 @@
export const API_CONFIG = {
BASE_URL: "http://192.168.1.66:8000", // Update this with your machine IP if running on device ex: http://192.168.1.5:8000
BASE_URL: "http://192.168.1.111:8000", // Update this with your machine IP if running on device ex: http://192.168.1.5:8000
TIMEOUT: 10000,
};
export const API_ENDPOINTS = {
SPORTS: "/v1/api/sports",
MATCHES_TODAY: "/v1/api/matches/today",
MATCH_DETAIL: (id: string) => `/v1/api/matches/${id}`,
};

View File

@@ -28,5 +28,32 @@
"title": "My Profile",
"name": "User Name",
"settings": "Settings"
},
"detail": {
"title": "Match Details",
"pending": "Pending",
"retry": "Retry",
"fetch_failed": "Failed to fetch details",
"not_found": "Match data not found",
"tabs": {
"info": "Details",
"h2h": "H2H",
"chat": "Chat"
},
"info_card": {
"title": "Match Info",
"country": "Country/Region",
"league": "League",
"stage": "Stage",
"stadium": "Stadium",
"referee": "Referee"
},
"score_table": {
"team": "Team",
"total": "Total"
},
"halftime": "Half: {{score}}",
"empty_h2h": "No H2H data",
"empty_chat": "Chat is not available"
}
}

View File

@@ -28,5 +28,32 @@
"title": "我的",
"name": "用户名",
"settings": "设置"
},
"detail": {
"title": "比赛详情",
"pending": "待处理",
"retry": "重试",
"fetch_failed": "获取详情失败",
"not_found": "未找到比赛数据",
"tabs": {
"info": "详情",
"h2h": "交锋往绩",
"chat": "聊天"
},
"info_card": {
"title": "比赛信息",
"country": "国家/地区",
"league": "赛事",
"stage": "阶段",
"stadium": "场馆",
"referee": "裁判"
},
"score_table": {
"team": "球队",
"total": "Total"
},
"halftime": "半场: {{score}}",
"empty_h2h": "暂无交锋数据",
"empty_chat": "聊天功能暂未开启"
}
}

View File

@@ -1,5 +1,11 @@
import { API_CONFIG, API_ENDPOINTS } from "@/constants/api";
import { ApiResponse, Match, Sport } from "@/types/api";
import {
ApiListResponse,
ApiResponse,
Match,
MatchDetailData,
Sport,
} from "@/types/api";
import axios from "axios";
const apiClient = axios.create({
@@ -12,7 +18,7 @@ const apiClient = axios.create({
export const fetchSports = async (): Promise<Sport[]> => {
try {
const response = await apiClient.get<ApiResponse<Sport>>(
const response = await apiClient.get<ApiResponse<ApiListResponse<Sport>>>(
API_ENDPOINTS.SPORTS
);
if (response.data.code === 0) {
@@ -28,7 +34,7 @@ export const fetchSports = async (): Promise<Sport[]> => {
export const fetchTodayMatches = async (sportId: number): Promise<Match[]> => {
try {
const response = await apiClient.get<ApiResponse<Match>>(
const response = await apiClient.get<ApiResponse<ApiListResponse<Match>>>(
API_ENDPOINTS.MATCHES_TODAY,
{
params: { sport_id: sportId },
@@ -44,3 +50,20 @@ export const fetchTodayMatches = async (sportId: number): Promise<Match[]> => {
throw error;
}
};
export const fetchMatchDetail = async (
id: string
): Promise<MatchDetailData> => {
try {
const response = await apiClient.get<ApiResponse<MatchDetailData>>(
API_ENDPOINTS.MATCH_DETAIL(id)
);
if (response.data.code === 0) {
return response.data.data;
}
throw new Error(response.data.message);
} catch (error) {
console.error("Fetch match detail error:", error);
throw error;
}
};

View File

@@ -22,8 +22,56 @@ export interface Match {
export interface ApiResponse<T> {
code: number;
message: string;
data: {
list: T[];
total: number;
data: T;
}
export interface ApiListResponse<T> {
list: T[];
total: number;
}
export interface MatchDetailData {
events: any[];
match: {
ID: number;
CreatedAt: string;
UpdatedAt: string;
DeletedAt: string | null;
eventKey: string;
eventDate: string;
eventTime: string;
eventHomeTeam: string;
homeTeamKey: string;
homeTeamLogo: string;
eventAwayTeam: string;
awayTeamKey: string;
awayTeamLogo: string;
eventHalftimeResult: string;
eventFinalResult: string;
eventFtResult: string;
eventPenaltyResult: string;
eventStatus: string;
countryName: string;
leagueName: string;
leagueKey: string;
leagueRound: string;
leagueSeason: string;
eventLive: string;
eventStadium: string;
eventReferee: string;
eventCountryKey: string;
leagueLogo: string;
countryLogo: string;
eventHomeFormation: string;
eventAwayFormation: string;
fkStageKey: string;
stageName: string;
leagueGroup: string;
sportId: number;
eventQuarter: string;
eventSet: string;
eventType: string;
eventToss: string;
eventManOfMatch: string;
};
}