225 lines
7.2 KiB
TypeScript
225 lines
7.2 KiB
TypeScript
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; |