feat: 更新训练计划和打卡功能
- 在训练计划中新增训练项目的添加、更新和删除功能,支持用户灵活管理训练内容 - 优化训练计划排课界面,提升用户体验 - 更新打卡功能,支持按日期加载和展示打卡记录 - 删除不再使用的打卡相关页面,简化代码结构 - 新增今日训练页面,集成今日训练计划和动作展示 - 更新样式以适应新功能的展示和交互
This commit is contained in:
@@ -1,31 +1,48 @@
|
||||
import { api } from '@/services/api';
|
||||
|
||||
export type ExerciseCategoryDto = {
|
||||
// 与后端保持一致的数据结构定义
|
||||
export interface ExerciseLibraryItem {
|
||||
key: string;
|
||||
name: string;
|
||||
sortOrder: number;
|
||||
};
|
||||
description?: string;
|
||||
category: string; // 中文分类名(显示用)
|
||||
targetMuscleGroups: string;
|
||||
equipmentName?: string;
|
||||
beginnerReps?: number;
|
||||
beginnerSets?: number;
|
||||
breathingCycles?: number;
|
||||
holdDuration?: number;
|
||||
specialInstructions?: string;
|
||||
}
|
||||
|
||||
export type ExerciseDto = {
|
||||
export interface ExerciseCategoryDto {
|
||||
key: string; // 英文 key
|
||||
name: string; // 中文名
|
||||
type: 'mat_pilates' | 'equipment_pilates';
|
||||
equipmentName?: string;
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export interface ExerciseDto {
|
||||
key: string;
|
||||
name: string;
|
||||
description: string;
|
||||
description?: string;
|
||||
categoryKey: string;
|
||||
categoryName: string;
|
||||
sortOrder: number;
|
||||
};
|
||||
targetMuscleGroups: string;
|
||||
equipmentName?: string;
|
||||
beginnerReps?: number;
|
||||
beginnerSets?: number;
|
||||
breathingCycles?: number;
|
||||
holdDuration?: number;
|
||||
specialInstructions?: string;
|
||||
sortOrder?: number;
|
||||
}
|
||||
|
||||
export type ExerciseConfigResponse = {
|
||||
export interface ExerciseConfigResponse {
|
||||
categories: ExerciseCategoryDto[];
|
||||
exercises: ExerciseDto[];
|
||||
};
|
||||
|
||||
export type ExerciseLibraryItem = {
|
||||
key: string;
|
||||
name: string;
|
||||
description: string;
|
||||
category: string; // display name
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchExerciseConfig(): Promise<ExerciseConfigResponse> {
|
||||
return await api.get<ExerciseConfigResponse>('/exercises/config');
|
||||
@@ -38,7 +55,13 @@ export function normalizeToLibraryItems(resp: ExerciseConfigResponse | null | un
|
||||
name: e.name,
|
||||
description: e.description,
|
||||
category: e.categoryName || e.categoryKey,
|
||||
targetMuscleGroups: e.targetMuscleGroups,
|
||||
equipmentName: e.equipmentName,
|
||||
beginnerReps: e.beginnerReps,
|
||||
beginnerSets: e.beginnerSets,
|
||||
breathingCycles: e.breathingCycles,
|
||||
holdDuration: e.holdDuration,
|
||||
specialInstructions: e.specialInstructions,
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
106
services/scheduleExerciseApi.ts
Normal file
106
services/scheduleExerciseApi.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { api } from './api';
|
||||
|
||||
// 训练项目数据结构
|
||||
export interface ScheduleExercise {
|
||||
id: string;
|
||||
trainingPlanId: string;
|
||||
userId: string;
|
||||
exerciseKey?: string;
|
||||
name: string;
|
||||
sets?: number;
|
||||
reps?: number;
|
||||
durationSec?: number;
|
||||
restSec?: number;
|
||||
note?: string;
|
||||
itemType: 'exercise' | 'rest' | 'note';
|
||||
completed: boolean;
|
||||
sortOrder: number;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deleted: boolean;
|
||||
|
||||
// 关联的动作信息(仅exercise类型时存在)
|
||||
exercise?: {
|
||||
key: string;
|
||||
name: string;
|
||||
description: string;
|
||||
categoryKey: string;
|
||||
categoryName: string;
|
||||
};
|
||||
}
|
||||
|
||||
// 创建训练项目的请求体
|
||||
export interface CreateScheduleExerciseDto {
|
||||
exerciseKey?: string;
|
||||
name: string;
|
||||
sets?: number;
|
||||
reps?: number;
|
||||
durationSec?: number;
|
||||
restSec?: number;
|
||||
note?: string;
|
||||
itemType: 'exercise' | 'rest' | 'note';
|
||||
}
|
||||
|
||||
// 更新训练项目的请求体
|
||||
export interface UpdateScheduleExerciseDto {
|
||||
exerciseKey?: string;
|
||||
name?: string;
|
||||
sets?: number;
|
||||
reps?: number;
|
||||
durationSec?: number;
|
||||
restSec?: number;
|
||||
note?: string;
|
||||
itemType?: 'exercise' | 'rest' | 'note';
|
||||
completed?: boolean;
|
||||
}
|
||||
|
||||
// 完成状态统计
|
||||
export interface CompletionStats {
|
||||
total: number;
|
||||
completed: number;
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
// 重新排序请求体
|
||||
export interface ReorderExercisesDto {
|
||||
exerciseIds: string[];
|
||||
}
|
||||
|
||||
class ScheduleExerciseApi {
|
||||
// 获取训练计划的所有项目
|
||||
async list(planId: string): Promise<ScheduleExercise[]> {
|
||||
return api.get<ScheduleExercise[]>(`/training-plans/${planId}/exercises`);
|
||||
}
|
||||
|
||||
// 获取训练项目详情
|
||||
async detail(planId: string, exerciseId: string): Promise<ScheduleExercise> {
|
||||
return api.get<ScheduleExercise>(`/training-plans/${planId}/exercises/${exerciseId}`);
|
||||
}
|
||||
|
||||
// 添加训练项目
|
||||
async create(planId: string, dto: CreateScheduleExerciseDto): Promise<ScheduleExercise> {
|
||||
return api.post<ScheduleExercise>(`/training-plans/${planId}/exercises`, dto);
|
||||
}
|
||||
|
||||
// 更新训练项目
|
||||
async update(planId: string, exerciseId: string, dto: UpdateScheduleExerciseDto): Promise<ScheduleExercise> {
|
||||
return api.put<ScheduleExercise>(`/training-plans/${planId}/exercises/${exerciseId}`, dto);
|
||||
}
|
||||
|
||||
// 删除训练项目
|
||||
async delete(planId: string, exerciseId: string): Promise<{ success: boolean }> {
|
||||
return api.delete<{ success: boolean }>(`/training-plans/${planId}/exercises/${exerciseId}`);
|
||||
}
|
||||
|
||||
// 更新训练项目排序
|
||||
async reorder(planId: string, dto: ReorderExercisesDto): Promise<{ success: boolean }> {
|
||||
return api.put<{ success: boolean }>(`/training-plans/${planId}/exercises/order`, dto);
|
||||
}
|
||||
|
||||
// 标记训练项目完成状态
|
||||
async updateCompletion(planId: string, exerciseId: string, completed: boolean): Promise<ScheduleExercise> {
|
||||
return api.put<ScheduleExercise>(`/training-plans/${planId}/exercises/${exerciseId}/complete`, { completed });
|
||||
}
|
||||
}
|
||||
|
||||
export const scheduleExerciseApi = new ScheduleExerciseApi();
|
||||
@@ -70,6 +70,15 @@ class TrainingPlanApi {
|
||||
async activate(id: string): Promise<{ success: boolean }> {
|
||||
return api.post<{ success: boolean }>(`/training-plans/${id}/activate`);
|
||||
}
|
||||
|
||||
async getActivePlan(): Promise<TrainingPlanResponse | null> {
|
||||
try {
|
||||
return api.get<TrainingPlanResponse>('/training-plans/active');
|
||||
} catch (error) {
|
||||
// 如果没有激活的计划,返回null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const trainingPlanApi = new TrainingPlanApi();
|
||||
204
services/workoutsApi.ts
Normal file
204
services/workoutsApi.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { api } from './api';
|
||||
|
||||
// ==================== 数据类型定义 ====================
|
||||
|
||||
export interface WorkoutSession {
|
||||
id: string;
|
||||
userId: string;
|
||||
trainingPlanId?: string;
|
||||
name: string;
|
||||
scheduledDate: string;
|
||||
startedAt?: string;
|
||||
completedAt?: string;
|
||||
status: 'planned' | 'in_progress' | 'completed' | 'cancelled';
|
||||
totalDurationSec?: number;
|
||||
caloriesBurned?: number;
|
||||
stats?: {
|
||||
totalExercises: number;
|
||||
completedExercises: number;
|
||||
totalSets: number;
|
||||
completedSets: number;
|
||||
totalReps: number;
|
||||
completedReps: number;
|
||||
};
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deleted: boolean;
|
||||
|
||||
// 关联数据
|
||||
trainingPlan?: {
|
||||
id: string;
|
||||
name: string;
|
||||
goal: string;
|
||||
};
|
||||
exercises?: WorkoutExercise[];
|
||||
}
|
||||
|
||||
export interface WorkoutExercise {
|
||||
id: string;
|
||||
workoutSessionId: string;
|
||||
userId: string;
|
||||
exerciseKey?: string;
|
||||
name: string;
|
||||
plannedSets?: number;
|
||||
plannedReps?: number;
|
||||
plannedDurationSec?: number;
|
||||
completedSets?: number;
|
||||
completedReps?: number;
|
||||
actualDurationSec?: number;
|
||||
restSec?: number;
|
||||
note?: string;
|
||||
itemType: 'exercise' | 'rest' | 'note';
|
||||
status: 'pending' | 'in_progress' | 'completed' | 'skipped';
|
||||
sortOrder: number;
|
||||
startedAt?: string;
|
||||
completedAt?: string;
|
||||
performanceData?: Record<string, any>;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deleted: boolean;
|
||||
|
||||
// 关联的动作信息
|
||||
exercise?: {
|
||||
key: string;
|
||||
name: string;
|
||||
description: string;
|
||||
categoryKey: string;
|
||||
categoryName: string;
|
||||
};
|
||||
}
|
||||
|
||||
// ==================== DTO 类型定义 ====================
|
||||
|
||||
export interface StartWorkoutDto {
|
||||
startedAt?: string;
|
||||
}
|
||||
|
||||
export interface StartWorkoutExerciseDto {
|
||||
startedAt?: string;
|
||||
}
|
||||
|
||||
export interface CompleteWorkoutExerciseDto {
|
||||
completedAt?: string;
|
||||
completedSets?: number;
|
||||
completedReps?: number;
|
||||
actualDurationSec?: number;
|
||||
performanceData?: Record<string, any>;
|
||||
}
|
||||
|
||||
export interface AddWorkoutExerciseDto {
|
||||
exerciseKey?: string;
|
||||
name: string;
|
||||
plannedSets?: number;
|
||||
plannedReps?: number;
|
||||
plannedDurationSec?: number;
|
||||
restSec?: number;
|
||||
note?: string;
|
||||
itemType?: 'exercise' | 'rest' | 'note';
|
||||
}
|
||||
|
||||
export interface UpdateWorkoutExerciseDto {
|
||||
name?: string;
|
||||
plannedSets?: number;
|
||||
plannedReps?: number;
|
||||
plannedDurationSec?: number;
|
||||
restSec?: number;
|
||||
note?: string;
|
||||
itemType?: 'exercise' | 'rest' | 'note';
|
||||
}
|
||||
|
||||
export interface WorkoutSessionListResponse {
|
||||
sessions: WorkoutSession[];
|
||||
pagination: {
|
||||
page: number;
|
||||
limit: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface WorkoutSessionStatsResponse {
|
||||
status: string;
|
||||
duration?: number;
|
||||
calories?: number;
|
||||
stats?: {
|
||||
totalExercises: number;
|
||||
completedExercises: number;
|
||||
totalSets: number;
|
||||
completedSets: number;
|
||||
totalReps: number;
|
||||
completedReps: number;
|
||||
};
|
||||
exerciseCount: number;
|
||||
completedExercises: number;
|
||||
}
|
||||
|
||||
// ==================== API 服务类 ====================
|
||||
|
||||
class WorkoutsApi {
|
||||
// ==================== 训练会话管理 ====================
|
||||
|
||||
async getSessions(page: number = 1, limit: number = 10): Promise<WorkoutSessionListResponse> {
|
||||
return api.get<WorkoutSessionListResponse>(`/workouts/sessions?page=${page}&limit=${limit}`);
|
||||
}
|
||||
|
||||
async getSessionDetail(sessionId: string): Promise<WorkoutSession> {
|
||||
return api.get<WorkoutSession>(`/workouts/sessions/${sessionId}`);
|
||||
}
|
||||
|
||||
async startSession(sessionId: string, dto: StartWorkoutDto = {}): Promise<WorkoutSession> {
|
||||
return api.post<WorkoutSession>(`/workouts/sessions/${sessionId}/start`, dto);
|
||||
}
|
||||
|
||||
async deleteSession(sessionId: string): Promise<{ success: boolean }> {
|
||||
return api.delete<{ success: boolean }>(`/workouts/sessions/${sessionId}`);
|
||||
}
|
||||
|
||||
// ==================== 训练动作管理 ====================
|
||||
|
||||
async getSessionExercises(sessionId: string): Promise<WorkoutExercise[]> {
|
||||
return api.get<WorkoutExercise[]>(`/workouts/sessions/${sessionId}/exercises`);
|
||||
}
|
||||
|
||||
async getExerciseDetail(sessionId: string, exerciseId: string): Promise<WorkoutExercise> {
|
||||
return api.get<WorkoutExercise>(`/workouts/sessions/${sessionId}/exercises/${exerciseId}`);
|
||||
}
|
||||
|
||||
async startExercise(sessionId: string, exerciseId: string, dto: StartWorkoutExerciseDto = {}): Promise<WorkoutExercise> {
|
||||
return api.post<WorkoutExercise>(`/workouts/sessions/${sessionId}/exercises/${exerciseId}/start`, dto);
|
||||
}
|
||||
|
||||
async completeExercise(sessionId: string, exerciseId: string, dto: CompleteWorkoutExerciseDto): Promise<WorkoutExercise> {
|
||||
return api.post<WorkoutExercise>(`/workouts/sessions/${sessionId}/exercises/${exerciseId}/complete`, dto);
|
||||
}
|
||||
|
||||
async skipExercise(sessionId: string, exerciseId: string): Promise<WorkoutExercise> {
|
||||
return api.post<WorkoutExercise>(`/workouts/sessions/${sessionId}/exercises/${exerciseId}/skip`);
|
||||
}
|
||||
|
||||
async updateExercise(sessionId: string, exerciseId: string, dto: UpdateWorkoutExerciseDto): Promise<WorkoutExercise> {
|
||||
return api.put<WorkoutExercise>(`/workouts/sessions/${sessionId}/exercises/${exerciseId}`, dto);
|
||||
}
|
||||
|
||||
async addExercise(sessionId: string, dto: AddWorkoutExerciseDto): Promise<WorkoutExercise> {
|
||||
return api.post<WorkoutExercise>(`/workouts/sessions/${sessionId}/exercises`, dto);
|
||||
}
|
||||
|
||||
// ==================== 统计和分析 ====================
|
||||
|
||||
async getSessionStats(sessionId: string): Promise<WorkoutSessionStatsResponse> {
|
||||
return api.get<WorkoutSessionStatsResponse>(`/workouts/sessions/${sessionId}/stats`);
|
||||
}
|
||||
|
||||
// ==================== 快捷操作 ====================
|
||||
|
||||
async getTodayWorkout(): Promise<WorkoutSession> {
|
||||
return api.get<WorkoutSession>('/workouts/today');
|
||||
}
|
||||
|
||||
async getRecentWorkouts(days: number = 7, limit: number = 10): Promise<{ sessions: WorkoutSession[]; period: string }> {
|
||||
return api.get<{ sessions: WorkoutSession[]; period: string }>(`/workouts/recent?days=${days}&limit=${limit}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const workoutsApi = new WorkoutsApi();
|
||||
Reference in New Issue
Block a user