更新联赛

This commit is contained in:
xianyi
2026-01-20 11:58:43 +08:00
parent fa13b3c17d
commit ebb40ec8f9
4 changed files with 295 additions and 241 deletions

View File

@@ -1,11 +1,13 @@
import { MatchCardLeague } from "@/components/match-card-league";
import { ThemedText } from "@/components/themed-text";
import { Colors } from "@/constants/theme";
import { useTheme } from "@/context/ThemeContext";
import { fetchLeagues, fetchTodayMatches } from "@/lib/api";
import { League, Match } from "@/types/api";
import { Image } from "expo-image";
import React, { useState } from "react";
import {
Image,
ActivityIndicator,
LayoutAnimation,
Platform,
ScrollView,
@@ -14,6 +16,11 @@ import {
UIManager,
View,
} from "react-native";
import Animated, {
useAnimatedStyle,
useSharedValue,
withTiming,
} from "react-native-reanimated";
// 开启 Android 上的 LayoutAnimation
if (
@@ -41,6 +48,34 @@ export function MatchesByLeague({
const { theme } = useTheme();
const isDark = theme === "dark";
function ChevronIcon({ isCollapsed, isDark }: { isCollapsed: boolean; isDark: boolean }) {
const rotation = useSharedValue(isCollapsed ? 0 : 180);
React.useEffect(() => {
rotation.value = withTiming(isCollapsed ? 0 : 180, { duration: 200 });
}, [isCollapsed, rotation]);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ rotate: `${rotation.value}deg` }],
}));
return (
<View style={styles.leagueHeaderRight}>
<Animated.View style={[styles.chevronContainer, animatedStyle]}>
<Image
source={
isDark
? require("@/assets/down.svg")
: require("@/assets/down-light.svg")
}
style={styles.chevronIcon}
contentFit="contain"
/>
</Animated.View>
</View>
);
}
const [leagues, setLeagues] = useState<League[]>([]);
const [collapsed, setCollapsed] = useState<Record<string, boolean>>({});
const [matchesByLeagueKey, setMatchesByLeagueKey] = useState<
@@ -49,6 +84,7 @@ export function MatchesByLeague({
const [loadingLeagueKey, setLoadingLeagueKey] = useState<
Record<string, boolean>
>({});
const [loadingLeagues, setLoadingLeagues] = useState(true);
const dateStr = React.useMemo(() => {
const year = date.getFullYear();
@@ -59,6 +95,7 @@ export function MatchesByLeague({
React.useEffect(() => {
let mounted = true;
setLoadingLeagues(true);
fetchLeagues({
sportId,
date: dateStr,
@@ -79,8 +116,12 @@ export function MatchesByLeague({
});
setMatchesByLeagueKey({});
setLoadingLeagueKey({});
setLoadingLeagues(false);
})
.catch(() => { });
.catch(() => {
if (!mounted) return;
setLoadingLeagues(false);
});
return () => {
mounted = false;
};
@@ -105,12 +146,25 @@ export function MatchesByLeague({
page: 1,
pageSize: 50,
});
console.log("choose", res);
setMatchesByLeagueKey((prev) => ({ ...prev, [leagueKey]: res.list }));
} finally {
setLoadingLeagueKey((prev) => ({ ...prev, [leagueKey]: false }));
}
};
if (loadingLeagues) {
return (
<View style={styles.emptyContainer}>
<ActivityIndicator
size="large"
color={isDark ? Colors.dark.tint : Colors.light.tint}
/>
<ThemedText style={{ marginTop: 10 }}>...</ThemedText>
</View>
);
}
if (leagues.length === 0) {
return (
<View style={styles.emptyContainer}>
@@ -119,13 +173,18 @@ export function MatchesByLeague({
);
}
const cardBg = isDark ? "#1C1C1E" : "#FFFFFF";
const dividerColor = isDark ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.06)";
const skeletonBg = isDark ? "#2C2C2E" : "#E5E5E5";
const headerBg = isDark ? "#1C1C1E" : "#FBFBFB";
return (
<ScrollView
style={[
styles.container,
{ backgroundColor: isDark ? "#000000" : "#F2F2F7" }
{ backgroundColor: isDark ? Colors.dark.background : "#FFFFFF" },
]}
contentContainerStyle={{ paddingBottom: 40 }}
contentContainerStyle={{ paddingBottom: 40, paddingTop: 8 }}
>
{leagues.map((league) => {
const isCollapsed = collapsed[league.key] !== false;
@@ -141,103 +200,81 @@ export function MatchesByLeague({
toggleCollapse(league.key);
}
}}
style={styles.leagueHeaderWrapper}
style={[styles.leagueHeaderWrapper, { backgroundColor: headerBg }]}
>
<View style={styles.leagueHeaderLeft}>
<Image
source={{
uri: league.logo || "https://placehold.co/40x40/png",
}}
style={styles.leagueLogo}
style={[styles.leagueLogo, { backgroundColor: isDark ? "#3A3A3C" : "#E5E5E5" }]}
/>
<View style={styles.leagueInfoText}>
<ThemedText style={styles.leagueTitle}>{league.name}</ThemedText>
<ThemedText
style={[
styles.leagueTitle,
{ color: isDark ? Colors.dark.text : Colors.light.text },
]}
>
{league.name}
</ThemedText>
<View style={styles.countryRow}>
<Image
source={{ uri: league.countryLogo || "https://placehold.co/20x20/png" }}
style={styles.countryFlag}
/>
<ThemedText style={styles.countryName}>
<ThemedText style={[styles.countryName, { color: isDark ? Colors.dark.text : Colors.light.text }]}>
{league.countryName || "International"}
</ThemedText>
</View>
</View>
</View>
<View style={styles.leagueHeaderRight}>
<ThemedText style={styles.matchCount}>
{league.matchCount}
</ThemedText>
{enableCollapsible && league.matchCount > 0 && (
<ThemedText style={styles.chevron}>
{isCollapsed ? "⌄" : "⌃"}
</ThemedText>
)}
</View>
{enableCollapsible && league.matchCount > 0 && (
<ChevronIcon isCollapsed={isCollapsed} isDark={isDark} />
)}
</TouchableOpacity>
{!isCollapsed && (
<View style={styles.matchListContainer}>
<View
style={[
styles.matchListContainer,
{ backgroundColor: cardBg, marginTop: 8 },
]}
>
{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 style={styles.matchCardWrapper}>
<View style={styles.skeletonRow}>
<View style={styles.leftColumn}>
<View style={[styles.skeletonLine, { width: 30, backgroundColor: skeletonBg }]} />
</View>
<View style={styles.teamsColumn}>
<View style={styles.skeletonTeamRow}>
<View style={[styles.skeletonAvatar, { backgroundColor: skeletonBg }]} />
<View style={[styles.skeletonLine, { flex: 1, backgroundColor: skeletonBg }]} />
</View>
<View style={[styles.skeletonTeamRow, { marginTop: 10 }]}>
<View style={[styles.skeletonAvatar, { backgroundColor: skeletonBg }]} />
<View style={[styles.skeletonLine, { flex: 1, backgroundColor: skeletonBg }]} />
</View>
</View>
))}
</>
<View style={styles.rightWrapper}>
<View style={[styles.skeletonScoreBox, { backgroundColor: skeletonBg }]} />
<View style={styles.favoriteButton}>
<View style={[styles.skeletonCircle, { backgroundColor: skeletonBg }]} />
</View>
</View>
</View>
</View>
) : (
leagueMatches.map((match, index) => (
<View
key={match.id}
style={[
styles.matchCardWrapper,
index < leagueMatches.length - 1 &&
styles.matchCardDivider,
index < leagueMatches.length - 1 && [styles.matchCardDivider, { borderBottomColor: dividerColor }],
]}
>
<MatchCardLeague
@@ -261,15 +298,16 @@ const styles = StyleSheet.create({
flex: 1,
},
leagueSection: {
marginBottom: 16,
marginBottom: 8,
},
leagueHeaderWrapper: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: 16,
paddingVertical: 12,
backgroundColor: "transparent",
paddingVertical: 14,
borderRadius: 12,
marginHorizontal: 16,
},
leagueHeaderLeft: {
flexDirection: "row",
@@ -277,81 +315,76 @@ const styles = StyleSheet.create({
flex: 1,
},
leagueLogo: {
width: 36,
height: 36,
borderRadius: 6,
width: 24,
height: 24,
borderRadius: 8,
marginRight: 12,
backgroundColor: "#3A3A3C",
marginLeft: 14,
},
leagueInfoText: {
justifyContent: "center",
},
leagueTitle: {
fontSize: 17,
fontWeight: "600",
color: "#FFFFFF",
marginBottom: 3,
fontSize: 14,
fontWeight: "700",
marginBottom: 4,
lineHeight: 16,
},
countryRow: {
flexDirection: "row",
alignItems: "center",
},
countryFlag: {
width: 16,
height: 12,
marginRight: 5,
width: 14,
height: 14,
marginRight: 6,
borderRadius: 2,
},
countryName: {
fontSize: 13,
color: "#8E8E93",
fontSize: 12,
fontWeight: "500",
lineHeight: 12,
},
leagueHeaderRight: {
flexDirection: "row",
alignItems: "center",
gap: 8,
},
matchCount: {
fontSize: 15,
color: "#8E8E93",
fontWeight: "600",
chevronContainer: {
width: 16,
height: 16,
justifyContent: "center",
alignItems: "center",
},
chevron: {
fontSize: 16,
color: "#8E8E93",
fontWeight: '600',
chevronIcon: {
width: 16,
height: 16,
},
matchListContainer: {
backgroundColor: "#1C1C1E",
borderRadius: 12,
borderRadius: 16,
marginHorizontal: 16,
overflow: "hidden",
},
// 布局与 MatchCardLeague 保持一致,便于骨架对齐
leftColumn: {
width: 50,
width: 48,
justifyContent: "center",
alignItems: "flex-start",
alignItems: "center",
marginRight: 8,
},
teamsColumn: {
flex: 1,
justifyContent: "center",
paddingRight: 8,
paddingRight: 12,
},
rightWrapper: {
flexDirection: "row",
alignItems: "center",
},
matchCardWrapper: {
},
matchCardWrapper: {},
matchCardDivider: {
borderBottomWidth: 0.5,
borderBottomColor: "#3A3A3C",
borderBottomWidth: StyleSheet.hairlineWidth,
},
favoriteButton: {
paddingLeft: 12,
paddingLeft: 16,
justifyContent: "center",
alignItems: "center",
},
@@ -359,35 +392,31 @@ const styles = StyleSheet.create({
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 16,
paddingVertical: 14,
paddingVertical: 16,
},
skeletonLine: {
height: 8,
borderRadius: 4,
backgroundColor: "#2C2C2E",
height: 10,
borderRadius: 5,
},
skeletonAvatar: {
width: 20,
height: 20,
borderRadius: 10,
backgroundColor: "#2C2C2E",
marginRight: 10,
width: 28,
height: 28,
borderRadius: 14,
marginRight: 12,
},
skeletonTeamRow: {
flexDirection: "row",
alignItems: "center",
},
skeletonScoreBox: {
width: 24,
height: 32,
width: 40,
height: 64,
borderRadius: 8,
backgroundColor: "#2C2C2E",
},
skeletonCircle: {
width: 16,
height: 16,
borderRadius: 8,
backgroundColor: "#2C2C2E",
width: 22,
height: 22,
borderRadius: 11,
},
emptyContainer: {
flex: 1,