
健身房重量进度计算器听起来并不复杂但真正开始做的时候你会发现它不是一个简单的“加法和乘法”工具。它需要把每次训练的动作、重量、次数、组数记录清楚还要在下次训练前给出一个合理的重量建议同时让用户能看出来自己到底有没有进步。现在这个项目还在开发中我按实际落地顺序把需求、数据结构、计算逻辑和验证方法拆一遍给正在做类似个人工具的人参考。这类工具最容易出现的问题是功能越加越多但核心流程反而没跑顺。比如有的版本支持很多动作模板却没有处理同一天多组记录有的版本能画漂亮的图表但数据存得乱七八糟。所以我更建议先把记录、存储、计算这三条链路想清楚再考虑界面和扩展。1. 这个计算器到底解决什么问题1.1 训练记录中的常见痛点健身房里做力量训练最简单的记录方式是带一个小本子或者直接在手机备忘录里写“今天深蹲 100kg 5 次 5 组”看起来够了但连续记录一个月后问题就出来了。你想知道上次卧推是多重、做了几次时需要往回翻很久。你想看深蹲的重量是不是在慢慢涨备忘录里的文字根本没法变成曲线。更麻烦的是不同动作、不同次数、不同组数混在一起手动算训练容量和估计 1RM 非常累。所以很多人最后会转向通用健身 App。但通用 App 的问题也很明显你只是想记录“动作、重量、次数、组数”这几个字段用不上社交、饮食、课程推荐等一堆附加功能。而且很多 App 的录入流程是固定的如果你想记录减载周、RPE 或者组间休息时间反而不方便。重量进度计算器要解决的就是把这层记录逻辑单独拿出来。你不用在大量无关功能里翻找深蹲历史也不用每次手动计算下一次该加多少重量。核心目标是让每次训练记录的录入成本足够低让“进度”这件事自动被算出来。1.2 适合谁用这个工具如果你属于下面几类人这个工具会比较有用刚开始接触力量训练还不确定每次该加多少重量。自己写训练计划不想被固定 App 模板绑架。做过一段训练但记录比较乱想用一个统一数据结构整理历史。想用前端技术做一个能和真实场景结合的个人项目用训练数据练手。这里要说明一点它不打算替代完整的训练计划。你不能指望一个重量计算器告诉你今天该练什么动作、该做几组几次。它要做的是在“你已经知道今天练什么动作”的前提下帮你快速记录并计算出合理的重量范围。1.3 为什么只记录“最大重量”还不够训练进度不是一条直线。就算你今天能用 100kg 做 5 组 5 次第二天起床后状态不好可能用同样重量只能做 3 次。睡眠、压力、饮食、动作熟练度都会影响当天的表现。如果计算器只看上一次的最大重量很容易给出一个不切实际的建议。比如你上次硬拉用了 140kg 做了 1 次下次推荐 142.5kg听着合理但如果你已经连续硬拉了两周没休息这次可能应该降回 135kg 做容量训练。所以我一般会让数据结构保留三块核心信息重量、次数、组数。重量乘次数乘组数就是本次训练容量容量曲线比单次最大重量更能反映长期趋势。计算器给出的建议也要结合最近几次表现而不是只看最后一条。2. 核心功能拆解从“记录重量”到“推荐增量”2.1 最小功能集第一版不需要做得很复杂最小功能集应该包含这些动作名称深蹲、卧推、硬拉、推举等应该可以自定义。日期默认当天可手动修改。重量单位一般用 kg也可以考虑 lb最好做成设置项。次数每组完成的次数。组数一组训练执行几组或者按组逐条记录。这里有一个关键选择数据怎么存。如果你把“一次训练”当成一条记录比如“深蹲 5 组 100kg 5 次”那数据结构会很简单但后续要分析某组重量的变化时会很别扭。更好的是把“一组”当成一条记录2025-01-06深蹲100kg5次第1组2025-01-06深蹲100kg5次第2组这样输入时看起来多了一点点但做统计和绘图非常方便。页面录入时可以把相同日期和动作合并成一条展示底层存储仍然按组存这是更训练逻辑一致的做法。2.2 递增规则不是越重越好给下一次训练推荐重量是计算器最容易做错的地方。一开始我以为直接写“上次重量 2.5kg”就行实际用下来发现不行。有些动作是 1.25kg 的小片有些哑铃动作只能按 1kg 或 2kg 递增减载周期里你根本不应该加重甚至要主动减重量。我建议把递增规则做成可配置至少支持三种固定增量每次在最近一次训练重量上加 2.5kg 或 5kg适合线性增长期。百分比增量按当前 1RM 估算值的 2% 到 5% 计算适合有一定训练经验的人。容量匹配这次是 5 次下次想保持同样重量做 6 次这也是一种递增因为它提高了总容量。这些规则不需要同时实现。第一版可以先只做固定增量但字段名和逻辑要预留扩展。我一般把规则写成一个配置对象后续增加规则时不用改主流程。2.3 1RM 估算公式提高进度的量化参考记录完重量和次数后另一个需要处理的问题是不同训练日之间怎么比较。比如今天用 100kg 做了 5 次明天用 92.5kg 做了 8 次哪个强度更高只看重量没法比估算 1RM 可以统一口径。常见近似公式有几种这里列出我常用的两个公式名称计算方式适用情况Epley1RM 重量 × (1 次数 / 30)次数不超过 10 时比较稳定Brzycki1RM 重量 × 36 / (37 - 次数)同样适合中低次数动作类型不同会有些偏差如果你用 12 次以上的高次数训练估算 1RM 偏差会变大。这种情况下我更建议直接看训练容量而不是强行算 1RM。公式只是参考最终判断还是要结合动作质量和当天状态。3. 技术选型先做纯前端 MVP3.1 为什么我建议用纯前端这个工具的数据模型非常简单不需要账号体系也不需要多人协作。纯前端方案有几个实际好处部署简单一个 HTML 页面加一个 JS 文件就能跑起来。数据在本地不用处理注册、登录、数据库也不会因为服务器挂了无法记录。快速验证可以立刻把页面加到手机桌面像一个小应用一样用。缺点也明显数据不跨设备同步换手机后需要手动迁移。这个取舍在 MVP 阶段可以接受。如果你已经熟悉 React、Vue 或 Svelte也可以用这些框架写组件但本质上数据层、计算逻辑是一样的。我个人建议第一版不要引入复杂工程配置先用原生 HTML/CSS/JavaScript 跑通核心流程等确定真的需要更多功能时再重构。3.2 数据存储选 localStorage 还是 IndexedDB浏览器端存储训练记录最常见的两个选择是 localStorage 和 IndexedDB。存储方式特点适合场景localStorage同步 API使用简单容量较小数据量几千条以内的训练记录IndexedDB异步 API容量更大支持索引数据量大、需要复杂查询的历史记录对一个重量×次数×组数的训练日志来说localStorage 足够。我第一版就是用 localStorage键名统一为 gym_records值是 JSON 字符串。每次变更后重新保存整个数组虽然看起来不太高效但胜在简单不容易出错。这里需要提醒一点localStorage 按“源”隔离不同域名不能互相读取清理浏览器数据也可能清除记录。所以早期就要考虑导出功能防止数据丢失。3.3 页面结构怎么搭页面不需要复杂三块区域足够录入区日期、动作、重量、次数、组数一个添加按钮。历史列表按日期倒序展示最近记录支持按动作筛选。进度区选择某个动作后展示该动作的重量、估计 1RM、训练容量曲线。布局上建议移动端优先因为大多数人在健身房是拿手机记录的。输入框不必用表格布局每个输入项占一行保证单手操作。按钮区域可以大一点避免误点。4. 核心代码实现从数据模型到进度计算4.1 数据结构定义先用扁平数组存一组一组的记录。每条记录包含// 示例一条训练记录 const record { id: 20250106123000_001, date: 2025-01-06, // 统一为 YYYY-MM-DD exercise: squat, // 动作名称建议用英文小写或拼音便于过滤 weightKg: 100, // 重量单位 kg reps: 5, // 这一组完成的次数 note: // 可选RPE、疲劳程度等 };字段名尽量让单位明确不要把单位混在数值里。比如不要写weight: 100kg否则后续计算要反复解析字符串很容易报错。动作名称最好也有统一规范不然“深蹲”“Squat”“squat”会被当成三个不同动作。4.2 添加训练记录添加记录时先读取原数组再 push 新记录到数组最后写回 localStorage。这里的关键是每次记录要生成一个唯一 id方便之后编辑和删除。function loadRecords() { const data localStorage.getItem(gym_records); try { return data ? JSON.parse(data) : []; } catch (e) { return []; } } function saveRecords(records) { localStorage.setItem(gym_records, JSON.stringify(records)); } function addRecord(date, exercise, weightKg, reps) { const id ${Date.now()}_${Math.random().toString(16).slice(2)}; const record { id, date, exercise, weightKg, reps }; const records loadRecords(); records.push(record); saveRecords(records); return record; }为什么使用随机 id因为同一天可能给同一个动作录多组。如果只用日期和动作做唯一标识更新和删除时很容易冲突。随机 id 能保证每条记录独立。4.3 计算每个动作的历史和推荐重量要得到“深蹲历史”就按动作名称过滤然后按日期排序。日期用 YYYY-MM-DD 的好处是字符串比较和数字比较结果一致不用再 new Date() 转来转去。function getExerciseHistory(records, exercise) { return records .filter(r r.exercise exercise) .sort((a, b) a.date.localeCompare(b.date) || a.id.localeCompare(b.id)); }这里用a.id.localeCompare(b.id)是为了保证同一天多次记录的顺序稳定。如果不加这一层排序结果可能不稳定。估算 1RM 的函数function estimateOneRM(weightKg, reps) { if (reps 0) return weightKg; if (reps 10) { // Epley 近似公式仅作参考 return Math.round(weightKg * (1 reps / 30) * 100) / 100; } return weightKg; // 高次数不做估算 }推荐重量函数就比较容易了function suggestNextWeight(history, incrementKg 2.5) { if (!history.length) return null; const last history[history.length - 1]; const next last.weightKg incrementKg; // 对齐到 0.5kg 的整数倍方便杠铃片组合 return Math.round(next * 2) / 2; }这段代码只是最简单版本。真实使用时要考虑重量单位、动作类型、减载周期所以规则需要可配置。不要把这个推荐值写死成最终答案。4.4 进度图表展示不引入复杂图表库也可以画出一个能看的折线图。用 Canvas 可以快速实现一个最简版本。function renderChart(canvas, history) { const ctx canvas.getContext(2d); // 清空画布再根据 history 中 weightKg 或 estimateOneRM 绘制折线 }具体坐标计算这里不展开。需要注意的点是纵轴刻度不建议从 0 开始否则数据波动会被压得很平。可以从历史最低值的 80% 开始让曲线变化更明显。横轴按日期排列如果记录太密可以按周聚合。例如把某一周内该动作的最高重量或平均重量作为这个点的值。这样图表不会因为频繁训练而显得杂乱。5. 落地验证与常见问题排查5.1 用一组模拟数据验证写完代码后不要直接开始录入真实训练。先用模拟数据跑一遍逻辑。我习惯的做法是添加 3 条同一个动作的记录日期分别是 1 月 1 日、1 月 8 日、1 月 15 日重量分别为 80kg、82.5kg、85kg次数从 8 次降到 5 次。刷新页面确认历史列表还保留着。选择深蹲查看估计 1RM 是否随训练日期递增。调用 suggestNextWeight确认推荐重量为 87.5kg如果增量设置为 2.5kg。如果这几个环节都正常再考虑录入实际数据。这个流程能帮你快速区分算法问题、存储问题和界面问题。不要在还没验证核心逻辑前就开始调样式。5.2 常见问题排查顺序现象优先检查项原因刷新后数据消失localStorage 键名是否一致隐私模式下是否被阻止写入键名不一致或浏览器限制推荐重量不更新是否读取了最新数组是否按日期排过序记录没有排序取到旧数据1RM 算出来不准确次数是否被当成字符串公式是否越界需要 Number 转换公式有适用范围界面卡顿是否每次渲染都遍历全部记录数据量大时需要按动作过滤和聚合日期错乱是否统一 YYYY-MM-DD时间戳格式不一致导致排序错误排查时先看 DevTools 的 Console 报错再打开 Application 面板查看 localStorage 里的键值最后检查计算函数。大多数问题不是出在算法本身而是输入格式和读取顺序。5.3 移动端录入优化在手机上反复点输入框很烦所以有几个小优化值得做日期默认今天不用每次都选。动作名自动补全维护一个常用动作列表点击下拉选择。重量输入框 step 设为 2.5但允许手动输入 1.25。添加完一组后自动聚焦到“重量”输入框方便连续录入。如果同一个动作连续多组且重量不变可以加一个“复制上一组”按钮。这样你只需要点一次确认就能把上一组数据复制下来录入效率提升很多。6. 从 MVP 到长期使用批量录入、导入导出和工程化6.1 批量录入手动一条条录入很累。如果之前已经在表格里记过一段时间记录可以做一个批量导入功能。最简单的格式是用 CSV 或纯文本每行一条2025-01-06,squat,100,5 2025-01-08,squat,100,5 2025-01-10,squat,102.5,5解析时按逗号拆开然后逐条调用 addRecord。批量导入要处理几类问题格式不正确跳过错行并提示具体行号。重复导入如果导入两次会出现重复记录所以最好先清空再导入或者用“日期动作重量次数”去重。单位不统一导入前让用户选择单位导入后统一转为 kg。这里最容易犯的错误是处理到一半时出错导致部分写入。更稳妥的做法是先解析并校验所有行全部通过后再一次性写入。不要边解析边写否则容易出现“前半部分进去了后半部分报错”的情况。6.2 导出备份与迁移本地存储并不可靠浏览器改设置、换电脑、手滑清除数据都会让记录消失。所以至少要提供一个导出按钮把整个记录数组导出为 JSON 文件。function exportRecords() { const records loadRecords(); const blob new Blob([JSON.stringify(records, null, 2)], { type: application/json }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download gym_records_${new Date().toISOString().slice(0, 10)}.json; a.click(); URL.revokeObjectURL(url); }导出文件包含了所有训练数据注意保管。如果将来做多设备同步这个 JSON 也可以作为迁移格式。每次大版本更新前先导出一份能避免很多数据丢失问题。6.3 后续功能优先级给后续功能排优先级时我的建议是这样图表曲线能够看到趋势比增加更多输入项更有价值。训练模板把深蹲、卧推、硬拉等常用计划预设成模板减少录入成本。导入导出尽量早做避免数据丢失。周期化支持如果已经开始做减载周需要允许“本次不推荐加重而是维持或减轻重量”。多用户同步放在最后除非真的需要和教练共享数据。不要一开始就把所有功能堆上去。个人工具的使用频率取决于录入体验而不是功能数量。如果每次记录都要点五下才能保存再强的功能也不会被长期使用。7. 开发过程中值得复盘的经验7.1 先跑通单条记录再做批量我第一版时一开始就想着要支持多种动作、图表、批量导入结果中途经常出现一个问题不知道是表单取值有问题还是存储有问题还是图表绘制有问题。后来的做法是只保留一条链路填写表单 → 添加记录 → 刷新页面 → 查看历史。跑通这条链路后再加批量导入、图表和推荐计算。这样每一步的问题边界都很清楚。如果你也在做类似工具建议按这个顺序推进。7.2 数据规范化比界面美观更重要训练日志最怕输入格式不统一。比如深蹲有时写成“Squat”有时写成“深蹲”过滤历史和画图时它们就会被当成两个动作。所以在录入表单里用下拉选项或者录入后做一个“动作名归一化”比如统一转成小写、去掉空格。单位也是同理。如果一个记录存的是 100kg另一个存的是 220lb进度计算就会乱掉。最好所有输入在保存前都转成固定单位界面展示时再转成用户想看到的单位。去掉单位干扰后数据结构会干净很多。7.3 真实健身场景比算法复杂得多不要把“重量进度计算器”当成一个能替你制定训练计划的黑盒。它只能基于输入的数据告诉你上次训练和本次可能使用的重量。真正决定要不要加重量还要看动作质量、关节状态、训练目的和当天状态。所以我在推荐重量旁边会加一行提示上次训练次数掉了吗本次热身是否感觉沉重如果答案是“掉了很多次”或“很沉重”建议继续使用当前重量甚至减量。这个项目最大的价值是把历史数据整理清楚让判断有依据而不是替你下结论。如果你也想做一个自己的健身记录工具可以从最小版本开始先把单条记录和核心计算跑稳再做批量导入和图表。最后记得加一个导出按钮防止记录丢失。重量进步是长期事件工具的唯一目标就是把这条曲线记录下来。