Merge branch 'main' of https://git.ambigrat.com/shenyan/physical-expo
This commit is contained in:
@@ -49,6 +49,9 @@ export default function HomeScreen() {
|
|||||||
const [liveLeagueIdByMatchId, setLiveLeagueIdByMatchId] = useState<
|
const [liveLeagueIdByMatchId, setLiveLeagueIdByMatchId] = useState<
|
||||||
Record<string, number>
|
Record<string, number>
|
||||||
>({});
|
>({});
|
||||||
|
const [page, setPage] = useState(1);
|
||||||
|
const [total, setTotal] = useState(0);
|
||||||
|
const [loadingMore, setLoadingMore] = useState(false);
|
||||||
|
|
||||||
const deviceTimeZone = useMemo(() => {
|
const deviceTimeZone = useMemo(() => {
|
||||||
try {
|
try {
|
||||||
@@ -96,6 +99,11 @@ export default function HomeScreen() {
|
|||||||
}
|
}
|
||||||
}, [selectedSportId, selectedDate]);
|
}, [selectedSportId, selectedDate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setPage(1);
|
||||||
|
setTotal(0);
|
||||||
|
}, [selectedSportId, selectedDate, state.selectedLeagueKey]);
|
||||||
|
|
||||||
const timezoneLabel = useMemo(() => {
|
const timezoneLabel = useMemo(() => {
|
||||||
// 仅展示 UTC 偏移(不展示时区名)
|
// 仅展示 UTC 偏移(不展示时区名)
|
||||||
const offsetMin = -now.getTimezoneOffset();
|
const offsetMin = -now.getTimezoneOffset();
|
||||||
@@ -303,8 +311,8 @@ export default function HomeScreen() {
|
|||||||
try {
|
try {
|
||||||
if (selectedSportId !== null) {
|
if (selectedSportId !== null) {
|
||||||
setLoadingLeagues(true);
|
setLoadingLeagues(true);
|
||||||
const list = await fetchLeagues(selectedSportId, "");
|
const res = await fetchLeagues({ sportId: selectedSportId });
|
||||||
setLeagues(list);
|
setLeagues(res.list);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
@@ -316,11 +324,16 @@ export default function HomeScreen() {
|
|||||||
const loadMatches = async (sportId: number) => {
|
const loadMatches = async (sportId: number) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const list = await fetchTodayMatches(
|
const res = await fetchTodayMatches({
|
||||||
sportId,
|
sportId,
|
||||||
selectedDate,
|
date: selectedDate,
|
||||||
deviceTimeZone,
|
timezone: deviceTimeZone,
|
||||||
);
|
leagueKey: state.selectedLeagueKey || "",
|
||||||
|
page: 1,
|
||||||
|
pageSize: 50,
|
||||||
|
});
|
||||||
|
setPage(1);
|
||||||
|
setTotal(res.total);
|
||||||
|
|
||||||
const normalizeDate = (d: Date) => {
|
const normalizeDate = (d: Date) => {
|
||||||
const year = d.getFullYear();
|
const year = d.getFullYear();
|
||||||
@@ -333,7 +346,7 @@ export default function HomeScreen() {
|
|||||||
const selectedStr = normalizeDate(selectedDate);
|
const selectedStr = normalizeDate(selectedDate);
|
||||||
const shouldMergeLive = selectedStr === todayStr;
|
const shouldMergeLive = selectedStr === todayStr;
|
||||||
|
|
||||||
let merged: Match[] = list.map((m) => ({
|
let merged: Match[] = res.list.map((m) => ({
|
||||||
...m,
|
...m,
|
||||||
date: m.date || selectedStr,
|
date: m.date || selectedStr,
|
||||||
sportId: m.sportId ?? sportId,
|
sportId: m.sportId ?? sportId,
|
||||||
@@ -402,11 +415,9 @@ export default function HomeScreen() {
|
|||||||
let listWithFavStatus = merged;
|
let listWithFavStatus = merged;
|
||||||
|
|
||||||
if (token) {
|
if (token) {
|
||||||
// 直接传递 match.id 查询是否收藏,并更新列表状态
|
|
||||||
listWithFavStatus = await Promise.all(
|
listWithFavStatus = await Promise.all(
|
||||||
merged.map(async (m) => {
|
merged.map(async (m) => {
|
||||||
try {
|
try {
|
||||||
// 查询比赛是否已被收藏
|
|
||||||
const favRes = await checkFavorite("match", m.id);
|
const favRes = await checkFavorite("match", m.id);
|
||||||
return { ...m, fav: favRes.isFavorite };
|
return { ...m, fav: favRes.isFavorite };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -434,6 +445,41 @@ export default function HomeScreen() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const loadMoreMatches = async () => {
|
||||||
|
if (loadingMore || loading) return;
|
||||||
|
if (!selectedSportId) return;
|
||||||
|
if (matches.length >= total) return;
|
||||||
|
|
||||||
|
setLoadingMore(true);
|
||||||
|
try {
|
||||||
|
const nextPage = page + 1;
|
||||||
|
const res = await fetchTodayMatches({
|
||||||
|
sportId: selectedSportId,
|
||||||
|
date: selectedDate,
|
||||||
|
timezone: deviceTimeZone,
|
||||||
|
leagueKey: state.selectedLeagueKey || "",
|
||||||
|
page: nextPage,
|
||||||
|
pageSize: 50,
|
||||||
|
});
|
||||||
|
|
||||||
|
setPage(nextPage);
|
||||||
|
setTotal(res.total);
|
||||||
|
|
||||||
|
setMatches((prev) => {
|
||||||
|
const byId = new Map<string, Match>();
|
||||||
|
prev.forEach((m) => byId.set(m.id, m));
|
||||||
|
res.list.forEach((m) => {
|
||||||
|
if (!byId.has(m.id)) byId.set(m.id, m);
|
||||||
|
});
|
||||||
|
return Array.from(byId.values());
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
setLoadingMore(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleFavoriteToggle = (matchId: string, isFav: boolean) => {
|
const handleFavoriteToggle = (matchId: string, isFav: boolean) => {
|
||||||
setMatches((prev) => {
|
setMatches((prev) => {
|
||||||
const updated = prev.map((m) =>
|
const updated = prev.map((m) =>
|
||||||
@@ -558,6 +604,15 @@ export default function HomeScreen() {
|
|||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
<MatchCard match={item} onFavoriteToggle={handleFavoriteToggle} />
|
<MatchCard match={item} onFavoriteToggle={handleFavoriteToggle} />
|
||||||
)}
|
)}
|
||||||
|
onEndReached={loadMoreMatches}
|
||||||
|
onEndReachedThreshold={0.4}
|
||||||
|
ListFooterComponent={
|
||||||
|
loadingMore ? (
|
||||||
|
<View style={styles.footer}>
|
||||||
|
<ActivityIndicator size="small" color={Colors[theme].tint} />
|
||||||
|
</View>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
contentContainerStyle={styles.listContent}
|
contentContainerStyle={styles.listContent}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
<View style={styles.center}>
|
<View style={styles.center}>
|
||||||
@@ -567,7 +622,9 @@ export default function HomeScreen() {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<MatchesByLeague
|
<MatchesByLeague
|
||||||
matches={matches}
|
sportId={selectedSportId || 1}
|
||||||
|
date={selectedDate}
|
||||||
|
timezone={deviceTimeZone}
|
||||||
onFavoriteToggle={handleFavoriteToggle}
|
onFavoriteToggle={handleFavoriteToggle}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -650,4 +707,9 @@ const styles = StyleSheet.create({
|
|||||||
padding: 16,
|
padding: 16,
|
||||||
paddingTop: 8,
|
paddingTop: 8,
|
||||||
},
|
},
|
||||||
|
footer: {
|
||||||
|
paddingVertical: 16,
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ export function MatchCardLeague({
|
|||||||
<View style={styles.leftColumn}>
|
<View style={styles.leftColumn}>
|
||||||
{/* 如果有状态字段 match.meta (如 'FT', 'AET'), 优先显示,否则显示时间 */}
|
{/* 如果有状态字段 match.meta (如 'FT', 'AET'), 优先显示,否则显示时间 */}
|
||||||
<ThemedText style={styles.statusText}>
|
<ThemedText style={styles.statusText}>
|
||||||
{(match.meta || match.time || "").toUpperCase()}
|
{(match.time || "").toUpperCase()}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { MatchCardLeague } from "@/components/match-card-league";
|
import { MatchCardLeague } from "@/components/match-card-league";
|
||||||
import { ThemedText } from "@/components/themed-text";
|
import { ThemedText } from "@/components/themed-text";
|
||||||
import { useTheme } from "@/context/ThemeContext";
|
import { useTheme } from "@/context/ThemeContext";
|
||||||
import { Match } from "@/types/api";
|
import { fetchLeagues, fetchTodayMatches } from "@/lib/api";
|
||||||
|
import { League, Match } from "@/types/api";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Image,
|
Image,
|
||||||
@@ -23,70 +24,97 @@ if (
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface MatchesByLeagueProps {
|
interface MatchesByLeagueProps {
|
||||||
matches: Match[];
|
sportId: number;
|
||||||
|
date: Date;
|
||||||
|
timezone: string;
|
||||||
onFavoriteToggle?: (matchId: string, isFav: boolean) => void;
|
onFavoriteToggle?: (matchId: string, isFav: boolean) => void;
|
||||||
/**
|
|
||||||
* 是否支持折叠收起
|
|
||||||
* @default true
|
|
||||||
*/
|
|
||||||
enableCollapsible?: boolean;
|
enableCollapsible?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function MatchesByLeague({
|
export function MatchesByLeague({
|
||||||
matches,
|
sportId,
|
||||||
|
date,
|
||||||
|
timezone,
|
||||||
onFavoriteToggle,
|
onFavoriteToggle,
|
||||||
enableCollapsible = true,
|
enableCollapsible = true,
|
||||||
}: MatchesByLeagueProps) {
|
}: MatchesByLeagueProps) {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const isDark = theme === "dark";
|
const isDark = theme === "dark";
|
||||||
|
|
||||||
// 数据分组逻辑
|
const [leagues, setLeagues] = useState<League[]>([]);
|
||||||
const matchesByLeague = React.useMemo(() => {
|
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
|
||||||
const grouped: Record<string, Match[]> = {};
|
const [matchesByLeagueKey, setMatchesByLeagueKey] = useState<
|
||||||
matches.forEach((match) => {
|
Record<string, Match[]>
|
||||||
const league = match.league || match.leagueName || "其他";
|
>({});
|
||||||
if (!grouped[league]) {
|
const [loadingLeagueKey, setLoadingLeagueKey] = useState<
|
||||||
grouped[league] = [];
|
|
||||||
}
|
|
||||||
grouped[league].push(match);
|
|
||||||
});
|
|
||||||
return grouped;
|
|
||||||
}, [matches]);
|
|
||||||
|
|
||||||
const leagueNames = Object.keys(matchesByLeague);
|
|
||||||
|
|
||||||
// 状态:记录哪些联赛被折叠了 (Key 为联赛名称),默认全部收起
|
|
||||||
const [collapsedSections, setCollapsedSections] = useState<
|
|
||||||
Record<string, boolean>
|
Record<string, boolean>
|
||||||
>({});
|
>({});
|
||||||
|
|
||||||
// 当联赛列表变化时,确保所有联赛默认都是收起状态
|
const dateStr = React.useMemo(() => {
|
||||||
|
const year = date.getFullYear();
|
||||||
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(date.getDate()).padStart(2, "0");
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}, [date]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setCollapsedSections((prev) => {
|
let mounted = true;
|
||||||
const updated: Record<string, boolean> = {};
|
fetchLeagues({
|
||||||
leagueNames.forEach((name) => {
|
sportId,
|
||||||
updated[name] = prev[name] !== undefined ? prev[name] : true;
|
date: dateStr,
|
||||||
});
|
page: 1,
|
||||||
return updated;
|
pageSize: 200,
|
||||||
});
|
sortBy: "matchCount",
|
||||||
}, [leagueNames.join(",")]);
|
sortOrder: "desc",
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (!mounted) return;
|
||||||
|
setLeagues(res.list);
|
||||||
|
setCollapsed((prev) => {
|
||||||
|
const next: Record<string, boolean> = {};
|
||||||
|
res.list.forEach((l) => {
|
||||||
|
next[l.key] = prev[l.key] !== undefined ? prev[l.key] : true;
|
||||||
|
});
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
setMatchesByLeagueKey({});
|
||||||
|
setLoadingLeagueKey({});
|
||||||
|
})
|
||||||
|
.catch(() => { });
|
||||||
|
return () => {
|
||||||
|
mounted = false;
|
||||||
|
};
|
||||||
|
}, [sportId, dateStr]);
|
||||||
|
|
||||||
// 切换折叠状态
|
const toggleCollapse = async (leagueKey: string) => {
|
||||||
const toggleCollapse = (leagueName: string) => {
|
|
||||||
if (!enableCollapsible) return;
|
if (!enableCollapsible) return;
|
||||||
|
|
||||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
||||||
setCollapsedSections((prev) => ({
|
setCollapsed((prev) => ({ ...prev, [leagueKey]: !prev[leagueKey] }));
|
||||||
...prev,
|
|
||||||
[leagueName]: !prev[leagueName],
|
const nextCollapsed = !collapsed[leagueKey];
|
||||||
}));
|
if (nextCollapsed) return;
|
||||||
|
if (matchesByLeagueKey[leagueKey]) return;
|
||||||
|
|
||||||
|
setLoadingLeagueKey((prev) => ({ ...prev, [leagueKey]: true }));
|
||||||
|
try {
|
||||||
|
const res = await fetchTodayMatches({
|
||||||
|
sportId,
|
||||||
|
date: dateStr,
|
||||||
|
timezone,
|
||||||
|
leagueKey,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 50,
|
||||||
|
});
|
||||||
|
setMatchesByLeagueKey((prev) => ({ ...prev, [leagueKey]: res.list }));
|
||||||
|
} finally {
|
||||||
|
setLoadingLeagueKey((prev) => ({ ...prev, [leagueKey]: false }));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (leagueNames.length === 0) {
|
if (leagues.length === 0) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.emptyContainer}>
|
<View style={styles.emptyContainer}>
|
||||||
<ThemedText>暂无比赛</ThemedText>
|
<ThemedText>暂无联赛</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -99,54 +127,51 @@ export function MatchesByLeague({
|
|||||||
]}
|
]}
|
||||||
contentContainerStyle={{ paddingBottom: 40 }}
|
contentContainerStyle={{ paddingBottom: 40 }}
|
||||||
>
|
>
|
||||||
{leagueNames.map((leagueName) => {
|
{leagues.map((league) => {
|
||||||
const leagueMatches = matchesByLeague[leagueName];
|
const isCollapsed = collapsed[league.key] !== false;
|
||||||
// 取该组第一场比赛的数据作为头部信息的来源(图标、国家等)
|
const leagueMatches = matchesByLeagueKey[league.key] || [];
|
||||||
const firstMatch = leagueMatches[0];
|
const isLoading = !!loadingLeagueKey[league.key];
|
||||||
const isCollapsed = collapsedSections[leagueName];
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View key={leagueName} style={styles.leagueSection}>
|
<View key={league.key} style={styles.leagueSection}>
|
||||||
{/* 联赛头部 */}
|
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
activeOpacity={enableCollapsible ? 0.7 : 1}
|
activeOpacity={enableCollapsible && league.matchCount > 0 ? 0.7 : 1}
|
||||||
onPress={() => toggleCollapse(leagueName)}
|
onPress={() => {
|
||||||
|
if (enableCollapsible && league.matchCount > 0) {
|
||||||
|
toggleCollapse(league.key);
|
||||||
|
}
|
||||||
|
}}
|
||||||
style={styles.leagueHeaderWrapper}
|
style={styles.leagueHeaderWrapper}
|
||||||
>
|
>
|
||||||
<View style={styles.leagueHeaderLeft}>
|
<View style={styles.leagueHeaderLeft}>
|
||||||
{/* 联赛 Logo */}
|
|
||||||
<Image
|
<Image
|
||||||
source={{
|
source={{
|
||||||
uri: firstMatch.leagueLogo || (firstMatch as any).leagueLogoUrl || "https://placehold.co/40x40/png",
|
uri: league.logo || "https://placehold.co/40x40/png",
|
||||||
}}
|
}}
|
||||||
style={styles.leagueLogo}
|
style={styles.leagueLogo}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<View style={styles.leagueInfoText}>
|
<View style={styles.leagueInfoText}>
|
||||||
{/* 联赛名称 */}
|
<ThemedText style={styles.leagueTitle}>{league.name}</ThemedText>
|
||||||
<ThemedText style={styles.leagueTitle}>{leagueName}</ThemedText>
|
|
||||||
|
|
||||||
{/* 国家信息行 */}
|
|
||||||
<View style={styles.countryRow}>
|
<View style={styles.countryRow}>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: firstMatch.countryLogo || (firstMatch as any).countryFlagUrl || "https://placehold.co/20x20/png" }}
|
source={{ uri: league.countryLogo || "https://placehold.co/20x20/png" }}
|
||||||
style={styles.countryFlag}
|
style={styles.countryFlag}
|
||||||
/>
|
/>
|
||||||
<ThemedText style={styles.countryName}>
|
<ThemedText style={styles.countryName}>
|
||||||
{firstMatch.countryName || (firstMatch as any).countryName || "International"}
|
{league.countryName || "International"}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.leagueHeaderRight}>
|
<View style={styles.leagueHeaderRight}>
|
||||||
{/* 比赛数量 */}
|
|
||||||
<ThemedText style={styles.matchCount}>
|
<ThemedText style={styles.matchCount}>
|
||||||
{leagueMatches.length}
|
{league.matchCount}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
|
|
||||||
{/* 折叠箭头 (仅当支持折叠时显示) */}
|
{enableCollapsible && league.matchCount > 0 && (
|
||||||
{enableCollapsible && (
|
|
||||||
<ThemedText style={styles.chevron}>
|
<ThemedText style={styles.chevron}>
|
||||||
{isCollapsed ? "⌄" : "⌃"}
|
{isCollapsed ? "⌄" : "⌃"}
|
||||||
</ThemedText>
|
</ThemedText>
|
||||||
@@ -154,23 +179,74 @@ export function MatchesByLeague({
|
|||||||
</View>
|
</View>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
|
|
||||||
{/* 比赛列表内容 (根据状态显示/隐藏) */}
|
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<View style={styles.matchListContainer}>
|
<View style={styles.matchListContainer}>
|
||||||
{leagueMatches.map((match, index) => (
|
{isLoading ? (
|
||||||
<View
|
<>
|
||||||
key={match.id}
|
{[0, 1, 2].map((i) => (
|
||||||
style={[
|
<View
|
||||||
styles.matchCardWrapper,
|
key={i}
|
||||||
index < leagueMatches.length - 1 && styles.matchCardDivider
|
style={[
|
||||||
]}
|
styles.matchCardWrapper,
|
||||||
>
|
i < 2 && styles.matchCardDivider,
|
||||||
<MatchCardLeague
|
]}
|
||||||
match={match}
|
>
|
||||||
onFavoriteToggle={onFavoriteToggle}
|
<View style={styles.skeletonRow}>
|
||||||
/>
|
<View style={styles.leftColumn}>
|
||||||
</View>
|
<View
|
||||||
))}
|
style={[styles.skeletonLine, { width: 30, marginBottom: 6 }]}
|
||||||
|
/>
|
||||||
|
<View
|
||||||
|
style={[styles.skeletonLine, { width: 24 }]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={styles.teamsColumn}>
|
||||||
|
<View style={styles.skeletonTeamRow}>
|
||||||
|
<View style={styles.skeletonAvatar} />
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.skeletonLine,
|
||||||
|
{ flex: 1 },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={[styles.skeletonTeamRow, { marginTop: 8 }]}>
|
||||||
|
<View style={styles.skeletonAvatar} />
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
styles.skeletonLine,
|
||||||
|
{ flex: 1 },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={styles.rightWrapper}>
|
||||||
|
<View style={styles.skeletonScoreBox} />
|
||||||
|
<View style={styles.favoriteButton}>
|
||||||
|
<View style={styles.skeletonCircle} />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
leagueMatches.map((match, index) => (
|
||||||
|
<View
|
||||||
|
key={match.id}
|
||||||
|
style={[
|
||||||
|
styles.matchCardWrapper,
|
||||||
|
index < leagueMatches.length - 1 &&
|
||||||
|
styles.matchCardDivider,
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<MatchCardLeague
|
||||||
|
match={match}
|
||||||
|
onFavoriteToggle={onFavoriteToggle}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
@@ -252,13 +328,67 @@ const styles = StyleSheet.create({
|
|||||||
marginHorizontal: 16,
|
marginHorizontal: 16,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
},
|
},
|
||||||
|
// 布局与 MatchCardLeague 保持一致,便于骨架对齐
|
||||||
|
leftColumn: {
|
||||||
|
width: 50,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
teamsColumn: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: "center",
|
||||||
|
paddingRight: 8,
|
||||||
|
},
|
||||||
|
rightWrapper: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
matchCardWrapper: {
|
matchCardWrapper: {
|
||||||
// 卡片包装器
|
|
||||||
},
|
},
|
||||||
matchCardDivider: {
|
matchCardDivider: {
|
||||||
borderBottomWidth: 0.5,
|
borderBottomWidth: 0.5,
|
||||||
borderBottomColor: "#3A3A3C",
|
borderBottomColor: "#3A3A3C",
|
||||||
},
|
},
|
||||||
|
favoriteButton: {
|
||||||
|
paddingLeft: 12,
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
skeletonRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 14,
|
||||||
|
},
|
||||||
|
skeletonLine: {
|
||||||
|
height: 8,
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: "#2C2C2E",
|
||||||
|
},
|
||||||
|
skeletonAvatar: {
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
borderRadius: 10,
|
||||||
|
backgroundColor: "#2C2C2E",
|
||||||
|
marginRight: 10,
|
||||||
|
},
|
||||||
|
skeletonTeamRow: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
},
|
||||||
|
skeletonScoreBox: {
|
||||||
|
width: 24,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: "#2C2C2E",
|
||||||
|
},
|
||||||
|
skeletonCircle: {
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: "#2C2C2E",
|
||||||
|
},
|
||||||
emptyContainer: {
|
emptyContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
|
|||||||
84
lib/api.ts
84
lib/api.ts
@@ -115,22 +115,22 @@ export const fetchCountries = async (): Promise<Country[]> => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchLeagues = async (
|
export const fetchLeagues = async (params: {
|
||||||
sportId: number,
|
sportId?: number;
|
||||||
countryKey: string
|
countryKey?: string;
|
||||||
): Promise<League[]> => {
|
date?: string;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
sortBy?: "name" | "matchCount" | "createdAt";
|
||||||
|
sortOrder?: "asc" | "desc";
|
||||||
|
}): Promise<ApiListResponse<League>> => {
|
||||||
try {
|
try {
|
||||||
const response = await apiClient.get<ApiResponse<ApiListResponse<League>>>(
|
const response = await apiClient.get<ApiResponse<ApiListResponse<League>>>(
|
||||||
API_ENDPOINTS.LEAGUES,
|
API_ENDPOINTS.LEAGUES,
|
||||||
{
|
{ params }
|
||||||
params: {
|
|
||||||
sportId,
|
|
||||||
countryKey,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
if (response.data.code === 0) {
|
if (response.data.code === 0) {
|
||||||
return response.data.data.list;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
throw new Error(response.data.message);
|
throw new Error(response.data.message);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -139,48 +139,54 @@ export const fetchLeagues = async (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fetchTodayMatches = async (
|
export const fetchTodayMatches = async (options: {
|
||||||
sportId: number,
|
sportId: number;
|
||||||
date?: Date | string,
|
date?: Date | string;
|
||||||
timezone?: string
|
timezone?: string;
|
||||||
): Promise<Match[]> => {
|
leagueKey?: string;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
}): Promise<ApiListResponse<Match>> => {
|
||||||
try {
|
try {
|
||||||
const params: { sportId: number; date?: string; timezone?: string } = {
|
const params: {
|
||||||
sportId,
|
sportId: number;
|
||||||
|
date?: string;
|
||||||
|
timezone?: string;
|
||||||
|
leagueKey?: string;
|
||||||
|
page?: number;
|
||||||
|
pageSize?: number;
|
||||||
|
} = {
|
||||||
|
sportId: options.sportId,
|
||||||
|
leagueKey: options.leagueKey,
|
||||||
|
page: options.page,
|
||||||
|
pageSize: options.pageSize,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 如果提供了日期,格式化为 YYYY-MM-DD 格式
|
if (options.date) {
|
||||||
if (date) {
|
if (options.date instanceof Date) {
|
||||||
let dateStr: string;
|
const year = options.date.getFullYear();
|
||||||
if (date instanceof Date) {
|
const month = String(options.date.getMonth() + 1).padStart(2, "0");
|
||||||
const year = date.getFullYear();
|
const day = String(options.date.getDate()).padStart(2, "0");
|
||||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
params.date = `${year}-${month}-${day}`;
|
||||||
const day = String(date.getDate()).padStart(2, "0");
|
|
||||||
dateStr = `${year}-${month}-${day}`;
|
|
||||||
} else {
|
} else {
|
||||||
dateStr = date;
|
params.date = options.date;
|
||||||
}
|
}
|
||||||
params.date = dateStr;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果提供了时区,传给后端;不传则由后端使用本地时区
|
if (options.timezone) {
|
||||||
if (timezone) {
|
params.timezone = options.timezone;
|
||||||
params.timezone = timezone;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await apiClient.get<ApiResponse<ApiListResponse<Match>>>(
|
const response = await apiClient.get<ApiResponse<ApiListResponse<Match>>>(
|
||||||
API_ENDPOINTS.MATCHES_TODAY,
|
API_ENDPOINTS.MATCHES_TODAY,
|
||||||
{
|
{ params }
|
||||||
params,
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
if (response.data.code === 0) {
|
if (response.data.code === 0) {
|
||||||
return response.data.data.list;
|
return response.data.data;
|
||||||
}
|
}
|
||||||
throw new Error(response.data.message);
|
throw new Error(response.data.message);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Fetch matches error:", error);
|
console.error("Fetch matches error:", error);
|
||||||
// Let the caller handle errors; rethrow the original error
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -214,9 +220,9 @@ export const fetchLiveScore = async (
|
|||||||
// });
|
// });
|
||||||
try {
|
try {
|
||||||
const params: { sport_id: number; league_id?: number; timezone?: string } =
|
const params: { sport_id: number; league_id?: number; timezone?: string } =
|
||||||
{
|
{
|
||||||
sport_id: sportId,
|
sport_id: sportId,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (leagueId) {
|
if (leagueId) {
|
||||||
params.league_id = leagueId;
|
params.league_id = leagueId;
|
||||||
|
|||||||
@@ -88,11 +88,11 @@ export interface LiveScoreMatch {
|
|||||||
substitutes?: {
|
substitutes?: {
|
||||||
time: string;
|
time: string;
|
||||||
home_scorer:
|
home_scorer:
|
||||||
| { in: string; out: string; in_id: number; out_id: number }
|
| { in: string; out: string; in_id: number; out_id: number }
|
||||||
| any[];
|
| any[];
|
||||||
away_scorer:
|
away_scorer:
|
||||||
| { in: string; out: string; in_id: number; out_id: number }
|
| { in: string; out: string; in_id: number; out_id: number }
|
||||||
| any[];
|
| any[];
|
||||||
info: string;
|
info: string;
|
||||||
info_time: string;
|
info_time: string;
|
||||||
score: string;
|
score: string;
|
||||||
@@ -153,6 +153,7 @@ export interface League {
|
|||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
matchCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GoalEvent {
|
export interface GoalEvent {
|
||||||
|
|||||||
Reference in New Issue
Block a user