HarmonyOS ArkTS 实战:实现一个全能单位换算应用
HarmonyOS ArkTS 实战:实现一个全能单位换算应用
项目效果
本文实现的是一个基于 HarmonyOS 和 ArkTS 的全能单位换算应用。项目中使用 ArkUI 组件完成页面布局,通过 @State 管理状态数据,实现长度、重量、温度、面积、体积等多种单位换算,实时计算结果,分类切换和双向输入等功能。
最终运行效果如下:
页面主要包含以下内容:
- 顶部应用标题;
- 单位分类标签页;
- 输入数值区域;
- 源单位选择;
- 目标单位选择;
- 实时换算结果;
- 交换单位按钮;
- 常用单位快捷选择;
- 页面整体采用 ArkUI 声明式布局。
本文重点是演示如何在 HarmonyOS 项目中使用 ArkTS 和 ArkUI 实现一个实用工具类单页面应用。项目代码主要写在 entry/src/main/ets/pages/Index.ets 文件中,适合作为 HarmonyOS ArkTS 入门到进阶之间的练习案例。
前言
在日常生活和工作中,我们经常需要进行各种单位换算,比如长度、重量、温度、货币等。手动计算既麻烦又容易出错,一个好用的单位换算工具可以快速准确地完成各种换算,提高效率。
从应用开发角度来看,这个项目不依赖后端接口,也不需要数据库,但能练习 ArkTS 中的状态管理、标签页切换、输入框绑定、实时计算、下拉选择和数学运算等内容。
本文基于 HarmonyOS 和 ArkTS 实现一个全能单位换算应用。用户可以选择不同的单位分类,输入数值,选择源单位和目标单位,实时看到换算结果,还可以快速交换单位。
这个项目的核心不是简单的数学计算,而是灵活的单位系统和良好的用户体验。每一次分类切换、数值输入和单位选择,本质上都是对换算状态的管理。状态变化后,页面会自动刷新,这正是 ArkUI 声明式开发的基本思想。
一、项目目标
本次实践主要实现以下目标:
- 创建 HarmonyOS ArkTS 页面;
- 使用
@Entry和@Component定义页面组件; - 使用
@State管理页面状态; - 使用标签页切换不同单位分类;
- 支持长度、重量、温度、面积、体积五种分类;
- 实现实时换算,输入即出结果;
- 支持源单位和目标单位选择;
- 一键交换单位;
- 数字键盘友好的输入;
- 常用单位快捷显示;
- 使用
@Builder封装分类标签和单位选择器; - 完成一个可以运行的单位换算页面。
这个项目虽然是单页面应用,但它支持多种单位类型,有完整的输入、选择、换算和交换功能,比普通静态页面更适合练习 ArkTS。
二、技术栈
| 类型 | 内容 |
|---|---|
| 开发方向 | HarmonyOS 应用开发 |
| 开发语言 | ArkTS |
| UI 框架 | ArkUI |
| SDK 版本 | HarmonyOS API 23 及以上 |
| 工程模型 | Stage 模型 |
| 核心组件 | Text / TextInput / Button / Tabs / TabContent / Column / Row / Select |
| 状态管理 | @State |
| 数据处理 | 单位换算 / 数学计算 / 实时响应 |
| 项目入口 | entry/src/main/ets/pages/Index.ets |
| 运行平台 | 模拟器或真机 |
本项目是 HarmonyOS 原生 ArkTS 项目。页面主体由 ArkUI 组件构建,核心逻辑写在 Index.ets 文件中,不依赖后端接口,也不需要额外配置数据库。
三、为什么选择单位换算项目
全能单位换算应用适合作为 ArkTS 练习项目,主要有以下几个原因。
第一,实用性强。单位换算是非常高频的需求,无论是学习、工作还是生活都会用到。
第二,业务场景真实。烹饪、购物、旅行、学习等场景都需要进行单位换算,是非常实用的工具。
第三,适合练习状态管理。当前分类、输入值、源单位、目标单位都属于页面状态。状态变化后,页面会自动刷新。
第四,适合练习标签页组件。Tabs组件是常用的导航组件,掌握它可以应用到很多应用中。
第五,适合练习实时计算。输入变化时立即计算结果,提供流畅的用户体验。
第六,扩展空间比较大。基础功能完成后,可以继续增加货币换算(实时汇率)、时间换算、速度换算、数据存储换算和自定义单位。
在本项目中,单位配置和换算状态是核心数据。页面中的结果显示和单位选择都由它们计算或渲染得到。
四、功能规则说明
支持的单位分类:
| 分类 | 包含单位 |
|---|---|
| 长度 | 毫米、厘米、米、千米、英寸、英尺、英里 |
| 重量 | 毫克、克、千克、吨、盎司、磅、斤 |
| 温度 | 摄氏度、华氏度、开尔文 |
| 面积 | 平方毫米、平方厘米、平方米、平方千米、公顷、亩、平方英尺 |
| 体积 | 毫升、升、立方米、立方厘米、加仑、盎司 |
换算规则:
- 以一个基准单位为中介进行换算
- 温度使用特殊公式换算
- 输入数值变化时立即重新计算
- 支持小数输入
- 结果保留4位小数
交换功能:点击交换按钮可以快速交换源单位和目标单位,同时重新计算结果。
五、项目结构
本项目主要修改首页文件:
entry
└── src
└── main
└── ets
└── pages
└── Index.ets
其中:
| 文件 | 作用 |
|---|---|
| Index.ets | 编写页面结构、状态数据和单位换算逻辑 |
本文不涉及复杂路由,也不需要额外创建多个页面。对于练习项目来说,把主要逻辑集中在一个 Index.ets 文件中更方便理解。
如果后续继续扩展,可以考虑把换算卡片、单位选择器和分类标签拆分成独立组件。
六、核心实现思路
本项目的核心流程如下:
- 定义各单位到基准单位的换算系数;
- 使用
@State保存当前分类、输入值、源单位索引、目标单位索引; - 用户切换分类标签;
- 用户输入数值;
- 用户选择源单位和目标单位;
- 将输入值从源单位换算到基准单位;
- 再从基准单位换算到目标单位;
- 实时显示换算结果;
- 点击交换按钮交换源单位和目标单位;
- 温度使用专门的换算公式。
项目中最重要的状态变量如下:
@State currentCategory: number = 0
@State inputValue: string = '1'
@State fromUnit: number = 2
@State toUnit: number = 1
其中:
| 状态变量 | 作用 |
|---|---|
| currentCategory | 当前选中的分类索引 |
| inputValue | 输入的数值 |
| fromUnit | 源单位索引 |
| toUnit | 目标单位索引 |
单位换算系数是本项目中最重要的数据,所有换算都基于这些系数进行。
七、Index.ets 完整代码
打开文件:
entry/src/main/ets/pages/Index.ets
将其中内容替换为下面代码:
@Entry
@Component
struct Index {
@State currentCategory: number = 0
@State inputValue: string = '1'
@State fromUnit: number = 2
@State toUnit: number = 1
private categories: string[] = ['长度', '重量', '温度', '面积', '体积']
private units: string[][] = [
['毫米', '厘米', '米', '千米', '英寸', '英尺', '英里'],
['毫克', '克', '千克', '吨', '盎司', '磅', '斤'],
['摄氏度', '华氏度', '开尔文'],
['平方毫米', '平方厘米', '平方米', '平方千米', '公顷', '亩', '平方英尺'],
['毫升', '升', '立方米', '立方厘米', '加仑', '液量盎司']
]
private toBase: number[][] = [
[0.001, 0.01, 1, 1000, 0.0254, 0.3048, 1609.344],
[0.000001, 0.001, 1, 1000, 0.0283495, 0.453592, 0.5],
[0, 0, 0],
[0.000001, 0.0001, 1, 1000000, 10000, 666.667, 0.092903],
[0.001, 1, 1000, 0.001, 3.78541, 0.0295735]
]
private convert(value: number, from: number, to: number, category: number): number {
if (category === 2) {
return this.convertTemperature(value, from, to)
}
let baseValue = value * this.toBase[category][from]
return baseValue / this.toBase[category][to]
}
private convertTemperature(value: number, from: number, to: number): number {
let celsius: number
if (from === 0) celsius = value
else if (from === 1) celsius = (value - 32) * 5 / 9
else celsius = value - 273.15
if (to === 0) return celsius
else if (to === 1) return celsius * 9 / 5 + 32
else return celsius + 273.15
}
private getResult(): string {
let num = parseFloat(this.inputValue)
if (isNaN(num)) return '0'
let result = this.convert(num, this.fromUnit, this.toUnit, this.currentCategory)
return result.toFixed(4).replace(/\.?0+$/, '')
}
private swapUnits(): void {
let temp = this.fromUnit
this.fromUnit = this.toUnit
this.toUnit = temp
}
private onCategoryChange(index: number): void {
this.currentCategory = index
this.fromUnit = 0
this.toUnit = 1
}
@Builder
UnitSelector(selected: number, onChange: (index: number) => void) {
Select([...this.units[this.currentCategory].map((u, i) => ({ value: u, id: i }))])
.selected(selected)
.value(this.units[this.currentCategory][selected])
.font({ size: 16, weight: FontWeight.Medium })
.fontColor('#182431')
.selectedOptionFont({ size: 16, weight: FontWeight.Medium })
.optionFont({ size: 16 })
.backgroundColor('#F5F7FA')
.height(50)
.borderRadius(12)
.padding({ left: 16, right: 16 })
.onSelect((index: number) => {
onChange(index)
})
}
@Builder
QuickUnitButton(text: string, unitIndex: number, isFrom: boolean) {
let selected = isFrom ? this.fromUnit : this.toUnit
Button(text)
.height(36)
.padding({ left: 12, right: 12 })
.fontSize(13)
.fontColor(selected === unitIndex ? Color.White : '#4B5563')
.backgroundColor(selected === unitIndex ? '#0A59F7' : '#F3F4F6')
.borderRadius(18)
.onClick(() => {
if (isFrom) {
this.fromUnit = unitIndex
} else {
this.toUnit = unitIndex
}
})
}
build() {
Column() {
Text('单位换算')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.fontColor('#182431')
.margin({ top: 22 })
Text('基于 HarmonyOS ArkTS 的全能换算工具')
.fontSize(14)
.fontColor('#6B7280')
.margin({ top: 8, bottom: 20 })
Tabs({ barPosition: BarPosition.Start }) {
ForEach(this.categories, (category: string, index: number) => {
TabContent() {
Column() {
Column() {
Text('输入数值')
.fontSize(14)
.fontColor('#6B7280')
.width('100%')
.margin({ bottom: 8 })
TextInput({
text: this.inputValue,
placeholder: '请输入数值'
})
.height(56)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#182431')
.backgroundColor('#F5F7FA')
.borderRadius(12)
.padding({ left: 16, right: 16 })
.type(InputType.Number)
.onChange((value: string) => {
this.inputValue = value
})
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(16)
.margin({ bottom: 16 })
.shadow({
radius: 8,
color: '#08000000',
offsetX: 0,
offsetY: 2
})
Column() {
Text('从')
.fontSize(14)
.fontColor('#6B7280')
.width('100%')
.margin({ bottom: 8 })
this.UnitSelector(this.fromUnit, (i: number) => { this.fromUnit = i })
Row() {
this.QuickUnitButton(this.units[this.currentCategory][0], 0, true)
Blank().width(8)
this.QuickUnitButton(this.units[this.currentCategory][1], 1, true)
Blank().width(8)
this.QuickUnitButton(this.units[this.currentCategory][2], 2, true)
}
.width('100%')
.margin({ top: 12 })
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(16)
.margin({ bottom: 16 })
.shadow({
radius: 8,
color: '#08000000',
offsetX: 0,
offsetY: 2
})
Button('⇅ 交换单位')
.width('100%')
.height(44)
.fontSize(15)
.fontColor('#0A59F7')
.backgroundColor('#EFF6FF')
.borderRadius(22)
.margin({ bottom: 16 })
.onClick(() => {
this.swapUnits()
})
Column() {
Text('到')
.fontSize(14)
.fontColor('#6B7280')
.width('100%')
.margin({ bottom: 8 })
this.UnitSelector(this.toUnit, (i: number) => { this.toUnit = i })
Row() {
this.QuickUnitButton(this.units[this.currentCategory][0], 0, false)
Blank().width(8)
this.QuickUnitButton(this.units[this.currentCategory][1], 1, false)
Blank().width(8)
this.QuickUnitButton(this.units[this.currentCategory][2], 2, false)
}
.width('100%')
.margin({ top: 12 })
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(16)
.margin({ bottom: 16 })
.shadow({
radius: 8,
color: '#08000000',
offsetX: 0,
offsetY: 2
})
Column() {
Text('换算结果')
.fontSize(14)
.fontColor('#6B7280')
.width('100%')
.margin({ bottom: 8 })
Row() {
Text(this.getResult())
.fontSize(36)
.fontWeight(FontWeight.Bold)
.fontColor('#0A59F7')
.layoutWeight(1)
Text(this.units[this.currentCategory][this.toUnit])
.fontSize(18)
.fontColor('#6B7280')
.margin({ top: 12 })
}
.width('100%')
.alignItems(VerticalAlign.Bottom)
}
.width('100%')
.padding(20)
.backgroundColor(Color.White)
.borderRadius(16)
.shadow({
radius: 8,
color: '#08000000',
offsetX: 0,
offsetY: 2
})
}
.width('100%')
.height('100%')
}
.tabBar(this.categories[index])
}, (c: string) => c)
}
.barMode(BarMode.Fixed)
.barWidth('100%')
.barHeight(50)
.animationDuration(300)
.onChange((index: number) => {
this.onCategoryChange(index)
})
.layoutWeight(1)
}
.width('100%')
.height('100%')
.padding({ left: 18, right: 18 })
.backgroundColor('#F5F7FA')
}
}
八、代码实现说明
1. 单位换算系统
使用基准单位法进行换算:先将源单位换算到基准单位,再换算到目标单位:
let baseValue = value * this.toBase[category][from]
return baseValue / this.toBase[category][to]
温度使用专门的换算公式,因为温度不是线性比例换算。
2. 标签页切换
使用Tabs组件实现分类切换,切换分类时重置单位选择:
private onCategoryChange(index: number): void {
this.currentCategory = index
this.fromUnit = 0
this.toUnit = 1
}
3. 实时换算
输入值变化时自动重新计算结果,不需要点击按钮:
private getResult(): string {
let num = parseFloat(this.inputValue)
if (isNaN(num)) return '0'
let result = this.convert(num, this.fromUnit, this.toUnit, this.currentCategory)
return result.toFixed(4).replace(/\.?0+$/, '')
}
结果自动去除末尾多余的零,显示更美观。
4. 单位选择器
使用Select组件实现单位下拉选择,同时提供常用单位的快捷按钮,方便快速选择。
5. 交换单位
一键交换源单位和目标单位,方便反向换算:
private swapUnits(): void {
let temp = this.fromUnit
this.fromUnit = this.toUnit
this.toUnit = temp
}
九、运行项目
代码编写完成后,在DevEco Studio中运行项目。测试不同分类的单位换算,检查结果是否准确,测试交换单位功能,确保实时计算正常工作。
十、开发中遇到的问题
- 温度换算特殊处理,不能使用简单的乘法系数
- 结果格式化,去除末尾多余的零
- 分类切换时重置单位选择,避免索引越界
- 数字输入限制,只允许输入数字和小数点
十一、总结
本文基于HarmonyOS和ArkTS实现了一个全能单位换算应用。项目通过@State管理换算状态,使用Tabs组件实现分类切换,实现了多种单位的实时换算、单位选择和交换功能。这个项目展示了ArkTS中表单输入、实时计算和组件使用的基本方法,适合作为入门练习项目。
更多推荐

所有评论(0)