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;
|
||||
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { configureStore, createListenerMiddleware } from '@reduxjs/toolkit';
|
||||
import challengeReducer from './challengeSlice';
|
||||
import checkinReducer, { addExercise, autoSyncCheckin, removeExercise, replaceExercises, setNote, toggleExerciseCompleted } from './checkinSlice';
|
||||
import exerciseLibraryReducer from './exerciseLibrarySlice';
|
||||
import scheduleExerciseReducer from './scheduleExerciseSlice';
|
||||
import trainingPlanReducer from './trainingPlanSlice';
|
||||
import userReducer from './userSlice';
|
||||
import workoutReducer from './workoutSlice';
|
||||
|
||||
// 创建监听器中间件来处理自动同步
|
||||
const listenerMiddleware = createListenerMiddleware();
|
||||
@@ -15,16 +18,16 @@ syncActions.forEach(action => {
|
||||
effect: async (action, listenerApi) => {
|
||||
const state = listenerApi.getState() as any;
|
||||
const date = action.payload?.date;
|
||||
|
||||
|
||||
if (!date) return;
|
||||
|
||||
|
||||
// 延迟一下,避免在同一事件循环中重复触发
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
|
||||
// 检查是否还有待同步的日期
|
||||
const currentState = listenerApi.getState() as any;
|
||||
const pendingSyncDates = currentState?.checkin?.pendingSyncDates || [];
|
||||
|
||||
|
||||
if (pendingSyncDates.includes(date)) {
|
||||
listenerApi.dispatch(autoSyncCheckin({ date }));
|
||||
}
|
||||
@@ -38,6 +41,9 @@ export const store = configureStore({
|
||||
challenge: challengeReducer,
|
||||
checkin: checkinReducer,
|
||||
trainingPlan: trainingPlanReducer,
|
||||
scheduleExercise: scheduleExerciseReducer,
|
||||
exerciseLibrary: exerciseLibraryReducer,
|
||||
workout: workoutReducer,
|
||||
},
|
||||
middleware: (getDefaultMiddleware) =>
|
||||
getDefaultMiddleware().prepend(listenerMiddleware.middleware),
|
||||
|
||||
233
store/scheduleExerciseSlice.ts
Normal file
233
store/scheduleExerciseSlice.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
import {
|
||||
scheduleExerciseApi,
|
||||
type CreateScheduleExerciseDto,
|
||||
type ReorderExercisesDto,
|
||||
type ScheduleExercise,
|
||||
type UpdateScheduleExerciseDto
|
||||
} from '@/services/scheduleExerciseApi';
|
||||
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
export type ScheduleExerciseState = {
|
||||
exercises: ScheduleExercise[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
currentPlanId: string | null;
|
||||
};
|
||||
|
||||
const initialState: ScheduleExerciseState = {
|
||||
exercises: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
currentPlanId: null,
|
||||
};
|
||||
|
||||
// 加载训练计划的所有项目
|
||||
export const loadExercises = createAsyncThunk(
|
||||
'scheduleExercise/loadExercises',
|
||||
async (planId: string, { rejectWithValue }) => {
|
||||
try {
|
||||
const exercises = await scheduleExerciseApi.list(planId);
|
||||
return { exercises, planId };
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || '加载训练项目失败');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 添加训练项目
|
||||
export const addExercise = createAsyncThunk(
|
||||
'scheduleExercise/addExercise',
|
||||
async ({ planId, dto }: { planId: string; dto: CreateScheduleExerciseDto }, { rejectWithValue }) => {
|
||||
try {
|
||||
const exercise = await scheduleExerciseApi.create(planId, dto);
|
||||
return { exercise };
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || '添加训练项目失败');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 更新训练项目
|
||||
export const updateExercise = createAsyncThunk(
|
||||
'scheduleExercise/updateExercise',
|
||||
async ({
|
||||
planId,
|
||||
exerciseId,
|
||||
dto
|
||||
}: {
|
||||
planId: string;
|
||||
exerciseId: string;
|
||||
dto: UpdateScheduleExerciseDto;
|
||||
}, { rejectWithValue }) => {
|
||||
try {
|
||||
const exercise = await scheduleExerciseApi.update(planId, exerciseId, dto);
|
||||
return { exercise };
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || '更新训练项目失败');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 删除训练项目
|
||||
export const deleteExercise = createAsyncThunk(
|
||||
'scheduleExercise/deleteExercise',
|
||||
async ({ planId, exerciseId }: { planId: string; exerciseId: string }, { rejectWithValue }) => {
|
||||
try {
|
||||
await scheduleExerciseApi.delete(planId, exerciseId);
|
||||
return { exerciseId };
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || '删除训练项目失败');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 重新排序训练项目
|
||||
export const reorderExercises = createAsyncThunk(
|
||||
'scheduleExercise/reorderExercises',
|
||||
async ({ planId, dto }: { planId: string; dto: ReorderExercisesDto }, { rejectWithValue }) => {
|
||||
try {
|
||||
await scheduleExerciseApi.reorder(planId, dto);
|
||||
// 重新加载排序后的列表
|
||||
const exercises = await scheduleExerciseApi.list(planId);
|
||||
return { exercises };
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || '重新排序失败');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 更新完成状态
|
||||
export const toggleCompletion = createAsyncThunk(
|
||||
'scheduleExercise/toggleCompletion',
|
||||
async ({
|
||||
planId,
|
||||
exerciseId,
|
||||
completed
|
||||
}: {
|
||||
planId: string;
|
||||
exerciseId: string;
|
||||
completed: boolean;
|
||||
}, { rejectWithValue }) => {
|
||||
try {
|
||||
const exercise = await scheduleExerciseApi.updateCompletion(planId, exerciseId, completed);
|
||||
return { exercise };
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || '更新完成状态失败');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const scheduleExerciseSlice = createSlice({
|
||||
name: 'scheduleExercise',
|
||||
initialState,
|
||||
reducers: {
|
||||
clearError(state) {
|
||||
state.error = null;
|
||||
},
|
||||
clearExercises(state) {
|
||||
state.exercises = [];
|
||||
state.currentPlanId = null;
|
||||
},
|
||||
// 本地更新排序(用于拖拽等即时反馈)
|
||||
updateLocalOrder(state, action: PayloadAction<string[]>) {
|
||||
const newOrder = action.payload;
|
||||
const orderedExercises = newOrder.map(id =>
|
||||
state.exercises.find(ex => ex.id === id)
|
||||
).filter(Boolean) as ScheduleExercise[];
|
||||
state.exercises = orderedExercises;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
// loadExercises
|
||||
.addCase(loadExercises.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(loadExercises.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.exercises = action.payload.exercises;
|
||||
state.currentPlanId = action.payload.planId;
|
||||
})
|
||||
.addCase(loadExercises.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string;
|
||||
})
|
||||
|
||||
// addExercise
|
||||
.addCase(addExercise.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(addExercise.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.exercises.push(action.payload.exercise);
|
||||
})
|
||||
.addCase(addExercise.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string;
|
||||
})
|
||||
|
||||
// updateExercise
|
||||
.addCase(updateExercise.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(updateExercise.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
const index = state.exercises.findIndex(ex => ex.id === action.payload.exercise.id);
|
||||
if (index !== -1) {
|
||||
state.exercises[index] = action.payload.exercise;
|
||||
}
|
||||
})
|
||||
.addCase(updateExercise.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string;
|
||||
})
|
||||
|
||||
// deleteExercise
|
||||
.addCase(deleteExercise.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(deleteExercise.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.exercises = state.exercises.filter(ex => ex.id !== action.payload.exerciseId);
|
||||
})
|
||||
.addCase(deleteExercise.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string;
|
||||
})
|
||||
|
||||
// reorderExercises
|
||||
.addCase(reorderExercises.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(reorderExercises.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.exercises = action.payload.exercises;
|
||||
})
|
||||
.addCase(reorderExercises.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string;
|
||||
})
|
||||
|
||||
// toggleCompletion
|
||||
.addCase(toggleCompletion.pending, (state) => {
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(toggleCompletion.fulfilled, (state, action) => {
|
||||
const index = state.exercises.findIndex(ex => ex.id === action.payload.exercise.id);
|
||||
if (index !== -1) {
|
||||
state.exercises[index] = action.payload.exercise;
|
||||
}
|
||||
})
|
||||
.addCase(toggleCompletion.rejected, (state, action) => {
|
||||
state.error = action.payload as string;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { clearError, clearExercises, updateLocalOrder } = scheduleExerciseSlice.actions;
|
||||
export default scheduleExerciseSlice.reducer;
|
||||
395
store/workoutSlice.ts
Normal file
395
store/workoutSlice.ts
Normal file
@@ -0,0 +1,395 @@
|
||||
import {
|
||||
workoutsApi,
|
||||
type AddWorkoutExerciseDto,
|
||||
type CompleteWorkoutExerciseDto,
|
||||
type StartWorkoutDto,
|
||||
type StartWorkoutExerciseDto,
|
||||
type UpdateWorkoutExerciseDto,
|
||||
type WorkoutExercise,
|
||||
type WorkoutSession,
|
||||
type WorkoutSessionStatsResponse,
|
||||
} from '@/services/workoutsApi';
|
||||
import { createAsyncThunk, createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
export interface WorkoutState {
|
||||
// 当前训练会话
|
||||
currentSession: WorkoutSession | null;
|
||||
exercises: WorkoutExercise[];
|
||||
stats: WorkoutSessionStatsResponse | null;
|
||||
|
||||
// 历史训练会话
|
||||
sessions: WorkoutSession[];
|
||||
sessionsPagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
} | null;
|
||||
|
||||
// 加载状态
|
||||
loading: boolean;
|
||||
exerciseLoading: string | null; // 正在操作的exercise ID
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
const initialState: WorkoutState = {
|
||||
currentSession: null,
|
||||
exercises: [],
|
||||
stats: null,
|
||||
sessions: [],
|
||||
sessionsPagination: null,
|
||||
loading: false,
|
||||
exerciseLoading: null,
|
||||
error: null,
|
||||
};
|
||||
|
||||
// ==================== 异步Action定义 ====================
|
||||
|
||||
// 获取今日训练
|
||||
export const loadTodayWorkout = createAsyncThunk(
|
||||
'workout/loadTodayWorkout',
|
||||
async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const session = await workoutsApi.getTodayWorkout();
|
||||
return session;
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || '获取今日训练失败');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 开始训练会话
|
||||
export const startWorkoutSession = createAsyncThunk(
|
||||
'workout/startSession',
|
||||
async ({ sessionId, dto }: { sessionId: string; dto?: StartWorkoutDto }, { rejectWithValue }) => {
|
||||
try {
|
||||
const session = await workoutsApi.startSession(sessionId, dto);
|
||||
return session;
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || '开始训练失败');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 开始训练动作
|
||||
export const startWorkoutExercise = createAsyncThunk(
|
||||
'workout/startExercise',
|
||||
async ({ sessionId, exerciseId, dto }: {
|
||||
sessionId: string;
|
||||
exerciseId: string;
|
||||
dto?: StartWorkoutExerciseDto
|
||||
}, { rejectWithValue }) => {
|
||||
try {
|
||||
const exercise = await workoutsApi.startExercise(sessionId, exerciseId, dto);
|
||||
return exercise;
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || '开始动作失败');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 完成训练动作
|
||||
export const completeWorkoutExercise = createAsyncThunk(
|
||||
'workout/completeExercise',
|
||||
async ({ sessionId, exerciseId, dto }: {
|
||||
sessionId: string;
|
||||
exerciseId: string;
|
||||
dto: CompleteWorkoutExerciseDto
|
||||
}, { rejectWithValue, getState }) => {
|
||||
try {
|
||||
const exercise = await workoutsApi.completeExercise(sessionId, exerciseId, dto);
|
||||
|
||||
// 完成动作后重新获取会话详情(检查是否自动完成)
|
||||
const updatedSession = await workoutsApi.getSessionDetail(sessionId);
|
||||
|
||||
return { exercise, updatedSession };
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || '完成动作失败');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 跳过训练动作
|
||||
export const skipWorkoutExercise = createAsyncThunk(
|
||||
'workout/skipExercise',
|
||||
async ({ sessionId, exerciseId }: { sessionId: string; exerciseId: string }, { rejectWithValue }) => {
|
||||
try {
|
||||
const exercise = await workoutsApi.skipExercise(sessionId, exerciseId);
|
||||
|
||||
// 跳过动作后重新获取会话详情(检查是否自动完成)
|
||||
const updatedSession = await workoutsApi.getSessionDetail(sessionId);
|
||||
|
||||
return { exercise, updatedSession };
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || '跳过动作失败');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 更新训练动作
|
||||
export const updateWorkoutExercise = createAsyncThunk(
|
||||
'workout/updateExercise',
|
||||
async ({ sessionId, exerciseId, dto }: {
|
||||
sessionId: string;
|
||||
exerciseId: string;
|
||||
dto: UpdateWorkoutExerciseDto
|
||||
}, { rejectWithValue }) => {
|
||||
try {
|
||||
const exercise = await workoutsApi.updateExercise(sessionId, exerciseId, dto);
|
||||
return exercise;
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || '更新动作失败');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 获取训练统计
|
||||
export const loadWorkoutStats = createAsyncThunk(
|
||||
'workout/loadStats',
|
||||
async (sessionId: string, { rejectWithValue }) => {
|
||||
try {
|
||||
const stats = await workoutsApi.getSessionStats(sessionId);
|
||||
return stats;
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || '获取统计数据失败');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 获取训练会话列表
|
||||
export const loadWorkoutSessions = createAsyncThunk(
|
||||
'workout/loadSessions',
|
||||
async ({ page = 1, limit = 10 }: { page?: number; limit?: number } = {}, { rejectWithValue }) => {
|
||||
try {
|
||||
const result = await workoutsApi.getSessions(page, limit);
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || '获取训练列表失败');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 添加动作到训练会话
|
||||
export const addWorkoutExercise = createAsyncThunk(
|
||||
'workout/addExercise',
|
||||
async ({ sessionId, dto }: { sessionId: string; dto: AddWorkoutExerciseDto }, { rejectWithValue }) => {
|
||||
try {
|
||||
const exercise = await workoutsApi.addExercise(sessionId, dto);
|
||||
return exercise;
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || '添加动作失败');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 删除训练会话
|
||||
export const deleteWorkoutSession = createAsyncThunk(
|
||||
'workout/deleteSession',
|
||||
async (sessionId: string, { rejectWithValue }) => {
|
||||
try {
|
||||
await workoutsApi.deleteSession(sessionId);
|
||||
return sessionId;
|
||||
} catch (error: any) {
|
||||
return rejectWithValue(error.message || '删除训练会话失败');
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// ==================== Slice定义 ====================
|
||||
|
||||
const workoutSlice = createSlice({
|
||||
name: 'workout',
|
||||
initialState,
|
||||
reducers: {
|
||||
clearWorkoutError(state) {
|
||||
state.error = null;
|
||||
},
|
||||
clearCurrentWorkout(state) {
|
||||
state.currentSession = null;
|
||||
state.exercises = [];
|
||||
state.stats = null;
|
||||
},
|
||||
// 本地更新exercise状态(用于乐观更新)
|
||||
updateExerciseLocally(state, action: PayloadAction<Partial<WorkoutExercise> & { id: string }>) {
|
||||
const { id, ...updates } = action.payload;
|
||||
const exerciseIndex = state.exercises.findIndex(ex => ex.id === id);
|
||||
if (exerciseIndex !== -1) {
|
||||
Object.assign(state.exercises[exerciseIndex], updates);
|
||||
}
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
// loadTodayWorkout
|
||||
.addCase(loadTodayWorkout.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(loadTodayWorkout.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.currentSession = action.payload;
|
||||
state.exercises = action.payload.exercises || [];
|
||||
})
|
||||
.addCase(loadTodayWorkout.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string;
|
||||
})
|
||||
|
||||
// startWorkoutSession
|
||||
.addCase(startWorkoutSession.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(startWorkoutSession.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.currentSession = action.payload;
|
||||
})
|
||||
.addCase(startWorkoutSession.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string;
|
||||
})
|
||||
|
||||
// startWorkoutExercise
|
||||
.addCase(startWorkoutExercise.pending, (state, action) => {
|
||||
state.exerciseLoading = action.meta.arg.exerciseId;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(startWorkoutExercise.fulfilled, (state, action) => {
|
||||
state.exerciseLoading = null;
|
||||
const exerciseIndex = state.exercises.findIndex(ex => ex.id === action.payload.id);
|
||||
if (exerciseIndex !== -1) {
|
||||
state.exercises[exerciseIndex] = action.payload;
|
||||
}
|
||||
})
|
||||
.addCase(startWorkoutExercise.rejected, (state, action) => {
|
||||
state.exerciseLoading = null;
|
||||
state.error = action.payload as string;
|
||||
})
|
||||
|
||||
// completeWorkoutExercise
|
||||
.addCase(completeWorkoutExercise.pending, (state, action) => {
|
||||
state.exerciseLoading = action.meta.arg.exerciseId;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(completeWorkoutExercise.fulfilled, (state, action) => {
|
||||
state.exerciseLoading = null;
|
||||
const { exercise, updatedSession } = action.payload;
|
||||
|
||||
// 更新exercise
|
||||
const exerciseIndex = state.exercises.findIndex(ex => ex.id === exercise.id);
|
||||
if (exerciseIndex !== -1) {
|
||||
state.exercises[exerciseIndex] = exercise;
|
||||
}
|
||||
|
||||
// 更新session(可能已自动完成)
|
||||
state.currentSession = updatedSession;
|
||||
})
|
||||
.addCase(completeWorkoutExercise.rejected, (state, action) => {
|
||||
state.exerciseLoading = null;
|
||||
state.error = action.payload as string;
|
||||
})
|
||||
|
||||
// skipWorkoutExercise
|
||||
.addCase(skipWorkoutExercise.pending, (state, action) => {
|
||||
state.exerciseLoading = action.meta.arg.exerciseId;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(skipWorkoutExercise.fulfilled, (state, action) => {
|
||||
state.exerciseLoading = null;
|
||||
const { exercise, updatedSession } = action.payload;
|
||||
|
||||
// 更新exercise
|
||||
const exerciseIndex = state.exercises.findIndex(ex => ex.id === exercise.id);
|
||||
if (exerciseIndex !== -1) {
|
||||
state.exercises[exerciseIndex] = exercise;
|
||||
}
|
||||
|
||||
// 更新session(可能已自动完成)
|
||||
state.currentSession = updatedSession;
|
||||
})
|
||||
.addCase(skipWorkoutExercise.rejected, (state, action) => {
|
||||
state.exerciseLoading = null;
|
||||
state.error = action.payload as string;
|
||||
})
|
||||
|
||||
// updateWorkoutExercise
|
||||
.addCase(updateWorkoutExercise.pending, (state, action) => {
|
||||
state.exerciseLoading = action.meta.arg.exerciseId;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(updateWorkoutExercise.fulfilled, (state, action) => {
|
||||
state.exerciseLoading = null;
|
||||
const exerciseIndex = state.exercises.findIndex(ex => ex.id === action.payload.id);
|
||||
if (exerciseIndex !== -1) {
|
||||
state.exercises[exerciseIndex] = action.payload;
|
||||
}
|
||||
})
|
||||
.addCase(updateWorkoutExercise.rejected, (state, action) => {
|
||||
state.exerciseLoading = null;
|
||||
state.error = action.payload as string;
|
||||
})
|
||||
|
||||
// loadWorkoutStats
|
||||
.addCase(loadWorkoutStats.fulfilled, (state, action) => {
|
||||
state.stats = action.payload;
|
||||
})
|
||||
|
||||
// loadWorkoutSessions
|
||||
.addCase(loadWorkoutSessions.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(loadWorkoutSessions.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.sessions = action.payload.sessions;
|
||||
state.sessionsPagination = action.payload.pagination;
|
||||
})
|
||||
.addCase(loadWorkoutSessions.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string;
|
||||
})
|
||||
|
||||
// addWorkoutExercise
|
||||
.addCase(addWorkoutExercise.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(addWorkoutExercise.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
// 将新添加的动作添加到exercises列表末尾
|
||||
state.exercises.push(action.payload);
|
||||
})
|
||||
.addCase(addWorkoutExercise.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string;
|
||||
})
|
||||
|
||||
// deleteWorkoutSession
|
||||
.addCase(deleteWorkoutSession.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(deleteWorkoutSession.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
const deletedSessionId = action.payload;
|
||||
|
||||
// 如果删除的是当前会话,清空当前会话数据
|
||||
if (state.currentSession?.id === deletedSessionId) {
|
||||
state.currentSession = null;
|
||||
state.exercises = [];
|
||||
state.stats = null;
|
||||
}
|
||||
|
||||
// 从会话列表中移除已删除的会话
|
||||
state.sessions = state.sessions.filter(session => session.id !== deletedSessionId);
|
||||
})
|
||||
.addCase(deleteWorkoutSession.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload as string;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { clearWorkoutError, clearCurrentWorkout, updateExerciseLocally } = workoutSlice.actions;
|
||||
export { addWorkoutExercise, deleteWorkoutSession };
|
||||
export default workoutSlice.reducer;
|
||||
Reference in New Issue
Block a user