实现详情页

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

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