From e6bbda9d0f9649ca3411db89af595c1b23dc17fc Mon Sep 17 00:00:00 2001 From: richarjiang Date: Mon, 25 Aug 2025 19:20:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E5=81=A5=E5=BA=B7?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD=E5=8F=8A?= =?UTF-8?q?=E7=9B=B8=E5=85=B3=E7=BB=84=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 healthSlice,用于管理健康数据的 Redux 状态 - 在 Statistics 组件中整合健康数据获取逻辑,优化数据展示 - 更新 NutritionRadarCard 组件,调整卡路里计算区域,提升用户体验 - 移除不必要的状态管理,简化组件逻辑 - 优化代码结构,提升可读性和维护性 --- app/(tabs)/statistics.tsx | 99 +++++++++++------------ components/NutritionRadarCard.tsx | 45 ++--------- store/healthSlice.ts | 126 ++++++++++++++++++++++++++++++ store/index.ts | 2 + 4 files changed, 179 insertions(+), 93 deletions(-) create mode 100644 store/healthSlice.ts diff --git a/app/(tabs)/statistics.tsx b/app/(tabs)/statistics.tsx index fb1e1f3..9f923c0 100644 --- a/app/(tabs)/statistics.tsx +++ b/app/(tabs)/statistics.tsx @@ -14,8 +14,8 @@ import { getTabBarBottomPadding } from '@/constants/TabBar'; import { useAppDispatch, useAppSelector } from '@/hooks/redux'; import { useAuthGuard } from '@/hooks/useAuthGuard'; import { useBackgroundTasks } from '@/hooks/useBackgroundTasks'; -import { useColorScheme } from '@/hooks/useColorScheme'; import { calculateNutritionSummary, getDietRecords, NutritionSummary } from '@/services/dietRecords'; +import { selectHealthDataByDate, setHealthData } from '@/store/healthSlice'; import { fetchDailyMoodCheckins, selectLatestMoodRecordByDate } from '@/store/moodSlice'; import { getMonthDaysZh, getTodayIndexInMonth } from '@/utils/date'; import { ensureHealthPermissions, fetchHealthDataForDate } from '@/utils/health'; @@ -85,11 +85,7 @@ const FloatingCard = ({ children, delay = 0, style }: { }; export default function ExploreScreen() { - const theme = (useColorScheme() ?? 'light') as 'light' | 'dark'; - const colorTokens = Colors[theme]; - const stepGoal = useAppSelector((s) => s.user.profile?.dailyStepsGoal) ?? 2000; - const userProfile = useAppSelector((s) => s.user.profile); const { pushIfAuthedElseLogin, isLoggedIn } = useAuthGuard(); @@ -101,28 +97,46 @@ export default function ExploreScreen() { return getTabBarBottomPadding(tabBarHeight) + (insets?.bottom ?? 0); }, [tabBarHeight, insets?.bottom]); - // HealthKit: 每次页面聚焦都拉取今日数据 - const [stepCount, setStepCount] = useState(null); - const [activeCalories, setActiveCalories] = useState(null); - // 基础代谢率(千卡) - const [basalMetabolism, setBasalMetabolism] = useState(null); - // 睡眠时长(分钟) - const [sleepDuration, setSleepDuration] = useState(null); - // HRV数据 - const [hrvValue, setHrvValue] = useState(0); - const [hrvUpdateTime, setHrvUpdateTime] = useState(new Date()); - // 健身圆环数据 - const [fitnessRingsData, setFitnessRingsData] = useState({ + // 获取当前选中日期 + const getCurrentSelectedDate = () => { + const days = getMonthDaysZh(); + return days[selectedIndex]?.date?.toDate() ?? new Date(); + }; + + + // 获取当前选中日期 + const currentSelectedDate = getCurrentSelectedDate(); + const currentSelectedDateString = dayjs(currentSelectedDate).format('YYYY-MM-DD'); + + // 从 Redux 获取指定日期的健康数据 + const healthData = useAppSelector(selectHealthDataByDate(currentSelectedDateString)); + + // 解构健康数据 + const stepCount = healthData?.steps ?? null; + const activeCalories = healthData?.activeEnergyBurned ?? null; + const basalMetabolism = healthData?.basalEnergyBurned ?? null; + const sleepDuration = healthData?.sleepDuration ?? null; + const hrvValue = healthData?.hrv ?? 0; + const oxygenSaturation = healthData?.oxygenSaturation ?? null; + const heartRate = healthData?.heartRate ?? null; + const fitnessRingsData = healthData ? { + activeCalories: healthData.activeEnergyBurned, + activeCaloriesGoal: healthData.activeCaloriesGoal, + exerciseMinutes: healthData.exerciseMinutes, + exerciseMinutesGoal: healthData.exerciseMinutesGoal, + standHours: healthData.standHours, + standHoursGoal: healthData.standHoursGoal, + } : { activeCalories: 0, activeCaloriesGoal: 350, exerciseMinutes: 0, exerciseMinutesGoal: 30, standHours: 0, - standHoursGoal: 12 - }); - // 血氧饱和度和心率数据 - const [oxygenSaturation, setOxygenSaturation] = useState(null); - const [heartRate, setHeartRate] = useState(null); + standHoursGoal: 12, + }; + + // HRV更新时间 + const [hrvUpdateTime, setHrvUpdateTime] = useState(new Date()); // 用于触发动画重置的 token(当日期或数据变化时更新) const [animToken, setAnimToken] = useState(0); @@ -140,15 +154,10 @@ export default function ExploreScreen() { const getDateKey = (d: Date) => `${dayjs(d).year()}-${dayjs(d).month() + 1}-${dayjs(d).date()}`; - // 获取当前选中日期 - const getCurrentSelectedDate = () => { - const days = getMonthDaysZh(); - return days[selectedIndex]?.date?.toDate() ?? new Date(); - }; // 从 Redux 获取当前日期的心情记录 const currentMoodCheckin = useAppSelector(selectLatestMoodRecordByDate( - dayjs(getCurrentSelectedDate()).format('YYYY-MM-DD') + currentSelectedDateString )); // 加载心情数据 @@ -205,33 +214,17 @@ export default function ExploreScreen() { console.log('设置UI状态:', data); // 仅当该请求仍是最新时,才应用结果 if (latestRequestKeyRef.current === requestKey) { - setStepCount(data.steps); - setActiveCalories(Math.round(data.activeEnergyBurned)); - setBasalMetabolism(Math.round(data.basalEnergyBurned)); - setSleepDuration(data.sleepDuration); - // 更新健身圆环数据 - setFitnessRingsData({ - activeCalories: data.activeEnergyBurned, - activeCaloriesGoal: data.activeCaloriesGoal, - exerciseMinutes: data.exerciseMinutes, - exerciseMinutesGoal: data.exerciseMinutesGoal, - standHours: data.standHours, - standHoursGoal: data.standHoursGoal - }); + const dateString = dayjs(derivedDate).format('YYYY-MM-DD'); - const hrv = data.hrv ?? 0; - setHrvValue(hrv); + // 使用 Redux 存储健康数据 + dispatch(setHealthData({ + date: dateString, + data: data + })); // 更新HRV数据时间 setHrvUpdateTime(new Date()); - // 设置血氧饱和度和心率数据 - setOxygenSaturation(data.oxygenSaturation ?? null); - setHeartRate(data.heartRate ?? null); - - console.log('血氧饱和度数据:', data.oxygenSaturation); - console.log('心率数据:', data.heartRate); - setAnimToken((t) => t + 1); } else { console.log('忽略过期健康数据请求结果,key=', requestKey, '最新key=', latestRequestKeyRef.current); @@ -240,9 +233,6 @@ export default function ExploreScreen() { } catch (error) { console.error('HealthKit流程出现异常:', error); - // 重置血氧饱和度和心率数据 - setOxygenSaturation(null); - setHeartRate(null); } }; @@ -281,7 +271,7 @@ export default function ExploreScreen() { useFocusEffect( React.useCallback(() => { // 聚焦时按当前选中的日期加载,避免与用户手动选择的日期不一致 - const currentDate = getCurrentSelectedDate(); + const currentDate = currentSelectedDate; if (currentDate) { loadHealthData(currentDate); if (isLoggedIn) { @@ -317,6 +307,7 @@ export default function ExploreScreen() { } }; + return ( {/* 背景渐变 */} diff --git a/components/NutritionRadarCard.tsx b/components/NutritionRadarCard.tsx index 5dda4b8..1d123ba 100644 --- a/components/NutritionRadarCard.tsx +++ b/components/NutritionRadarCard.tsx @@ -2,7 +2,6 @@ import { AnimatedNumber } from '@/components/AnimatedNumber'; import { ROUTES } from '@/constants/Routes'; import { NutritionSummary } from '@/services/dietRecords'; import { Ionicons } from '@expo/vector-icons'; -import Feather from '@expo/vector-icons/Feather'; import dayjs from 'dayjs'; import { router } from 'expo-router'; import React, { useMemo } from 'react'; @@ -117,8 +116,10 @@ export function NutritionRadarCard({ 营养摄入分析 - 更新: {dayjs(nutritionSummary?.updatedAt).format('YYYY-MM-DD HH:mm')} - + 更新: {dayjs(nutritionSummary?.updatedAt).format('MM-DD HH:mm')} + + + @@ -146,8 +147,8 @@ export function NutritionRadarCard({ {/* 卡路里计算区域 */} - 还能吃(千卡) + 还能吃(千卡) Math.round(v).toString()} /> - - - - - 缺口 - - Math.round(v).toString()} - /> - - {/* 餐次选择区域 */} - {/* - {meals.map((meal) => ( - onMealPress?.(meal.type)} - activeOpacity={0.7} - > - - {meal.emoji} - - - - - {meal.name} - - ))} - */} ); @@ -310,8 +280,8 @@ const styles = StyleSheet.create({ calorieSubtitle: { fontSize: 10, color: '#64748B', - marginBottom: 8, fontWeight: '600', + marginRight: 4, }, calculationRow: { flexDirection: 'row', @@ -363,9 +333,6 @@ const styles = StyleSheet.create({ fontSize: 24, }, addButton: { - position: 'absolute', - top: -2, - right: -2, width: 16, height: 16, borderRadius: 8, diff --git a/store/healthSlice.ts b/store/healthSlice.ts new file mode 100644 index 0000000..cb42e3c --- /dev/null +++ b/store/healthSlice.ts @@ -0,0 +1,126 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { AppDispatch, RootState } from './index'; + +// 健康数据类型定义 +export interface FitnessRingsData { + activeCalories: number; + activeCaloriesGoal: number; + exerciseMinutes: number; + exerciseMinutesGoal: number; + standHours: number; + standHoursGoal: number; +} + +export interface HealthData { + steps: number | null; + activeCalories: number | null; + basalEnergyBurned: number | null; + sleepDuration: number | null; + hrv: number | null; + oxygenSaturation: number | null; + heartRate: number | null; + activeEnergyBurned: number; + activeCaloriesGoal: number; + exerciseMinutes: number; + exerciseMinutesGoal: number; + standHours: number; + standHoursGoal: number; +} + +export interface HealthState { + // 按日期存储的历史数据 + dataByDate: Record; + + // 加载状态 + loading: boolean; + error: string | null; + + // 最后更新时间 + lastUpdateTime: string | null; +} + +// 初始状态 +const initialState: HealthState = { + dataByDate: {}, + loading: false, + error: null, + lastUpdateTime: null, +}; + +const healthSlice = createSlice({ + name: 'health', + initialState, + reducers: { + // 设置加载状态 + setLoading: (state, action: PayloadAction) => { + state.loading = action.payload; + }, + + // 设置错误信息 + setError: (state, action: PayloadAction) => { + state.error = action.payload; + }, + + // 设置完整的健康数据 + setHealthData: (state, action: PayloadAction<{ + date: string; + data: HealthData; + }>) => { + const { date, data } = action.payload; + + // 存储到历史数据 + state.dataByDate[date] = data; + + state.lastUpdateTime = new Date().toISOString(); + }, + + // 清除特定日期的数据 + clearHealthDataForDate: (state, action: PayloadAction) => { + const date = action.payload; + delete state.dataByDate[date]; + + }, + + // 清除所有健康数据 + clearAllHealthData: (state) => { + state.dataByDate = {}; + state.error = null; + state.lastUpdateTime = null; + }, + }, +}); + +// Action creators +export const { + setLoading, + setError, + setHealthData, + clearHealthDataForDate, + clearAllHealthData, +} = healthSlice.actions; + +// Thunk action to fetch and set health data for a specific date +export const fetchHealthDataForDate = (date: Date) => { + return async (dispatch: AppDispatch, getState: () => RootState) => { + try { + dispatch(setLoading(true)); + dispatch(setError(null)); + + // 这里可以添加实际的 API 调用逻辑 + // 目前我们假设数据已经通过其他方式获取 + + dispatch(setLoading(false)); + } catch (error) { + dispatch(setError(error instanceof Error ? error.message : '获取健康数据失败')); + dispatch(setLoading(false)); + } + }; +}; + +// Selectors +export const selectHealthDataByDate = (date: string) => (state: RootState) => state.health.dataByDate[date]; +export const selectHealthLoading = (state: RootState) => state.health.loading; +export const selectHealthError = (state: RootState) => state.health.error; +export const selectLastUpdateTime = (state: RootState) => state.health.lastUpdateTime; + +export default healthSlice.reducer; \ No newline at end of file diff --git a/store/index.ts b/store/index.ts index 06a52e0..4049e34 100644 --- a/store/index.ts +++ b/store/index.ts @@ -3,6 +3,7 @@ import challengeReducer from './challengeSlice'; import checkinReducer, { addExercise, autoSyncCheckin, removeExercise, replaceExercises, setNote, toggleExerciseCompleted } from './checkinSlice'; import exerciseLibraryReducer from './exerciseLibrarySlice'; import goalsReducer from './goalsSlice'; +import healthReducer from './healthSlice'; import moodReducer from './moodSlice'; import scheduleExerciseReducer from './scheduleExerciseSlice'; import tasksReducer from './tasksSlice'; @@ -44,6 +45,7 @@ export const store = configureStore({ challenge: challengeReducer, checkin: checkinReducer, goals: goalsReducer, + health: healthReducer, mood: moodReducer, tasks: tasksReducer, trainingPlan: trainingPlanReducer,