feat: 更新训练计划和打卡功能

- 在训练计划中新增训练项目的添加、更新和删除功能,支持用户灵活管理训练内容
- 优化训练计划排课界面,提升用户体验
- 更新打卡功能,支持按日期加载和展示打卡记录
- 删除不再使用的打卡相关页面,简化代码结构
- 新增今日训练页面,集成今日训练计划和动作展示
- 更新样式以适应新功能的展示和交互
This commit is contained in:
richarjiang
2025-08-15 17:01:33 +08:00
parent f95401c1ce
commit dacbee197c
19 changed files with 3052 additions and 1197 deletions

View File

@@ -4,13 +4,14 @@ import { SearchBox } from '@/components/SearchBox';
import { ThemedText } from '@/components/ThemedText';
import { ThemedView } from '@/components/ThemedView';
import { Colors } from '@/constants/Colors';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useColorScheme } from '@/hooks/useColorScheme';
import { listRecommendedArticles } from '@/services/articles';
import { fetchRecommendations, RecommendationType } from '@/services/recommendations';
import { loadPlans, type TrainingPlan } from '@/store/trainingPlanSlice';
// Removed WorkoutCard import since we no longer use the horizontal carousel
import { useAuthGuard } from '@/hooks/useAuthGuard';
import { getChineseGreeting } from '@/utils/date';
import dayjs from 'dayjs';
import { useRouter } from 'expo-router';
import React from 'react';
import { Animated, Image, PanResponder, Pressable, SafeAreaView, ScrollView, StyleSheet, useWindowDimensions, View } from 'react-native';
@@ -20,12 +21,17 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
export default function HomeScreen() {
const router = useRouter();
const dispatch = useAppDispatch();
const { pushIfAuthedElseLogin, isLoggedIn } = useAuthGuard();
const theme = (useColorScheme() ?? 'light') as 'light' | 'dark';
const colorTokens = Colors[theme];
const insets = useSafeAreaInsets();
const { width: windowWidth, height: windowHeight } = useWindowDimensions();
// 训练计划状态
const { plans, currentId } = useAppSelector((s) => s.trainingPlan);
const [activePlan, setActivePlan] = React.useState<TrainingPlan | null>(null);
// Draggable coach badge state
const pan = React.useRef(new Animated.ValueXY()).current;
const [coachSize, setCoachSize] = React.useState({ width: 0, height: 0 });
@@ -101,6 +107,17 @@ export default function HomeScreen() {
// 打底数据(接口不可用时)
const getFallbackItems = React.useCallback((): RecommendItem[] => {
return [
{
type: 'plan',
key: 'today-workout',
image:
'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/imagedemo.jpeg',
title: '今日训练',
subtitle: '完成一次普拉提训练,记录你的坚持',
level: '初学者',
progress: 0,
onPress: () => pushIfAuthedElseLogin('/workout/today'),
},
{
type: 'plan',
key: 'assess',
@@ -122,10 +139,27 @@ export default function HomeScreen() {
readCount: a.readCount,
})),
];
}, [router]);
}, [router, pushIfAuthedElseLogin]);
const [items, setItems] = React.useState<RecommendItem[]>(() => getFallbackItems());
// 加载训练计划数据
React.useEffect(() => {
if (isLoggedIn) {
dispatch(loadPlans());
}
}, [isLoggedIn, dispatch]);
// 获取激活的训练计划
React.useEffect(() => {
if (isLoggedIn && currentId && plans.length > 0) {
const currentPlan = plans.find(p => p.id === currentId);
setActivePlan(currentPlan || null);
} else {
setActivePlan(null);
}
}, [isLoggedIn, currentId, plans]);
// 拉取推荐接口(已登录时)
React.useEffect(() => {
let canceled = false;
@@ -158,10 +192,10 @@ export default function HomeScreen() {
type: 'plan',
key: c.id || 'checkin',
image: 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/imagedemo.jpeg',
title: c.title || '今日打卡',
title: c.title || '今日训练',
subtitle: c.subtitle || '完成一次普拉提训练,记录你的坚持',
progress: 0,
onPress: () => pushIfAuthedElseLogin('/checkin?date=' + dayjs().format('YYYY-MM-DD')),
onPress: () => pushIfAuthedElseLogin('/workout/today'),
});
}
}
@@ -176,6 +210,14 @@ export default function HomeScreen() {
return () => { canceled = true; };
}, [isLoggedIn, pushIfAuthedElseLogin, getFallbackItems]);
// 处理点击训练计划卡片跳转到锻炼tab
const handlePlanCardPress = () => {
if (activePlan) {
// 跳转到训练计划页面的锻炼tab并传递planId参数
router.push(`/training-plan?planId=${activePlan.id}&tab=schedule` as any);
}
};
return (
<SafeAreaView style={[styles.safeArea, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}>
<ThemedView style={[styles.container, { backgroundColor: theme === 'light' ? colorTokens.pageBackgroundEmphasis : colorTokens.background }]}>
@@ -244,36 +286,37 @@ export default function HomeScreen() {
style={[styles.featureCard, styles.featureCardPrimary]}
onPress={() => router.push('/ai-posture-assessment')}
>
<View style={styles.featureIconWrapper}>
<Image
source={require('@/assets/images/demo/imageBody.jpeg')}
style={styles.featureIconImage}
/>
</View>
<ThemedText style={styles.featureTitle}>AI体态评估</ThemedText>
<ThemedText style={styles.featureSubtitle}>3</ThemedText>
</Pressable>
<Pressable
style={[styles.featureCard, styles.featureCardSecondary]}
onPress={() => router.push('/ai-coach-chat?name=Sarah' as any)}
>
<ThemedText style={styles.featureTitle}>线</ThemedText>
<ThemedText style={styles.featureSubtitle}> · 11</ThemedText>
</Pressable>
<Pressable
style={[styles.featureCard, styles.featureCardTertiary]}
onPress={() => pushIfAuthedElseLogin('/checkin?date=' + dayjs().format('YYYY-MM-DD'))}
>
<ThemedText style={styles.featureTitle}></ThemedText>
<ThemedText style={styles.featureSubtitle}> · </ThemedText>
</Pressable>
<Pressable
style={[styles.featureCard, styles.featureCardQuaternary]}
onPress={() => pushIfAuthedElseLogin('/training-plan')}
>
<ThemedText style={styles.featureTitle}></ThemedText>
<ThemedText style={styles.featureSubtitle}> · </ThemedText>
<View style={styles.featureIconWrapper}>
<View style={styles.featureIconPlaceholder}>
<ThemedText style={styles.featureIconText}>💪</ThemedText>
</View>
</View>
<ThemedText style={styles.featureTitle}></ThemedText>
</Pressable>
</View>
</View>
{/* My Plan Section - 显示激活的训练计划 */}
{/* {activePlan && (
<MyPlanCard
plan={activePlan}
onPress={handlePlanCardPress}
/>
)} */}
{/* Today Plan Section */}
<View style={styles.sectionContainer}>
<ThemedText style={styles.sectionTitle}></ThemedText>
@@ -408,48 +451,89 @@ const styles = StyleSheet.create({
featureGrid: {
paddingHorizontal: 24,
flexDirection: 'row',
justifyContent: 'space-between',
flexWrap: 'wrap',
gap: 12,
},
featureCard: {
width: '48%',
flex: 1,
flexDirection: 'row',
alignItems: 'center',
borderRadius: 12,
padding: 12,
paddingHorizontal: 12,
paddingVertical: 10,
backgroundColor: '#FFFFFF',
marginBottom: 12,
// 轻量阴影,减少臃肿感
// 精致的阴影效果
shadowColor: '#000',
shadowOpacity: 0.04,
shadowOpacity: 0.06,
shadowRadius: 8,
shadowOffset: { width: 0, height: 4 },
elevation: 2,
shadowOffset: { width: 0, height: 2 },
elevation: 3,
// 渐变边框效果
borderWidth: 1,
borderColor: 'rgba(255, 255, 255, 0.8)',
// 添加微妙的内阴影效果
position: 'relative',
minHeight: 48,
},
featureCardPrimary: {
backgroundColor: '#EEF2FF', // 柔和的靛蓝背景
// 由于RN不支持CSS渐变使用渐变色背景
backgroundColor: '#667eea',
},
featureCardSecondary: {
backgroundColor: '#F0FDFA', // 柔和的青绿背景
backgroundColor: '#4facfe',
},
featureCardTertiary: {
backgroundColor: '#FFF7ED', // 柔和的橙色背景
backgroundColor: '#43e97b',
},
featureCardQuaternary: {
backgroundColor: '#F5F3FF', // 柔和的紫色背景
backgroundColor: '#fa709a',
},
featureIcon: {
fontSize: 28,
marginBottom: 8,
featureIconWrapper: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: 'rgba(255, 255, 255, 0.25)',
alignItems: 'center',
justifyContent: 'center',
marginRight: 10,
// 图标容器的阴影
shadowColor: '#000',
shadowOpacity: 0.06,
shadowRadius: 4,
shadowOffset: { width: 0, height: 1 },
elevation: 2,
},
featureIconImage: {
width: 20,
height: 20,
borderRadius: 10,
resizeMode: 'cover',
},
featureIconPlaceholder: {
width: 20,
height: 20,
borderRadius: 10,
backgroundColor: 'rgba(255, 255, 255, 0.3)',
alignItems: 'center',
justifyContent: 'center',
},
featureIconText: {
fontSize: 12,
},
featureTitle: {
fontSize: 16,
fontSize: 14,
fontWeight: '700',
color: '#0F172A',
marginBottom: 4,
color: '#FFFFFF',
textAlign: 'left',
letterSpacing: 0.2,
flex: 1,
},
featureSubtitle: {
fontSize: 11,
color: '#6B7280',
lineHeight: 15,
fontSize: 12,
color: 'rgba(255, 255, 255, 0.85)',
lineHeight: 16,
textAlign: 'center',
fontWeight: '500',
},
planList: {
paddingHorizontal: 24,