Files
digital-pilates/app/challenge/index.tsx
richarjiang d76ba48424 feat(ui): 统一应用主题色为天空蓝并优化渐变背景
将应用主色调从 '#BBF246' 更改为 '#87CEEB'(天空蓝),并更新所有相关组件和页面中的颜色引用。同时为多个页面添加统一的渐变背景,提升视觉效果和用户体验。新增压力分析模态框组件,并优化压力计组件的交互与显示逻辑。更新应用图标和启动图资源。
2025-08-20 09:38:25 +08:00

142 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, SafeAreaView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
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 },
});