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 { useAuthGuard } from '@/hooks/useAuthGuard'; import { useColorScheme } from '@/hooks/useColorScheme'; import { ChallengeSource } from '@/services/challengesApi'; import { archiveCustomChallengeThunk, fetchChallengeDetail, fetchChallengeRankings, fetchChallenges, joinChallenge, leaveChallenge, reportChallengeProgress, selectArchiveError, selectArchiveStatus, selectChallengeById, selectChallengeDetailError, selectChallengeDetailStatus, selectChallengeRankingList, selectJoinError, selectJoinStatus, selectLeaveError, selectLeaveStatus, selectProgressStatus } from '@/store/challengesSlice'; import { Toast } from '@/utils/toast.utils'; import { Ionicons } from '@expo/vector-icons'; import dayjs from 'dayjs'; import { BlurView } from 'expo-blur'; import * as Clipboard from 'expo-clipboard'; import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; import * as Haptics from 'expo-haptics'; import { Image } from 'expo-image'; import { LinearGradient } from 'expo-linear-gradient'; import { useLocalSearchParams, useRouter } from 'expo-router'; import LottieView from 'lottie-react-native'; import React, { useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { ActivityIndicator, Alert, Dimensions, Platform, ScrollView, Share, StatusBar, StyleSheet, Text, TouchableOpacity, View, } from 'react-native'; import { SafeAreaView, useSafeAreaInsets } from 'react-native-safe-area-context'; import { captureRef } from 'react-native-view-shot'; const { width } = Dimensions.get('window'); const HERO_HEIGHT = width * 0.76; const CTA_GRADIENT: [string, string] = ['#5E8BFF', '#6B6CFF']; const CTA_DISABLED_GRADIENT: [string, string] = ['#d3d7e8', '#c1c6da']; const isHttpUrl = (value: string) => /^https?:\/\//i.test(value); const formatMonthDay = (value?: string): string | undefined => { if (!value) return undefined; const date = new Date(value); if (Number.isNaN(date.getTime())) return undefined; return `${date.getMonth() + 1}月${date.getDate()}日`; }; const buildDateRangeLabel = (challenge?: { startAt?: string; endAt?: string; periodLabel?: string; durationLabel?: string; }): string => { if (!challenge) return ''; const startLabel = formatMonthDay(challenge.startAt); const endLabel = formatMonthDay(challenge.endAt); if (startLabel && endLabel) { return `${startLabel} - ${endLabel}`; } return challenge.periodLabel ?? challenge.durationLabel ?? ''; }; const formatParticipantsLabel = (count?: number): string => { if (typeof count !== 'number') return '持续更新中'; return `${count.toLocaleString('zh-CN')} 人正在参与`; }; export default function ChallengeDetailScreen() { 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 { t } = useTranslation(); const { ensureLoggedIn } = useAuthGuard(); // 用于截图分享的引用 const shareCardRef = useRef(null); 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 joinStatusSelector = useMemo(() => (id ? selectJoinStatus(id) : undefined), [id]); const joinStatus = useAppSelector((state) => (joinStatusSelector ? joinStatusSelector(state) : 'idle')); const joinErrorSelector = useMemo(() => (id ? selectJoinError(id) : undefined), [id]); const joinError = useAppSelector((state) => (joinErrorSelector ? joinErrorSelector(state) : undefined)); const leaveStatusSelector = useMemo(() => (id ? selectLeaveStatus(id) : undefined), [id]); const leaveStatus = useAppSelector((state) => (leaveStatusSelector ? leaveStatusSelector(state) : 'idle')); const leaveErrorSelector = useMemo(() => (id ? selectLeaveError(id) : undefined), [id]); const leaveError = useAppSelector((state) => (leaveErrorSelector ? leaveErrorSelector(state) : undefined)); const archiveStatusSelector = useMemo(() => (id ? selectArchiveStatus(id) : undefined), [id]); const archiveStatus = useAppSelector((state) => (archiveStatusSelector ? archiveStatusSelector(state) : 'idle')); const archiveErrorSelector = useMemo(() => (id ? selectArchiveError(id) : undefined), [id]); const archiveError = useAppSelector((state) => (archiveErrorSelector ? archiveErrorSelector(state) : undefined)); const progressStatusSelector = useMemo(() => (id ? selectProgressStatus(id) : undefined), [id]); const progressStatus = useAppSelector((state) => (progressStatusSelector ? progressStatusSelector(state) : 'idle')); const rankingListSelector = useMemo(() => (id ? selectChallengeRankingList(id) : undefined), [id]); const rankingList = useAppSelector((state) => (rankingListSelector ? rankingListSelector(state) : undefined)); useEffect(() => { const getData = async (id: string) => { try { await dispatch(fetchChallengeDetail(id)).unwrap; } catch (error) { } } if (id) { getData(id); } }, [dispatch, id]); useEffect(() => { if (id && !rankingList) { void dispatch(fetchChallengeRankings({ id })); } }, [dispatch, id, rankingList]); const [showCelebration, setShowCelebration] = useState(false); useEffect(() => { if (!showCelebration) { return; } const timer = setTimeout(() => { setShowCelebration(false); }, 2400); return () => { clearTimeout(timer); }; }, [showCelebration]); const progress = challenge?.progress; const isJoined = challenge?.isJoined ?? false; const isCustomChallenge = challenge?.source === ChallengeSource.CUSTOM; const isCreator = challenge?.isCreator ?? false; const isCustomCreator = isCustomChallenge && isCreator; const canEdit = isCustomChallenge && isCreator; const lastProgressAt = useMemo(() => { const progressRecord = challenge?.progress as { lastProgressAt?: string; last_progress_at?: string } | undefined; return progressRecord?.lastProgressAt ?? progressRecord?.last_progress_at; }, [challenge?.progress]); const hasCheckedInToday = useMemo(() => { if (!challenge?.progress) { return false; } if (lastProgressAt) { const lastDate = dayjs(lastProgressAt); if (lastDate.isValid() && lastDate.isSame(dayjs(), 'day')) { return true; } } return challenge.progress.checkedInToday ?? false; }, [challenge?.progress, lastProgressAt]); const rankingData = useMemo(() => { const source = rankingList?.items ?? challenge?.rankings ?? []; return source.slice(0, 10); }, [challenge?.rankings, rankingList?.items]); const participantAvatars = useMemo( () => rankingData.filter((item) => item.avatar).map((item) => item.avatar as string).slice(0, 6), [rankingData], ); const showShareCode = isJoined && Boolean(challenge?.shareCode); const handleViewAllRanking = () => { if (!id) { return; } router.push({ pathname: '/challenges/[id]/leaderboard', params: { id } }); }; const dateRangeLabel = useMemo( () => buildDateRangeLabel({ startAt: challenge?.startAt, endAt: challenge?.endAt, periodLabel: challenge?.periodLabel, durationLabel: challenge?.durationLabel, }), [challenge?.startAt, challenge?.endAt, challenge?.periodLabel, challenge?.durationLabel], ); const handleShare = async () => { if (!challenge || !shareCardRef.current) { return; } try { Toast.show({ type: 'info', text1: t('challengeDetail.share.generating'), }); // 捕获分享卡片视图 const uri = await captureRef(shareCardRef, { format: 'png', quality: 0.9, }); // 分享图片 const shareMessage = isJoined && progress ? t('challengeDetail.share.messageJoined', { title: challenge.title, completed: progress.completed, target: progress.target }) : t('challengeDetail.share.messageNotJoined', { title: challenge.title }); await Share.share({ title: challenge.title, message: shareMessage, url: Platform.OS === 'ios' ? uri : `file://${uri}`, }); } catch (error) { console.warn('分享失败', error); Toast.error(t('challengeDetail.share.failed')); } }; const handleJoin = async () => { if (!id || joinStatus === 'loading') { return; } const isLoggedIn = await ensureLoggedIn(); if (!isLoggedIn) { // 如果未登录,用户会被重定向到登录页面 return; } try { await dispatch(joinChallenge(id)).unwrap(); await dispatch(fetchChallengeDetail(id)).unwrap(); await dispatch(fetchChallengeRankings({ id })); setShowCelebration(true) } catch (error) { Toast.error(t('challengeDetail.alert.joinFailed')) } }; const handleLeave = async () => { if (!id || leaveStatus === 'loading') { return; } try { await dispatch(leaveChallenge(id)).unwrap(); await dispatch(fetchChallengeDetail(id)).unwrap(); } catch (error) { Toast.error(t('challengeDetail.alert.leaveFailed')); } }; const handleArchive = async () => { if (!id || archiveStatus === 'loading') { return; } try { await dispatch(archiveCustomChallengeThunk(id)).unwrap(); Toast.success(t('challengeDetail.alert.archiveSuccess')); await dispatch(fetchChallenges()); router.back(); } catch (error) { Toast.error(t('challengeDetail.alert.archiveFailed')); } }; const handleLeaveConfirm = () => { if (!id || leaveStatus === 'loading') { return; } Alert.alert( t('challengeDetail.alert.leaveConfirm.title'), t('challengeDetail.alert.leaveConfirm.message'), [ { text: t('challengeDetail.alert.leaveConfirm.cancel'), style: 'cancel' }, { text: t('challengeDetail.alert.leaveConfirm.confirm'), style: 'destructive', onPress: () => { void handleLeave(); }, }, ] ); }; const handleArchiveConfirm = () => { if (!id || archiveStatus === 'loading') { return; } Alert.alert( t('challengeDetail.alert.archiveConfirm.title'), t('challengeDetail.alert.archiveConfirm.message'), [ { text: t('challengeDetail.alert.archiveConfirm.cancel'), style: 'cancel' }, { text: t('challengeDetail.alert.archiveConfirm.confirm'), style: 'destructive', onPress: () => { void handleArchive(); }, }, ] ); }; const handleProgressReport = async () => { if (!id || progressStatus === 'loading') { return; } if (hasCheckedInToday) { Toast.info(t('challengeDetail.checkIn.toast.alreadyChecked')); return; } if (challenge?.status === 'upcoming') { Toast.info(t('challengeDetail.checkIn.toast.notStarted')); return; } if (challenge?.status === 'expired') { Toast.info(t('challengeDetail.checkIn.toast.expired')); return; } const isLoggedIn = await ensureLoggedIn(); if (!isLoggedIn) { return; } if (!isJoined) { Toast.info(t('challengeDetail.checkIn.toast.mustJoin')); return; } try { await dispatch(reportChallengeProgress({ id, value: 1 })).unwrap(); Toast.success(t('challengeDetail.checkIn.toast.success')); } catch (error) { Toast.error(t('challengeDetail.checkIn.toast.failed')); } }; const handleCopyShareCode = async () => { if (!challenge?.shareCode) return; await Clipboard.setStringAsync(challenge.shareCode); // 添加震动反馈 Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success); Toast.success(t('challengeDetail.shareCode.copied')); }; const isLoadingInitial = detailStatus === 'loading' && !challenge; if (!id) { return ( router.back()} withSafeTop transparent={false} /> {t('challengeDetail.notFound')} ); } if (isLoadingInitial) { return ( router.back()} withSafeTop transparent={false} /> {t('challengeDetail.loading')} ); } if (!challenge) { return ( router.back()} withSafeTop transparent={false} /> {detailError ?? t('challengeDetail.notFound')} dispatch(fetchChallengeDetail(id))} > {t('challengeDetail.retry')} ); } const highlightTitle = challenge.highlightTitle ?? t('challengeDetail.highlight.join.title'); const highlightSubtitle = challenge.highlightSubtitle ?? t('challengeDetail.highlight.join.subtitle'); const joinCtaLabel = joinStatus === 'loading' ? t('challengeDetail.cta.joining') : challenge.ctaLabel ?? t('challengeDetail.cta.join'); const isUpcoming = challenge.status === 'upcoming'; const isExpired = challenge.status === 'expired'; const deleteCtaLabel = archiveStatus === 'loading' ? t('challengeDetail.cta.deleting') : t('challengeDetail.cta.delete'); const upcomingStartLabel = formatMonthDay(challenge.startAt); const upcomingHighlightTitle = t('challengeDetail.highlight.upcoming.title'); const upcomingHighlightSubtitle = upcomingStartLabel ? t('challengeDetail.highlight.upcoming.subtitle', { date: upcomingStartLabel }) : t('challengeDetail.highlight.upcoming.subtitleFallback'); const upcomingCtaLabel = t('challengeDetail.cta.upcoming'); const expiredEndLabel = formatMonthDay(challenge.endAt); const expiredHighlightTitle = t('challengeDetail.highlight.expired.title'); const expiredHighlightSubtitle = expiredEndLabel ? t('challengeDetail.highlight.expired.subtitle', { date: expiredEndLabel }) : t('challengeDetail.highlight.expired.subtitleFallback'); const expiredCtaLabel = t('challengeDetail.cta.expired'); const leaveHighlightTitle = t('challengeDetail.highlight.leave.title'); const leaveHighlightSubtitle = t('challengeDetail.highlight.leave.subtitle'); const leaveCtaLabel = leaveStatus === 'loading' ? t('challengeDetail.cta.leaving') : t('challengeDetail.cta.leave'); let floatingHighlightTitle = highlightTitle; let floatingHighlightSubtitle = highlightSubtitle; let floatingCtaLabel = joinCtaLabel; let floatingOnPress: (() => void) | undefined = handleJoin; let floatingDisabled = joinStatus === 'loading'; let floatingError = joinError; let isDisabledButtonState = false; if (isJoined) { floatingHighlightTitle = showShareCode ? `分享码 ${challenge?.shareCode ?? ''}` : leaveHighlightTitle; floatingHighlightSubtitle = showShareCode ? '' : leaveHighlightSubtitle; if (isCustomCreator) { floatingCtaLabel = deleteCtaLabel; floatingOnPress = handleArchiveConfirm; floatingDisabled = archiveStatus === 'loading'; floatingError = archiveError; } else { floatingCtaLabel = leaveCtaLabel; floatingOnPress = handleLeaveConfirm; floatingDisabled = leaveStatus === 'loading'; floatingError = leaveError; } } if (isUpcoming) { floatingHighlightTitle = upcomingHighlightTitle; floatingHighlightSubtitle = upcomingHighlightSubtitle; floatingCtaLabel = upcomingCtaLabel; floatingOnPress = undefined; floatingDisabled = true; floatingError = undefined; isDisabledButtonState = true; } if (isExpired) { floatingHighlightTitle = expiredHighlightTitle; floatingHighlightSubtitle = expiredHighlightSubtitle; floatingCtaLabel = expiredCtaLabel; floatingOnPress = undefined; floatingDisabled = true; floatingError = undefined; isDisabledButtonState = true; } const floatingGradientColors = isDisabledButtonState ? CTA_DISABLED_GRADIENT : CTA_GRADIENT; const participantsLabel = formatParticipantsLabel(challenge.participantsCount); const inlineErrorMessage = detailStatus === 'failed' && detailError ? detailError : undefined; const checkInDisabled = progressStatus === 'loading' || hasCheckedInToday || !isJoined || isUpcoming || isExpired; const checkInButtonLabel = progressStatus === 'loading' ? t('challengeDetail.checkIn.button.checking') : hasCheckedInToday ? t('challengeDetail.checkIn.button.checked') : !isJoined ? t('challengeDetail.checkIn.button.notJoined') : isUpcoming ? t('challengeDetail.checkIn.button.upcoming') : isExpired ? t('challengeDetail.checkIn.button.expired') : t('challengeDetail.checkIn.button.checkIn'); const checkInSubtitle = hasCheckedInToday ? t('challengeDetail.checkIn.subtitleChecked') : t('challengeDetail.checkIn.subtitle'); return ( {/* 隐藏的分享卡片,用于截图 */} {/* 背景图片 */} {/* 分享卡片内容 */} {challenge.title} {challenge.summary ? ( {challenge.summary} ) : null} {/* 根据是否加入显示不同内容 */} {isJoined && progress ? ( // 已加入:显示个人进度 {t('challengeDetail.shareCard.progress.label')} {t('challengeDetail.shareCard.progress.days', { completed: progress.completed, target: progress.target })} {/* 进度条 */} {progress.completed === progress.target ? t('challengeDetail.shareCard.progress.completed') : t('challengeDetail.shareCard.progress.remaining', { remaining: progress.target - progress.completed })} ) : ( // 未加入:显示挑战信息 {dateRangeLabel} {challenge.durationLabel ? ( {challenge.durationLabel} ) : null} {challenge.requirementLabel} {t('challengeDetail.shareCard.info.checkInDaily')} {participantsLabel} {t('challengeDetail.shareCard.info.joinUs')} )} {/* 底部标识 */} {t('challengeDetail.shareCard.footer')} {canEdit && ( isLiquidGlassAvailable() ? ( router.push({ pathname: '/challenges/create-custom', params: { id, mode: 'edit' } })} activeOpacity={0.7} style={styles.editButton} > ) : ( router.push({ pathname: '/challenges/create-custom', params: { id, mode: 'edit' } })} activeOpacity={0.7} style={[styles.editButton, styles.fallbackEditButton]} > ) )} {isLiquidGlassAvailable() ? ( ) : ( )} } /> {challenge.title} {challenge.summary ? {challenge.summary} : null} {inlineErrorMessage ? ( {inlineErrorMessage} ) : null} {progress ? ( ) : null} {dateRangeLabel} {challenge.durationLabel} {challenge.requirementLabel} {t('challengeDetail.detail.requirement')} {participantsLabel} {participantAvatars.length ? ( {participantAvatars.map((avatar, index) => ( 0 && styles.avatarOffset]} cachePolicy={'memory-disk'} /> ))} {challenge.participantsCount && challenge.participantsCount > participantAvatars.length ? ( {t('challengeDetail.participants.more')} ) : null} ) : null} {isCustomChallenge ? ( {hasCheckedInToday ? t('challengeDetail.checkIn.todayChecked') : t('challengeDetail.checkIn.title')} {checkInSubtitle} {checkInButtonLabel} ) : null} {t('challengeDetail.ranking.title')} {t('challengeDetail.detail.viewAllRanking')} {challenge.rankingDescription ? ( {challenge.rankingDescription} ) : null} {rankingData.length ? ( rankingData.map((item, index) => ( 0} unit={challenge?.unit} /> )) ) : ( {t('challengeDetail.ranking.empty')} )} {isLiquidGlassAvailable() ? ( {/* 顶部高光线条 */} {/* 内部微光渐变 */} {showShareCode ? ( {floatingHighlightTitle} {floatingHighlightSubtitle ? ( {floatingHighlightSubtitle} ) : null} {floatingError ? {floatingError} : null} ) : ( {floatingHighlightTitle} {floatingHighlightSubtitle} {floatingError ? {floatingError} : null} )} {/* 按钮内部高光 */} {floatingCtaLabel} ) : ( {showShareCode ? ( {floatingHighlightTitle} {floatingHighlightSubtitle ? ( {floatingHighlightSubtitle} ) : null} {floatingError ? {floatingError} : null} ) : ( {floatingHighlightTitle} {floatingHighlightSubtitle} {floatingError ? {floatingError} : null} )} {floatingCtaLabel} )} {showCelebration && ( )} ); } const styles = StyleSheet.create({ container: { flex: 1, }, safeArea: { flex: 1, backgroundColor: '#f3f4fb', }, headerOverlay: { position: 'absolute', left: 0, right: 0, top: 0, zIndex: 20, }, heroContainer: { height: HERO_HEIGHT, width: '100%', overflow: 'hidden', position: 'absolute', top: 0 }, heroImage: { width: '100%', height: '100%', }, scrollView: { flex: 1, }, scrollContent: { paddingBottom: Platform.select({ ios: 40, default: 28 }), }, progressCardWrapper: { marginTop: 20, marginHorizontal: 24, }, floatingCTAContainer: { position: 'absolute', left: 0, right: 0, bottom: 0, paddingHorizontal: 20, zIndex: 100, }, floatingCTABlur: { borderRadius: 24, overflow: 'hidden', borderWidth: 1, borderColor: 'rgba(255,255,255,0.6)', backgroundColor: 'rgba(243, 244, 251, 0.9)', shadowColor: '#000', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.1, shadowRadius: 12, elevation: 5, }, glassWrapper: { borderRadius: 24, overflow: 'hidden', backgroundColor: 'rgba(255, 255, 255, 0.05)', borderWidth: 1, borderColor: 'rgba(255, 255, 255, 0.3)', shadowColor: '#5E8BFF', shadowOffset: { width: 0, height: 8, }, shadowOpacity: 0.18, shadowRadius: 20, elevation: 10, }, glassHighlight: { position: 'absolute', top: 0, left: 0, right: 0, height: 1, zIndex: 2, opacity: 0.9, }, glassContainer: { borderRadius: 24, overflow: 'hidden', }, floatingCTAContent: { flexDirection: 'row', alignItems: 'center', paddingVertical: 16, paddingHorizontal: 20, }, highlightCopy: { flex: 1, marginRight: 16, }, highlightCopyCompact: { marginRight: 12, gap: 4, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, shareCodeRow: { flexDirection: 'row', alignItems: 'center', gap: 8, flex: 1, }, headerTextBlock: { paddingHorizontal: 24, marginTop: HERO_HEIGHT - 60, alignItems: 'center', }, periodLabel: { fontSize: 14, color: '#596095', letterSpacing: 0.2, fontFamily: 'AliRegular', }, title: { marginTop: 10, fontSize: 24, fontWeight: '800', color: '#1c1f3a', textAlign: 'center', fontFamily: 'AliBold' }, summary: { marginTop: 12, fontSize: 14, lineHeight: 20, color: '#7080b4', textAlign: 'center', fontFamily: 'AliRegular', }, inlineError: { marginTop: 12, paddingHorizontal: 12, paddingVertical: 8, borderRadius: 12, backgroundColor: 'rgba(255, 107, 107, 0.12)', flexDirection: 'row', alignItems: 'center', }, inlineErrorText: { marginLeft: 6, fontSize: 12, color: '#FF6B6B', flexShrink: 1, }, detailCard: { marginTop: 28, marginHorizontal: 20, padding: 20, borderRadius: 28, backgroundColor: '#ffffff', shadowColor: 'rgba(30, 41, 59, 0.18)', shadowOpacity: 0.2, shadowRadius: 20, shadowOffset: { width: 0, height: 12 }, elevation: 8, gap: 20, }, detailRow: { flexDirection: 'row', alignItems: 'center', }, detailIconWrapper: { width: 42, height: 42, alignItems: 'center', justifyContent: 'center', }, detailTextWrapper: { marginLeft: 14, }, detailLabel: { fontSize: 15, fontWeight: '600', color: '#1c1f3a', fontFamily: 'AliBold', }, detailMeta: { marginTop: 4, fontSize: 12, color: '#6f7ba7', fontFamily: 'AliRegular', }, avatarRow: { flexDirection: 'row', alignItems: 'center', marginTop: 12, }, avatar: { width: 36, height: 36, borderRadius: 18, borderWidth: 2, borderColor: '#fff', }, avatarOffset: { marginLeft: -12, }, moreAvatarButton: { marginLeft: 12, paddingHorizontal: 12, paddingVertical: 6, borderRadius: 14, backgroundColor: '#EEF0FF', }, moreAvatarText: { fontSize: 12, color: '#4F5BD5', fontWeight: '600', fontFamily: 'AliRegular', }, checkInCard: { marginTop: 4, padding: 14, borderRadius: 18, backgroundColor: '#f5f6ff', flexDirection: 'row', alignItems: 'center', gap: 12, }, checkInCopy: { flex: 1, }, checkInTitle: { fontSize: 14, fontWeight: '700', color: '#1c1f3a', fontFamily: 'AliBold', }, checkInSubtitle: { marginTop: 4, fontSize: 12, color: '#6f7ba7', lineHeight: 18, fontFamily: 'AliRegular', }, checkInButton: { borderRadius: 18, overflow: 'hidden', }, checkInButtonBackground: { paddingVertical: 10, paddingHorizontal: 14, borderRadius: 18, minWidth: 96, alignItems: 'center', justifyContent: 'center', }, checkInButtonLabel: { fontSize: 13, fontWeight: '700', color: '#ffffff', fontFamily: 'AliBold', }, checkInButtonLabelDisabled: { color: '#6f7799', }, sectionHeader: { marginTop: 36, marginHorizontal: 24, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, sectionTitle: { fontSize: 18, fontWeight: '700', color: '#1c1f3a', fontFamily: 'AliBold', }, sectionAction: { fontSize: 13, fontWeight: '600', color: '#5F6BF0', fontFamily: 'AliBold', }, sectionSubtitle: { marginTop: 8, marginHorizontal: 24, fontSize: 13, color: '#6f7ba7', lineHeight: 18, fontFamily: 'AliRegular', }, rankingCard: { marginTop: 20, 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', }, emptyRankingText: { fontSize: 14, color: '#6f7ba7', fontFamily: 'AliRegular', }, highlightTitle: { fontSize: 16, fontWeight: '700', color: '#1c1f3a', fontFamily: 'AliBold', }, highlightSubtitle: { marginTop: 4, fontSize: 12, color: '#5f6a97', lineHeight: 18, fontFamily: 'AliRegular', }, shareCodeIconButton: { paddingHorizontal: 4, paddingVertical: 4, }, ctaErrorText: { marginTop: 8, fontSize: 12, color: '#FF6B6B', }, highlightButton: { borderRadius: 22, overflow: 'hidden', }, highlightButtonBackground: { borderRadius: 22, paddingVertical: 10, paddingHorizontal: 18, alignItems: 'center', justifyContent: 'center', }, highlightButtonLabel: { fontSize: 14, fontWeight: '700', color: '#ffffff', fontFamily: 'AliBold', }, highlightButtonLabelDisabled: { color: '#6f7799', }, headerButtons: { flexDirection: 'row', alignItems: 'center', gap: 12, }, editButton: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center', overflow: 'hidden', }, editButtonGlass: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center', overflow: 'hidden', }, fallbackEditButton: { backgroundColor: 'rgba(255, 255, 255, 0.24)', borderWidth: 1, borderColor: 'rgba(255, 255, 255, 0.45)', }, shareButton: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center', overflow: 'hidden', }, fallbackShareButton: { backgroundColor: 'rgba(255, 255, 255, 0.24)', borderWidth: 1, borderColor: 'rgba(255, 255, 255, 0.45)', }, missingContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 32, }, missingText: { fontSize: 16, textAlign: 'center', fontFamily: 'AliRegular', }, retryButton: { marginTop: 18, paddingHorizontal: 20, paddingVertical: 10, borderRadius: 22, borderWidth: 1, }, retryText: { fontSize: 14, fontWeight: '600', fontFamily: 'AliBold', }, celebrationOverlay: { ...StyleSheet.absoluteFillObject, alignItems: 'center', justifyContent: 'center', zIndex: 40, }, celebrationAnimation: { width: width * 1.3, height: width * 1.3, }, // 分享卡片样式 offscreenContainer: { position: 'absolute', left: -9999, top: -9999, opacity: 0, }, shareCard: { width: 375, height: 500, backgroundColor: '#fff', overflow: 'hidden', borderRadius: 24, }, shareCardBg: { ...StyleSheet.absoluteFillObject, width: '100%', height: '100%', }, shareCardContent: { flex: 1, padding: 24, justifyContent: 'space-between', }, shareCardTitle: { fontSize: 28, fontWeight: '800', color: '#ffffff', marginTop: 20, textShadowColor: 'rgba(0, 0, 0, 0.3)', textShadowOffset: { width: 0, height: 2 }, textShadowRadius: 4, fontFamily: 'AliBold', }, shareCardSummary: { fontSize: 15, color: '#ffffff', marginTop: 12, lineHeight: 22, opacity: 0.95, textShadowColor: 'rgba(0, 0, 0, 0.25)', textShadowOffset: { width: 0, height: 1 }, textShadowRadius: 3, fontFamily: 'AliRegular', }, shareProgressContainer: { backgroundColor: 'rgba(255, 255, 255, 0.95)', borderRadius: 20, padding: 20, marginTop: 'auto', }, shareInfoContainer: { backgroundColor: 'rgba(255, 255, 255, 0.95)', borderRadius: 20, padding: 20, marginTop: 'auto', gap: 16, }, shareInfoRow: { flexDirection: 'row', alignItems: 'center', }, shareInfoIconWrapper: { width: 40, height: 40, borderRadius: 20, backgroundColor: '#EEF0FF', alignItems: 'center', justifyContent: 'center', }, shareInfoTextWrapper: { marginLeft: 12, flex: 1, }, shareInfoLabel: { fontSize: 14, fontWeight: '600', color: '#1c1f3a', fontFamily: 'AliBold', }, shareInfoMeta: { fontSize: 12, color: '#707baf', marginTop: 2, fontFamily: 'AliRegular', }, shareProgressHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16, }, shareProgressLabel: { fontSize: 14, fontWeight: '600', color: '#1c1f3a', fontFamily: 'AliBold', }, shareProgressValue: { fontSize: 18, fontWeight: '800', color: '#5E8BFF', fontFamily: 'AliBold', }, shareProgressTrack: { height: 8, backgroundColor: '#eceffa', borderRadius: 4, overflow: 'hidden', }, shareProgressBar: { height: '100%', backgroundColor: '#5E8BFF', borderRadius: 4, }, shareProgressSubtext: { fontSize: 13, color: '#707baf', marginTop: 12, textAlign: 'center', fontWeight: '500', fontFamily: 'AliRegular', }, shareCardFooter: { alignItems: 'center', paddingTop: 16, }, shareCardFooterText: { fontSize: 12, color: '#ffffff', opacity: 0.8, fontWeight: '600', fontFamily: 'AliBold', }, });