- 在训练计划中添加了新的类型定义,优化了排课功能 - 修改了今日训练页面的布局,提升用户体验 - 删除了不再使用的排课相关文件,简化代码结构 - 更新了 Redux 状态管理,确保数据处理的准确性和稳定性
398 lines
12 KiB
TypeScript
398 lines
12 KiB
TypeScript
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;
|
||
if (action.payload) {
|
||
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 default workoutSlice.reducer;
|