feat(health): 重构营养卡片数据获取逻辑,支持基础代谢与运动消耗分离

- 新增 fetchCompleteNutritionCardData 异步 action,统一拉取营养、健康与基础代谢数据
- NutritionRadarCard 改用 Redux 数据源,移除 props 透传,自动根据日期刷新
- BasalMetabolismCard 新增详情弹窗,展示 BMR 计算公式、正常区间及提升策略
- StepsCard 与 StepsCardOptimized 引入 InteractionManager 与动画懒加载,减少 UI 阻塞
- HealthKitManager 新增饮水读写接口,支持将饮水记录同步至 HealthKit
- 移除 statistics 页面冗余 mock 与 nutrition/health 重复请求,缓存时间统一为 5 分钟
This commit is contained in:
richarjiang
2025-09-23 10:01:50 +08:00
parent d082c66b72
commit e6dfd4d59a
11 changed files with 1115 additions and 203 deletions

View File

@@ -1,4 +1,5 @@
import { calculateNutritionSummary, deleteDietRecord, DietRecord, getDietRecords, NutritionSummary } from '@/services/dietRecords';
import { fetchBasalEnergyBurned, fetchHealthDataForDate, TodayHealthData } from '@/utils/health';
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import dayjs from 'dayjs';
@@ -10,10 +11,18 @@ export interface NutritionState {
// 按日期存储的营养摘要
summaryByDate: Record<string, NutritionSummary>;
// 按日期存储的健康数据(基础代谢、运动消耗等)
healthDataByDate: Record<string, TodayHealthData>;
// 按日期存储的基础代谢数据
basalMetabolismByDate: Record<string, number>;
// 加载状态
loading: {
records: boolean;
delete: boolean;
healthData: boolean;
basalMetabolism: boolean;
};
// 错误信息
@@ -35,9 +44,13 @@ export interface NutritionState {
const initialState: NutritionState = {
recordsByDate: {},
summaryByDate: {},
healthDataByDate: {},
basalMetabolismByDate: {},
loading: {
records: false,
delete: false,
healthData: false,
basalMetabolism: false,
},
error: null,
pagination: {
@@ -126,6 +139,74 @@ export const fetchDailyNutritionData = createAsyncThunk(
}
);
// 异步操作:获取指定日期的健康数据
export const fetchDailyHealthData = createAsyncThunk(
'nutrition/fetchHealthData',
async (date: Date, { rejectWithValue }) => {
try {
const dateString = dayjs(date).format('YYYY-MM-DD');
const healthData = await fetchHealthDataForDate(date);
return {
dateKey: dateString,
healthData,
};
} catch (error: any) {
return rejectWithValue(error.message || '获取健康数据失败');
}
}
);
// 异步操作:获取指定日期的基础代谢数据
export const fetchDailyBasalMetabolism = createAsyncThunk(
'nutrition/fetchBasalMetabolism',
async (date: Date, { rejectWithValue }) => {
try {
const dateString = dayjs(date).format('YYYY-MM-DD');
const startDate = dayjs(date).startOf('day').toISOString();
const endDate = dayjs(date).endOf('day').toISOString();
const basalMetabolism = await fetchBasalEnergyBurned({
startDate,
endDate,
});
return {
dateKey: dateString,
basalMetabolism,
};
} catch (error: any) {
return rejectWithValue(error.message || '获取基础代谢数据失败');
}
}
);
// 异步操作:获取指定日期的完整营养卡片数据(营养数据 + 健康数据)
export const fetchCompleteNutritionCardData = createAsyncThunk(
'nutrition/fetchCompleteCardData',
async (date: Date, { rejectWithValue, dispatch }) => {
try {
const dateString = dayjs(date).format('YYYY-MM-DD');
// 并行获取营养数据和健康数据
const [nutritionResult, healthResult, basalResult] = await Promise.allSettled([
dispatch(fetchDailyNutritionData(date)).unwrap(),
dispatch(fetchDailyHealthData(date)).unwrap(),
dispatch(fetchDailyBasalMetabolism(date)).unwrap(),
]);
return {
dateKey: dateString,
nutritionSuccess: nutritionResult.status === 'fulfilled',
healthSuccess: healthResult.status === 'fulfilled',
basalSuccess: basalResult.status === 'fulfilled',
};
} catch (error: any) {
return rejectWithValue(error.message || '获取完整营养卡片数据失败');
}
}
);
const nutritionSlice = createSlice({
name: 'nutrition',
initialState,
@@ -140,12 +221,16 @@ const nutritionSlice = createSlice({
const dateKey = action.payload;
delete state.recordsByDate[dateKey];
delete state.summaryByDate[dateKey];
delete state.healthDataByDate[dateKey];
delete state.basalMetabolismByDate[dateKey];
},
// 清除所有数据
clearAllData: (state) => {
state.recordsByDate = {};
state.summaryByDate = {};
state.healthDataByDate = {};
state.basalMetabolismByDate = {};
state.error = null;
state.lastUpdateTime = null;
state.pagination = initialState.pagination;
@@ -258,6 +343,61 @@ const nutritionSlice = createSlice({
state.loading.records = false;
state.error = action.payload as string;
});
// fetchDailyHealthData
builder
.addCase(fetchDailyHealthData.pending, (state) => {
state.loading.healthData = true;
state.error = null;
})
.addCase(fetchDailyHealthData.fulfilled, (state, action) => {
state.loading.healthData = false;
const { dateKey, healthData } = action.payload;
state.healthDataByDate[dateKey] = healthData;
state.lastUpdateTime = new Date().toISOString();
})
.addCase(fetchDailyHealthData.rejected, (state, action) => {
state.loading.healthData = false;
state.error = action.payload as string;
});
// fetchDailyBasalMetabolism
builder
.addCase(fetchDailyBasalMetabolism.pending, (state) => {
state.loading.basalMetabolism = true;
state.error = null;
})
.addCase(fetchDailyBasalMetabolism.fulfilled, (state, action) => {
state.loading.basalMetabolism = false;
const { dateKey, basalMetabolism } = action.payload;
state.basalMetabolismByDate[dateKey] = basalMetabolism;
state.lastUpdateTime = new Date().toISOString();
})
.addCase(fetchDailyBasalMetabolism.rejected, (state, action) => {
state.loading.basalMetabolism = false;
state.error = action.payload as string;
});
// fetchCompleteNutritionCardData
builder
.addCase(fetchCompleteNutritionCardData.pending, (state) => {
state.loading.records = true;
state.loading.healthData = true;
state.loading.basalMetabolism = true;
state.error = null;
})
.addCase(fetchCompleteNutritionCardData.fulfilled, (state, action) => {
state.loading.records = false;
state.loading.healthData = false;
state.loading.basalMetabolism = false;
state.lastUpdateTime = new Date().toISOString();
})
.addCase(fetchCompleteNutritionCardData.rejected, (state, action) => {
state.loading.records = false;
state.loading.healthData = false;
state.loading.basalMetabolism = false;
state.error = action.payload as string;
});
},
});
@@ -276,6 +416,12 @@ export const selectNutritionRecordsByDate = (dateKey: string) => (state: { nutri
export const selectNutritionSummaryByDate = (dateKey: string) => (state: { nutrition: NutritionState }) =>
state.nutrition.summaryByDate[dateKey] || null;
export const selectHealthDataByDate = (dateKey: string) => (state: { nutrition: NutritionState }) =>
state.nutrition.healthDataByDate[dateKey] || null;
export const selectBasalMetabolismByDate = (dateKey: string) => (state: { nutrition: NutritionState }) =>
state.nutrition.basalMetabolismByDate[dateKey] || 0;
export const selectNutritionLoading = (state: { nutrition: NutritionState }) =>
state.nutrition.loading;
@@ -285,4 +431,13 @@ export const selectNutritionError = (state: { nutrition: NutritionState }) =>
export const selectNutritionPagination = (state: { nutrition: NutritionState }) =>
state.nutrition.pagination;
// 复合选择器:获取指定日期的完整营养卡片数据
export const selectNutritionCardDataByDate = (dateKey: string) => (state: { nutrition: NutritionState }) => ({
nutritionSummary: state.nutrition.summaryByDate[dateKey] || null,
healthData: state.nutrition.healthDataByDate[dateKey] || null,
basalMetabolism: state.nutrition.basalMetabolismByDate[dateKey] || 0,
loading: state.nutrition.loading,
error: state.nutrition.error,
});
export default nutritionSlice.reducer;