import ChallengeProgressCard from '@/components/challenges/ChallengeProgressCard'; import { ChallengeRankingItem } from '@/components/challenges/ChallengeRankingItem'; import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useColorScheme } from '@/hooks/useColorScheme'; import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; import { fetchChallengeDetail, fetchChallengeRankings, selectChallengeById, selectChallengeDetailError, selectChallengeDetailStatus, selectChallengeRankingError, selectChallengeRankingList, selectChallengeRankingLoadMoreStatus, selectChallengeRankingStatus, } from '@/store/challengesSlice'; import { useLocalSearchParams, useRouter } from 'expo-router'; import React, { useEffect, useMemo } from 'react'; import type { NativeScrollEvent, NativeSyntheticEvent } from 'react-native'; import { ActivityIndicator, RefreshControl, ScrollView, StyleSheet, Text, View } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; export default function ChallengeLeaderboardScreen() { const safeAreaTop = useSafeAreaTop() const { id } = useLocalSearchParams<{ id?: string }>(); const router = useRouter(); const dispatch = useAppDispatch(); const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colorTokens = Colors[theme]; const insets = useSafeAreaInsets(); const challengeSelector = useMemo(() => (id ? selectChallengeById(id) : undefined), [id]); const challenge = useAppSelector((state) => (challengeSelector ? challengeSelector(state) : undefined)); const detailStatusSelector = useMemo(() => (id ? selectChallengeDetailStatus(id) : undefined), [id]); const detailStatus = useAppSelector((state) => (detailStatusSelector ? detailStatusSelector(state) : 'idle')); const detailErrorSelector = useMemo(() => (id ? selectChallengeDetailError(id) : undefined), [id]); const detailError = useAppSelector((state) => (detailErrorSelector ? detailErrorSelector(state) : undefined)); const rankingListSelector = useMemo(() => (id ? selectChallengeRankingList(id) : undefined), [id]); const rankingList = useAppSelector((state) => (rankingListSelector ? rankingListSelector(state) : undefined)); const rankingStatusSelector = useMemo(() => (id ? selectChallengeRankingStatus(id) : undefined), [id]); const rankingStatus = useAppSelector((state) => (rankingStatusSelector ? rankingStatusSelector(state) : 'idle')); const rankingLoadMoreStatusSelector = useMemo( () => (id ? selectChallengeRankingLoadMoreStatus(id) : undefined), [id] ); const rankingLoadMoreStatus = useAppSelector((state) => rankingLoadMoreStatusSelector ? rankingLoadMoreStatusSelector(state) : 'idle' ); const rankingErrorSelector = useMemo(() => (id ? selectChallengeRankingError(id) : undefined), [id]); const rankingError = useAppSelector((state) => (rankingErrorSelector ? rankingErrorSelector(state) : undefined)); useEffect(() => { if (id) { void dispatch(fetchChallengeDetail(id)); } }, [dispatch, id]); useEffect(() => { if (id && !rankingList) { void dispatch(fetchChallengeRankings({ id })); } }, [dispatch, id, rankingList]); if (!id) { return ( router.back()} withSafeTop /> 未找到该挑战。 ); } if (detailStatus === 'loading' && !challenge) { return ( router.back()} withSafeTop /> 加载榜单中… ); } const hasMore = rankingList?.hasMore ?? false; const isRefreshing = rankingStatus === 'loading'; const isLoadingMore = rankingLoadMoreStatus === 'loading'; const defaultPageSize = rankingList?.pageSize ?? 20; const showInitialRankingLoading = isRefreshing && (!rankingList || rankingList.items.length === 0); const handleRefresh = () => { if (!id) { return; } void dispatch(fetchChallengeRankings({ id, page: 1, pageSize: defaultPageSize })); }; const handleLoadMore = () => { if (!id || !rankingList || !hasMore || isLoadingMore || rankingStatus === 'loading') { return; } void dispatch( fetchChallengeRankings({ id, page: rankingList.page + 1, pageSize: rankingList.pageSize }) ); }; const handleScroll = (event: NativeSyntheticEvent) => { const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent; const paddingToBottom = 160; if (layoutMeasurement.height + contentOffset.y >= contentSize.height - paddingToBottom) { handleLoadMore(); } }; if (!challenge) { return ( router.back()} withSafeTop /> {detailError ?? '暂时无法加载榜单,请稍后再试。'} ); } const rankingData = rankingList?.items ?? challenge.rankings ?? []; const subtitle = challenge.rankingDescription ?? challenge.summary; return ( router.back()} withSafeTop /> } onScroll={handleScroll} scrollEventThrottle={16} > {challenge.title} {subtitle ? {subtitle} : null} {challenge.progress ? ( ) : null} {showInitialRankingLoading ? ( 加载榜单中… ) : rankingData.length ? ( rankingData.map((item, index) => ( 0} unit={challenge?.unit} /> )) ) : rankingError ? ( {rankingError} ) : ( 榜单即将开启,快来抢占席位。 )} {isLoadingMore ? ( 加载更多… ) : null} {rankingLoadMoreStatus === 'failed' ? ( 加载更多失败,请下拉刷新重试 ) : null} ); } const styles = StyleSheet.create({ safeArea: { flex: 1, }, scrollView: { flex: 1, backgroundColor: 'transparent', }, pageHeader: { paddingHorizontal: 24, paddingTop: 24, }, challengeTitle: { fontSize: 22, fontWeight: '800', color: '#1c1f3a', }, challengeSubtitle: { marginTop: 8, fontSize: 14, color: '#6f7ba7', lineHeight: 20, }, progressCardWrapper: { marginTop: 20, }, rankingCard: { marginTop: 24, marginHorizontal: 24, borderRadius: 24, backgroundColor: '#ffffff', paddingVertical: 10, shadowColor: 'rgba(30, 41, 59, 0.12)', shadowOpacity: 0.16, shadowRadius: 18, shadowOffset: { width: 0, height: 10 }, elevation: 6, }, emptyRanking: { paddingVertical: 40, alignItems: 'center', justifyContent: 'center', }, emptyRankingText: { fontSize: 14, color: '#6f7ba7', }, rankingLoading: { paddingVertical: 32, alignItems: 'center', justifyContent: 'center', }, rankingErrorText: { fontSize: 14, color: '#eb5757', }, loadMoreIndicator: { paddingVertical: 16, alignItems: 'center', justifyContent: 'center', }, loadMoreErrorText: { fontSize: 13, color: '#eb5757', }, loadingContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', }, loadingText: { marginTop: 16, fontSize: 14, }, missingContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 32, }, missingText: { fontSize: 14, textAlign: 'center', }, });