鸿蒙开发日记:撸了个单位换算器,顺便把温度换算搞明白了

缘起

上周末做饭,菜谱上写的是"盎司",我家厨房秤只显示"克"。掏出手机现找换算工具,心想:这玩意儿我也能做啊!

正好最近在学鸿蒙开发,就当练手项目了。说干就干,一个周末下来,还真让我给撸出来了。


第一天:想清楚要做什么

功能定位

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

  • 长度:mm、cm、m、km、英寸、英尺
  • 重量:mg、g、kg、吨、磅、盎司
  • 温度:摄氏、华氏、开尔文
  • 面积:mm²、cm²、m²、km²、公顷、英亩

这些基本覆盖日常需求了。

UI 怎么设计?

参考了几个主流换算App,发现大家都用类似的布局:

  1. 顶部类别切换(Tab)
  2. 输入区域(选单位 + 输数值)
  3. 输出区域(选单位 + 看结果)
  4. 交换按钮

简洁明了,我也这么干。


第二天:数据结构,磨刀不误砍柴工

单位怎么定义?

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

interface UnitInfo {
  label: string    // 显示名称
  toBase: number   // 换算到基准单位的系数
}

系数是啥意思?

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

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

这样换算起来就简单了:

输入值 × toBase = 基准单位值
基准单位值 ÷ 目标toBase = 结果

类别怎么定义?

interface Category {
  name: string
  icon: string
  units: UnitInfo[]
  convertFn?: (val, fromIdx, toIdx) => number  // 特殊换算函数
}

为什么需要 convertFn

因为温度不一样!摄氏、华氏、开尔文之间不是简单的乘除关系,需要特殊公式。这个后面细说。

完整数据

const CATEGORIES = [
  {
    name: '长度', icon: '📏',
    units: [
      { label: '毫米 (mm)', toBase: 0.001 },
      { label: '厘米 (cm)', toBase: 0.01 },
      { label: '米 (m)', toBase: 1 },
      { label: '千米 (km)', toBase: 1000 },
      { label: '英寸 (in)', toBase: 0.0254 },
      { label: '英尺 (ft)', toBase: 0.3048 },
    ]
  },
  // ... 其他类别
]

第三天:换算算法,基准单位法

普通单位的换算

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

convert(): void {
  const num = parseFloat(this.inputValue)
  const cat = CATEGORIES[this.activeCategory]
  
  // 输入值 → 基准单位
  const base = num * cat.units[this.fromIdx].toBase
  
  // 基准单位 → 目标单位
  const result = base / cat.units[this.toIdx].toBase
  
  this.resultValue = this.formatResult(result)
}

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

  1. 输入:5
  2. 转基准:5 × 1000 = 5000
  3. 转目标:5000 ÷ 1 = 5000

完美!

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

我一开始把温度也用系数法定义了,结果测试时发现:100°C 换算成华氏度,结果是 100,而不是 212。

问题在哪?

温度换算不是线性的!摄氏度和华氏度的关系是:

  • °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
}

第四天:UI 布局,卡片式设计

整体结构

Column
├── 标题
├── 类别Tab(Row + ForEach)
├── 换算卡片(Column)
│   ├── 输入区
│   ├── 交换按钮
│   └── 输出区
└── 底部提示

类别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')
    .onClick(() => { this.switchCategory(idx) })
  })
}
.backgroundColor('#F0F2F5')
.borderRadius(12)

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

单位选择器

这个有点意思,我做成了横向滚动的标签:

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

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

输入框

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

亮点:每次输入都触发换算,不需要点"计算"按钮,输入即出结果。

交换按钮

中间放个按钮,点击互换源和目标单位:

Button() { Text('⇅').fontSize(24) }
  .onClick(() => {
    const tmp = this.fromIdx
    this.fromIdx = this.toIdx
    this.toIdx = tmp
    this.convert()
  })

第五天:细节优化,处理边界情况

第二个坑:浮点数精度

测试时输入 0.1 + 0.2,结果显示 0.30000000000000004

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

解决:用 toFixed 限制精度,再去掉多余的零:

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

第三个坑:单位索引越界

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

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

解决:切换类别时重置索引:

switchCategory(idx) {
  this.activeCategory = idx
  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)
}
if (Math.abs(val) > 999999999) {
  return val.toExponential(4)
}

第六天:测试,各种边界情况

在这里插入图片描述


总结

学到了什么

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

踩坑总结

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

代码量

最后统计了一下,主文件 Index.ets 约 200 行。对于一个完整的应用来说,这个量级很合适——既不复杂,又有技术含量。


开发时间:2天
核心代码:~200行

做菜时再也不用到处找换算工具了,挺有成就感的!

Logo

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

更多推荐