Files
digital-pilates/store/workoutSlice.ts
2025-08-16 14:15:11 +08:00

437 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
workoutsApi,
type AddWorkoutExerciseDto,
type CompleteWorkoutExerciseDto,
type CreateWorkoutSessionDto,
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, append = false }: { page?: number; limit?: number; append?: boolean } = {}, { rejectWithValue }) => {
try {
const result = await workoutsApi.getSessions(page, limit);
return { ...result, append };
} 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 createWorkoutSession = createAsyncThunk(
'workout/createSession',
async (dto: CreateWorkoutSessionDto, { rejectWithValue }) => {
try {
console.log('createWorkoutSession', dto);
const session = await workoutsApi.createSession(dto);
return session;
} 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;
if (action.payload.append) {
// 追加数据(分页加载更多)
state.sessions = [...state.sessions, ...action.payload.sessions];
} else {
// 替换数据(刷新或首次加载)
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;
})
// createWorkoutSession
.addCase(createWorkoutSession.pending, (state) => {
state.loading = true;
state.error = null;
})
.addCase(createWorkoutSession.fulfilled, (state, action) => {
state.loading = false;
// 将新创建的会话添加到列表开头
state.sessions.unshift(action.payload);
// 设置为当前会话
state.currentSession = action.payload;
state.exercises = action.payload.exercises || [];
})
.addCase(createWorkoutSession.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;