feat: 新增体重记录功能,优化用户资料更新及图片组件缓存
This commit is contained in:
@@ -2,13 +2,13 @@ import { Ionicons } from '@expo/vector-icons';
|
|||||||
import { BlurView } from 'expo-blur';
|
import { BlurView } from 'expo-blur';
|
||||||
import * as Haptics from 'expo-haptics';
|
import * as Haptics from 'expo-haptics';
|
||||||
import * as ImagePicker from 'expo-image-picker';
|
import * as ImagePicker from 'expo-image-picker';
|
||||||
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
import { useLocalSearchParams } from 'expo-router';
|
import { useLocalSearchParams } from 'expo-router';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Alert,
|
Alert,
|
||||||
FlatList,
|
FlatList,
|
||||||
Image,
|
|
||||||
Keyboard,
|
Keyboard,
|
||||||
Modal,
|
Modal,
|
||||||
Platform,
|
Platform,
|
||||||
@@ -34,7 +34,7 @@ import { loadAiCoachSessionCache, saveAiCoachSessionCache } from '@/services/aiC
|
|||||||
import { api, getAuthToken, postTextStream } from '@/services/api';
|
import { api, getAuthToken, postTextStream } from '@/services/api';
|
||||||
import { selectLatestMoodRecordByDate } from '@/store/moodSlice';
|
import { selectLatestMoodRecordByDate } from '@/store/moodSlice';
|
||||||
import { generateWelcomeMessage, hasRecordedMoodToday } from '@/utils/welcomeMessage';
|
import { generateWelcomeMessage, hasRecordedMoodToday } from '@/utils/welcomeMessage';
|
||||||
import { LinearGradient } from 'expo-linear-gradient';
|
import { Image } from 'expo-image';
|
||||||
import { HistoryModal } from '../../components/model/HistoryModal';
|
import { HistoryModal } from '../../components/model/HistoryModal';
|
||||||
import { ActionSheet } from '../../components/ui/ActionSheet';
|
import { ActionSheet } from '../../components/ui/ActionSheet';
|
||||||
|
|
||||||
@@ -1902,8 +1902,9 @@ export default function CoachScreen() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Image
|
<Image
|
||||||
source={require('@/assets/images/icons/iconFlash.png')}
|
source={{ uri: 'https://plates-1251306435.cos.ap-guangzhou.myqcloud.com/images/icons/icon-profile-fish.png' }}
|
||||||
style={styles.usageIcon}
|
style={styles.usageIcon}
|
||||||
|
cachePolicy="memory-disk"
|
||||||
/>
|
/>
|
||||||
<Text style={styles.usageText}>
|
<Text style={styles.usageText}>
|
||||||
{userProfile?.isVip ? '不限' : `${userProfile?.freeUsageCount || 0}/${userProfile?.maxUsageCount || 0}`}
|
{userProfile?.isVip ? '不限' : `${userProfile?.freeUsageCount || 0}/${userProfile?.maxUsageCount || 0}`}
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ import { FitnessRingsCard } from '@/components/FitnessRingsCard';
|
|||||||
import { MoodCard } from '@/components/MoodCard';
|
import { MoodCard } from '@/components/MoodCard';
|
||||||
import { NutritionRadarCard } from '@/components/NutritionRadarCard';
|
import { NutritionRadarCard } from '@/components/NutritionRadarCard';
|
||||||
import { ProgressBar } from '@/components/ProgressBar';
|
import { ProgressBar } from '@/components/ProgressBar';
|
||||||
import { StressMeter } from '@/components/StressMeter';
|
|
||||||
import { WeightHistoryCard } from '@/components/WeightHistoryCard';
|
|
||||||
import HeartRateCard from '@/components/statistic/HeartRateCard';
|
import HeartRateCard from '@/components/statistic/HeartRateCard';
|
||||||
import OxygenSaturationCard from '@/components/statistic/OxygenSaturationCard';
|
import OxygenSaturationCard from '@/components/statistic/OxygenSaturationCard';
|
||||||
|
import { StressMeter } from '@/components/StressMeter';
|
||||||
|
import { WeightHistoryCard } from '@/components/weight/WeightHistoryCard';
|
||||||
import { Colors } from '@/constants/Colors';
|
import { Colors } from '@/constants/Colors';
|
||||||
import { getTabBarBottomPadding } from '@/constants/TabBar';
|
import { getTabBarBottomPadding } from '@/constants/TabBar';
|
||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import { Colors } from '@/constants/Colors';
|
|||||||
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
import { useColorScheme } from '@/hooks/useColorScheme';
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
import { useCosUpload } from '@/hooks/useCosUpload';
|
import { useCosUpload } from '@/hooks/useCosUpload';
|
||||||
import { updateUser as updateUserApi } from '@/services/users';
|
import { fetchMyProfile, updateUserProfile } from '@/store/userSlice';
|
||||||
import { fetchMyProfile } from '@/store/userSlice';
|
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import DateTimePicker from '@react-native-community/datetimepicker';
|
import DateTimePicker from '@react-native-community/datetimepicker';
|
||||||
@@ -175,8 +174,7 @@ export default function EditProfileScreen() {
|
|||||||
|
|
||||||
// 同步到后端(仅更新后端需要的字段)
|
// 同步到后端(仅更新后端需要的字段)
|
||||||
try {
|
try {
|
||||||
await updateUserApi({
|
await dispatch(updateUserProfile({
|
||||||
userId,
|
|
||||||
name: next.name || undefined,
|
name: next.name || undefined,
|
||||||
gender: (next.gender === 'male' || next.gender === 'female') ? next.gender : undefined,
|
gender: (next.gender === 'male' || next.gender === 'female') ? next.gender : undefined,
|
||||||
// 头像采用已上传的 URL(若有)
|
// 头像采用已上传的 URL(若有)
|
||||||
@@ -185,7 +183,7 @@ export default function EditProfileScreen() {
|
|||||||
height: next.height || undefined,
|
height: next.height || undefined,
|
||||||
birthDate: next.birthDate ? new Date(next.birthDate).getTime() / 1000 : undefined,
|
birthDate: next.birthDate ? new Date(next.birthDate).getTime() / 1000 : undefined,
|
||||||
activityLevel: next.activityLevel || undefined,
|
activityLevel: next.activityLevel || undefined,
|
||||||
});
|
}));
|
||||||
// 拉取最新用户信息,刷新全局状态
|
// 拉取最新用户信息,刷新全局状态
|
||||||
await dispatch(fetchMyProfile() as any);
|
await dispatch(fetchMyProfile() as any);
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
|
|||||||
692
app/weight-records.tsx
Normal file
692
app/weight-records.tsx
Normal file
@@ -0,0 +1,692 @@
|
|||||||
|
import { Colors } from '@/constants/Colors';
|
||||||
|
import { getTabBarBottomPadding } from '@/constants/TabBar';
|
||||||
|
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
|
||||||
|
import { useColorScheme } from '@/hooks/useColorScheme';
|
||||||
|
import { fetchWeightHistory, updateUserProfile, WeightHistoryItem } from '@/store/userSlice';
|
||||||
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import { LinearGradient } from 'expo-linear-gradient';
|
||||||
|
import { router } from 'expo-router';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import {
|
||||||
|
Modal,
|
||||||
|
ScrollView,
|
||||||
|
StyleSheet,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
TouchableOpacity,
|
||||||
|
View
|
||||||
|
} from 'react-native';
|
||||||
|
|
||||||
|
|
||||||
|
export default function WeightRecordsPage() {
|
||||||
|
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'>('current');
|
||||||
|
const [inputWeight, setInputWeight] = useState('');
|
||||||
|
|
||||||
|
const colorScheme = useColorScheme();
|
||||||
|
const themeColors = Colors[colorScheme ?? 'light'];
|
||||||
|
|
||||||
|
console.log('userProfile:', userProfile);
|
||||||
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
loadWeightHistory();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const loadWeightHistory = async () => {
|
||||||
|
try {
|
||||||
|
await dispatch(fetchWeightHistory() as any);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载体重历史失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGoBack = () => {
|
||||||
|
router.back();
|
||||||
|
};
|
||||||
|
|
||||||
|
const initializeInput = (weight: number) => {
|
||||||
|
setInputWeight(weight.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddWeight = () => {
|
||||||
|
setPickerType('current');
|
||||||
|
const weight = userProfile?.weight ? parseFloat(userProfile.weight) : 70.0;
|
||||||
|
initializeInput(weight);
|
||||||
|
setShowWeightPicker(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditInitialWeight = () => {
|
||||||
|
setPickerType('initial');
|
||||||
|
const initialWeight = userProfile?.initialWeight || userProfile?.weight || '70.0';
|
||||||
|
initializeInput(parseFloat(initialWeight));
|
||||||
|
setShowWeightPicker(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditTargetWeight = () => {
|
||||||
|
setPickerType('target');
|
||||||
|
const targetWeight = userProfile?.targetWeight || '60.0';
|
||||||
|
initializeInput(parseFloat(targetWeight));
|
||||||
|
setShowWeightPicker(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleWeightSave = async () => {
|
||||||
|
const weight = parseFloat(inputWeight);
|
||||||
|
if (isNaN(weight) || weight <= 0 || weight > 500) {
|
||||||
|
alert('请输入有效的体重值(0-500kg)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (pickerType === 'current') {
|
||||||
|
// Update current weight in profile and add weight record
|
||||||
|
await dispatch(updateUserProfile({ weight: weight }) as any);
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
setShowWeightPicker(false);
|
||||||
|
setInputWeight('');
|
||||||
|
await loadWeightHistory();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存体重失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// 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 monthKey = dayjs(item.createdAt).format('YYYY年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;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<LinearGradient
|
||||||
|
colors={[themeColors.backgroundGradientStart, themeColors.backgroundGradientEnd]}
|
||||||
|
style={styles.gradient}
|
||||||
|
start={{ x: 0, y: 0 }}
|
||||||
|
end={{ x: 0, y: 1 }}
|
||||||
|
>
|
||||||
|
{/* Header */}
|
||||||
|
<View style={styles.header}>
|
||||||
|
<TouchableOpacity onPress={handleGoBack} style={styles.backButton}>
|
||||||
|
<Ionicons name="chevron-back" size={24} color="#192126" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
<TouchableOpacity onPress={handleAddWeight} style={styles.addButton}>
|
||||||
|
<Ionicons name="add" size={24} color="#192126" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
{/* Weight Statistics */}
|
||||||
|
<View style={styles.statsContainer}>
|
||||||
|
<View style={styles.statsRow}>
|
||||||
|
<View style={styles.statItem}>
|
||||||
|
<Text style={styles.statValue}>{totalWeightLoss.toFixed(1)}kg</Text>
|
||||||
|
<Text style={styles.statLabel}>累计减重</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.statItem}>
|
||||||
|
<Text style={styles.statValue}>{currentWeight.toFixed(1)}kg</Text>
|
||||||
|
<Text style={styles.statLabel}>当前体重</Text>
|
||||||
|
</View>
|
||||||
|
<View style={styles.statItem}>
|
||||||
|
<Text style={styles.statValue}>{initialWeight.toFixed(1)}kg</Text>
|
||||||
|
<View style={styles.statLabelContainer}>
|
||||||
|
<Text style={styles.statLabel}>初始体重</Text>
|
||||||
|
<TouchableOpacity onPress={handleEditInitialWeight} style={styles.editIcon}>
|
||||||
|
<Ionicons name="create-outline" size={14} color="#FF9500" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<View style={styles.statItem}>
|
||||||
|
<Text style={styles.statValue}>{targetWeight.toFixed(1)}kg</Text>
|
||||||
|
<View style={styles.statLabelContainer}>
|
||||||
|
<Text style={styles.statLabel}>目标体重</Text>
|
||||||
|
<TouchableOpacity onPress={handleEditTargetWeight} style={styles.editIcon}>
|
||||||
|
<Ionicons name="create-outline" size={14} color="#FF9500" />
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<ScrollView
|
||||||
|
style={styles.content}
|
||||||
|
contentContainerStyle={[styles.contentContainer, { paddingBottom: getTabBarBottomPadding() + 20 }]}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
>
|
||||||
|
|
||||||
|
{/* Monthly Records */}
|
||||||
|
{Object.keys(groupedHistory).length > 0 ? (
|
||||||
|
Object.entries(groupedHistory).map(([month, records]) => (
|
||||||
|
<View key={month} style={styles.monthContainer}>
|
||||||
|
{/* Month Header Card */}
|
||||||
|
{/* <View style={styles.monthHeaderCard}>
|
||||||
|
<View style={styles.monthTitleRow}>
|
||||||
|
<Text style={styles.monthNumber}>
|
||||||
|
{dayjs(month, 'YYYY年MM月').format('MM')}
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.monthText}>月</Text>
|
||||||
|
<Text style={styles.yearText}>
|
||||||
|
{dayjs(month, 'YYYY年MM月').format('YYYY年')}
|
||||||
|
</Text>
|
||||||
|
<View style={styles.expandIcon}>
|
||||||
|
<Ionicons name="chevron-up" size={16} color="#FF9500" />
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<Text style={styles.monthStatsText}>
|
||||||
|
累计减重:<Text style={styles.statsBold}>{totalWeightLoss.toFixed(1)}kg</Text> 日均减重:<Text style={styles.statsBold}>{avgWeightLoss.toFixed(1)}kg</Text>
|
||||||
|
</Text>
|
||||||
|
</View> */}
|
||||||
|
|
||||||
|
{/* 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 (
|
||||||
|
<View key={`${record.createdAt}-${recordIndex}`} style={styles.recordCard}>
|
||||||
|
<View style={styles.recordHeader}>
|
||||||
|
<Text style={styles.recordDateTime}>
|
||||||
|
{dayjs(record.createdAt).format('MM月DD日 HH:mm')}
|
||||||
|
</Text>
|
||||||
|
{/* <TouchableOpacity style={styles.recordEditButton}>
|
||||||
|
<Ionicons name="create-outline" size={16} color="#FF9500" />
|
||||||
|
</TouchableOpacity> */}
|
||||||
|
</View>
|
||||||
|
<View style={styles.recordContent}>
|
||||||
|
<Text style={styles.recordWeightLabel}>体重:</Text>
|
||||||
|
<Text style={styles.recordWeightValue}>{record.weight}kg</Text>
|
||||||
|
{Math.abs(weightChange) > 0 && (
|
||||||
|
<View style={[
|
||||||
|
styles.weightChangeTag,
|
||||||
|
{ backgroundColor: weightChange < 0 ? '#E8F5E8' : '#FFF2E8' }
|
||||||
|
]}>
|
||||||
|
<Ionicons
|
||||||
|
name={weightChange < 0 ? "arrow-down" : "arrow-up"}
|
||||||
|
size={12}
|
||||||
|
color={weightChange < 0 ? Colors.light.accentGreen : '#FF9500'}
|
||||||
|
/>
|
||||||
|
<Text style={[
|
||||||
|
styles.weightChangeText,
|
||||||
|
{ color: weightChange < 0 ? Colors.light.accentGreen : '#FF9500' }
|
||||||
|
]}>
|
||||||
|
{Math.abs(weightChange).toFixed(1)}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<View style={styles.emptyContainer}>
|
||||||
|
<View style={styles.emptyContent}>
|
||||||
|
<Text style={styles.emptyText}>暂无体重记录</Text>
|
||||||
|
<Text style={styles.emptySubtext}>点击右上角添加按钮开始记录</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
|
||||||
|
{/* Weight Input Modal */}
|
||||||
|
<Modal
|
||||||
|
visible={showWeightPicker}
|
||||||
|
animationType="fade"
|
||||||
|
transparent
|
||||||
|
onRequestClose={() => setShowWeightPicker(false)}
|
||||||
|
>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={styles.modalBackdrop}
|
||||||
|
activeOpacity={1}
|
||||||
|
onPress={() => setShowWeightPicker(false)}
|
||||||
|
/>
|
||||||
|
<View style={[styles.modalSheet, { backgroundColor: themeColors.background }]}>
|
||||||
|
{/* Header */}
|
||||||
|
<View style={styles.modalHeader}>
|
||||||
|
<TouchableOpacity onPress={() => setShowWeightPicker(false)}>
|
||||||
|
<Ionicons name="close" size={24} color={themeColors.text} />
|
||||||
|
</TouchableOpacity>
|
||||||
|
<Text style={[styles.modalTitle, { color: themeColors.text }]}>
|
||||||
|
{pickerType === 'current' && '记录体重'}
|
||||||
|
{pickerType === 'initial' && '编辑初始体重'}
|
||||||
|
{pickerType === 'target' && '编辑目标体重'}
|
||||||
|
</Text>
|
||||||
|
<View style={{ width: 24 }} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
<View style={styles.modalContent}>
|
||||||
|
{/* Weight Input Section */}
|
||||||
|
<View style={styles.inputSection}>
|
||||||
|
<View style={styles.weightInputContainer}>
|
||||||
|
<View style={styles.weightIcon}>
|
||||||
|
<Ionicons name="scale-outline" size={20} color="#6366F1" />
|
||||||
|
</View>
|
||||||
|
<View style={styles.inputWrapper}>
|
||||||
|
<TextInput
|
||||||
|
style={[styles.weightInput, { color: themeColors.text }]}
|
||||||
|
placeholder="输入体重"
|
||||||
|
placeholderTextColor={themeColors.textSecondary}
|
||||||
|
value={inputWeight}
|
||||||
|
onChangeText={setInputWeight}
|
||||||
|
keyboardType="decimal-pad"
|
||||||
|
autoFocus={true}
|
||||||
|
selectTextOnFocus={true}
|
||||||
|
/>
|
||||||
|
<Text style={[styles.unitLabel, { color: themeColors.textSecondary }]}>kg</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Weight Range Hint */}
|
||||||
|
<Text style={[styles.hintText, { color: themeColors.textSecondary }]}>
|
||||||
|
请输入 0-500 之间的数值,支持小数
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Quick Selection */}
|
||||||
|
<View style={styles.quickSelectionSection}>
|
||||||
|
<Text style={[styles.quickSelectionTitle, { color: themeColors.text }]}>快速选择</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())}
|
||||||
|
>
|
||||||
|
<Text style={[
|
||||||
|
styles.quickButtonText,
|
||||||
|
inputWeight === weight.toString() && styles.quickButtonTextSelected
|
||||||
|
]}>
|
||||||
|
{weight}kg
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Save Button */}
|
||||||
|
<View style={styles.modalFooter}>
|
||||||
|
<TouchableOpacity
|
||||||
|
style={[
|
||||||
|
styles.saveButton,
|
||||||
|
{ opacity: !inputWeight.trim() ? 0.5 : 1 }
|
||||||
|
]}
|
||||||
|
onPress={handleWeightSave}
|
||||||
|
disabled={!inputWeight.trim()}
|
||||||
|
>
|
||||||
|
<Text style={styles.saveButtonText}>确定</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
</LinearGradient>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
gradient: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingTop: 60,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingBottom: 10,
|
||||||
|
},
|
||||||
|
backButton: {
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 20,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
addButton: {
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
borderRadius: 20,
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.9)',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
flex: 1,
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
},
|
||||||
|
contentContainer: {
|
||||||
|
flexGrow: 1,
|
||||||
|
},
|
||||||
|
statsContainer: {
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
borderRadius: 16,
|
||||||
|
padding: 20,
|
||||||
|
marginBottom: 20,
|
||||||
|
marginLeft: 20,
|
||||||
|
marginRight: 20,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowRadius: 8,
|
||||||
|
elevation: 3,
|
||||||
|
},
|
||||||
|
statsRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
},
|
||||||
|
statItem: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
statValue: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '800',
|
||||||
|
color: '#192126',
|
||||||
|
marginBottom: 4,
|
||||||
|
},
|
||||||
|
statLabelContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
statLabel: {
|
||||||
|
fontSize: 12,
|
||||||
|
color: '#687076',
|
||||||
|
marginRight: 4,
|
||||||
|
},
|
||||||
|
editIcon: {
|
||||||
|
padding: 2,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: 'rgba(255, 149, 0, 0.1)',
|
||||||
|
},
|
||||||
|
monthContainer: {
|
||||||
|
marginBottom: 16,
|
||||||
|
},
|
||||||
|
monthHeaderCard: {
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
borderRadius: 16,
|
||||||
|
padding: 20,
|
||||||
|
marginBottom: 12,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 2 },
|
||||||
|
shadowOpacity: 0.08,
|
||||||
|
shadowRadius: 12,
|
||||||
|
elevation: 3,
|
||||||
|
},
|
||||||
|
monthTitleRow: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'baseline',
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
monthNumber: {
|
||||||
|
fontSize: 48,
|
||||||
|
fontWeight: '800',
|
||||||
|
color: '#192126',
|
||||||
|
lineHeight: 48,
|
||||||
|
},
|
||||||
|
monthText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
color: '#192126',
|
||||||
|
marginLeft: 4,
|
||||||
|
marginRight: 8,
|
||||||
|
},
|
||||||
|
yearText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '500',
|
||||||
|
color: '#687076',
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
expandIcon: {
|
||||||
|
padding: 4,
|
||||||
|
},
|
||||||
|
monthStatsText: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#687076',
|
||||||
|
lineHeight: 20,
|
||||||
|
},
|
||||||
|
statsBold: {
|
||||||
|
fontWeight: '700',
|
||||||
|
color: '#192126',
|
||||||
|
},
|
||||||
|
recordCard: {
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
borderRadius: 16,
|
||||||
|
padding: 20,
|
||||||
|
marginBottom: 12,
|
||||||
|
shadowColor: '#000',
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: 0.06,
|
||||||
|
shadowRadius: 8,
|
||||||
|
elevation: 2,
|
||||||
|
},
|
||||||
|
recordHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
recordDateTime: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#687076',
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
recordEditButton: {
|
||||||
|
padding: 6,
|
||||||
|
borderRadius: 8,
|
||||||
|
backgroundColor: 'rgba(255, 149, 0, 0.1)',
|
||||||
|
},
|
||||||
|
recordContent: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
recordWeightLabel: {
|
||||||
|
fontSize: 16,
|
||||||
|
color: '#687076',
|
||||||
|
fontWeight: '500',
|
||||||
|
},
|
||||||
|
recordWeightValue: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: '#192126',
|
||||||
|
marginLeft: 4,
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
weightChangeTag: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingHorizontal: 8,
|
||||||
|
paddingVertical: 4,
|
||||||
|
borderRadius: 12,
|
||||||
|
marginLeft: 12,
|
||||||
|
},
|
||||||
|
weightChangeText: {
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: '600',
|
||||||
|
marginLeft: 2,
|
||||||
|
},
|
||||||
|
emptyContainer: {
|
||||||
|
flex: 1,
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
minHeight: 300,
|
||||||
|
},
|
||||||
|
emptyContent: {
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
emptyText: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '700',
|
||||||
|
color: '#192126',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
emptySubtext: {
|
||||||
|
fontSize: 14,
|
||||||
|
color: '#687076',
|
||||||
|
},
|
||||||
|
// Modal Styles
|
||||||
|
modalBackdrop: {
|
||||||
|
...StyleSheet.absoluteFillObject,
|
||||||
|
backgroundColor: 'rgba(0,0,0,0.35)',
|
||||||
|
},
|
||||||
|
modalSheet: {
|
||||||
|
position: 'absolute',
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
borderTopLeftRadius: 20,
|
||||||
|
borderTopRightRadius: 20,
|
||||||
|
maxHeight: '80%',
|
||||||
|
},
|
||||||
|
modalHeader: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingTop: 20,
|
||||||
|
paddingBottom: 10,
|
||||||
|
},
|
||||||
|
modalTitle: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
modalContent: {
|
||||||
|
paddingHorizontal: 20,
|
||||||
|
paddingBottom: 10,
|
||||||
|
},
|
||||||
|
inputSection: {
|
||||||
|
backgroundColor: '#FFFFFF',
|
||||||
|
borderRadius: 16,
|
||||||
|
padding: 16,
|
||||||
|
marginBottom: 12,
|
||||||
|
},
|
||||||
|
weightInputContainer: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
weightIcon: {
|
||||||
|
width: 32,
|
||||||
|
height: 32,
|
||||||
|
borderRadius: 16,
|
||||||
|
backgroundColor: '#F0F9FF',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginRight: 12,
|
||||||
|
},
|
||||||
|
inputWrapper: {
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
borderBottomWidth: 2,
|
||||||
|
borderBottomColor: '#E5E7EB',
|
||||||
|
paddingBottom: 6,
|
||||||
|
},
|
||||||
|
weightInput: {
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: '600',
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
unitLabel: {
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '500',
|
||||||
|
marginLeft: 8,
|
||||||
|
},
|
||||||
|
hintText: {
|
||||||
|
fontSize: 12,
|
||||||
|
textAlign: 'center',
|
||||||
|
marginTop: 4,
|
||||||
|
},
|
||||||
|
quickSelectionSection: {
|
||||||
|
paddingHorizontal: 4,
|
||||||
|
marginBottom: 8,
|
||||||
|
},
|
||||||
|
quickSelectionTitle: {
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: '600',
|
||||||
|
marginBottom: 12,
|
||||||
|
textAlign: 'center',
|
||||||
|
},
|
||||||
|
quickButtons: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 8,
|
||||||
|
},
|
||||||
|
quickButton: {
|
||||||
|
paddingHorizontal: 14,
|
||||||
|
paddingVertical: 8,
|
||||||
|
borderRadius: 18,
|
||||||
|
backgroundColor: '#F3F4F6',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#E5E7EB',
|
||||||
|
minWidth: 60,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
quickButtonSelected: {
|
||||||
|
backgroundColor: '#6366F1',
|
||||||
|
borderColor: '#6366F1',
|
||||||
|
},
|
||||||
|
quickButtonText: {
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: '500',
|
||||||
|
color: '#6B7280',
|
||||||
|
},
|
||||||
|
quickButtonTextSelected: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
modalFooter: {
|
||||||
|
padding: 20,
|
||||||
|
paddingBottom: 25,
|
||||||
|
},
|
||||||
|
saveButton: {
|
||||||
|
backgroundColor: '#6366F1',
|
||||||
|
borderRadius: 16,
|
||||||
|
paddingVertical: 16,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
saveButtonText: {
|
||||||
|
color: '#FFFFFF',
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: '600',
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -59,8 +59,6 @@ const ActivityHeatMap = () => {
|
|||||||
return data;
|
return data;
|
||||||
}, [activityData, weeksToShow]);
|
}, [activityData, weeksToShow]);
|
||||||
|
|
||||||
console.log('generateActivityData', generateActivityData);
|
|
||||||
|
|
||||||
|
|
||||||
// 根据活跃度计算颜色 - 优化配色方案
|
// 根据活跃度计算颜色 - 优化配色方案
|
||||||
const getActivityColor = (level: number): string => {
|
const getActivityColor = (level: number): string => {
|
||||||
|
|||||||
@@ -54,13 +54,13 @@ export function WeightHistoryCard() {
|
|||||||
|
|
||||||
const hasWeight = userProfile?.weight && parseFloat(userProfile.weight) > 0;
|
const hasWeight = userProfile?.weight && parseFloat(userProfile.weight) > 0;
|
||||||
const hasHeight = userProfile?.height && parseFloat(userProfile.height) > 0;
|
const hasHeight = userProfile?.height && parseFloat(userProfile.height) > 0;
|
||||||
|
|
||||||
// BMI 计算
|
// BMI 计算
|
||||||
const canCalculate = canCalculateBMI(
|
const canCalculate = canCalculateBMI(
|
||||||
userProfile?.weight ? parseFloat(userProfile.weight) : undefined,
|
userProfile?.weight ? parseFloat(userProfile.weight) : undefined,
|
||||||
userProfile?.height ? parseFloat(userProfile.height) : undefined
|
userProfile?.height ? parseFloat(userProfile.height) : undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
const bmiResult = canCalculate && userProfile?.weight && userProfile?.height
|
const bmiResult = canCalculate && userProfile?.weight && userProfile?.height
|
||||||
? getBMIResult(parseFloat(userProfile.weight), parseFloat(userProfile.height))
|
? getBMIResult(parseFloat(userProfile.weight), parseFloat(userProfile.height))
|
||||||
: null;
|
: null;
|
||||||
@@ -95,6 +95,10 @@ export function WeightHistoryCard() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 切换图表显示状态的动画函数
|
// 切换图表显示状态的动画函数
|
||||||
|
const navigateToWeightRecords = () => {
|
||||||
|
pushIfAuthedElseLogin(ROUTES.WEIGHT_RECORDS);
|
||||||
|
};
|
||||||
|
|
||||||
const toggleChart = () => {
|
const toggleChart = () => {
|
||||||
if (isAnimating) return; // 防止动画期间重复触发
|
if (isAnimating) return; // 防止动画期间重复触发
|
||||||
|
|
||||||
@@ -195,7 +199,7 @@ export function WeightHistoryCard() {
|
|||||||
// 如果正在加载,显示加载状态
|
// 如果正在加载,显示加载状态
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.card}>
|
<TouchableOpacity style={styles.card} onPress={navigateToWeightRecords} activeOpacity={0.8}>
|
||||||
<View style={styles.cardHeader}>
|
<View style={styles.cardHeader}>
|
||||||
<View style={styles.iconSquare}>
|
<View style={styles.iconSquare}>
|
||||||
<Image source={require('@/assets/images/icons/iconWeight.png')} style={{ width: 18, height: 18 }} />
|
<Image source={require('@/assets/images/icons/iconWeight.png')} style={{ width: 18, height: 18 }} />
|
||||||
@@ -205,14 +209,14 @@ export function WeightHistoryCard() {
|
|||||||
<View style={styles.emptyContent}>
|
<View style={styles.emptyContent}>
|
||||||
<Text style={styles.emptyDescription}>加载中...</Text>
|
<Text style={styles.emptyDescription}>加载中...</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有体重数据,显示引导卡片
|
// 如果没有体重数据,显示引导卡片
|
||||||
if (!hasWeight) {
|
if (!hasWeight) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.card}>
|
<TouchableOpacity style={styles.card} onPress={navigateToWeightRecords} activeOpacity={0.8}>
|
||||||
<View style={styles.cardHeader}>
|
<View style={styles.cardHeader}>
|
||||||
<View style={styles.iconSquare}>
|
<View style={styles.iconSquare}>
|
||||||
<Image source={require('@/assets/images/icons/iconWeight.png')} style={{ width: 18, height: 18 }} />
|
<Image source={require('@/assets/images/icons/iconWeight.png')} style={{ width: 18, height: 18 }} />
|
||||||
@@ -227,14 +231,17 @@ export function WeightHistoryCard() {
|
|||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.recordButton}
|
style={styles.recordButton}
|
||||||
onPress={navigateToCoach}
|
onPress={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
navigateToCoach();
|
||||||
|
}}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
<Ionicons name="add" size={18} color="#192126" />
|
<Ionicons name="add" size={18} color="#192126" />
|
||||||
<Text style={styles.recordButtonText}>记录</Text>
|
<Text style={styles.recordButtonText}>记录</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,7 +252,7 @@ export function WeightHistoryCard() {
|
|||||||
|
|
||||||
if (sortedHistory.length === 0) {
|
if (sortedHistory.length === 0) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.card}>
|
<TouchableOpacity style={styles.card} onPress={navigateToWeightRecords} activeOpacity={0.8}>
|
||||||
<View style={styles.cardHeader}>
|
<View style={styles.cardHeader}>
|
||||||
<View style={styles.iconSquare}>
|
<View style={styles.iconSquare}>
|
||||||
<Image source={require('@/assets/images/icons/iconWeight.png')} style={{ width: 18, height: 18 }} />
|
<Image source={require('@/assets/images/icons/iconWeight.png')} style={{ width: 18, height: 18 }} />
|
||||||
@@ -259,14 +266,17 @@ export function WeightHistoryCard() {
|
|||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.recordButton}
|
style={styles.recordButton}
|
||||||
onPress={navigateToCoach}
|
onPress={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
navigateToCoach();
|
||||||
|
}}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
<Ionicons name="add" size={18} color="#FFFFFF" />
|
<Ionicons name="add" size={18} color="#FFFFFF" />
|
||||||
<Text style={styles.recordButtonText}>记录体重</Text>
|
<Text style={styles.recordButtonText}>记录体重</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,7 +306,7 @@ export function WeightHistoryCard() {
|
|||||||
pathData;
|
pathData;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.card}>
|
<TouchableOpacity style={styles.card} onPress={navigateToWeightRecords} activeOpacity={0.8}>
|
||||||
<View style={styles.cardHeader}>
|
<View style={styles.cardHeader}>
|
||||||
<View style={styles.iconSquare}>
|
<View style={styles.iconSquare}>
|
||||||
<Image source={require('@/assets/images/icons/iconWeight.png')} style={{ width: 18, height: 18 }} />
|
<Image source={require('@/assets/images/icons/iconWeight.png')} style={{ width: 18, height: 18 }} />
|
||||||
@@ -305,7 +315,10 @@ export function WeightHistoryCard() {
|
|||||||
<View style={styles.headerButtons}>
|
<View style={styles.headerButtons}>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.chartToggleButton}
|
style={styles.chartToggleButton}
|
||||||
onPress={toggleChart}
|
onPress={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleChart();
|
||||||
|
}}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
<Ionicons
|
<Ionicons
|
||||||
@@ -316,7 +329,10 @@ export function WeightHistoryCard() {
|
|||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
style={styles.addButton}
|
style={styles.addButton}
|
||||||
onPress={navigateToCoach}
|
onPress={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
navigateToCoach();
|
||||||
|
}}
|
||||||
activeOpacity={0.8}
|
activeOpacity={0.8}
|
||||||
>
|
>
|
||||||
<Ionicons name="add" size={16} color={Colors.light.primary} />
|
<Ionicons name="add" size={16} color={Colors.light.primary} />
|
||||||
@@ -352,7 +368,10 @@ export function WeightHistoryCard() {
|
|||||||
{bmiResult.value}
|
{bmiResult.value}
|
||||||
</Text>
|
</Text>
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
onPress={handleShowBMIModal}
|
onPress={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleShowBMIModal();
|
||||||
|
}}
|
||||||
style={styles.bmiInfoButton}
|
style={styles.bmiInfoButton}
|
||||||
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
|
||||||
>
|
>
|
||||||
@@ -499,7 +518,7 @@ export function WeightHistoryCard() {
|
|||||||
{BMI_CATEGORIES.map((category, index) => {
|
{BMI_CATEGORIES.map((category, index) => {
|
||||||
const colors = [
|
const colors = [
|
||||||
{ bg: '#FEF3C7', text: '#B45309', border: '#F59E0B' }, // 偏瘦
|
{ bg: '#FEF3C7', text: '#B45309', border: '#F59E0B' }, // 偏瘦
|
||||||
{ bg: '#E8F5E8', text: Colors.light.accentGreenDark, border: Colors.light.accentGreen }, // 正常
|
{ bg: '#E8F5E8', text: Colors.light.accentGreen, border: Colors.light.accentGreen }, // 正常
|
||||||
{ bg: '#FEF3C7', text: '#B45309', border: '#F59E0B' }, // 超重
|
{ bg: '#FEF3C7', text: '#B45309', border: '#F59E0B' }, // 超重
|
||||||
{ bg: '#FEE2E2', text: '#B91C1C', border: '#EF4444' } // 肥胖
|
{ bg: '#FEE2E2', text: '#B91C1C', border: '#EF4444' } // 肥胖
|
||||||
][index];
|
][index];
|
||||||
@@ -563,7 +582,7 @@ export function WeightHistoryCard() {
|
|||||||
</View>
|
</View>
|
||||||
</LinearGradient>
|
</LinearGradient>
|
||||||
</Modal>
|
</Modal>
|
||||||
</View>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -711,7 +730,7 @@ const styles = StyleSheet.create({
|
|||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
color: '#192126',
|
color: '#192126',
|
||||||
},
|
},
|
||||||
|
|
||||||
// BMI 相关样式
|
// BMI 相关样式
|
||||||
bmiValueContainer: {
|
bmiValueContainer: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
@@ -734,7 +753,7 @@ const styles = StyleSheet.create({
|
|||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
},
|
},
|
||||||
|
|
||||||
// BMI 弹窗样式
|
// BMI 弹窗样式
|
||||||
bmiModalContainer: {
|
bmiModalContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@@ -148,7 +148,7 @@ export const Colors = {
|
|||||||
ornamentAccent: palette.success[100],
|
ornamentAccent: palette.success[100],
|
||||||
|
|
||||||
// 背景渐变色
|
// 背景渐变色
|
||||||
backgroundGradientStart: palette.gray[25],
|
backgroundGradientStart: palette.purple[25],
|
||||||
backgroundGradientEnd: palette.base.white,
|
backgroundGradientEnd: palette.base.white,
|
||||||
},
|
},
|
||||||
dark: {
|
dark: {
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ export const ROUTES = {
|
|||||||
// 营养相关路由
|
// 营养相关路由
|
||||||
NUTRITION_RECORDS: '/nutrition/records',
|
NUTRITION_RECORDS: '/nutrition/records',
|
||||||
|
|
||||||
|
// 体重记录相关路由
|
||||||
|
WEIGHT_RECORDS: '/weight-records',
|
||||||
|
|
||||||
// 任务相关路由
|
// 任务相关路由
|
||||||
TASK_DETAIL: '/task-detail',
|
TASK_DETAIL: '/task-detail',
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { api } from '@/services/api';
|
|||||||
export type Gender = 'male' | 'female';
|
export type Gender = 'male' | 'female';
|
||||||
|
|
||||||
export type UpdateUserDto = {
|
export type UpdateUserDto = {
|
||||||
userId: string;
|
|
||||||
name?: string;
|
name?: string;
|
||||||
avatar?: string; // base64 字符串
|
avatar?: string; // base64 字符串
|
||||||
gender?: Gender;
|
gender?: Gender;
|
||||||
@@ -14,6 +13,8 @@ export type UpdateUserDto = {
|
|||||||
weight?: number;
|
weight?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
activityLevel?: number; // 活动水平 1-4
|
activityLevel?: number; // 活动水平 1-4
|
||||||
|
initialWeight?: number; // 初始体重
|
||||||
|
targetWeight?: number; // 目标体重
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function updateUser(dto: UpdateUserDto): Promise<Record<string, any>> {
|
export async function updateUser(dto: UpdateUserDto): Promise<Record<string, any>> {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { api, loadPersistedToken, setAuthToken, STORAGE_KEYS } from '@/services/api';
|
import { api, loadPersistedToken, setAuthToken, STORAGE_KEYS } from '@/services/api';
|
||||||
|
import { updateUser, UpdateUserDto } from '@/services/users';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
@@ -11,6 +12,8 @@ export type UserProfile = {
|
|||||||
birthDate?: string;
|
birthDate?: string;
|
||||||
weight?: string;
|
weight?: string;
|
||||||
height?: string;
|
height?: string;
|
||||||
|
initialWeight?: string; // 初始体重
|
||||||
|
targetWeight?: string; // 目标体重
|
||||||
avatar?: string | null;
|
avatar?: string | null;
|
||||||
dailyStepsGoal?: number; // 每日步数目标(用于 Explore 页等)
|
dailyStepsGoal?: number; // 每日步数目标(用于 Explore 页等)
|
||||||
dailyCaloriesGoal?: number; // 每日卡路里消耗目标
|
dailyCaloriesGoal?: number; // 每日卡路里消耗目标
|
||||||
@@ -194,6 +197,20 @@ export const fetchActivityHistory = createAsyncThunk('user/fetchActivityHistory'
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 更新用户资料(包括体重)
|
||||||
|
export const updateUserProfile = createAsyncThunk(
|
||||||
|
'user/updateProfile',
|
||||||
|
async (updateDto: UpdateUserDto, { rejectWithValue }) => {
|
||||||
|
try {
|
||||||
|
const data = await updateUser(updateDto);
|
||||||
|
console.log('updateUserProfile', data);
|
||||||
|
return data;
|
||||||
|
} catch (err: any) {
|
||||||
|
return rejectWithValue(err?.message ?? '更新用户资料失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const userSlice = createSlice({
|
const userSlice = createSlice({
|
||||||
name: 'user',
|
name: 'user',
|
||||||
initialState,
|
initialState,
|
||||||
@@ -263,6 +280,12 @@ const userSlice = createSlice({
|
|||||||
})
|
})
|
||||||
.addCase(fetchActivityHistory.rejected, (state, action) => {
|
.addCase(fetchActivityHistory.rejected, (state, action) => {
|
||||||
state.error = (action.payload as string) ?? '获取用户活动历史记录失败';
|
state.error = (action.payload as string) ?? '获取用户活动历史记录失败';
|
||||||
|
})
|
||||||
|
.addCase(updateUserProfile.fulfilled, (state, action) => {
|
||||||
|
state.profile = { ...state.profile, ...action.payload };
|
||||||
|
})
|
||||||
|
.addCase(updateUserProfile.rejected, (state, action) => {
|
||||||
|
state.error = (action.payload as string) ?? '更新用户资料失败';
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user