feat: 更新健康数据管理功能及相关组件

- 新增 healthSlice,用于管理健康数据的 Redux 状态
- 在 Statistics 组件中整合健康数据获取逻辑,优化数据展示
- 更新 NutritionRadarCard 组件,调整卡路里计算区域,提升用户体验
- 移除不必要的状态管理,简化组件逻辑
- 优化代码结构,提升可读性和维护性
This commit is contained in:
richarjiang
2025-08-25 19:20:56 +08:00
parent 91b7b0cb99
commit e6bbda9d0f
4 changed files with 179 additions and 93 deletions

View File

@@ -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<number | null>(null);
const [activeCalories, setActiveCalories] = useState<number | null>(null);
// 基础代谢率(千卡)
const [basalMetabolism, setBasalMetabolism] = useState<number | null>(null);
// 睡眠时长(分钟)
const [sleepDuration, setSleepDuration] = useState<number | null>(null);
// HRV数据
const [hrvValue, setHrvValue] = useState<number>(0);
const [hrvUpdateTime, setHrvUpdateTime] = useState<Date>(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<number | null>(null);
const [heartRate, setHeartRate] = useState<number | null>(null);
standHoursGoal: 12,
};
// HRV更新时间
const [hrvUpdateTime, setHrvUpdateTime] = useState<Date>(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 (
<View style={styles.container}>
{/* 背景渐变 */}