feat: 添加训练计划和打卡功能
- 新增训练计划页面,允许用户制定个性化的训练计划 - 集成打卡功能,用户可以记录每日的训练情况 - 更新 Redux 状态管理,添加训练计划相关的 reducer - 在首页中添加训练计划卡片,支持用户点击跳转 - 更新样式和布局,以适应新功能的展示和交互 - 添加日期选择器和相关依赖,支持用户选择训练日期
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { HeaderBar } from '@/components/ui/HeaderBar';
|
||||
import { Colors } from '@/constants/Colors';
|
||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
@@ -6,12 +7,12 @@ import * as Haptics from 'expo-haptics';
|
||||
import { useRouter } from 'expo-router';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
SafeAreaView,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
TouchableOpacity,
|
||||
View,
|
||||
} from 'react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
@@ -56,34 +57,34 @@ export default function GoalsScreen() {
|
||||
try {
|
||||
const parsed = JSON.parse(p);
|
||||
if (Array.isArray(parsed)) setPurposes(parsed.filter((x) => typeof x === 'string'));
|
||||
} catch {}
|
||||
} catch { }
|
||||
}
|
||||
} catch {}
|
||||
} catch { }
|
||||
};
|
||||
load();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
AsyncStorage.setItem(STORAGE_KEYS.calories, String(calories)).catch(() => {});
|
||||
AsyncStorage.setItem(STORAGE_KEYS.calories, String(calories)).catch(() => { });
|
||||
}, [calories]);
|
||||
|
||||
useEffect(() => {
|
||||
AsyncStorage.setItem(STORAGE_KEYS.steps, String(steps)).catch(() => {});
|
||||
AsyncStorage.setItem(STORAGE_KEYS.steps, String(steps)).catch(() => { });
|
||||
}, [steps]);
|
||||
|
||||
useEffect(() => {
|
||||
AsyncStorage.setItem(STORAGE_KEYS.purposes, JSON.stringify(purposes)).catch(() => {});
|
||||
AsyncStorage.setItem(STORAGE_KEYS.purposes, JSON.stringify(purposes)).catch(() => { });
|
||||
}, [purposes]);
|
||||
|
||||
const caloriesPercent = useMemo(() =>
|
||||
(Math.min(CALORIES_RANGE.max, Math.max(CALORIES_RANGE.min, calories)) - CALORIES_RANGE.min) /
|
||||
(CALORIES_RANGE.max - CALORIES_RANGE.min),
|
||||
[calories]);
|
||||
[calories]);
|
||||
|
||||
const stepsPercent = useMemo(() =>
|
||||
(Math.min(STEPS_RANGE.max, Math.max(STEPS_RANGE.min, steps)) - STEPS_RANGE.min) /
|
||||
(STEPS_RANGE.max - STEPS_RANGE.min),
|
||||
[steps]);
|
||||
[steps]);
|
||||
|
||||
const changeWithHaptics = (next: number, setter: (v: number) => void) => {
|
||||
if (process.env.EXPO_OS === 'ios') {
|
||||
@@ -101,34 +102,34 @@ export default function GoalsScreen() {
|
||||
|
||||
const SectionCard: React.FC<{ title: string; subtitle?: string; children: React.ReactNode }>
|
||||
= ({ title, subtitle, children }) => (
|
||||
<View style={[styles.card, { backgroundColor: colors.surface }]}>
|
||||
<Text style={[styles.cardTitle, { color: colors.text }]}>{title}</Text>
|
||||
{subtitle ? <Text style={[styles.cardSubtitle, { color: colors.textSecondary }]}>{subtitle}</Text> : null}
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
<View style={[styles.card, { backgroundColor: colors.surface }]}>
|
||||
<Text style={[styles.cardTitle, { color: colors.text }]}>{title}</Text>
|
||||
{subtitle ? <Text style={[styles.cardSubtitle, { color: colors.textSecondary }]}>{subtitle}</Text> : null}
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
|
||||
const PresetChip: React.FC<{ label: string; active?: boolean; onPress: () => void }>
|
||||
= ({ label, active, onPress }) => (
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
style={[styles.chip, { backgroundColor: active ? colors.primary : colors.card, borderColor: colors.border }]}
|
||||
>
|
||||
<Text style={[styles.chipText, { color: active ? colors.onPrimary : colors.text }]}>{label}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
<TouchableOpacity
|
||||
onPress={onPress}
|
||||
style={[styles.chip, { backgroundColor: active ? colors.primary : colors.card, borderColor: colors.border }]}
|
||||
>
|
||||
<Text style={[styles.chipText, { color: active ? colors.onPrimary : colors.text }]}>{label}</Text>
|
||||
</TouchableOpacity>
|
||||
);
|
||||
|
||||
const Stepper: React.FC<{ onDec: () => void; onInc: () => void }>
|
||||
= ({ onDec, onInc }) => (
|
||||
<View style={styles.stepperRow}>
|
||||
<TouchableOpacity onPress={onDec} style={[styles.stepperBtn, { backgroundColor: colors.card, borderColor: colors.border }]}>
|
||||
<Text style={[styles.stepperText, { color: colors.text }]}>-</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={onInc} style={[styles.stepperBtn, { backgroundColor: colors.card, borderColor: colors.border }]}>
|
||||
<Text style={[styles.stepperText, { color: colors.text }]}>+</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
<View style={styles.stepperRow}>
|
||||
<TouchableOpacity onPress={onDec} style={[styles.stepperBtn, { backgroundColor: colors.card, borderColor: colors.border }]}>
|
||||
<Text style={[styles.stepperText, { color: colors.text }]}>-</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity onPress={onInc} style={[styles.stepperBtn, { backgroundColor: colors.card, borderColor: colors.border }]}>
|
||||
<Text style={[styles.stepperText, { color: colors.text }]}>+</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
);
|
||||
|
||||
const PURPOSE_OPTIONS: { id: string; label: string; icon: any }[] = [
|
||||
{ id: 'core', label: '增强核心力量', icon: 'barbell-outline' },
|
||||
@@ -152,19 +153,8 @@ export default function GoalsScreen() {
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={[styles.container, { backgroundColor: theme === 'light' ? '#F5F5F5' : colors.background }]}>
|
||||
{/* Header(参照 AI 体态测评页面的实现) */}
|
||||
<View style={[styles.header, { paddingTop: insets.top + 8 }]}>
|
||||
<TouchableOpacity
|
||||
accessibilityRole="button"
|
||||
onPress={() => router.back()}
|
||||
style={[styles.backButton, { backgroundColor: theme === 'dark' ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.06)' }]}
|
||||
>
|
||||
<Ionicons name="chevron-back" size={24} color={theme === 'dark' ? '#ECEDEE' : colors.text} />
|
||||
</TouchableOpacity>
|
||||
<Text style={[styles.headerTitle, { color: theme === 'dark' ? '#ECEDEE' : colors.text }]}>目标管理</Text>
|
||||
<View style={{ width: 32 }} />
|
||||
</View>
|
||||
<View style={[styles.container, { backgroundColor: theme === 'light' ? '#F5F5F5' : colors.background }]}>
|
||||
<HeaderBar title="目标管理" onBack={() => router.back()} withSafeTop={false} tone={theme} transparent />
|
||||
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<ScrollView contentContainerStyle={[styles.content, { paddingBottom: Math.max(20, insets.bottom + 16) }]}
|
||||
|
||||
Reference in New Issue
Block a user