修改指南
常用修改模板
1. 添加新的训练类型
typescript
// Step 1: core/types.ts - 添加类型
type TrainingType = '...' | 'newType';
// Step 2: core/theme.ts - 添加主题
export const TRAINING_THEMES: Record<TrainingType, TrainingTheme> = {
// ...existing
newType: { color: 'var(--tr-newtype)', icon: '🆕', label: '新类型' },
};
// Step 3: core/theme.css - 添加颜色变量
:root {
--tr-newtype: #yourcolor;
--tr-newtype-bg: rgba(r, g, b, 0.1);
}
.dark {
--tr-newtype: #darkcolor;
--tr-newtype-bg: rgba(r, g, b, 0.15);
}2. 修改周二训练 (间歇/节奏切换)
typescript
// training-config.ts 中的 weeklyPlan
{
day: '周二',
sessions: [
{
type: 'interval',
name: '间歇训练',
details: `热身: 2km\n主体: 800m x 6-8\n冷身: 2km`,
distance: 10,
targetHR: { min: 167, max: 185 },
weekParity: 'odd', // 单周显示
tips: `🌙 前晚:22:00 前入睡\n🍽️ 训练后:30分钟内补充碳水+蛋白质`,
},
{
type: 'tempo',
name: '节奏跑',
details: `热身: 2km\n主体: 8km @ 5:00/km\n冷身: 2km`,
distance: 10,
targetHR: { min: 160, max: 173 },
weekParity: 'even', // 双周显示
tips: `🌙 前晚:22:00 前入睡\n🍽️ 训练前 2h 进食`,
},
],
},3. 修改 LSD 周期化距离
typescript
// training-config.ts
const lsdDistanceByWeek: number[] = [
15,
16,
17,
12, // 基础期 (W1-W4): 3+1 递增, 减量
17,
18,
19,
14, // 强化期I (W5-W8)
19,
20,
21,
15, // 强化期II (W9-W12)
21,
18, // 巅峰期 (W13-W14)
12,
10, // 减量期 (W15-W16)
];4. 修改训练周期
typescript
// training-config.ts
const cycles: TrainingCycle[] = [
{ name: '基础期', weeks: 12, phase: 'base', description: '建立有氧基础...' },
{ name: '巅峰期', weeks: 2, phase: 'peak', description: '模拟比赛配速...' },
{ name: '减量期', weeks: 2, phase: 'taper', description: '减量恢复...' },
];5. 添加/同步训练记录
方式一:Strava 自动同步(推荐)
bash
pnpm strava:sync # 同步最近 7 天
pnpm strava:sync --all # 同步所有数据方式二:手动编辑 data/workouts.json
json
// data/workouts.json → workouts 数组末尾追加
// ⚠️ 必须使用真实 Strava 数据,禁止编造
{
"date": "2026-02-05",
"type": "run",
"name": "有氧跑",
"timeRange": "18:00-19:00",
"location": "Fuzhou",
"duration": "1:00:00",
"distance": 10.5,
"activeCalories": 600,
"totalCalories": 680,
"elevation": 50,
"avgPower": 200,
"avgCadence": 180,
"avgPace": "5'42\"/km",
"avgHeartRate": 142,
"effort": 5,
"effortDescription": "Moderate",
"zoneDistribution": [
{ "zone": 1, "duration": "05:00" },
{ "zone": 2, "duration": "45:00" },
{ "zone": 3, "duration": "10:00" },
{ "zone": 4, "duration": "00:00" },
{ "zone": 5, "duration": "00:00" }
]
}
progressStats由computeProgressStats()动态计算,无需手动更新。
组件 Props 速查
MarathonDashboard
tsx
<MarathonDashboard
showProgress={true} // 显示 ProgressTracker
showCalendar={true} // 显示 TrainingCalendar
/>ProgressTracker
管理 4 个统计卡片,每个卡片可点击打开对应弹窗。
tsx
<ProgressTracker
stats={progressStats} // ProgressStats (含 dailyTargets)
workoutRecords={workoutRecords} // WorkoutRecord[] (柱状图数据)
cycles={trainingPlan.cycles} // TrainingCycle[] → 计划进度弹窗
personalRecords={personalRecords} // PersonalRecord[] → 完成训练弹窗
title="训练进度"
/>内部弹窗状态管理:
| 卡片 | 状态变量 | 打开的弹窗 |
|---|---|---|
| 本周里程 | showWeeklyModal | WeeklyDistributionModal |
| 计划进度 | showCycleModal | CycleOverviewModal |
| 累计公里 | (无) | (不可点击) |
| 完成训练 | showPRModal | PersonalRecords |
弹窗导航流程:
WeeklyDistributionModal → click bar → WorkoutModal → close → 回到 WeeklyDistributionModalTrainingCalendar
tsx
<TrainingCalendar
plan={trainingPlan} // TrainingPlan 对象
startDate="2026-01-19" // 训练开始日期
workoutRecords={workoutRecords} // 实际训练记录
/>CycleOverviewModal
tsx
<CycleOverviewModal
cycles={cycles} // TrainingCycle[]
currentWeek={3} // 当前训练周
onClose={() => setCycleModalOpen(false)}
/>PersonalRecords
tsx
<PersonalRecords
records={personalRecords} // PersonalRecord[]
onClose={() => setPRModalOpen(false)}
/>WorkoutModal
tsx
<WorkoutModal
records={workouts} // WorkoutRecord[]
date={new Date()}
onClose={() => setSelectedDay(null)}
/>TrainingDayModal
tsx
<TrainingDayModal
date={new Date()}
weekNumber={3}
sessions={[...]} // TrainingSession[]
workouts={[...]} // WorkoutRecord[]
heartRateZones={zones}
onClose={() => setOpen(false)}
/>Card (支持点击)
tsx
<Card
className="custom-class"
onClick={() => setModalOpen(true)} // 可选,添加后自动添加 role="button"
>
内容
</Card>ProgressRing
tsx
<ProgressRing
value={28.7} // 当前值
max={47} // 最大值
size={120} // SVG 尺寸 (px)
strokeWidth={8}
color="var(--tr-primary)" // 环形颜色
label="本周里程"
sublabel="28.7/47 km"
/>Badge
tsx
<Badge type="tempo" size="md" />
// size: 'sm' | 'md' | 'lg'
// type: TrainingType弹窗开发规范
新建弹窗模板
- 选择 CSS 前缀(避免冲突):
| 已用前缀 | 组件 |
|---|---|
tr- | ProgressTracker |
wd- | WeeklyDistributionModal |
co- | CycleOverviewModal |
pr- | PersonalRecords |
tdm- | TrainingDayModal |
- 遵循弹窗结构:
tsx
function MyModal({ onClose }: { onClose: () => void }) {
return (
<div
className="xx-overlay"
role="presentation"
onClick={onClose}
onKeyDown={(e) => {
if (e.key === 'Escape') onClose();
}}
>
<div
className="xx-modal"
role="dialog"
onClick={(e) => e.stopPropagation()}
onKeyDown={(e) => e.stopPropagation()}
>
<header className="xx-header">
<div className="xx-header__info">
<h3 className="xx-header__title">标题</h3>
</div>
<button className="xx-header__close" onClick={onClose}>
×
</button>
</header>
{/* 内容 */}
</div>
</div>
);
}- z-index 规则:
- 一级弹窗(从主面板打开):
1000 - 二级弹窗(从弹窗内打开):
1010
- 一级弹窗(从主面板打开):
柱状图填充逻辑
WeeklyDistributionModal 中的柱状图使用每日计划目标作为 100% 基准:
tsx
// dailyTargets 来自 computeProgressStats() 的 computeDailyTargets()
const dayTarget = dailyTargets[day.dayIndex] || 0;
const ref = dayTarget > 0 ? dayTarget : chartMax;
const fillPercent = Math.min((day.distance / ref) * 100, 100);
// 达到或超过目标 → 满条 (100%)布局规格
ProgressTracker 主面板
┌──────────┬──────────┬──────────┬──────────┐
│ 本周里程 │ 计划进度 │ 累计公里 │ 完成训练 │
│ (ring) │ (ring) │ (数字) │ (数字) │
│ 可点击 │ 可点击 │ │ 可点击 │
└──────────┴──────────┴──────────┴──────────┘
↓ click ↓ click ↓ click
Weekly Cycle Personal
Distribution Overview Records
Modal Modal ModalWeeklyDistributionModal 布局
┌─ Header ──────────────────────────┐
│ 本周里程 [61%] [×] │
├─ Summary ─────────────────────────┤
│ 28.7 47 3 │
│ 公里 目标 训练日 │
├─ Bar Chart ───────────────────────┤
│ ░░ ██ ░░ ██ ██ ░░ ░░ │
│ 10.4 10.1 8.3 │
│ 一 二 三 四 五 六 日 │
│ ↑ click → WorkoutModal │
└───────────────────────────────────┘WorkoutCard 布局
┌──────────────────────────────────────┐
│ 🏃 热身跑 [4] │
│ 19:52-20:09 · Fuzhou MODERATE │
├──────────────────────────────────────┤
│ 时长 距离 配速 心率 │
│ 0:17:20 2.31KM 7'29'/km 135BPM│
│ 卡路里 爬升 功率 步频 │
│ 139KCAL 2M 160W 177SPM│
├──────────────────────────────────────┤
│ 心率区间分布 │
│ Zone 1 [██░░░░░░░░] 03:27 <131BPM │
│ Zone 2 [████████░░] 13:53 132-145 │
│ Zone 3 [░░░░░░░░░░] 00:00 146-159 │
│ Zone 4 [░░░░░░░░░░] 00:00 160-173 │
│ Zone 5 [░░░░░░░░░░] 00:00 174+ │
└──────────────────────────────────────┘Strava 同步脚本
| 文件 | 用途 |
|---|---|
scripts/strava/auth.ts | OAuth 授权 (port 8888) |
scripts/strava/client.ts | API 客户端 (活动/心率流/区间) |
scripts/strava/config.ts | 凭证管理 (.strava-credentials) |
scripts/strava/sync.ts | 同步 CLI (合并去重到 workouts.json) |
scripts/strava/transform.ts | Strava → WorkoutRecord 转换 |
心率数据获取优先级:
- Streams API → 原始心率流,自定义区间计算
- Zones API → Strava 预计算分布(备选)
- 无心率数据 → 记录活动但 zoneDistribution 为空
data/workouts.json 结构:
json
{
"workouts": [...],
"personalRecords": [...],
"config": {
"trainingStartDate": "2026-01-19",
"totalWeeks": 16,
"weeklyTarget": 50,
"historicalTotalDistance": 0,
"historicalTotalSessions": 0
},
"metadata": {
"lastUpdated": "ISO timestamp",
"source": "strava"
}
}