Files
digital-pilates/app/challenge/index.tsx
richarjiang ee84a801fb feat: 更新多个组件以使用 SafeAreaView
- 在 goals-list、task-list、explore、personal、challenge/day 和 challenge/index 组件中引入 SafeAreaView,确保内容在安全区域内显示
- 移除不必要的 SafeAreaView 导入,优化代码结构
- 更新相关样式,提升用户体验和界面一致性
2025-08-25 09:37:12 +08:00

143 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { HeaderBar } from '@/components/ui/HeaderBar';
import { Colors } from '@/constants/Colors';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useAuthGuard } from '@/hooks/useAuthGuard';
import { initChallenge } from '@/store/challengeSlice';
import { estimateSessionMinutesWithCustom } from '@/utils/pilatesPlan';
import { Ionicons } from '@expo/vector-icons';
import { useRouter } from 'expo-router';
import React, { useEffect, useMemo } from 'react';
import { Dimensions, FlatList, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
export default function ChallengeHomeScreen() {
const dispatch = useAppDispatch();
const router = useRouter();
const { ensureLoggedIn } = useAuthGuard();
const challenge = useAppSelector((s) => (s as any).challenge);
useEffect(() => {
dispatch(initChallenge());
}, [dispatch]);
const progress = useMemo(() => {
const total = challenge?.days?.length || 30;
const done = challenge?.days?.filter((d: any) => d.status === 'completed').length || 0;
return total ? done / total : 0;
}, [challenge?.days]);
return (
<SafeAreaView style={styles.safeArea}>
<View style={styles.container}>
<HeaderBar title="30天普拉提打卡" onBack={() => router.back()} withSafeTop={false} transparent />
<Text style={styles.subtitle}> · </Text>
{/* 进度环与统计 */}
<View style={styles.summaryCard}>
<View style={styles.summaryLeft}>
<View style={styles.progressPill}>
<View style={[styles.progressFill, { width: `${Math.round((progress || 0) * 100)}%` }]} />
</View>
<Text style={styles.progressText}>{Math.round((progress || 0) * 100)}%</Text>
</View>
<View style={styles.summaryRight}>
<Text style={styles.summaryItem}><Text style={styles.summaryItemValue}>{challenge?.streak ?? 0}</Text> </Text>
<Text style={styles.summaryItem}><Text style={styles.summaryItemValue}>{(challenge?.days?.filter((d: any) => d.status === 'completed').length) ?? 0}</Text> / 30 </Text>
</View>
</View>
{/* 日历格子(简单 6x5 网格) */}
<FlatList
data={challenge?.days || []}
keyExtractor={(item) => String(item.plan.dayNumber)}
numColumns={5}
columnWrapperStyle={{ justifyContent: 'space-between', marginBottom: 12 }}
contentContainerStyle={{ paddingHorizontal: 20, paddingTop: 10, paddingBottom: 40 }}
renderItem={({ item }) => {
const { plan, status } = item;
const isLocked = status === 'locked';
const isCompleted = status === 'completed';
const minutes = estimateSessionMinutesWithCustom(plan, item.custom);
return (
<TouchableOpacity
disabled={isLocked}
onPress={async () => {
if (!(await ensureLoggedIn({ redirectTo: '/challenge', redirectParams: {} }))) return;
router.push({ pathname: '/challenge/day', params: { day: String(plan.dayNumber) } });
}}
style={[styles.dayCell, isLocked && styles.dayCellLocked, isCompleted && styles.dayCellCompleted]}
activeOpacity={0.8}
>
<Text style={[styles.dayNumber, isLocked && styles.dayNumberLocked]}>{plan.dayNumber}</Text>
<Text style={styles.dayMinutes}>{minutes}</Text>
{isCompleted && <Ionicons name="checkmark-circle" size={18} color="#10B981" style={{ position: 'absolute', top: 6, right: 6 }} />}
{isLocked && <Ionicons name="lock-closed" size={16} color="#9CA3AF" style={{ position: 'absolute', top: 6, right: 6 }} />}
</TouchableOpacity>
);
}}
/>
{/* 底部 CTA */}
<View style={styles.bottomBar}>
<TouchableOpacity style={styles.startButton} onPress={async () => {
if (!(await ensureLoggedIn({ redirectTo: '/challenge' }))) return;
router.push({ pathname: '/challenge/day', params: { day: String((challenge?.days?.find((d: any) => d.status === 'available')?.plan.dayNumber) || 1) } });
}}>
<Text style={styles.startButtonText}></Text>
</TouchableOpacity>
</View>
</View>
</SafeAreaView>
);
}
const { width } = Dimensions.get('window');
const cellSize = (width - 40 - 4 * 12) / 5; // 20 padding *2, 12 spacing *4
const styles = StyleSheet.create({
safeArea: { flex: 1, backgroundColor: '#F7F8FA' },
container: { flex: 1, backgroundColor: '#F7F8FA' },
header: { paddingHorizontal: 20, paddingTop: 10 },
headerRow: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
backButton: { width: 32, height: 32, borderRadius: 16, alignItems: 'center', justifyContent: 'center', backgroundColor: '#E5E7EB' },
headerTitle: { fontSize: 22, fontWeight: '800', color: '#1A1A1A' },
subtitle: { marginTop: 6, fontSize: 12, color: '#6B7280' },
summaryCard: {
marginTop: 16,
marginHorizontal: 20,
backgroundColor: '#FFFFFF',
borderRadius: 16,
padding: 16,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
shadowColor: '#000', shadowOpacity: 0.06, shadowRadius: 12, shadowOffset: { width: 0, height: 6 }, elevation: 3,
},
summaryLeft: { flexDirection: 'row', alignItems: 'center' },
progressPill: { width: 120, height: 10, borderRadius: 999, backgroundColor: '#E5E7EB', overflow: 'hidden' },
progressFill: { height: '100%', backgroundColor: Colors.light.accentGreen },
progressText: { marginLeft: 12, fontWeight: '700', color: '#111827' },
summaryRight: {},
summaryItem: { fontSize: 12, color: '#6B7280' },
summaryItemValue: { fontWeight: '800', color: '#111827' },
dayCell: {
width: cellSize,
height: cellSize,
borderRadius: 16,
backgroundColor: '#FFFFFF',
alignItems: 'center',
justifyContent: 'center',
shadowColor: '#000', shadowOpacity: 0.06, shadowRadius: 12, shadowOffset: { width: 0, height: 6 }, elevation: 3,
},
dayCellLocked: { backgroundColor: '#F3F4F6' },
dayCellCompleted: { backgroundColor: '#ECFDF5', borderWidth: 1, borderColor: '#A7F3D0' },
dayNumber: { fontWeight: '800', color: '#111827', fontSize: 16 },
dayNumberLocked: { color: '#9CA3AF' },
dayMinutes: { marginTop: 4, fontSize: 12, color: '#6B7280' },
bottomBar: { padding: 20 },
startButton: { backgroundColor: Colors.light.accentGreen, paddingVertical: 14, borderRadius: 999, alignItems: 'center' },
startButtonText: { color: '#192126', fontWeight: '800', fontSize: 16 },
});