Skip to content

修改指南

常用修改模板

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" }
  ]
}

progressStatscomputeProgressStats() 动态计算,无需手动更新。


组件 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="训练进度"
/>

内部弹窗状态管理:

卡片状态变量打开的弹窗
本周里程showWeeklyModalWeeklyDistributionModal
计划进度showCycleModalCycleOverviewModal
累计公里(无)(不可点击)
完成训练showPRModalPersonalRecords

弹窗导航流程:

WeeklyDistributionModal → click bar → WorkoutModal → close → 回到 WeeklyDistributionModal

TrainingCalendar

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

弹窗开发规范

新建弹窗模板

  1. 选择 CSS 前缀(避免冲突):
已用前缀组件
tr-ProgressTracker
wd-WeeklyDistributionModal
co-CycleOverviewModal
pr-PersonalRecords
tdm-TrainingDayModal
  1. 遵循弹窗结构:
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>
  );
}
  1. 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                 Modal

WeeklyDistributionModal 布局

┌─ 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.tsOAuth 授权 (port 8888)
scripts/strava/client.tsAPI 客户端 (活动/心率流/区间)
scripts/strava/config.ts凭证管理 (.strava-credentials)
scripts/strava/sync.ts同步 CLI (合并去重到 workouts.json)
scripts/strava/transform.tsStrava → WorkoutRecord 转换

心率数据获取优先级:

  1. Streams API → 原始心率流,自定义区间计算
  2. Zones API → Strava 预计算分布(备选)
  3. 无心率数据 → 记录活动但 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"
  }
}

Released under the CC BY-SA 4.0 License. (134a8ec)