import { HeaderBar } from '@/components/ui/HeaderBar'; import { Colors } from '@/constants/Colors'; import { useColorScheme } from '@/hooks/useColorScheme'; import { Ionicons } from '@expo/vector-icons'; import AsyncStorage from '@react-native-async-storage/async-storage'; 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, } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { ProgressBar } from '@/components/ProgressBar'; import { useAppDispatch } from '@/hooks/redux'; import { setDailyCaloriesGoal, setDailyStepsGoal, setPilatesPurposes } from '@/store/userSlice'; const STORAGE_KEYS = { calories: '@goal_calories_burn', steps: '@goal_daily_steps', purposes: '@goal_pilates_purposes', } as const; const CALORIES_RANGE = { min: 100, max: 1500, step: 50 }; const STEPS_RANGE = { min: 2000, max: 20000, step: 500 }; export default function GoalsScreen() { const router = useRouter(); const insets = useSafeAreaInsets(); const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; const colors = Colors[theme]; const dispatch = useAppDispatch(); const [calories, setCalories] = useState(400); const [steps, setSteps] = useState(8000); const [purposes, setPurposes] = useState([]); useEffect(() => { const load = async () => { try { const [c, s, p] = await Promise.all([ AsyncStorage.getItem(STORAGE_KEYS.calories), AsyncStorage.getItem(STORAGE_KEYS.steps), AsyncStorage.getItem(STORAGE_KEYS.purposes), ]); if (c) { const v = parseInt(c, 10); if (!Number.isNaN(v)) setCalories(v); } if (s) { const v = parseInt(s, 10); if (!Number.isNaN(v)) setSteps(v); } if (p) { try { const parsed = JSON.parse(p); if (Array.isArray(parsed)) setPurposes(parsed.filter((x) => typeof x === 'string')); } catch { } } } catch { } }; load(); }, []); useEffect(() => { AsyncStorage.setItem(STORAGE_KEYS.calories, String(calories)).catch(() => { }); dispatch(setDailyCaloriesGoal(calories)); }, [calories]); useEffect(() => { AsyncStorage.setItem(STORAGE_KEYS.steps, String(steps)).catch(() => { }); dispatch(setDailyStepsGoal(steps)); }, [steps]); useEffect(() => { AsyncStorage.setItem(STORAGE_KEYS.purposes, JSON.stringify(purposes)).catch(() => { }); dispatch(setPilatesPurposes(purposes)); }, [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]); const stepsPercent = useMemo(() => (Math.min(STEPS_RANGE.max, Math.max(STEPS_RANGE.min, steps)) - STEPS_RANGE.min) / (STEPS_RANGE.max - STEPS_RANGE.min), [steps]); const changeWithHaptics = (next: number, setter: (v: number) => void) => { if (process.env.EXPO_OS === 'ios') { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light); } setter(next); }; const inc = (value: number, range: { step: number; max: number }) => { return Math.min(range.max, value + range.step); }; const dec = (value: number, range: { step: number; min: number }) => { return Math.max(range.min, value - range.step); }; const SectionCard: React.FC<{ title: string; subtitle?: string; children: React.ReactNode }> = ({ title, subtitle, children }) => ( {title} {subtitle ? {subtitle} : null} {children} ); const PresetChip: React.FC<{ label: string; active?: boolean; onPress: () => void }> = ({ label, active, onPress }) => ( {label} ); const Stepper: React.FC<{ onDec: () => void; onInc: () => void }> = ({ onDec, onInc }) => ( - + ); const PURPOSE_OPTIONS: { id: string; label: string; icon: any }[] = [ { id: 'core', label: '增强核心力量', icon: 'barbell-outline' }, { id: 'posture', label: '改善姿势体态', icon: 'body-outline' }, { id: 'flexibility', label: '提高柔韧灵活', icon: 'walk-outline' }, { id: 'balance', label: '强化平衡稳定', icon: 'accessibility-outline' }, { id: 'shape', label: '塑形与线条', icon: 'heart-outline' }, { id: 'stress', label: '减压与身心放松', icon: 'leaf-outline' }, { id: 'backpain', label: '预防/改善腰背痛', icon: 'shield-checkmark-outline' }, { id: 'rehab', label: '术后/伤后康复', icon: 'medkit-outline' }, { id: 'performance', label: '提升运动表现', icon: 'fitness-outline' }, ]; const togglePurpose = (id: string) => { if (process.env.EXPO_OS === 'ios') { Haptics.selectionAsync(); } setPurposes((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id] ); }; return ( router.back()} withSafeTop={false} tone={theme} transparent /> {calories} kcal changeWithHaptics(dec(calories, CALORIES_RANGE), setCalories)} onInc={() => changeWithHaptics(inc(calories, CALORIES_RANGE), setCalories)} /> {[200, 300, 400, 500, 600].map((v) => ( changeWithHaptics(v, setCalories)} /> ))} 建议范围 {CALORIES_RANGE.min}-{CALORIES_RANGE.max} kcal,步进 {CALORIES_RANGE.step} {steps.toLocaleString()} 步 changeWithHaptics(dec(steps, STEPS_RANGE), setSteps)} onInc={() => changeWithHaptics(inc(steps, STEPS_RANGE), setSteps)} /> {[6000, 8000, 10000, 12000, 15000].map((v) => ( changeWithHaptics(v, setSteps)} /> ))} 建议范围 {STEPS_RANGE.min.toLocaleString()}-{STEPS_RANGE.max.toLocaleString()},步进 {STEPS_RANGE.step} {PURPOSE_OPTIONS.map((opt) => { const active = purposes.includes(opt.id); return ( togglePurpose(opt.id)} > {opt.label} ); })} {purposes.length > 0 && ( 已选择 {purposes.length} 项 )} ); } const styles = StyleSheet.create({ container: { flex: 1, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 16, }, backButton: { width: 32, height: 32, borderRadius: 16, alignItems: 'center', justifyContent: 'center', }, headerTitle: { fontSize: 20, fontWeight: '700', }, safeArea: { flex: 1, }, content: { paddingHorizontal: 20, paddingTop: 8, }, card: { borderRadius: 16, padding: 16, marginTop: 16, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.08, shadowRadius: 8, elevation: 2, }, cardTitle: { fontSize: 18, fontWeight: '700', }, cardSubtitle: { fontSize: 13, marginTop: 4, }, rowBetween: { marginTop: 12, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', }, valueText: { fontSize: 24, fontWeight: '800', }, chipsRow: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, marginTop: 14, }, chip: { paddingHorizontal: 12, paddingVertical: 8, borderRadius: 20, borderWidth: 1, }, chipText: { fontSize: 14, fontWeight: '600', }, stepperRow: { flexDirection: 'row', gap: 10, }, stepperBtn: { width: 36, height: 36, borderRadius: 18, alignItems: 'center', justifyContent: 'center', borderWidth: 1, }, stepperText: { fontSize: 20, fontWeight: '700', }, rangeHint: { fontSize: 12, marginTop: 10, }, progressMargin: { marginTop: 12, }, grid: { marginTop: 12, flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between', rowGap: 12, }, optionCard: { width: '48%', borderRadius: 14, paddingVertical: 14, paddingHorizontal: 12, borderWidth: 1, flexDirection: 'row', alignItems: 'center', gap: 10, }, optionIconWrap: { width: 28, height: 28, borderRadius: 14, alignItems: 'center', justifyContent: 'center', backgroundColor: 'rgba(0,0,0,0.04)', }, optionLabel: { flex: 1, fontSize: 14, fontWeight: '700', }, selectedHint: { marginTop: 10, fontSize: 12, }, });