feat: 更新训练计划和打卡功能
- 在训练计划中新增训练项目的添加、更新和删除功能,支持用户灵活管理训练内容 - 优化训练计划排课界面,提升用户体验 - 更新打卡功能,支持按日期加载和展示打卡记录 - 删除不再使用的打卡相关页面,简化代码结构 - 新增今日训练页面,集成今日训练计划和动作展示 - 更新样式以适应新功能的展示和交互
This commit is contained in:
126
store/exerciseLibrarySlice.ts
Normal file
126
store/exerciseLibrarySlice.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import {
|
||||
fetchExerciseConfig,
|
||||
normalizeToLibraryItems,
|
||||
type ExerciseCategoryDto,
|
||||
type ExerciseConfigResponse,
|
||||
type ExerciseLibraryItem,
|
||||
} from '@/services/exercises';
|
||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||
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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user