feat: 更新训练计划和打卡功能
- 在训练计划中新增训练项目的添加、更新和删除功能,支持用户灵活管理训练内容 - 优化训练计划排课界面,提升用户体验 - 更新打卡功能,支持按日期加载和展示打卡记录 - 删除不再使用的打卡相关页面,简化代码结构 - 新增今日训练页面,集成今日训练计划和动作展示 - 更新样式以适应新功能的展示和交互
This commit is contained in:
@@ -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}>认证教练 · 1对1即时解答</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,
|
||||
|
||||
Reference in New Issue
Block a user