码农周末:撸了个单位换算器,被温度换算折腾了一下午

周末在家做饭,菜谱上写着"牛肉 8 盎司",我家厨房秤只显示克。掏出手机搜了半天,才换算出来是 227 克。

当时就想:这玩意儿我也能做啊!正好最近在学鸿蒙开发,干脆撸一个出来。

说干就干,一个周末下来,还真搞定了。虽然中间被温度换算折腾了一下午,但最终还是做出来了。


一、先想清楚要啥功能

既然是练手项目,功能不用太复杂,但也得实用。我决定支持四大类:

  • 长度:mm、cm、m、km、英寸、英尺(装修用得上)
  • 重量:mg、g、kg、吨、磅、盎司(做菜用得上)
  • 温度:摄氏、华氏、开尔文(看国外天气用得上)
  • 面积:mm²、m²、km²、公顷、英亩(买房用得上)

UI 设计嘛,参考了几个主流 App,发现大家都差不多:

  1. 顶部类别切换
  2. 输入区:选单位 + 输数值
  3. 输出区:选单位 + 看结果
  4. 中间一个交换按钮

简洁明了,我也这么干。


二、数据结构:磨刀不误砍柴工

单位怎么定义?

每个单位需要两个东西:名字和换算系数。

interface UnitInfo {
  label: string    // "米 (m)"
  toBase: number   // 换算到基准的系数
}

系数是啥意思?

比如长度,我选"米"作为基准:

  • 1厘米 = 0.01米 → toBase = 0.01
  • 1千米 = 1000米 → toBase = 1000
  • 1英寸 = 0.0254米 → toBase = 0.0254(对,就这么精确)

这样换算就简单了:输入值 × toBase = 基准值,再 基准值 ÷ 目标toBase = 结果

类别怎么定义?

interface Category {
  name: string
  icon: string
  units: UnitInfo[]
  convertFn?: (val, fromIdx, toIdx) => number
}

为啥需要 convertFn

这个后面细说,是个大坑。


三、换算算法:基准单位法

普通单位的换算

思路很简单:先把输入转成基准单位,再转成目标单位。

// 输入值 → 基准单位
const base = num * cat.units[this.fromIdx].toBase

// 基准单位 → 目标单位
const result = base / cat.units[this.toIdx].toBase

举个例子:5千米换算成米

  1. 5 × 1000 = 5000(转基准)
  2. 5000 ÷ 1 = 5000(转目标)

完美!

第一个大坑:温度不是这么算的

我一开始自信满满,把温度也用系数法定义了。结果一测试:

输入 100°C,换算成华氏度,结果:100。

应该是什么?212°F 啊!

问题在哪?

温度换算不是简单的乘除!摄氏度和华氏度的关系是:

  • °C → °F:C × 9/5 + 32
  • °F → °C:(F - 32) × 5/9

开尔文也一样:

  • °C → K:C + 273.15
  • K → °C:K - 273.15

折腾了一下午,最后写了个特殊函数:

convertFn: (val, fromIdx, toIdx) => {
  // 先转摄氏度
  let celsius
  if (fromIdx === 0) celsius = val
  else if (fromIdx === 1) celsius = (val - 32) * 5/9
  else celsius = val - 273.15

  // 再转目标单位
  if (toIdx === 0) return celsius
  if (toIdx === 1) return celsius * 9/5 + 32
  return celsius + 273.15
}

思路是:不管什么温度,先转成摄氏度,再转成目标单位。这样只需要 6 个公式,而不是 9 个(3×3)。


四、UI 布局:卡片式设计

类别Tab

ForEach 遍历,每个 Tab 是一个 Column

Row() {
  ForEach(CATEGORIES, (cat, idx) => {
    Column() {
      Text(cat.icon).fontSize(22)
      Text(cat.name).fontSize(13)
    }
    .backgroundColor(this.activeCategory === idx ? '#EEF3FD' : 'transparent')
  })
}

选中状态:浅蓝背景 + 蓝字;未选中:透明 + 灰字。

单位选择器

做了个横向滚动的标签:

Scroll() {
  Row() {
    ForEach(cat.units, (unit, idx) => {
      Text(unit.label)
        .backgroundColor(this.fromIdx === idx ? '#5B8DEF' : '#F0F2F5')
    })
  }
}
.scrollable(ScrollDirection.Horizontal)

选中:蓝色背景 + 白字;未选中:灰色背景。

输入框

TextInput({ placeholder: '输入数值' })
  .type(InputType.Number)
  .onChange((val) => {
    this.inputValue = val
    this.convert()  // 实时换算!
  })

亮点:每次输入都触发换算,不需要点按钮,体验更流畅。


五、更多坑:边界情况

第二个坑:浮点精度

测试时输入 0.1,结果显示 0.10000000000000001

原因:JavaScript 经典的浮点数精度问题。

解决toFixed 限制精度,再去零:

const fixed = val.toFixed(6)
return parseFloat(fixed).toString()

第三个坑:索引越界

在长度类别选了第 6 个单位(英尺),切到温度时崩了。

原因:温度只有 3 个单位,索引 5 越界。

解决:切换类别时重置:

this.fromIdx = 0
this.toIdx = Math.min(1, CATEGORIES[idx].units.length - 1)

第四个坑:极大极小值

1 毫米 = 0.000001 千米,这显示也太长了。

解决:科学计数法:

if (Math.abs(val) < 0.000001 && val !== 0) {
  return val.toExponential(4)
}

六、运行效果

在这里插入图片描述

七、总结

学到了啥

  1. 数据结构很重要:好的数据结构能让算法简单很多
  2. 边界情况要考虑:越界、精度、极大极小值,都是坑
  3. 实时交互体验好:输入即出结果,比点按钮爽

踩坑记录

表现 解决
温度换算 结果错误 特殊函数处理
浮点精度 小数位太长 toFixed + parseFloat
索引越界 切换崩溃 重置索引
极值显示 太长 科学计数法

代码量

主文件 Index.ets 约 200 行。对于这种完整应用,这量级刚刚好。


八、后续改进

  1. 更多单位:体积、速度、时间、货币
  2. 历史记录:保存最近使用的换算
  3. 复制功能:一键复制结果
  4. 暗黑模式:适配系统深色主题

开发时间:一个周末
代码行数:~200 行

做菜时再也不用到处搜换算了,挺有成就感的。有问题欢迎评论区交流!

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐