更新联赛
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user