import NumberKeyboard from '@/components/NumberKeyboard'; import { HeaderBar } from '@/components/ui/HeaderBar'; import { WeightProgressBar } from '@/components/weight/WeightProgressBar'; import { WeightRecordCard } from '@/components/weight/WeightRecordCard'; import { Colors } from '@/constants/Colors'; import { getTabBarBottomPadding } from '@/constants/TabBar'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useColorScheme } from '@/hooks/useColorScheme'; import { useI18n } from '@/hooks/useI18n'; import { useSafeAreaTop } from '@/hooks/useSafeAreaWithPadding'; import { appStoreReviewService } from '@/services/appStoreReview'; import { deleteWeightRecord, fetchWeightHistory, updateUserProfile, updateWeightRecord, WeightHistoryItem } from '@/store/userSlice'; import { Ionicons } from '@expo/vector-icons'; import dayjs from 'dayjs'; import { GlassView, isLiquidGlassAvailable } from 'expo-glass-effect'; import { LinearGradient } from 'expo-linear-gradient'; import React, { useCallback, useEffect, useState } from 'react'; import { Alert, Image, Modal, ScrollView, StyleSheet, Text, TouchableOpacity, View } from 'react-native'; export default function WeightRecordsPage() { const { t } = useI18n(); const safeAreaTop = useSafeAreaTop() const dispatch = useAppDispatch(); const userProfile = useAppSelector((s) => s.user.profile); const weightHistory = useAppSelector((s) => s.user.weightHistory); const [showWeightPicker, setShowWeightPicker] = useState(false); const [pickerType, setPickerType] = useState<'current' | 'initial' | 'target' | 'edit'>('current'); const [inputWeight, setInputWeight] = useState(''); const [editingRecord, setEditingRecord] = useState(null); const colorScheme = useColorScheme(); const themeColors = Colors[colorScheme ?? 'light']; const { isLoggedIn, ensureLoggedIn } = useAuthGuard(); const loadWeightHistory = useCallback(async () => { if (!isLoggedIn) return; try { await dispatch(fetchWeightHistory() as any); } catch (error) { console.error(t('weightRecords.loadingHistory'), error); } }, [dispatch, isLoggedIn]); useEffect(() => { loadWeightHistory(); }, [loadWeightHistory]); const initializeInput = (weight: number) => { setInputWeight(weight.toString()); }; const handleAddWeight = async () => { const ok = await ensureLoggedIn(); if (!ok) return; setPickerType('current'); const weight = userProfile?.weight ? parseFloat(userProfile.weight) : 70.0; initializeInput(weight); setShowWeightPicker(true); }; const handleEditInitialWeight = async () => { const ok = await ensureLoggedIn(); if (!ok) return; setPickerType('initial'); const initialWeight = userProfile?.initialWeight || userProfile?.weight || '70.0'; initializeInput(parseFloat(initialWeight)); setShowWeightPicker(true); }; const handleEditTargetWeight = async () => { const ok = await ensureLoggedIn(); if (!ok) return; setPickerType('target'); const targetWeight = userProfile?.targetWeight || '60.0'; initializeInput(parseFloat(targetWeight)); setShowWeightPicker(true); }; const handleEditWeightRecord = async (record: WeightHistoryItem) => { const ok = await ensureLoggedIn(); if (!ok) return; setPickerType('edit'); setEditingRecord(record); initializeInput(parseFloat(record.weight)); setShowWeightPicker(true); }; const handleDeleteWeightRecord = async (id: string) => { const ok = await ensureLoggedIn(); if (!ok) return; try { await dispatch(deleteWeightRecord(id) as any); await loadWeightHistory(); } catch (error) { console.error(t('weightRecords.alerts.deleteFailed'), error); Alert.alert('错误', t('weightRecords.alerts.deleteFailed')); } }; const handleWeightSave = async () => { const weight = parseFloat(inputWeight); if (isNaN(weight) || weight <= 0 || weight > 500) { alert(t('weightRecords.alerts.invalidWeight')); return; } try { if (pickerType === 'current') { // Update current weight in profile and add weight record await dispatch(updateUserProfile({ weight: weight }) as any); // 记录体重后尝试请求应用评分(延迟1秒,避免阻塞主流程) setTimeout(() => { appStoreReviewService.requestReview().catch((error) => { console.error('应用评分请求失败:', error); }); }, 1000); } else if (pickerType === 'initial') { // Update initial weight in profile console.log('更新初始体重'); await dispatch(updateUserProfile({ initialWeight: weight }) as any); } else if (pickerType === 'target') { // Update target weight in profile await dispatch(updateUserProfile({ targetWeight: weight }) as any); } else if (pickerType === 'edit' && editingRecord) { await dispatch(updateWeightRecord({ id: editingRecord.id, weight }) as any); } setShowWeightPicker(false); setInputWeight(''); setEditingRecord(null); await loadWeightHistory(); } catch (error) { console.error(t('weightRecords.alerts.saveFailed'), error); Alert.alert('错误', t('weightRecords.alerts.saveFailed')); } }; const handleNumberPress = (number: string) => { setInputWeight(prev => { // 防止输入多个0开头 if (prev === '0' && number === '0') return prev; // 如果当前是0,输入非0数字时替换 if (prev === '0' && number !== '0') return number; return prev + number; }); }; const handleDeletePress = () => { setInputWeight(prev => prev.slice(0, -1)); }; const handleDecimalPress = () => { setInputWeight(prev => { if (prev.includes('.')) return prev; // 如果没有输入任何数字,自动添加0 if (!prev) return '0.'; return prev + '.'; }); }; // Process weight history data const sortedHistory = [...weightHistory] .sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()); // Group by month const groupedHistory = sortedHistory.reduce((acc, item) => { const date = dayjs(item.createdAt); const monthKey = t('weightRecords.historyMonthFormat', { year: date.format('YYYY'), month: date.format('MM') }); if (!acc[monthKey]) { acc[monthKey] = []; } acc[monthKey].push(item); return acc; }, {} as Record); // Calculate statistics const currentWeight = userProfile?.weight ? parseFloat(userProfile.weight) : 0; const initialWeight = userProfile?.initialWeight ? parseFloat(userProfile.initialWeight) : (sortedHistory.length > 0 ? parseFloat(sortedHistory[sortedHistory.length - 1].weight) : currentWeight); const targetWeight = userProfile?.targetWeight ? parseFloat(userProfile.targetWeight) : 60.0; const totalWeightLoss = initialWeight - currentWeight; // 计算减重进度 const hasTargetWeight = targetWeight > 0 && initialWeight > targetWeight; const totalToLose = initialWeight - targetWeight; const actualLost = initialWeight - currentWeight; const weightProgress = hasTargetWeight && totalToLose > 0 ? actualLost / totalToLose : 0; return ( {/* 背景 */} {/* 顶部装饰性渐变 */} ) : ( ) } /> {t('weightRecords.title')} {t('weightRecords.pageSubtitle')} {/* Weight Statistics Cards */} {/* Current Weight - Hero Card */} {t('weightRecords.stats.currentWeight')} {currentWeight.toFixed(1)} kg {totalWeightLoss > 0 ? '+' : ''}{totalWeightLoss.toFixed(1)} kg {/* Secondary Stats Row */} {/* Initial Weight */} {t('weightRecords.stats.initialWeight')} {initialWeight.toFixed(1)}kg {/* Target Weight */} {t('weightRecords.stats.targetWeight')} {targetWeight.toFixed(1)}kg {/* 减重进度条 - 仅在设置了目标体重时显示 */} {hasTargetWeight && ( )} {/* Monthly Records */} {Object.keys(groupedHistory).length > 0 ? ( {t('weightRecords.history')} {Object.entries(groupedHistory).map(([month, records]) => ( {month} {/* Individual Record Cards */} {records.map((record, recordIndex) => { // Calculate weight change from previous record const prevRecord = recordIndex < records.length - 1 ? records[recordIndex + 1] : null; const weightChange = prevRecord ? parseFloat(record.weight) - parseFloat(prevRecord.weight) : 0; return ( ); })} ))} ) : ( {t('weightRecords.empty.title')} {t('weightRecords.empty.subtitle')} )} {/* Weight Input Modal */} setShowWeightPicker(false)} > setShowWeightPicker(false)} /> {/* Header */} setShowWeightPicker(false)} hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }} > {pickerType === 'current' && t('weightRecords.modal.recordWeight')} {pickerType === 'initial' && t('weightRecords.modal.editInitialWeight')} {pickerType === 'target' && t('weightRecords.modal.editTargetWeight')} {pickerType === 'edit' && t('weightRecords.modal.editRecord')} {/* Weight Display Section */} {inputWeight || t('weightRecords.modal.inputPlaceholder')} {t('weightRecords.modal.unit')} {/* Quick Selection */} {t('weightRecords.modal.quickSelection')} {[50, 60, 70, 80, 90].map((weight) => ( setInputWeight(weight.toString())} activeOpacity={0.7} > {weight}{t('weightRecords.modal.unit')} ))} {/* Custom Number Keyboard */} {/* Save Button */} {t('weightRecords.modal.confirm')} ); } const styles = StyleSheet.create({ container: { flex: 1, }, topGradient: { position: 'absolute', left: 0, right: 0, top: 0, height: 300, }, content: { flex: 1, }, contentContainer: { flexGrow: 1, paddingBottom: 40, }, headerBlock: { paddingHorizontal: 24, marginTop: 10, marginBottom: 24, }, pageTitle: { fontSize: 28, fontWeight: '800', color: '#1c1f3a', fontFamily: 'AliBold', marginBottom: 4, }, pageSubtitle: { fontSize: 16, color: '#6f7ba7', fontFamily: 'AliRegular', }, // Add Button Styles addButtonGlass: { width: 40, height: 40, alignItems: 'center', justifyContent: 'center', borderRadius: 20, overflow: 'hidden', }, addButtonFallback: { width: 40, height: 40, alignItems: 'center', justifyContent: 'center', borderRadius: 20, backgroundColor: '#ffffff', borderWidth: 1, borderColor: 'rgba(0,0,0,0.05)', }, // Stats Grid statsGrid: { paddingHorizontal: 24, marginBottom: 32, gap: 16, }, mainStatCard: { backgroundColor: '#4F5BD5', borderRadius: 28, padding: 24, height: 160, position: 'relative', overflow: 'hidden', shadowColor: '#4F5BD5', shadowOffset: { width: 0, height: 10 }, shadowOpacity: 0.3, shadowRadius: 16, elevation: 8, }, mainStatContent: { zIndex: 2, height: '100%', justifyContent: 'space-between', }, mainStatLabel: { fontSize: 16, color: 'rgba(255, 255, 255, 0.9)', fontFamily: 'AliRegular', }, mainStatValueContainer: { flexDirection: 'row', alignItems: 'baseline', }, mainStatValue: { fontSize: 48, fontWeight: '800', color: '#ffffff', fontFamily: 'AliBold', marginRight: 8, }, mainStatUnit: { fontSize: 20, fontWeight: '600', color: 'rgba(255, 255, 255, 0.9)', fontFamily: 'AliRegular', }, totalLossTag: { flexDirection: 'row', alignItems: 'center', backgroundColor: 'rgba(255, 255, 255, 0.2)', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 12, alignSelf: 'flex-start', }, totalLossText: { fontSize: 14, fontWeight: '600', color: '#ffffff', marginLeft: 4, fontFamily: 'AliBold', }, statIconBg: { position: 'absolute', right: -20, bottom: -20, width: 140, height: 140, opacity: 0.2, transform: [{ rotate: '-15deg' }], tintColor: '#ffffff' }, secondaryStatsRow: { flexDirection: 'row', gap: 16, }, secondaryStatCard: { flex: 1, backgroundColor: '#ffffff', borderRadius: 24, padding: 16, shadowColor: 'rgba(30, 41, 59, 0.06)', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.1, shadowRadius: 12, elevation: 3, }, secondaryStatHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, }, secondaryStatLabel: { fontSize: 13, color: '#6f7ba7', fontFamily: 'AliRegular', }, secondaryStatValue: { fontSize: 20, fontWeight: '700', color: '#1c1f3a', fontFamily: 'AliBold', }, secondaryStatUnit: { fontSize: 14, color: '#6f7ba7', fontWeight: '500', fontFamily: 'AliRegular', marginLeft: 2, }, // Progress Container progressContainer: { marginHorizontal: 24, marginBottom: 24, backgroundColor: '#ffffff', borderRadius: 24, padding: 20, shadowColor: 'rgba(30, 41, 59, 0.06)', shadowOffset: { width: 0, height: 4 }, shadowOpacity: 0.1, shadowRadius: 12, elevation: 3, }, // History Section historySection: { paddingHorizontal: 24, }, sectionTitle: { fontSize: 18, fontWeight: '700', color: '#1c1f3a', marginBottom: 16, fontFamily: 'AliBold', }, monthContainer: { marginBottom: 24, }, monthHeader: { marginBottom: 12, paddingHorizontal: 4, }, monthTitle: { fontSize: 15, fontWeight: '600', color: '#6f7ba7', fontFamily: 'AliRegular', }, recordsList: { gap: 12, }, emptyContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingVertical: 60, }, emptyContent: { alignItems: 'center', }, emptyText: { fontSize: 16, fontWeight: '700', color: '#1c1f3a', marginBottom: 8, fontFamily: 'AliBold', }, emptySubtext: { fontSize: 14, color: '#687076', fontFamily: 'AliRegular', }, // Modal Styles (Retain but refined) modalContainer: { flex: 1, }, modalBackdrop: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.4)', }, modalSheet: { position: 'absolute', left: 0, right: 0, bottom: 0, borderTopLeftRadius: 32, borderTopRightRadius: 32, maxHeight: '85%', minHeight: 500, shadowColor: '#000', shadowOffset: { width: 0, height: -4 }, shadowOpacity: 0.1, shadowRadius: 12, elevation: 10, }, modalHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 24, paddingTop: 24, paddingBottom: 16, }, modalTitle: { fontSize: 18, fontWeight: '700', color: '#1c1f3a', fontFamily: 'AliBold', }, modalContent: { flex: 1, paddingHorizontal: 24, }, inputSection: { backgroundColor: '#F8F9FC', borderRadius: 24, padding: 24, marginBottom: 24, marginTop: 8, }, weightInputContainer: { flexDirection: 'row', alignItems: 'center', }, weightIcon: { width: 48, height: 48, borderRadius: 24, backgroundColor: '#EEF0FF', alignItems: 'center', justifyContent: 'center', marginRight: 16, }, inputWrapper: { flex: 1, flexDirection: 'row', alignItems: 'baseline', borderBottomWidth: 2, borderBottomColor: '#E2E8F0', paddingBottom: 8, }, weightDisplay: { flex: 1, fontSize: 36, fontWeight: '700', textAlign: 'center', color: '#1c1f3a', fontFamily: 'AliBold', }, unitLabel: { fontSize: 18, fontWeight: '600', color: '#6f7ba7', marginLeft: 8, fontFamily: 'AliRegular', }, quickSelectionSection: { marginBottom: 24, }, quickSelectionTitle: { fontSize: 14, fontWeight: '600', color: '#6f7ba7', marginBottom: 12, fontFamily: 'AliRegular', marginLeft: 4, }, quickButtons: { flexDirection: 'row', flexWrap: 'wrap', gap: 8, }, quickButton: { paddingHorizontal: 16, paddingVertical: 10, borderRadius: 20, backgroundColor: '#F1F5F9', minWidth: 64, alignItems: 'center', }, quickButtonSelected: { backgroundColor: '#4F5BD5', }, quickButtonText: { fontSize: 14, fontWeight: '600', color: '#64748B', fontFamily: 'AliRegular', }, quickButtonTextSelected: { color: '#FFFFFF', fontWeight: '700', }, modalFooter: { paddingHorizontal: 24, paddingTop: 16, paddingBottom: 34, borderTopWidth: 1, borderTopColor: '#F1F5F9', }, saveButton: { borderRadius: 24, overflow: 'hidden', shadowColor: '#4F5BD5', shadowOffset: { width: 0, height: 8 }, shadowOpacity: 0.3, shadowRadius: 12, elevation: 8, }, saveButtonGradient: { paddingVertical: 16, alignItems: 'center', }, saveButtonText: { color: '#FFFFFF', fontSize: 18, fontWeight: '700', fontFamily: 'AliBold', }, });