feat: 添加挑战页面和相关功能

- 在布局中新增挑战页面的导航
- 在首页中添加挑战计划卡片,支持用户点击跳转
- 更新登录页面的标题样式,调整字体粗细
- 集成 Redux 状态管理,新增挑战相关的 reducer
This commit is contained in:
2025-08-12 22:54:23 +08:00
parent 00ddec25c5
commit 5f05abc3d5
9 changed files with 659 additions and 1 deletions

190
utils/pilatesPlan.ts Normal file
View File

@@ -0,0 +1,190 @@
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));
}