Files
digital-pilates/store/exerciseLibrarySlice.ts
richarjiang 2357596665 refactor(storage): 迁移 AsyncStorage 至 expo-sqlite/kv-store
- 统一替换所有 @react-native-async-storage/async-storage 导入为自定义 kvStore
- 新增 kvStore.ts 封装 expo-sqlite/kv-store,保持与 AsyncStorage 完全兼容
- 新增同步读写方法,提升性能
- 引入 expo-sqlite 依赖并更新 lock 文件

BREAKING CHANGE: 移除 @react-native-async-storage/async-storage 依赖,需重新安装依赖并清理旧数据
2025-09-15 12:51:18 +08:00

127 lines
4.2 KiB
TypeScript

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<ExerciseConfigResponse>
) {
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;