Files
physical-expo/app/(tabs)/finished.tsx
2026-01-15 16:52:03 +08:00

560 lines
15 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 { MatchCard } from "@/components/match-card";
import { SelectionModal } from "@/components/selection-modal";
import { CalendarModal } from "@/components/simple-calendar";
import { ThemedText } from "@/components/themed-text";
import { ThemedView } from "@/components/themed-view";
import { IconSymbol } from "@/components/ui/icon-symbol";
import { Colors } from "@/constants/theme";
import { useAppState } from "@/context/AppStateContext";
import { useTheme } from "@/context/ThemeContext";
import { fetchLeagues, fetchSports, fetchTodayMatches } from "@/lib/api";
import { League, Match, Sport } from "@/types/api";
import React, { useEffect, useMemo, 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 { state, updateSportId, updateDate, updateLeagueKey, updateTimezone } =
useAppState();
const [sports, setSports] = useState<Sport[]>([]);
const [leagues, setLeagues] = useState<League[]>([]);
const [matches, setMatches] = useState<Match[]>([]);
const [loading, setLoading] = useState(true);
const [loadingLeagues, setLoadingLeagues] = useState(false);
const [now, setNow] = useState(() => new Date());
const deviceTimeZone = useMemo(() => {
try {
console.log(
"deviceTimeZone",
Intl.DateTimeFormat().resolvedOptions().timeZone
);
return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
} catch {
return "UTC";
}
}, []);
// 初始化时同步设备时区到 Context
useEffect(() => {
updateTimezone(deviceTimeZone);
}, [deviceTimeZone]);
// Selection States - 从 Context 读取
const selectedSportId = state.selectedSportId;
const selectedDate = state.selectedDate;
const selectedLeagueKey = state.selectedLeagueKey;
const [filterMode, setFilterMode] = useState<"time" | "league">("time"); // 时间或联赛模式
// Modal Visibilities
const [showSportModal, setShowSportModal] = useState(false);
const [showCalendarModal, setShowCalendarModal] = useState(false);
const [showLeagueModal, setShowLeagueModal] = useState(false);
// Load Sports and Leagues
useEffect(() => {
loadSports();
loadLeagues();
}, []);
// 当前时间:每秒刷新(用于展示“现在时间/时区”)
useEffect(() => {
const id = setInterval(() => setNow(new Date()), 1000);
return () => clearInterval(id);
}, []);
// Load Matches when sport or date changes
useEffect(() => {
if (selectedSportId !== null) {
loadMatches(selectedSportId);
}
}, [selectedSportId, selectedDate]);
const timezoneLabel = useMemo(() => {
// 仅展示 UTC 偏移(不展示时区名)
const offsetMin = -now.getTimezoneOffset();
const sign = offsetMin >= 0 ? "+" : "-";
const abs = Math.abs(offsetMin);
const hh = String(Math.floor(abs / 60)).padStart(2, "0");
const mm = String(abs % 60).padStart(2, "0");
return `UTC${sign}${hh}`;
}, [now]);
const nowTimeText = useMemo(() => {
const hh = String(now.getHours()).padStart(2, "0");
const mm = String(now.getMinutes()).padStart(2, "0");
const ss = String(now.getSeconds()).padStart(2, "0");
return `${hh}:${mm}`;
}, [now]);
// 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);
}
};
// 加载联赛
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 (sportId: number) => {
setLoading(true);
try {
const list = await fetchTodayMatches(
sportId,
selectedDate,
deviceTimeZone
);
setMatches(list);
} catch (e) {
console.error(e);
} finally {
setLoading(false);
}
};
const currentSport = sports.find((s) => s.id === selectedSportId);
const filteredMatches = useMemo(
() => matches.filter((m) => m.scoreText && m.scoreText.trim() !== "-"),
[matches]
);
// 获取当前运动的国际化名称
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) => {
updateLeagueKey(leagueKey);
console.log("Selected league:", leagueKey);
};
// 缓存运动选项,避免每次渲染都重新计算
const sportOptions = useMemo(() => {
return 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,
};
});
}, [sports]);
const renderHeader = () => (
<View style={styles.filterContainer}>
{/* Time/League Filter Toggle */}
<TouchableOpacity
style={[styles.filterBtn, { backgroundColor: filterBg }]}
onPress={() => setFilterMode(filterMode === "time" ? "league" : "time")}
>
<IconSymbol
name={filterMode === "time" ? "time-outline" : "trophy-outline"}
size={18}
color={iconColor}
/>
<ThemedText style={styles.filterText}>
{filterMode === "time" ? t("home.time") : 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>
{/* Date/League Selector */}
{filterMode === "time" ? (
<TouchableOpacity
style={[styles.filterBtn, { backgroundColor: filterBg }]}
onPress={() => setShowCalendarModal(true)}
>
<ThemedText style={styles.dateDayText}>
{selectedDate.getDate()}
</ThemedText>
<ThemedText style={styles.dateMonthText}>
{timezoneLabel} {nowTimeText}
</ThemedText>
</TouchableOpacity>
) : (
<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.select_league")}
</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={filteredMatches}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <MatchCard match={item} />}
contentContainerStyle={styles.listContent}
ListEmptyComponent={
<View style={styles.center}>
<ThemedText>{t("home.no_matches")}</ThemedText>
</View>
}
/>
)}
{/* Modals - 条件渲染,只在可见时渲染 */}
{showSportModal && (
<SelectionModal
visible={showSportModal}
onClose={() => setShowSportModal(false)}
title={t("home.select_sport")}
options={sportOptions}
selectedValue={selectedSportId}
onSelect={updateSportId}
/>
)}
{showCalendarModal && (
<CalendarModal
visible={showCalendarModal}
onClose={() => setShowCalendarModal(false)}
selectedDate={selectedDate}
onSelectDate={updateDate}
/>
)}
{showLeagueModal && (
<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,
lineHeight: 16,
fontWeight: "bold",
},
dateMonthText: {
fontSize: 12,
lineHeight: 12,
opacity: 0.6,
},
listContent: {
padding: 16,
paddingTop: 8,
},
});