Files
digital-pilates/store/nutritionSlice.ts
2025-08-29 09:41:05 +08:00

288 lines
8.1 KiB
TypeScript

import { calculateNutritionSummary, deleteDietRecord, DietRecord, getDietRecords, NutritionSummary } from '@/services/dietRecords';
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
import dayjs from 'dayjs';
// 营养数据状态类型定义
export interface NutritionState {
// 按日期存储的营养记录
recordsByDate: Record<string, DietRecord[]>;
// 按日期存储的营养摘要
summaryByDate: Record<string, NutritionSummary>;
// 加载状态
loading: {
records: boolean;
delete: boolean;
};
// 错误信息
error: string | null;
// 分页信息
pagination: {
page: number;
limit: number;
total: number;
hasMore: boolean;
};
// 最后更新时间
lastUpdateTime: string | null;
}
// 初始状态
const initialState: NutritionState = {
recordsByDate: {},
summaryByDate: {},
loading: {
records: false,
delete: 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 || '获取营养数据失败');
}
}
);
const nutritionSlice = createSlice({
name: 'nutrition',
initialState,
reducers: {
// 清除错误
clearError: (state) => {
state.error = null;
},
// 清除指定日期的数据
clearDataForDate: (state, action: PayloadAction<string>) => {
const dateKey = action.payload;
delete state.recordsByDate[dateKey];
delete state.summaryByDate[dateKey];
},
// 清除所有数据
clearAllData: (state) => {
state.recordsByDate = {};
state.summaryByDate = {};
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;
});
},
});
// 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 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 default nutritionSlice.reducer;