From ba2d829e02661661b22127e4c16d46ca2f237567 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Wed, 27 Aug 2025 19:18:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E4=BD=93=E9=87=8D?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E5=8A=9F=E8=83=BD=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E8=B5=84=E6=96=99=E6=9B=B4=E6=96=B0=E5=8F=8A?= =?UTF-8?q?=E5=9B=BE=E7=89=87=E7=BB=84=E4=BB=B6=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(tabs)/coach.tsx | 7 +- app/(tabs)/statistics.tsx | 4 +- app/profile/edit.tsx | 8 +- app/weight-records.tsx | 692 ++++++++++++++++++ components/ActivityHeatMap.tsx | 2 - components/{ => weight}/WeightHistoryCard.tsx | 55 +- constants/Colors.ts | 2 +- constants/Routes.ts | 3 + services/users.ts | 3 +- store/userSlice.ts | 23 + 10 files changed, 767 insertions(+), 32 deletions(-) create mode 100644 app/weight-records.tsx rename components/{ => weight}/WeightHistoryCard.tsx (95%) 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) ?? '更新用户资料失败'; }); }, });