Files
physical-expo/app/(tabs)/upcoming.tsx
2026-01-14 10:31:55 +08:00

340 lines
11 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 { HomeHeader } from "@/components/home-header";
import { LeagueModal } from "@/components/league-modal";
import { SelectionModal } from "@/components/selection-modal";
import { ThemedText } from "@/components/themed-text";
import { ThemedView } from "@/components/themed-view";
import { IconSymbol } from "@/components/ui/icon-symbol";
import { UpcomingMatchCard } from "@/components/upcoming-match-card";
import { Colors } from "@/constants/theme";
import { useTheme } from "@/context/ThemeContext";
import { fetchLeagues, fetchSports, fetchUpcomingMatches } from "@/lib/api";
import { League, Sport, UpcomingMatch } from "@/types/api";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import {
ActivityIndicator,
FlatList,
StyleSheet,
TouchableOpacity,
View,
} from "react-native";
export default function HomeScreen() {
const { theme } = useTheme();
const { t } = useTranslation();
const isDark = theme === "dark";
const iconColor = isDark ? Colors.dark.icon : Colors.light.icon;
const filterBg = isDark ? "#2C2C2E" : "#F2F2F7";
const [sports, setSports] = useState<Sport[]>([]);
const [leagues, setLeagues] = useState<League[]>([]);
const [matches, setMatches] = useState<UpcomingMatch[]>([]);
const [loading, setLoading] = useState(true);
const [loadingLeagues, setLoadingLeagues] = useState(false);
// Selection States
// 默认足球
const [selectedSportId, setSelectedSportId] = useState<number | null>(1);
const [selectedLeagueKey, setSelectedLeagueKey] = useState<string>(""); // 默认空字符串
// Modal Visibilities
const [showSportModal, setShowSportModal] = useState(false);
const [showLeagueModal, setShowLeagueModal] = useState(false);
// Load Sports and Leagues
useEffect(() => {
loadSports();
loadLeagues();
}, []);
// Load Matches when sport or league changes
useEffect(() => {
if (selectedSportId !== null) {
loadMatches();
}
}, [selectedSportId, selectedLeagueKey]);
// Load Leagues when sport changes
useEffect(() => {
if (selectedSportId !== null) {
loadLeagues();
}
}, [selectedSportId]);
const loadSports = async () => {
try {
const apiList = await fetchSports();
// 创建8个运动的完整列表
const defaultSports: Sport[] = [
{ id: 1, name: "football", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" },
{ id: 2, name: "basketball", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" },
{ id: 3, name: "tennis", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" },
{ id: 4, name: "cricket", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" },
{ id: 5, name: "baseball", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" },
{ id: 6, name: "badminton", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" },
{ id: 7, name: "snooker", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" },
{ id: 8, name: "volleyball", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" },
];
// 合并API返回的运动和默认列表
const sportsMap = new Map<number, Sport>();
apiList.forEach((sport) => {
sportsMap.set(sport.id, sport);
});
// 补充默认运动到8个
defaultSports.forEach((sport) => {
if (!sportsMap.has(sport.id)) {
sportsMap.set(sport.id, sport);
}
});
const allSports = Array.from(sportsMap.values())
.sort((a, b) => a.id - b.id)
.slice(0, 8);
setSports(allSports);
} catch (e) {
console.error(e);
// API失败时使用默认8个运动
const defaultSports: Sport[] = [
{ id: 1, name: "football", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" },
{ id: 2, name: "basketball", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" },
{ id: 3, name: "tennis", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" },
{ id: 4, name: "cricket", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" },
{ id: 5, name: "baseball", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" },
{ id: 6, name: "badminton", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" },
{ id: 7, name: "snooker", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" },
{ id: 8, name: "volleyball", description: "", icon: "", isActive: true, updatedAt: "", createdAt: "" },
];
setSports(defaultSports);
setSelectedSportId(1);
}
};
// 加载联赛
const loadLeagues = async () => {
try {
if (selectedSportId !== null) {
setLoadingLeagues(true);
const list = await fetchLeagues(selectedSportId, "");
setLeagues(list);
}
} catch (e) {
console.error(e);
} finally {
setLoadingLeagues(false);
}
};
const loadMatches = async () => {
if (selectedSportId === null) return;
setLoading(true);
try {
// 使用 fetchUpcomingMatches默认 leagueKey 为空字符串
const list = await fetchUpcomingMatches(selectedSportId, selectedLeagueKey || "");
setMatches(list);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
const currentSport = sports.find((s) => s.id === selectedSportId);
// 获取当前运动的国际化名称
const getSportName = (sport: Sport | undefined): string => {
if (!sport) return t("home.select_sport");
const sportKeyMap: { [key: number]: string } = {
1: "football",
2: "basketball",
3: "tennis",
4: "cricket",
5: "baseball",
6: "badminton",
7: "snooker",
8: "volleyball",
};
const sportKey = sportKeyMap[sport.id] || sport.name.toLowerCase();
return t(`sports.${sportKey}`, { defaultValue: sport.name });
};
const handleLeagueSelect = (leagueKey: string) => {
setSelectedLeagueKey(leagueKey);
console.log("Selected league:", leagueKey);
};
const renderHeader = () => (
<View style={styles.filterContainer}>
{/* League Filter (Fixed) */}
<TouchableOpacity
style={[styles.filterBtn, { backgroundColor: filterBg }]}
disabled
>
<IconSymbol name="trophy-outline" size={18} color={iconColor} />
<ThemedText style={styles.filterText}>
{t("home.league")}
</ThemedText>
</TouchableOpacity>
{/* Sport Selector */}
<TouchableOpacity
style={[
styles.filterBtn,
styles.mainFilterBtn,
{ backgroundColor: filterBg },
]}
onPress={() => setShowSportModal(true)}
>
<IconSymbol name="football-outline" size={18} color={iconColor} />
<ThemedText style={styles.filterText}>
{getSportName(currentSport)}
</ThemedText>
</TouchableOpacity>
{/* League Selector */}
<TouchableOpacity
style={[styles.filterBtn, { backgroundColor: filterBg }]}
onPress={() => {
// 立即显示弹窗
setShowLeagueModal(true);
// 如果联赛列表为空立即设置loading状态并加载
if (selectedSportId !== null) {
if (leagues.length === 0) {
setLoadingLeagues(true);
}
loadLeagues();
}
}}
>
<IconSymbol name="trophy-outline" size={18} color={iconColor} />
<ThemedText style={styles.filterText} numberOfLines={1}>
{selectedLeagueKey
? leagues.find(l => l.key === selectedLeagueKey)?.name || t("home.select_league")
: t("home.all_leagues")}
</ThemedText>
</TouchableOpacity>
</View>
);
return (
<ThemedView style={styles.container}>
<HomeHeader />
{renderHeader()}
{loading ? (
<View style={styles.center}>
<ActivityIndicator size="large" color={Colors[theme].tint} />
<ThemedText style={{ marginTop: 10 }}>{t("home.loading")}</ThemedText>
</View>
) : (
<FlatList
data={matches}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => <UpcomingMatchCard match={item} />}
contentContainerStyle={styles.listContent}
ListEmptyComponent={
<View style={styles.center}>
<ThemedText>{t("home.no_matches")}</ThemedText>
</View>
}
/>
)}
{/* Modals */}
<SelectionModal
visible={showSportModal}
onClose={() => setShowSportModal(false)}
title={t("home.select_sport")}
options={sports.map((s) => {
const sportKeyMap: { [key: number]: string } = {
1: "football",
2: "basketball",
3: "tennis",
4: "cricket",
5: "baseball",
6: "badminton",
7: "snooker",
8: "volleyball",
};
const sportKey = sportKeyMap[s.id] || s.name.toLowerCase();
return {
id: s.id,
label: s.name, // 保留原始名称用于图标识别
value: s.id,
icon: s.icon,
};
})}
selectedValue={selectedSportId}
onSelect={setSelectedSportId}
/>
<LeagueModal
visible={showLeagueModal}
onClose={() => setShowLeagueModal(false)}
leagues={leagues}
selectedLeagueKey={selectedLeagueKey}
loading={loadingLeagues}
onSelect={handleLeagueSelect}
/>
</ThemedView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
center: {
flex: 1,
justifyContent: "center",
alignItems: "center",
paddingTop: 50,
},
filterContainer: {
flexDirection: "row",
paddingHorizontal: 16,
paddingVertical: 12,
gap: 12,
},
filterBtn: {
flex: 1,
height: 44, // Increased from 36
flexDirection: "column", // Stacked logic for Date, or Row for others
justifyContent: "center",
alignItems: "center",
borderRadius: 8, // Rounded corners
// iOS shadow
shadowColor: "#000",
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
// Android elevation
elevation: 2,
},
mainFilterBtn: {
flex: 2, // Wider for sport
flexDirection: "row",
gap: 8,
},
filterText: {
fontSize: 14,
fontWeight: "500",
},
dateDayText: {
fontSize: 16,
fontWeight: "bold",
},
dateMonthText: {
fontSize: 10,
opacity: 0.6,
},
listContent: {
padding: 16,
paddingTop: 8,
},
});