875 lines
26 KiB
TypeScript
875 lines
26 KiB
TypeScript
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<WeightHistoryItem | null>(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<string, WeightHistoryItem[]>);
|
||
|
||
// 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 (
|
||
<View style={styles.container}>
|
||
{/* 背景 */}
|
||
<LinearGradient
|
||
colors={['#f3f4fb', '#f3f4fb']}
|
||
style={StyleSheet.absoluteFillObject}
|
||
/>
|
||
{/* 顶部装饰性渐变 */}
|
||
<LinearGradient
|
||
colors={['rgba(229, 252, 254, 0.8)', 'rgba(243, 244, 251, 0)']}
|
||
style={styles.topGradient}
|
||
start={{ x: 0.5, y: 0 }}
|
||
end={{ x: 0.5, y: 1 }}
|
||
/>
|
||
|
||
<HeaderBar
|
||
title={t('weightRecords.title')}
|
||
right={
|
||
isLiquidGlassAvailable() ? (
|
||
<TouchableOpacity
|
||
onPress={handleAddWeight}
|
||
activeOpacity={0.7}
|
||
>
|
||
<GlassView
|
||
style={styles.addButtonGlass}
|
||
glassEffectStyle="regular"
|
||
tintColor="rgba(255, 255, 255, 0.4)"
|
||
isInteractive={true}
|
||
>
|
||
<Ionicons name="add" size={24} color="#1c1f3a" />
|
||
</GlassView>
|
||
</TouchableOpacity>
|
||
) : (
|
||
<TouchableOpacity
|
||
style={styles.addButtonFallback}
|
||
onPress={handleAddWeight}
|
||
activeOpacity={0.7}
|
||
>
|
||
<Ionicons name="add" size={24} color="#1c1f3a" />
|
||
</TouchableOpacity>
|
||
)
|
||
}
|
||
/>
|
||
|
||
<ScrollView
|
||
style={styles.content}
|
||
contentContainerStyle={[styles.contentContainer, { paddingBottom: getTabBarBottomPadding() + 20, paddingTop: safeAreaTop }]}
|
||
showsVerticalScrollIndicator={false}
|
||
>
|
||
<View style={styles.headerBlock}>
|
||
<Text style={styles.pageTitle}>{t('weightRecords.title')}</Text>
|
||
<Text style={styles.pageSubtitle}>{t('weightRecords.pageSubtitle')}</Text>
|
||
</View>
|
||
|
||
{/* Weight Statistics Cards */}
|
||
<View style={styles.statsGrid}>
|
||
{/* Current Weight - Hero Card */}
|
||
<View style={styles.mainStatCard}>
|
||
<View style={styles.mainStatContent}>
|
||
<Text style={styles.mainStatLabel}>{t('weightRecords.stats.currentWeight')}</Text>
|
||
<View style={styles.mainStatValueContainer}>
|
||
<Text style={styles.mainStatValue}>{currentWeight.toFixed(1)}</Text>
|
||
<Text style={styles.mainStatUnit}>kg</Text>
|
||
</View>
|
||
<View style={styles.totalLossTag}>
|
||
<Ionicons name={totalWeightLoss <= 0 ? "trending-down" : "trending-up"} size={16} color="#ffffff" />
|
||
<Text style={styles.totalLossText}>
|
||
{totalWeightLoss > 0 ? '+' : ''}{totalWeightLoss.toFixed(1)} kg
|
||
</Text>
|
||
</View>
|
||
</View>
|
||
<LinearGradient
|
||
colors={['#4F5BD5', '#6B6CFF']}
|
||
style={StyleSheet.absoluteFillObject}
|
||
start={{ x: 0, y: 0 }}
|
||
end={{ x: 1, y: 1 }}
|
||
// @ts-ignore
|
||
borderRadius={24}
|
||
/>
|
||
<Image
|
||
source={require('@/assets/images/icons/iconWeight.png')}
|
||
style={styles.statIconBg}
|
||
/>
|
||
</View>
|
||
|
||
{/* Secondary Stats Row */}
|
||
<View style={styles.secondaryStatsRow}>
|
||
{/* Initial Weight */}
|
||
<TouchableOpacity
|
||
style={styles.secondaryStatCard}
|
||
onPress={handleEditInitialWeight}
|
||
activeOpacity={0.7}
|
||
>
|
||
<View style={styles.secondaryStatHeader}>
|
||
<Text style={styles.secondaryStatLabel}>{t('weightRecords.stats.initialWeight')}</Text>
|
||
<Ionicons name="create-outline" size={14} color="#9ba3c7" />
|
||
</View>
|
||
<Text style={styles.secondaryStatValue}>{initialWeight.toFixed(1)}<Text style={styles.secondaryStatUnit}>kg</Text></Text>
|
||
</TouchableOpacity>
|
||
|
||
{/* Target Weight */}
|
||
<TouchableOpacity
|
||
style={styles.secondaryStatCard}
|
||
onPress={handleEditTargetWeight}
|
||
activeOpacity={0.7}
|
||
>
|
||
<View style={styles.secondaryStatHeader}>
|
||
<Text style={styles.secondaryStatLabel}>{t('weightRecords.stats.targetWeight')}</Text>
|
||
<Ionicons name="create-outline" size={14} color="#9ba3c7" />
|
||
</View>
|
||
<Text style={styles.secondaryStatValue}>{targetWeight.toFixed(1)}<Text style={styles.secondaryStatUnit}>kg</Text></Text>
|
||
</TouchableOpacity>
|
||
</View>
|
||
</View>
|
||
|
||
{/* 减重进度条 - 仅在设置了目标体重时显示 */}
|
||
{hasTargetWeight && (
|
||
<View style={styles.progressContainer}>
|
||
<WeightProgressBar
|
||
progress={weightProgress}
|
||
currentWeight={currentWeight}
|
||
targetWeight={targetWeight}
|
||
initialWeight={initialWeight}
|
||
showTopBorder={false}
|
||
/>
|
||
</View>
|
||
)}
|
||
|
||
{/* Monthly Records */}
|
||
{Object.keys(groupedHistory).length > 0 ? (
|
||
<View style={styles.historySection}>
|
||
<Text style={styles.sectionTitle}>{t('weightRecords.history')}</Text>
|
||
{Object.entries(groupedHistory).map(([month, records]) => (
|
||
<View key={month} style={styles.monthContainer}>
|
||
<View style={styles.monthHeader}>
|
||
<Text style={styles.monthTitle}>{month}</Text>
|
||
</View>
|
||
|
||
{/* Individual Record Cards */}
|
||
<View style={styles.recordsList}>
|
||
{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 (
|
||
<WeightRecordCard
|
||
key={`${record.createdAt}-${recordIndex}`}
|
||
record={record}
|
||
onPress={handleEditWeightRecord}
|
||
onDelete={handleDeleteWeightRecord}
|
||
weightChange={weightChange}
|
||
/>
|
||
);
|
||
})}
|
||
</View>
|
||
</View>
|
||
))}
|
||
</View>
|
||
) : (
|
||
<View style={styles.emptyContainer}>
|
||
<Image
|
||
source={require('@/assets/images/icons/iconWeight.png')}
|
||
style={{ width: 80, height: 80, opacity: 0.5, marginBottom: 16, tintColor: '#cbd5e1' }}
|
||
/>
|
||
<View style={styles.emptyContent}>
|
||
<Text style={styles.emptyText}>{t('weightRecords.empty.title')}</Text>
|
||
<Text style={styles.emptySubtext}>{t('weightRecords.empty.subtitle')}</Text>
|
||
</View>
|
||
</View>
|
||
)}
|
||
</ScrollView>
|
||
|
||
{/* Weight Input Modal */}
|
||
<Modal
|
||
visible={showWeightPicker}
|
||
animationType="fade"
|
||
transparent
|
||
onRequestClose={() => setShowWeightPicker(false)}
|
||
>
|
||
<View style={styles.modalContainer}>
|
||
<TouchableOpacity
|
||
style={styles.modalBackdrop}
|
||
activeOpacity={1}
|
||
onPress={() => setShowWeightPicker(false)}
|
||
/>
|
||
<View style={[styles.modalSheet, { backgroundColor: '#ffffff' }]}>
|
||
{/* Header */}
|
||
<View style={styles.modalHeader}>
|
||
<TouchableOpacity
|
||
onPress={() => setShowWeightPicker(false)}
|
||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||
>
|
||
<Ionicons name="close" size={24} color="#1c1f3a" />
|
||
</TouchableOpacity>
|
||
<Text style={styles.modalTitle}>
|
||
{pickerType === 'current' && t('weightRecords.modal.recordWeight')}
|
||
{pickerType === 'initial' && t('weightRecords.modal.editInitialWeight')}
|
||
{pickerType === 'target' && t('weightRecords.modal.editTargetWeight')}
|
||
{pickerType === 'edit' && t('weightRecords.modal.editRecord')}
|
||
</Text>
|
||
<View style={{ width: 24 }} />
|
||
</View>
|
||
|
||
<ScrollView
|
||
style={styles.modalContent}
|
||
showsVerticalScrollIndicator={false}
|
||
>
|
||
{/* Weight Display Section */}
|
||
<View style={styles.inputSection}>
|
||
<View style={styles.weightInputContainer}>
|
||
<View style={styles.weightIcon}>
|
||
<Image
|
||
source={require('@/assets/images/icons/iconWeight.png')}
|
||
style={{ width: 24, height: 24, tintColor: '#4F5BD5' }}
|
||
/>
|
||
</View>
|
||
<View style={styles.inputWrapper}>
|
||
<Text style={[
|
||
styles.weightDisplay,
|
||
{ color: inputWeight ? '#1c1f3a' : '#9ba3c7' }
|
||
]}>
|
||
{inputWeight || t('weightRecords.modal.inputPlaceholder')}
|
||
</Text>
|
||
<Text style={styles.unitLabel}>{t('weightRecords.modal.unit')}</Text>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
|
||
{/* Quick Selection */}
|
||
<View style={styles.quickSelectionSection}>
|
||
<Text style={styles.quickSelectionTitle}>{t('weightRecords.modal.quickSelection')}</Text>
|
||
<View style={styles.quickButtons}>
|
||
{[50, 60, 70, 80, 90].map((weight) => (
|
||
<TouchableOpacity
|
||
key={weight}
|
||
style={[
|
||
styles.quickButton,
|
||
inputWeight === weight.toString() && styles.quickButtonSelected
|
||
]}
|
||
onPress={() => setInputWeight(weight.toString())}
|
||
activeOpacity={0.7}
|
||
>
|
||
<Text style={[
|
||
styles.quickButtonText,
|
||
inputWeight === weight.toString() && styles.quickButtonTextSelected
|
||
]}>
|
||
{weight}{t('weightRecords.modal.unit')}
|
||
</Text>
|
||
</TouchableOpacity>
|
||
))}
|
||
</View>
|
||
</View>
|
||
</ScrollView>
|
||
|
||
{/* Custom Number Keyboard */}
|
||
<NumberKeyboard
|
||
onNumberPress={handleNumberPress}
|
||
onDeletePress={handleDeletePress}
|
||
onDecimalPress={handleDecimalPress}
|
||
hasDecimal={inputWeight.includes('.')}
|
||
maxLength={6}
|
||
currentValue={inputWeight}
|
||
/>
|
||
|
||
{/* Save Button */}
|
||
<View style={styles.modalFooter}>
|
||
<TouchableOpacity
|
||
style={[
|
||
styles.saveButton,
|
||
{ opacity: !inputWeight.trim() ? 0.5 : 1 }
|
||
]}
|
||
onPress={handleWeightSave}
|
||
disabled={!inputWeight.trim()}
|
||
activeOpacity={0.8}
|
||
>
|
||
<LinearGradient
|
||
colors={['#4F5BD5', '#6B6CFF']}
|
||
start={{ x: 0, y: 0 }}
|
||
end={{ x: 1, y: 0 }}
|
||
style={styles.saveButtonGradient}
|
||
>
|
||
<Text style={styles.saveButtonText}>{t('weightRecords.modal.confirm')}</Text>
|
||
</LinearGradient>
|
||
</TouchableOpacity>
|
||
</View>
|
||
</View>
|
||
</View>
|
||
</Modal>
|
||
</View>
|
||
);
|
||
}
|
||
|
||
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',
|
||
},
|
||
}); |