实现详情页

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,
},
});