diff --git a/app/(tabs)/coach.tsx b/app/(tabs)/coach.tsx
index 348c04b..d89f287 100644
--- a/app/(tabs)/coach.tsx
+++ b/app/(tabs)/coach.tsx
@@ -2,13 +2,13 @@ import { Ionicons } from '@expo/vector-icons';
import { BlurView } from 'expo-blur';
import * as Haptics from 'expo-haptics';
import * as ImagePicker from 'expo-image-picker';
+import { LinearGradient } from 'expo-linear-gradient';
import { useLocalSearchParams } from 'expo-router';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import {
ActivityIndicator,
Alert,
FlatList,
- Image,
Keyboard,
Modal,
Platform,
@@ -34,7 +34,7 @@ import { loadAiCoachSessionCache, saveAiCoachSessionCache } from '@/services/aiC
import { api, getAuthToken, postTextStream } from '@/services/api';
import { selectLatestMoodRecordByDate } from '@/store/moodSlice';
import { generateWelcomeMessage, hasRecordedMoodToday } from '@/utils/welcomeMessage';
-import { LinearGradient } from 'expo-linear-gradient';
+import { Image } from 'expo-image';
import { HistoryModal } from '../../components/model/HistoryModal';
import { ActionSheet } from '../../components/ui/ActionSheet';
@@ -1902,8 +1902,9 @@ export default function CoachScreen() {
}}
>
{userProfile?.isVip ? '不限' : `${userProfile?.freeUsageCount || 0}/${userProfile?.maxUsageCount || 0}`}
diff --git a/app/(tabs)/statistics.tsx b/app/(tabs)/statistics.tsx
index 5ef91e1..c100dbf 100644
--- a/app/(tabs)/statistics.tsx
+++ b/app/(tabs)/statistics.tsx
@@ -5,10 +5,10 @@ import { FitnessRingsCard } from '@/components/FitnessRingsCard';
import { MoodCard } from '@/components/MoodCard';
import { NutritionRadarCard } from '@/components/NutritionRadarCard';
import { ProgressBar } from '@/components/ProgressBar';
-import { StressMeter } from '@/components/StressMeter';
-import { WeightHistoryCard } from '@/components/WeightHistoryCard';
import HeartRateCard from '@/components/statistic/HeartRateCard';
import OxygenSaturationCard from '@/components/statistic/OxygenSaturationCard';
+import { StressMeter } from '@/components/StressMeter';
+import { WeightHistoryCard } from '@/components/weight/WeightHistoryCard';
import { Colors } from '@/constants/Colors';
import { getTabBarBottomPadding } from '@/constants/TabBar';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
diff --git a/app/profile/edit.tsx b/app/profile/edit.tsx
index 845ea6b..ded429a 100644
--- a/app/profile/edit.tsx
+++ b/app/profile/edit.tsx
@@ -3,8 +3,7 @@ import { Colors } from '@/constants/Colors';
import { useAppDispatch, useAppSelector } from '@/hooks/redux';
import { useColorScheme } from '@/hooks/useColorScheme';
import { useCosUpload } from '@/hooks/useCosUpload';
-import { updateUser as updateUserApi } from '@/services/users';
-import { fetchMyProfile } from '@/store/userSlice';
+import { fetchMyProfile, updateUserProfile } from '@/store/userSlice';
import { Ionicons } from '@expo/vector-icons';
import AsyncStorage from '@react-native-async-storage/async-storage';
import DateTimePicker from '@react-native-community/datetimepicker';
@@ -175,8 +174,7 @@ export default function EditProfileScreen() {
// 同步到后端(仅更新后端需要的字段)
try {
- await updateUserApi({
- userId,
+ await dispatch(updateUserProfile({
name: next.name || undefined,
gender: (next.gender === 'male' || next.gender === 'female') ? next.gender : undefined,
// 头像采用已上传的 URL(若有)
@@ -185,7 +183,7 @@ export default function EditProfileScreen() {
height: next.height || undefined,
birthDate: next.birthDate ? new Date(next.birthDate).getTime() / 1000 : undefined,
activityLevel: next.activityLevel || undefined,
- });
+ }));
// 拉取最新用户信息,刷新全局状态
await dispatch(fetchMyProfile() as any);
} catch (e: any) {
diff --git a/app/weight-records.tsx b/app/weight-records.tsx
new file mode 100644
index 0000000..bc7ea79
--- /dev/null
+++ b/app/weight-records.tsx
@@ -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);
+
+ // 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 (
+
+
+ {/* Header */}
+
+
+
+
+
+
+
+
+ {/* Weight Statistics */}
+
+
+
+ {totalWeightLoss.toFixed(1)}kg
+ 累计减重
+
+
+ {currentWeight.toFixed(1)}kg
+ 当前体重
+
+
+ {initialWeight.toFixed(1)}kg
+
+ 初始体重
+
+
+
+
+
+
+ {targetWeight.toFixed(1)}kg
+
+ 目标体重
+
+
+
+
+
+
+
+
+
+
+ {/* Monthly Records */}
+ {Object.keys(groupedHistory).length > 0 ? (
+ Object.entries(groupedHistory).map(([month, records]) => (
+
+ {/* Month Header Card */}
+ {/*
+
+
+ {dayjs(month, 'YYYY年MM月').format('MM')}
+
+ 月
+
+ {dayjs(month, 'YYYY年MM月').format('YYYY年')}
+
+
+
+
+
+
+ 累计减重:{totalWeightLoss.toFixed(1)}kg 日均减重:{avgWeightLoss.toFixed(1)}kg
+
+ */}
+
+ {/* 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 (
+
+
+
+ {dayjs(record.createdAt).format('MM月DD日 HH:mm')}
+
+ {/*
+
+ */}
+
+
+ 体重:
+ {record.weight}kg
+ {Math.abs(weightChange) > 0 && (
+
+
+
+ {Math.abs(weightChange).toFixed(1)}
+
+
+ )}
+
+
+ );
+ })}
+
+ ))
+ ) : (
+
+
+ 暂无体重记录
+ 点击右上角添加按钮开始记录
+
+
+ )}
+
+
+ {/* Weight Input Modal */}
+ setShowWeightPicker(false)}
+ >
+ setShowWeightPicker(false)}
+ />
+
+ {/* Header */}
+
+ setShowWeightPicker(false)}>
+
+
+
+ {pickerType === 'current' && '记录体重'}
+ {pickerType === 'initial' && '编辑初始体重'}
+ {pickerType === 'target' && '编辑目标体重'}
+
+
+
+
+
+ {/* Weight Input Section */}
+
+
+
+
+
+
+
+ kg
+
+
+
+ {/* Weight Range Hint */}
+
+ 请输入 0-500 之间的数值,支持小数
+
+
+
+ {/* Quick Selection */}
+
+ 快速选择
+
+ {[50, 60, 70, 80, 90].map((weight) => (
+ setInputWeight(weight.toString())}
+ >
+
+ {weight}kg
+
+
+ ))}
+
+
+
+
+ {/* Save Button */}
+
+
+ 确定
+
+
+
+
+
+
+ );
+}
+
+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',
+ },
+});
\ No newline at end of file
diff --git a/components/ActivityHeatMap.tsx b/components/ActivityHeatMap.tsx
index 0a7c86a..dba58d5 100644
--- a/components/ActivityHeatMap.tsx
+++ b/components/ActivityHeatMap.tsx
@@ -59,8 +59,6 @@ const ActivityHeatMap = () => {
return data;
}, [activityData, weeksToShow]);
- console.log('generateActivityData', generateActivityData);
-
// 根据活跃度计算颜色 - 优化配色方案
const getActivityColor = (level: number): string => {
diff --git a/components/WeightHistoryCard.tsx b/components/weight/WeightHistoryCard.tsx
similarity index 95%
rename from components/WeightHistoryCard.tsx
rename to components/weight/WeightHistoryCard.tsx
index a69b6d9..bdac92b 100644
--- a/components/WeightHistoryCard.tsx
+++ b/components/weight/WeightHistoryCard.tsx
@@ -54,13 +54,13 @@ export function WeightHistoryCard() {
const hasWeight = userProfile?.weight && parseFloat(userProfile.weight) > 0;
const hasHeight = userProfile?.height && parseFloat(userProfile.height) > 0;
-
+
// BMI 计算
const canCalculate = canCalculateBMI(
userProfile?.weight ? parseFloat(userProfile.weight) : undefined,
userProfile?.height ? parseFloat(userProfile.height) : undefined
);
-
+
const bmiResult = canCalculate && userProfile?.weight && userProfile?.height
? getBMIResult(parseFloat(userProfile.weight), parseFloat(userProfile.height))
: null;
@@ -95,6 +95,10 @@ export function WeightHistoryCard() {
};
// 切换图表显示状态的动画函数
+ const navigateToWeightRecords = () => {
+ pushIfAuthedElseLogin(ROUTES.WEIGHT_RECORDS);
+ };
+
const toggleChart = () => {
if (isAnimating) return; // 防止动画期间重复触发
@@ -195,7 +199,7 @@ export function WeightHistoryCard() {
// 如果正在加载,显示加载状态
if (isLoading) {
return (
-
+
@@ -205,14 +209,14 @@ export function WeightHistoryCard() {
加载中...
-
+
);
}
// 如果没有体重数据,显示引导卡片
if (!hasWeight) {
return (
-
+
@@ -227,14 +231,17 @@ export function WeightHistoryCard() {
{
+ e.stopPropagation();
+ navigateToCoach();
+ }}
activeOpacity={0.8}
>
记录
-
+
);
}
@@ -245,7 +252,7 @@ export function WeightHistoryCard() {
if (sortedHistory.length === 0) {
return (
-
+
@@ -259,14 +266,17 @@ export function WeightHistoryCard() {
{
+ e.stopPropagation();
+ navigateToCoach();
+ }}
activeOpacity={0.8}
>
记录体重
-
+
);
}
@@ -296,7 +306,7 @@ export function WeightHistoryCard() {
pathData;
return (
-
+
@@ -305,7 +315,10 @@ export function WeightHistoryCard() {
{
+ e.stopPropagation();
+ toggleChart();
+ }}
activeOpacity={0.8}
>
{
+ e.stopPropagation();
+ navigateToCoach();
+ }}
activeOpacity={0.8}
>
@@ -352,7 +368,10 @@ export function WeightHistoryCard() {
{bmiResult.value}
{
+ e.stopPropagation();
+ handleShowBMIModal();
+ }}
style={styles.bmiInfoButton}
hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
>
@@ -499,7 +518,7 @@ export function WeightHistoryCard() {
{BMI_CATEGORIES.map((category, index) => {
const colors = [
{ 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: '#FEE2E2', text: '#B91C1C', border: '#EF4444' } // 肥胖
][index];
@@ -563,7 +582,7 @@ export function WeightHistoryCard() {
-
+
);
}
@@ -711,7 +730,7 @@ const styles = StyleSheet.create({
fontWeight: '700',
color: '#192126',
},
-
+
// BMI 相关样式
bmiValueContainer: {
flexDirection: 'row',
@@ -734,7 +753,7 @@ const styles = StyleSheet.create({
fontSize: 10,
fontWeight: '700',
},
-
+
// BMI 弹窗样式
bmiModalContainer: {
flex: 1,
diff --git a/constants/Colors.ts b/constants/Colors.ts
index b91c6c0..0a409e3 100644
--- a/constants/Colors.ts
+++ b/constants/Colors.ts
@@ -148,7 +148,7 @@ export const Colors = {
ornamentAccent: palette.success[100],
// 背景渐变色
- backgroundGradientStart: palette.gray[25],
+ backgroundGradientStart: palette.purple[25],
backgroundGradientEnd: palette.base.white,
},
dark: {
diff --git a/constants/Routes.ts b/constants/Routes.ts
index d1f340f..9755e43 100644
--- a/constants/Routes.ts
+++ b/constants/Routes.ts
@@ -37,6 +37,9 @@ export const ROUTES = {
// 营养相关路由
NUTRITION_RECORDS: '/nutrition/records',
+ // 体重记录相关路由
+ WEIGHT_RECORDS: '/weight-records',
+
// 任务相关路由
TASK_DETAIL: '/task-detail',
diff --git a/services/users.ts b/services/users.ts
index 0d6b120..1b1f9e5 100644
--- a/services/users.ts
+++ b/services/users.ts
@@ -3,7 +3,6 @@ import { api } from '@/services/api';
export type Gender = 'male' | 'female';
export type UpdateUserDto = {
- userId: string;
name?: string;
avatar?: string; // base64 字符串
gender?: Gender;
@@ -14,6 +13,8 @@ export type UpdateUserDto = {
weight?: number;
height?: number;
activityLevel?: number; // 活动水平 1-4
+ initialWeight?: number; // 初始体重
+ targetWeight?: number; // 目标体重
};
export async function updateUser(dto: UpdateUserDto): Promise> {
diff --git a/store/userSlice.ts b/store/userSlice.ts
index 991932d..73dff26 100644
--- a/store/userSlice.ts
+++ b/store/userSlice.ts
@@ -1,4 +1,5 @@
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 { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
@@ -11,6 +12,8 @@ export type UserProfile = {
birthDate?: string;
weight?: string;
height?: string;
+ initialWeight?: string; // 初始体重
+ targetWeight?: string; // 目标体重
avatar?: string | null;
dailyStepsGoal?: number; // 每日步数目标(用于 Explore 页等)
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({
name: 'user',
initialState,
@@ -263,6 +280,12 @@ const userSlice = createSlice({
})
.addCase(fetchActivityHistory.rejected, (state, action) => {
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) ?? '更新用户资料失败';
});
},
});