联赛显示
This commit is contained in:
@@ -121,7 +121,7 @@ export function MatchCardLeague({
|
||||
<View style={styles.leftColumn}>
|
||||
{/* 如果有状态字段 match.meta (如 'FT', 'AET'), 优先显示,否则显示时间 */}
|
||||
<ThemedText style={styles.statusText}>
|
||||
{(match.meta || match.time || "").toUpperCase()}
|
||||
{(match.time || "").toUpperCase()}
|
||||
</ThemedText>
|
||||
</View>
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { MatchCardLeague } from "@/components/match-card-league";
|
||||
import { ThemedText } from "@/components/themed-text";
|
||||
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 {
|
||||
Image,
|
||||
@@ -23,70 +24,97 @@ if (
|
||||
}
|
||||
|
||||
interface MatchesByLeagueProps {
|
||||
matches: Match[];
|
||||
sportId: number;
|
||||
date: Date;
|
||||
timezone: string;
|
||||
onFavoriteToggle?: (matchId: string, isFav: boolean) => void;
|
||||
/**
|
||||
* 是否支持折叠收起
|
||||
* @default true
|
||||
*/
|
||||
enableCollapsible?: boolean;
|
||||
}
|
||||
|
||||
|
||||
export function MatchesByLeague({
|
||||
matches,
|
||||
sportId,
|
||||
date,
|
||||
timezone,
|
||||
onFavoriteToggle,
|
||||
enableCollapsible = true,
|
||||
}: MatchesByLeagueProps) {
|
||||
const { theme } = useTheme();
|
||||
const isDark = theme === "dark";
|
||||
|
||||
// 数据分组逻辑
|
||||
const matchesByLeague = React.useMemo(() => {
|
||||
const grouped: Record<string, Match[]> = {};
|
||||
matches.forEach((match) => {
|
||||
const league = match.league || match.leagueName || "其他";
|
||||
if (!grouped[league]) {
|
||||
grouped[league] = [];
|
||||
}
|
||||
grouped[league].push(match);
|
||||
});
|
||||
return grouped;
|
||||
}, [matches]);
|
||||
|
||||
const leagueNames = Object.keys(matchesByLeague);
|
||||
|
||||
// 状态:记录哪些联赛被折叠了 (Key 为联赛名称),默认全部收起
|
||||
const [collapsedSections, setCollapsedSections] = useState<
|
||||
const [leagues, setLeagues] = useState<League[]>([]);
|
||||
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
|
||||
const [matchesByLeagueKey, setMatchesByLeagueKey] = useState<
|
||||
Record<string, Match[]>
|
||||
>({});
|
||||
const [loadingLeagueKey, setLoadingLeagueKey] = useState<
|
||||
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(() => {
|
||||
setCollapsedSections((prev) => {
|
||||
const updated: Record<string, boolean> = {};
|
||||
leagueNames.forEach((name) => {
|
||||
updated[name] = prev[name] !== undefined ? prev[name] : true;
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
}, [leagueNames.join(",")]);
|
||||
let mounted = true;
|
||||
fetchLeagues({
|
||||
sportId,
|
||||
date: dateStr,
|
||||
page: 1,
|
||||
pageSize: 200,
|
||||
sortBy: "matchCount",
|
||||
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 = (leagueName: string) => {
|
||||
const toggleCollapse = async (leagueKey: string) => {
|
||||
if (!enableCollapsible) return;
|
||||
|
||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
|
||||
setCollapsedSections((prev) => ({
|
||||
...prev,
|
||||
[leagueName]: !prev[leagueName],
|
||||
}));
|
||||
setCollapsed((prev) => ({ ...prev, [leagueKey]: !prev[leagueKey] }));
|
||||
|
||||
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 (
|
||||
<View style={styles.emptyContainer}>
|
||||
<ThemedText>暂无比赛</ThemedText>
|
||||
<ThemedText>暂无联赛</ThemedText>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -99,54 +127,51 @@ export function MatchesByLeague({
|
||||
]}
|
||||
contentContainerStyle={{ paddingBottom: 40 }}
|
||||
>
|
||||
{leagueNames.map((leagueName) => {
|
||||
const leagueMatches = matchesByLeague[leagueName];
|
||||
// 取该组第一场比赛的数据作为头部信息的来源(图标、国家等)
|
||||
const firstMatch = leagueMatches[0];
|
||||
const isCollapsed = collapsedSections[leagueName];
|
||||
{leagues.map((league) => {
|
||||
const isCollapsed = collapsed[league.key] !== false;
|
||||
const leagueMatches = matchesByLeagueKey[league.key] || [];
|
||||
const isLoading = !!loadingLeagueKey[league.key];
|
||||
|
||||
return (
|
||||
<View key={leagueName} style={styles.leagueSection}>
|
||||
{/* 联赛头部 */}
|
||||
<View key={league.key} style={styles.leagueSection}>
|
||||
<TouchableOpacity
|
||||
activeOpacity={enableCollapsible ? 0.7 : 1}
|
||||
onPress={() => toggleCollapse(leagueName)}
|
||||
activeOpacity={enableCollapsible && league.matchCount > 0 ? 0.7 : 1}
|
||||
onPress={() => {
|
||||
if (enableCollapsible && league.matchCount > 0) {
|
||||
toggleCollapse(league.key);
|
||||
}
|
||||
}}
|
||||
style={styles.leagueHeaderWrapper}
|
||||
>
|
||||
<View style={styles.leagueHeaderLeft}>
|
||||
{/* 联赛 Logo */}
|
||||
<Image
|
||||
source={{
|
||||
uri: firstMatch.leagueLogo || (firstMatch as any).leagueLogoUrl || "https://placehold.co/40x40/png",
|
||||
uri: league.logo || "https://placehold.co/40x40/png",
|
||||
}}
|
||||
style={styles.leagueLogo}
|
||||
/>
|
||||
|
||||
<View style={styles.leagueInfoText}>
|
||||
{/* 联赛名称 */}
|
||||
<ThemedText style={styles.leagueTitle}>{leagueName}</ThemedText>
|
||||
<ThemedText style={styles.leagueTitle}>{league.name}</ThemedText>
|
||||
|
||||
{/* 国家信息行 */}
|
||||
<View style={styles.countryRow}>
|
||||
<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}
|
||||
/>
|
||||
<ThemedText style={styles.countryName}>
|
||||
{firstMatch.countryName || (firstMatch as any).countryName || "International"}
|
||||
{league.countryName || "International"}
|
||||
</ThemedText>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.leagueHeaderRight}>
|
||||
{/* 比赛数量 */}
|
||||
<ThemedText style={styles.matchCount}>
|
||||
{leagueMatches.length}
|
||||
{league.matchCount}
|
||||
</ThemedText>
|
||||
|
||||
{/* 折叠箭头 (仅当支持折叠时显示) */}
|
||||
{enableCollapsible && (
|
||||
{enableCollapsible && league.matchCount > 0 && (
|
||||
<ThemedText style={styles.chevron}>
|
||||
{isCollapsed ? "⌄" : "⌃"}
|
||||
</ThemedText>
|
||||
@@ -154,23 +179,74 @@ export function MatchesByLeague({
|
||||
</View>
|
||||
</TouchableOpacity>
|
||||
|
||||
{/* 比赛列表内容 (根据状态显示/隐藏) */}
|
||||
{!isCollapsed && (
|
||||
<View style={styles.matchListContainer}>
|
||||
{leagueMatches.map((match, index) => (
|
||||
<View
|
||||
key={match.id}
|
||||
style={[
|
||||
styles.matchCardWrapper,
|
||||
index < leagueMatches.length - 1 && styles.matchCardDivider
|
||||
]}
|
||||
>
|
||||
<MatchCardLeague
|
||||
match={match}
|
||||
onFavoriteToggle={onFavoriteToggle}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
{isLoading ? (
|
||||
<>
|
||||
{[0, 1, 2].map((i) => (
|
||||
<View
|
||||
key={i}
|
||||
style={[
|
||||
styles.matchCardWrapper,
|
||||
i < 2 && styles.matchCardDivider,
|
||||
]}
|
||||
>
|
||||
<View style={styles.skeletonRow}>
|
||||
<View style={styles.leftColumn}>
|
||||
<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>
|
||||
@@ -252,13 +328,67 @@ const styles = StyleSheet.create({
|
||||
marginHorizontal: 16,
|
||||
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: {
|
||||
// 卡片包装器
|
||||
},
|
||||
matchCardDivider: {
|
||||
borderBottomWidth: 0.5,
|
||||
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: {
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
|
||||
Reference in New Issue
Block a user