我构建了一个 Subway 营养计算器
Source: Dev.to
请提供您希望翻译的完整文本内容,我将按照要求将其翻译为简体中文并保留原始的格式、Markdown 语法以及技术术语。谢谢!
介绍
仅仅是一个包含大量 JavaScript、庞大的类 JSON 数据结构的单一 HTML 文件,并且固执地不让糟糕的 UI 破坏我的午餐。
我将带你了解我是如何构建它的、途中出现了哪些问题,以及下次我会有什么不同的做法。
问题
- 面包热量
- 肉类热量
- 蔬菜热量(大多数为零,但橄榄和鳄梨不是)
- 酱汁热量
然后将其翻倍以适用于 foot‑long,记得奶酪会增加脂肪和钠含量,别忘了盐和胡椒。
这既繁琐又容易出错——正是一个简单的网络工具可以解决的问题。
数据收集
我需要一个 完整且一致的数据集,覆盖所有可能的配料:
| 类别 | 项目 |
|---|---|
| 面包 | 12 |
| 预制三明治 | 30+ |
| 蛋白质(单点) | 20+ |
| 奶酪 | 5 |
| 蔬菜 | 15 |
| 调味品 | 25+(常规 & 轻量) |
| 调味料 | 3 |
| 配菜 | 2 |
| 沙拉 | 25+ |
| 卷饼 | 25+ |
| 无面包碗 | 25+ |
| 蛋白口袋 | 4 |
| 汤 | 3 |
| 甜点 | 7 |
| 小配菜 | 8 |
| 总计 | ≈ 300+ |
来源
- Subway 官方美国营养 PDF(2026 版)
- 他们的在线菜单
- 第三方聚合网站
- 门店级别的配料表
经验教训: 永远不要只相信单一来源。要交叉核对所有信息,并对数据来源及估算部分保持透明。
数据结构
我创建了一个巨大的 JavaScript 对象,名为 subwayMenu。它包含每个类别的数组(breads、proteins、cheeses、vegetables、condiments 等)。
每个项目遵循相同的模式:
{
id: 'artisan-italian-bread-6inch',
name: '6" Artisan Italian Bread',
servingSize_g: 71,
calories: 210,
totalFat: 2,
saturatedFat: 1,
transFat: 0,
cholesterol: 0,
sodium: 380,
totalCarbs: 39,
dietaryFiber: 1,
sugars: 3,
addedSugars: 2,
protein: 8,
vitaminA_mcg: 0,
vitaminC_mg: 0,
calcium_mg: 1040,
iron_mg: 16.2,
category: 'bread'
}所有营养字段对每个项目都存在,这使得循环、计算和过滤变得直观简便。
核心逻辑
大小乘数
// quantity system (simplified)
const sizeMultiplier = isFootlong ? 2 : 1;calculateTotalNutrition
- 为每个营养字段初始化一个全为零的 totals 对象。
- 对随三明治大小而变化的类别应用大小乘数(脚长为 2)。
- 遍历每个已选类别(面包、蛋白质、奶酪、蔬菜、调味品,……),将项目的营养值乘以 数量 × 乘数 后累加。
- 返回最终的 totals 以及 成分列表(用于 “ingredients” 面板)。
四舍五入在最后一步才进行,数值会四舍五入到小数点后一位,以便显示整洁。
UI 设计
不使用外部库 – 仅使用纯 HTML、CSS 和原生 JavaScript。
| 区域 | 描述 |
|---|---|
| 左侧 | 构建器,包含可折叠的分类(面包、蛋白质、奶酪,…)以及用于切换菜单类型(三明治、沙拉、卷饼,…)的标签切换器。 |
| 右侧 | 结果区,显示营养标签、卡路里进度条、当前选择列表和操作按钮。 |
标签系统与可折叠分类
<!-- simplified markup -->
<div class="category-header" onclick="subwayToggleDropdown(this)">
<span>Breads</span>
<svg><!-- arrow icon --></svg>
</div>
<div class="category-items">
<!-- list of breads -->
</div>subwayToggleDropdown 会添加/移除 expanded 类并旋转 SVG 箭头。
在分类内部搜索
一个简单的输入框会过滤当前打开分类中可见的项目。
“当前选择”面板
列出每个已选项目及其数量和卡路里。
每条记录都有一个 X 按钮,调用
subwayRemoveItemFromSelection,该函数会:- 更新
currentSelection对象。 - 重新渲染受影响的分类(以取消勾选该项目)。
- 重新渲染选择面板本身。
- 更新
此小型的使用便利功能可避免用户必须滚动回顶部查看已添加的内容。
导出数据
function subwaySaveNutritionInfo() {
const { totals, ingredients } = calculateTotalNutrition();
const lines = [
`Meal: ${selectedMenuType}`,
`Size: ${isFootlong ? 'Foot‑long' : '6‑inch'}`,
'',
'Ingredients:',
...ingredients.map(i => `- ${i.name} ×${i.qty}`),
'',
'Nutrition Facts:',
`Calories: ${totals.calories} kcal`,
// …other fields…
];
const blob = new Blob([lines.join('\n')], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'subway-nutrition.txt';
a.click();
URL.revokeObjectURL(url);
// temporary “Saved!” feedback
showSaveFeedback();
}纯文本导出在所有平台均可使用,方便用户将数据粘贴到笔记或电子表格中。
Known Issues & Gotchas
- Bread radio group – 每次只能选择一种面包。
- Foot‑long multiplier – 必须应用于所有尺寸相关的配料。
- Search resetting – 更改项目会重置搜索框;需要保留查询。
- Save button – 在某些旧浏览器中不起作用(需要 Blob 支持)。
要点
- 数据优先: 清洁、一致的数据约占工作量的80%。
- 先逻辑后界面: 在构建界面之前,先在控制台中让计算引擎运行。
- 以用户为中心的设计: “当前选择”面板和简便的导出功能使工具真正有用。
最后思考
代码有点凌乱,数据也可以更完整一些,但计算器能够正常工作。你可以自行搭配 Subway 餐点,精准查看自己的摄入内容,并将结果保存以备后用。
如果你是一名开发者,正考虑为其他连锁餐厅打造类似工具,建议如下:
- 从数据开始。
- 先构建计算引擎。
- 添加真正解决痛点的 UI 功能(搜索、选择列表、导出)。
祝编码愉快! 🚀
与真实用户的测试
我把它交给了几个朋友,并观察他们在哪儿感到困惑。于是我发现了 foot‑long multiplier bug。
保持简洁
你不需要后端、数据库或构建系统。一个包含内联 CSS 和 JS 的单个 HTML 文件就足以满足此类工具的需求。
实际使用
现在,每当我走进Subway时,我就确切知道我要点什么。如果我不确定,我会在手机上打开这个计算器,在到柜台之前先把它算好。
目标
这就是全部意义——工具应该让你的生活更轻松,而不是更困难。
结束语
我希望本教程能帮助其他人构建有用的东西。如果你只是想使用计算器,那么现在你已经了解了它的内部工作原理。