import { fetchExerciseConfig, normalizeToLibraryItems, type ExerciseCategoryDto, type ExerciseConfigResponse, type ExerciseLibraryItem, } from '@/services/exercises'; import AsyncStorage from '@/utils/kvStore'; import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit'; export interface ExerciseLibraryState { categories: ExerciseCategoryDto[]; exercises: ExerciseLibraryItem[]; loading: boolean; error: string | null; lastUpdatedAt: number | null; } const CACHE_KEY = '@exercise_config_v2'; const initialState: ExerciseLibraryState = { categories: [], exercises: [], loading: false, error: null, lastUpdatedAt: null, }; export const loadExerciseLibrary = createAsyncThunk( 'exerciseLibrary/load', async (_: void, { rejectWithValue }) => { // 先读本地缓存(最佳体验),随后静默刷新远端 try { const cached = await AsyncStorage.getItem(CACHE_KEY); if (cached) { const data = JSON.parse(cached) as ExerciseConfigResponse; return { source: 'cache' as const, data }; } } catch { } try { const fresh = await fetchExerciseConfig(); try { await AsyncStorage.setItem(CACHE_KEY, JSON.stringify(fresh)); } catch { } return { source: 'network' as const, data: fresh }; } catch (error: any) { return rejectWithValue(error.message || '加载动作库失败'); } } ); export const refreshExerciseLibrary = createAsyncThunk( 'exerciseLibrary/refresh', async (_: void, { rejectWithValue }) => { try { const fresh = await fetchExerciseConfig(); try { await AsyncStorage.setItem(CACHE_KEY, JSON.stringify(fresh)); } catch { } return fresh; } catch (error: any) { return rejectWithValue(error.message || '刷新动作库失败'); } } ); const exerciseLibrarySlice = createSlice({ name: 'exerciseLibrary', initialState, reducers: { clearExerciseLibraryError(state) { state.error = null; }, setExerciseLibraryFromData( state, action: PayloadAction ) { const data = action.payload; state.categories = (data.categories || []).slice().sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); const sorted: ExerciseConfigResponse = { ...data, exercises: (data.exercises || []).slice().sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)), }; state.exercises = normalizeToLibraryItems(sorted); state.lastUpdatedAt = Date.now(); }, }, extraReducers: (builder) => { builder .addCase(loadExerciseLibrary.pending, (state) => { state.loading = true; state.error = null; }) .addCase(loadExerciseLibrary.fulfilled, (state, action) => { const { data } = action.payload as { source: 'cache' | 'network'; data: ExerciseConfigResponse }; state.loading = false; state.categories = (data.categories || []).slice().sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); const sorted: ExerciseConfigResponse = { ...data, exercises: (data.exercises || []).slice().sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)), }; state.exercises = normalizeToLibraryItems(sorted); state.lastUpdatedAt = Date.now(); }) .addCase(loadExerciseLibrary.rejected, (state, action) => { state.loading = false; state.error = action.payload as string; }) .addCase(refreshExerciseLibrary.fulfilled, (state, action) => { const data = action.payload as ExerciseConfigResponse; state.categories = (data.categories || []).slice().sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)); const sorted: ExerciseConfigResponse = { ...data, exercises: (data.exercises || []).slice().sort((a, b) => (a.sortOrder || 0) - (b.sortOrder || 0)), }; state.exercises = normalizeToLibraryItems(sorted); state.lastUpdatedAt = Date.now(); }) .addCase(refreshExerciseLibrary.rejected, (state, action) => { state.error = action.payload as string; }); }, }); export const { clearExerciseLibraryError, setExerciseLibraryFromData } = exerciseLibrarySlice.actions; export default exerciseLibrarySlice.reducer;