feat: 支持食物库接口
This commit is contained in:
225
store/foodLibrarySlice.ts
Normal file
225
store/foodLibrarySlice.ts
Normal file
@@ -0,0 +1,225 @@
|
||||
import { foodLibraryApi } from '@/services/foodLibraryApi';
|
||||
import type {
|
||||
FoodCategory,
|
||||
FoodCategoryDto,
|
||||
FoodItem,
|
||||
FoodItemDto,
|
||||
FoodLibraryState
|
||||
} from '@/types/food';
|
||||
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
// 数据转换工具函数
|
||||
const transformFoodItemDto = (dto: FoodItemDto): FoodItem => ({
|
||||
id: dto.id.toString(),
|
||||
name: dto.name,
|
||||
emoji: '🍽️', // 默认 emoji,可以根据分类或其他逻辑设置
|
||||
calories: dto.caloriesPer100g || 0,
|
||||
unit: '100克',
|
||||
description: dto.description,
|
||||
protein: dto.proteinPer100g,
|
||||
carbohydrate: dto.carbohydratePer100g,
|
||||
fat: dto.fatPer100g,
|
||||
fiber: dto.fiberPer100g,
|
||||
sugar: dto.sugarPer100g,
|
||||
sodium: dto.sodiumPer100g,
|
||||
additionalNutrition: dto.additionalNutrition,
|
||||
imageUrl: dto.imageUrl,
|
||||
});
|
||||
|
||||
const transformFoodCategoryDto = (dto: FoodCategoryDto): FoodCategory => ({
|
||||
id: dto.key,
|
||||
name: dto.name,
|
||||
foods: dto.foods.map(transformFoodItemDto),
|
||||
icon: dto.icon,
|
||||
sortOrder: dto.sortOrder,
|
||||
isSystem: dto.isSystem,
|
||||
});
|
||||
|
||||
// 异步 thunks
|
||||
export const fetchFoodLibrary = createAsyncThunk(
|
||||
'foodLibrary/fetchFoodLibrary',
|
||||
async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await foodLibraryApi.getFoodLibrary();
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || '获取食物库失败');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const searchFoods = createAsyncThunk(
|
||||
'foodLibrary/searchFoods',
|
||||
async (keyword: string, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await foodLibraryApi.searchFoods(keyword);
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || '搜索食物失败');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
export const getFoodById = createAsyncThunk(
|
||||
'foodLibrary/getFoodById',
|
||||
async (id: number, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await foodLibraryApi.getFoodById(id);
|
||||
return response;
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || '获取食物详情失败');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 初始状态
|
||||
const initialState: FoodLibraryState = {
|
||||
categories: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
searchResults: [],
|
||||
searchLoading: false,
|
||||
lastUpdated: null,
|
||||
};
|
||||
|
||||
// 创建 slice
|
||||
const foodLibrarySlice = createSlice({
|
||||
name: 'foodLibrary',
|
||||
initialState,
|
||||
reducers: {
|
||||
// 清除错误
|
||||
clearError: (state) => {
|
||||
state.error = null;
|
||||
},
|
||||
// 清除搜索结果
|
||||
clearSearchResults: (state) => {
|
||||
state.searchResults = [];
|
||||
},
|
||||
// 添加自定义食物到指定分类
|
||||
addCustomFood: (state, action: PayloadAction<{ categoryId: string; food: FoodItem }>) => {
|
||||
const { categoryId, food } = action.payload;
|
||||
const category = state.categories.find(cat => cat.id === categoryId);
|
||||
if (category) {
|
||||
category.foods.push(food);
|
||||
}
|
||||
},
|
||||
// 从指定分类移除食物
|
||||
removeFoodFromCategory: (state, action: PayloadAction<{ categoryId: string; foodId: string }>) => {
|
||||
const { categoryId, foodId } = action.payload;
|
||||
const category = state.categories.find(cat => cat.id === categoryId);
|
||||
if (category) {
|
||||
category.foods = category.foods.filter(food => food.id !== foodId);
|
||||
}
|
||||
},
|
||||
// 添加食物到收藏
|
||||
addToFavorites: (state, action: PayloadAction<FoodItem>) => {
|
||||
const favoriteCategory = state.categories.find(cat => cat.id === 'favorite');
|
||||
if (favoriteCategory) {
|
||||
// 检查是否已存在
|
||||
const exists = favoriteCategory.foods.some(food => food.id === action.payload.id);
|
||||
if (!exists) {
|
||||
favoriteCategory.foods.push(action.payload);
|
||||
}
|
||||
}
|
||||
},
|
||||
// 从收藏移除食物
|
||||
removeFromFavorites: (state, action: PayloadAction<string>) => {
|
||||
const favoriteCategory = state.categories.find(cat => cat.id === 'favorite');
|
||||
if (favoriteCategory) {
|
||||
favoriteCategory.foods = favoriteCategory.foods.filter(food => food.id !== action.payload);
|
||||
}
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
// 获取食物库
|
||||
builder
|
||||
.addCase(fetchFoodLibrary.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(fetchFoodLibrary.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.categories = action.payload.categories.map(transformFoodCategoryDto);
|
||||
state.lastUpdated = Date.now();
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(fetchFoodLibrary.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string;
|
||||
});
|
||||
|
||||
// 搜索食物
|
||||
builder
|
||||
.addCase(searchFoods.pending, (state) => {
|
||||
state.searchLoading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(searchFoods.fulfilled, (state, action) => {
|
||||
state.searchLoading = false;
|
||||
state.searchResults = action.payload.map(transformFoodItemDto);
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(searchFoods.rejected, (state, action) => {
|
||||
state.searchLoading = false;
|
||||
state.error = action.payload as string;
|
||||
state.searchResults = [];
|
||||
});
|
||||
|
||||
// 获取食物详情
|
||||
builder
|
||||
.addCase(getFoodById.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(getFoodById.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = null;
|
||||
// 可以在这里处理获取到的食物详情,比如更新缓存等
|
||||
})
|
||||
.addCase(getFoodById.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// 导出 actions
|
||||
export const {
|
||||
clearError,
|
||||
clearSearchResults,
|
||||
addCustomFood,
|
||||
removeFoodFromCategory,
|
||||
addToFavorites,
|
||||
removeFromFavorites,
|
||||
} = foodLibrarySlice.actions;
|
||||
|
||||
// 选择器
|
||||
export const selectFoodLibrary = (state: { foodLibrary: FoodLibraryState }) => state.foodLibrary;
|
||||
export const selectFoodCategories = (state: { foodLibrary: FoodLibraryState }) => state.foodLibrary.categories;
|
||||
export const selectFoodLibraryLoading = (state: { foodLibrary: FoodLibraryState }) => state.foodLibrary.loading;
|
||||
export const selectFoodLibraryError = (state: { foodLibrary: FoodLibraryState }) => state.foodLibrary.error;
|
||||
export const selectSearchResults = (state: { foodLibrary: FoodLibraryState }) => state.foodLibrary.searchResults;
|
||||
export const selectSearchLoading = (state: { foodLibrary: FoodLibraryState }) => state.foodLibrary.searchLoading;
|
||||
|
||||
// 复合选择器
|
||||
export const selectFoodCategoryById = (categoryId: string) =>
|
||||
(state: { foodLibrary: FoodLibraryState }) =>
|
||||
state.foodLibrary.categories.find(cat => cat.id === categoryId);
|
||||
|
||||
export const selectFoodById = (foodId: string) =>
|
||||
(state: { foodLibrary: FoodLibraryState }) => {
|
||||
for (const category of state.foodLibrary.categories) {
|
||||
const food = category.foods.find(f => f.id === foodId);
|
||||
if (food) return food;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const selectFavoritesFoods = (state: { foodLibrary: FoodLibraryState }) =>
|
||||
state.foodLibrary.categories.find(cat => cat.id === 'favorite')?.foods || [];
|
||||
|
||||
export const selectCommonFoods = (state: { foodLibrary: FoodLibraryState }) =>
|
||||
state.foodLibrary.categories.find(cat => cat.id === 'common')?.foods || [];
|
||||
|
||||
// 导出 reducer
|
||||
export default foodLibrarySlice.reducer;
|
||||
Reference in New Issue
Block a user