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'; // 营养数据状态类型定义 export interface NutritionState { // 按日期存储的营养记录 recordsByDate: Record; // 按日期存储的营养摘要 summaryByDate: Record; // 按日期存储的健康数据(基础代谢、运动消耗等) healthDataByDate: Record; // 按日期存储的基础代谢数据 basalMetabolismByDate: Record; // 加载状态 loading: { records: boolean; delete: boolean; healthData: boolean; basalMetabolism: boolean; }; // 错误信息 error: string | null; // 分页信息 pagination: { page: number; limit: number; total: number; hasMore: boolean; }; // 最后更新时间 lastUpdateTime: string | null; } // 初始状态 const initialState: NutritionState = { recordsByDate: {}, summaryByDate: {}, healthDataByDate: {}, basalMetabolismByDate: {}, loading: { records: false, delete: false, healthData: false, basalMetabolism: false, }, error: null, pagination: { page: 1, limit: 10, total: 0, hasMore: true, }, lastUpdateTime: null, }; // 异步操作:获取营养记录 export const fetchNutritionRecords = createAsyncThunk( 'nutrition/fetchRecords', async (params: { startDate?: string; endDate?: string; page?: number; limit?: number; append?: boolean; }, { rejectWithValue }) => { try { const { startDate, endDate, page = 1, limit = 10, append = false } = params; const response = await getDietRecords({ startDate, endDate, page, limit, }); return { ...response, append, dateKey: startDate ? dayjs(startDate).format('YYYY-MM-DD') : null, }; } catch (error: any) { return rejectWithValue(error.message || '获取营养记录失败'); } } ); // 异步操作:删除营养记录 export const deleteNutritionRecord = createAsyncThunk( 'nutrition/deleteRecord', async (params: { recordId: number; dateKey: string }, { rejectWithValue }) => { try { await deleteDietRecord(params.recordId); return params; } catch (error: any) { return rejectWithValue(error.message || '删除营养记录失败'); } } ); // 异步操作:获取指定日期的营养数据 export const fetchDailyNutritionData = createAsyncThunk( 'nutrition/fetchDailyData', 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 response = await getDietRecords({ startDate, endDate, limit: 100, // 获取当天所有记录 }); // 计算营养摘要 let summary: NutritionSummary | null = null; if (response.records.length > 0) { summary = calculateNutritionSummary(response.records); summary.updatedAt = response.records[0].updatedAt; } return { dateKey: dateString, records: response.records, summary, }; } catch (error: any) { return rejectWithValue(error.message || '获取营养数据失败'); } } ); // 异步操作:获取指定日期的健康数据 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, reducers: { // 清除错误 clearError: (state) => { state.error = null; }, // 清除指定日期的数据 clearDataForDate: (state, action: PayloadAction) => { 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; }, // 重置分页 resetPagination: (state) => { state.pagination = initialState.pagination; }, }, extraReducers: (builder) => { // fetchNutritionRecords builder .addCase(fetchNutritionRecords.pending, (state) => { state.loading.records = true; state.error = null; }) .addCase(fetchNutritionRecords.fulfilled, (state, action) => { state.loading.records = false; const { records, total, page, limit, append, dateKey } = action.payload; // 更新分页信息 state.pagination = { page, limit, total, hasMore: records.length === limit, }; if (dateKey) { // 按日期存储记录 if (append && state.recordsByDate[dateKey]) { state.recordsByDate[dateKey] = [...state.recordsByDate[dateKey], ...records]; } else { state.recordsByDate[dateKey] = records; } // 计算并存储营养摘要 if (records.length > 0) { const summary = calculateNutritionSummary(records); summary.updatedAt = records[0].updatedAt; state.summaryByDate[dateKey] = summary; } else { delete state.summaryByDate[dateKey]; } } state.lastUpdateTime = new Date().toISOString(); }) .addCase(fetchNutritionRecords.rejected, (state, action) => { state.loading.records = false; state.error = action.payload as string; }); // deleteNutritionRecord builder .addCase(deleteNutritionRecord.pending, (state) => { state.loading.delete = true; state.error = null; }) .addCase(deleteNutritionRecord.fulfilled, (state, action) => { state.loading.delete = false; const { recordId, dateKey } = action.payload; // 从记录中移除已删除的项 if (state.recordsByDate[dateKey]) { state.recordsByDate[dateKey] = state.recordsByDate[dateKey].filter( record => record.id !== recordId ); // 重新计算营养摘要 const remainingRecords = state.recordsByDate[dateKey]; if (remainingRecords.length > 0) { const summary = calculateNutritionSummary(remainingRecords); summary.updatedAt = remainingRecords[0].updatedAt; state.summaryByDate[dateKey] = summary; } else { delete state.summaryByDate[dateKey]; } } state.lastUpdateTime = new Date().toISOString(); }) .addCase(deleteNutritionRecord.rejected, (state, action) => { state.loading.delete = false; state.error = action.payload as string; }); // fetchDailyNutritionData builder .addCase(fetchDailyNutritionData.pending, (state) => { state.loading.records = true; state.error = null; }) .addCase(fetchDailyNutritionData.fulfilled, (state, action) => { state.loading.records = false; const { dateKey, records, summary } = action.payload; // 存储记录和摘要 state.recordsByDate[dateKey] = records; if (summary) { state.summaryByDate[dateKey] = summary; } else { delete state.summaryByDate[dateKey]; } state.lastUpdateTime = new Date().toISOString(); }) .addCase(fetchDailyNutritionData.rejected, (state, action) => { 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; }); }, }); // Action creators export const { clearError, clearDataForDate, clearAllData, resetPagination, } = nutritionSlice.actions; // Selectors export const selectNutritionRecordsByDate = (dateKey: string) => (state: { nutrition: NutritionState }) => state.nutrition.recordsByDate[dateKey] || []; 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; export const selectNutritionError = (state: { nutrition: NutritionState }) => state.nutrition.error; 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;