feat: 添加训练计划和打卡功能
- 新增训练计划页面,允许用户制定个性化的训练计划 - 集成打卡功能,用户可以记录每日的训练情况 - 更新 Redux 状态管理,添加训练计划相关的 reducer - 在首页中添加训练计划卡片,支持用户点击跳转 - 更新样式和布局,以适应新功能的展示和交互 - 添加日期选择器和相关依赖,支持用户选择训练日期
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||
import { completeDay, setCustom } from '@/store/challengeSlice';
|
||||
import type { Exercise, ExerciseCustomConfig } from '@/utils/pilatesPlan';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useLocalSearchParams, useRouter } from 'expo-router';
|
||||
import React, { useState } from 'react';
|
||||
import { FlatList, SafeAreaView, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
|
||||
@@ -55,17 +55,9 @@ export default function ChallengeDayScreen() {
|
||||
return (
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<View style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerRow}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={styles.backButton} accessibilityRole="button">
|
||||
<Ionicons name="chevron-back" size={24} color="#111827" />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.headerTitle}>第{plan.dayNumber}天</Text>
|
||||
<View style={{ width: 32 }} />
|
||||
</View>
|
||||
<Text style={styles.title}>{plan.title}</Text>
|
||||
<Text style={styles.subtitle}>{plan.focus}</Text>
|
||||
</View>
|
||||
<HeaderBar title={`第${plan.dayNumber}天`} onBack={() => router.back()} withSafeTop={false} transparent />
|
||||
<Text style={styles.title}>{plan.title}</Text>
|
||||
<Text style={styles.subtitle}>{plan.focus}</Text>
|
||||
|
||||
<FlatList
|
||||
data={plan.exercises}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||
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';
|
||||
@@ -9,6 +11,7 @@ import { Dimensions, FlatList, SafeAreaView, StyleSheet, Text, TouchableOpacity,
|
||||
export default function ChallengeHomeScreen() {
|
||||
const dispatch = useAppDispatch();
|
||||
const router = useRouter();
|
||||
const { ensureLoggedIn } = useAuthGuard();
|
||||
const challenge = useAppSelector((s) => (s as any).challenge);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -24,16 +27,8 @@ export default function ChallengeHomeScreen() {
|
||||
return (
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<View style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.headerRow}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={styles.backButton} accessibilityRole="button">
|
||||
<Ionicons name="chevron-back" size={24} color="#111827" />
|
||||
</TouchableOpacity>
|
||||
<Text style={styles.headerTitle}>30天普拉提打卡</Text>
|
||||
<View style={{ width: 32 }} />
|
||||
</View>
|
||||
<Text style={styles.subtitle}>专注核心、体态与柔韧 · 连续完成解锁徽章</Text>
|
||||
</View>
|
||||
<HeaderBar title="30天普拉提打卡" onBack={() => router.back()} withSafeTop={false} transparent />
|
||||
<Text style={styles.subtitle}>专注核心、体态与柔韧 · 连续完成解锁徽章</Text>
|
||||
|
||||
{/* 进度环与统计 */}
|
||||
<View style={styles.summaryCard}>
|
||||
@@ -45,7 +40,7 @@ export default function ChallengeHomeScreen() {
|
||||
</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>
|
||||
<Text style={styles.summaryItem}><Text style={styles.summaryItemValue}>{(challenge?.days?.filter((d: any) => d.status === 'completed').length) ?? 0}</Text> / 30 完成</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
@@ -64,7 +59,10 @@ export default function ChallengeHomeScreen() {
|
||||
return (
|
||||
<TouchableOpacity
|
||||
disabled={isLocked}
|
||||
onPress={() => router.push({ pathname: '/challenge/day', params: { day: String(plan.dayNumber) } })}
|
||||
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}
|
||||
>
|
||||
@@ -79,7 +77,10 @@ export default function ChallengeHomeScreen() {
|
||||
|
||||
{/* 底部 CTA */}
|
||||
<View style={styles.bottomBar}>
|
||||
<TouchableOpacity style={styles.startButton} onPress={() => router.push({ pathname: '/challenge/day', params: { day: String((challenge?.days?.find((d:any)=>d.status==='available')?.plan.dayNumber) || 1) } })}>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user