Files
digital-pilates/utils/pilatesPlan.ts
richarjiang 5f05abc3d5 feat: 添加挑战页面和相关功能
- 在布局中新增挑战页面的导航
- 在首页中添加挑战计划卡片,支持用户点击跳转
- 更新登录页面的标题样式,调整字体粗细
- 集成 Redux 状态管理,新增挑战相关的 reducer
2025-08-12 22:54:23 +08:00

191 lines
6.1 KiB
TypeScript
Raw Permalink 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.

export type ExerciseSet = {
durationSec?: number; // 计时型(秒)
reps?: number; // 次数型(可选)
restSecAfter?: number; // 组间休息
};
export type Exercise = {
key: string;
name: string;
description: string;
videoUrl?: string;
sets: ExerciseSet[];
tips?: string[];
};
export type DayPlan = {
dayNumber: number; // 1..30
type: 'training' | 'recovery' | 'rest';
title: string;
focus: string; // 训练重点
exercises: Exercise[]; // rest/recovery 可为空数组
};
export type PilatesLevel = 'beginner' | 'intermediate' | 'advanced';
const BASE_EXERCISES: Omit<Exercise, 'sets'>[] = [
{
key: 'hundred',
name: '百次拍击 (The Hundred)',
description: '仰卧,抬腿至桌面位,核心收紧,小幅快速摆动手臂并配合呼吸。',
videoUrl: undefined,
},
{
key: 'single_leg_stretch',
name: '单腿伸展 (Single Leg Stretch)',
description: '仰卧,交替伸直一条腿,另一条腿屈膝抱向胸口,核心稳定。',
},
{
key: 'double_leg_stretch',
name: '双腿伸展 (Double Leg Stretch)',
description: '仰卧同时伸直双腿与双臂,呼气收回环抱膝盖。',
},
{
key: 'roll_up',
name: '卷起 (Roll Up)',
description: '由仰卧卷起至坐并前屈,再控制还原,强调脊柱分节活动。',
},
{
key: 'spine_twist',
name: '脊柱扭转 (Spine Twist)',
description: '坐姿,躯干直立,左右控制旋转,强调轴向延展与核心稳定。',
},
{
key: 'bridge',
name: '桥式 (Bridge)',
description: '仰卧,卷尾抬起骨盆至肩桥位,感受臀腿后侧发力,控制还原。',
},
{
key: 'side_leg_lift',
name: '侧抬腿 (Side Leg Lift)',
description: '侧卧,髋稳定,抬高上侧腿,控制下放。',
},
{
key: 'swimming',
name: '游泳式 (Swimming)',
description: '俯卧,交替抬起对侧手臂与腿,保持脊柱中立并延展。',
},
{
key: 'cat_cow',
name: '猫牛式 (Cat-Cow)',
description: '四点支撑,呼吸带动脊柱屈伸,作为热身或整理放松。',
},
{
key: 'saw',
name: '锯式 (Saw)',
description: '坐姿分腿,旋转并前屈,斜向触碰对侧脚尖。',
},
{
key: 'plank',
name: '平板支撑 (Plank)',
description: '前臂/掌支撑,身体成直线,保持核心紧致。',
},
];
function buildSets(level: PilatesLevel, baseSec: number, setCount: number): ExerciseSet[] {
const effort = baseSec + (level === 'intermediate' ? 10 : level === 'advanced' ? 20 : 0);
const rest = 15;
return Array.from({ length: setCount }).map(() => ({ durationSec: effort, restSecAfter: rest }));
}
function pickExercises(keys: string[], level: PilatesLevel, baseSec: number, setCount: number): Exercise[] {
return keys.map((k) => {
const base = BASE_EXERCISES.find((e) => e.key === k)!;
return {
...base,
sets: buildSets(level, baseSec, setCount),
tips: [
'保持呼吸与动作节奏一致',
'核心始终收紧,避免腰椎塌陷',
'以控制优先于速度,专注动作质量',
],
};
});
}
export function generatePilates30DayPlan(level: PilatesLevel = 'beginner'): DayPlan[] {
// 周期化每7天安排恢复/拉伸;逐周增加时长或组数
const plan: DayPlan[] = [];
for (let d = 1; d <= 30; d += 1) {
const weekIndex = Math.ceil(d / 7); // 1..5
const isRecovery = d % 7 === 0 || d === 14 || d === 21 || d === 28; // 每周末恢复
if (isRecovery) {
plan.push({
dayNumber: d,
type: 'recovery',
title: '恢复与拉伸日',
focus: '呼吸、脊柱分节、柔韧性',
exercises: pickExercises(['cat_cow', 'saw'], level, 40, 2),
});
continue;
}
// 训练参数随周数递增
const baseSec = 35 + (weekIndex - 1) * 5; // 35,40,45,50,55
const setCount = 2 + (weekIndex > 3 ? 1 : 0); // 第4周开始 3 组
// 交替不同侧重点
let title = '核心激活与稳定';
let focus = '腹横肌、骨盆稳定、呼吸控制';
let keys: string[] = ['hundred', 'single_leg_stretch', 'double_leg_stretch', 'roll_up', 'spine_twist'];
if (d % 3 === 0) {
title = '后链力量与体态';
focus = '臀腿后侧、胸椎伸展、姿态矫正';
keys = ['bridge', 'swimming', 'plank', 'spine_twist', 'roll_up'];
} else if (d % 3 === 2) {
title = '髋稳定与侧链';
focus = '中臀肌、侧链控制、骨盆稳定';
keys = ['side_leg_lift', 'single_leg_stretch', 'bridge', 'saw', 'plank'];
}
const exercises = pickExercises(keys, level, baseSec, setCount);
plan.push({ dayNumber: d, type: 'training', title, focus, exercises });
}
return plan;
}
export function estimateSessionMinutes(day: DayPlan): number {
if (day.type !== 'training' && day.type !== 'recovery') return 0;
const totalSec = day.exercises.reduce((sum, ex) => {
return (
sum +
ex.sets.reduce((s, st) => s + (st.durationSec ?? 0) + (st.restSecAfter ?? 0), 0)
);
}, 0);
return Math.max(10, Math.round(totalSec / 60));
}
// 用户自定义配置(用于挑战页可调节)
export type ExerciseCustomConfig = {
key: string;
enabled: boolean;
sets: number; // 目标组数(计时型)
durationSec?: number; // 每组计时秒数(若为空可按默认)
reps?: number; // 每组次数(可选)
};
export function buildDefaultCustomFromPlan(day: DayPlan): ExerciseCustomConfig[] {
return (day.exercises || []).map((ex) => ({
key: ex.key,
enabled: true,
sets: Math.max(1, ex.sets?.length || 2),
durationSec: ex.sets?.[0]?.durationSec ?? 40,
reps: ex.sets?.[0]?.reps,
}));
}
export function estimateSessionMinutesWithCustom(day: DayPlan, custom: ExerciseCustomConfig[] | undefined): number {
if (!custom || custom.length === 0) return estimateSessionMinutes(day);
const restSec = 15; // 估算默认休息
const totalSec = custom.reduce((sum, c) => {
if (!c.enabled) return sum;
const perSet = (c.durationSec ?? 40) + restSec;
return sum + perSet * Math.max(1, c.sets);
}, 0);
return Math.max(5, Math.round(totalSec / 60));
}