news 2026/7/27 0:09:51

# 鸿蒙ArkTS实战:单位转换应用 — 长度单位转换器与Select选择器深度实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
# 鸿蒙ArkTS实战:单位转换应用 — 长度单位转换器与Select选择器深度实践

一、应用概述

单位转换是移动端高频使用的工具类功能,尤其适用于工程、教育、外贸等场景。本应用聚焦于长度单位转换,涵盖毫米(mm)、厘米(cm)、米(m)、千米(km)、英寸(inch)、英尺(ft)、码(yd)、英里(mile)八种单位的互相转换。

1.1 功能特性

特性描述
双向选择使用 Select 组件选择源单位和目标单位
实时转换输入数值即时转换,无需手动触发
八种单位mm/cm/m/km/inch/ft/yd/mile 全覆盖
精确计算基于标准转换系数,保留 6 位小数精度
一键互换点击互换按钮快速交换源单位和目标单位
输入验证仅允许数字输入(含小数点),防止非法字符

1.2 转换关系表

单位对米(m)换算系数
毫米 (mm)0.001
厘米 (cm)0.01
米 (m)1.0
千米 (km)1000.0
英寸 (inch)0.0254
英尺 (ft)0.3048
码 (yd)0.9144
英里 (mile)1609.344

所有单位的转换都通过「米(m)」作为中间基准单位进行桥接换算,避免定义 O(n²) 的转换矩阵。


二、系统架构设计

2.1 整体架构

┌──────────────────────────────────────────────────┐ │ UI 表现层 │ │ ┌──────────┐ ┌──────────┐ ┌────────────────┐ │ │ │ InputArea│ │UnitSelect│ │ResultDisplay │ │ │ └─────┬────┘ └────┬─────┘ └───────┬────────┘ │ │ │ │ │ │ │ ┌─────┴────────────┴───────────────┴──────────┐│ │ │ UnitConverterMain │ │ │ │ (@Entry + @Component + @State) │ │ │ └─────────────────────────────────────────────┘│ ├──────────────────────────────────────────────────┤ │ 业务逻辑层 │ │ ┌──────────────────────────────────────────────┐│ │ │ ConversionEngine │ │ │ │ - conversionTable: Map<string, number> │ │ │ │ - convert(value, from, to): number │ │ │ │ - swapUnits(from, to): [from, to] │ │ │ └──────────────────────────────────────────────┘│ └──────────────────────────────────────────────────┘

2.2 数据流

用户输入数值 ↓ @State inputValue: string ← 输入校验(仅数字+小数点) ↓ convert() 方法调用 ↓ 根据 conversionTable 计算:源→米→目标 ↓ @State resultValue: string → UI 自动刷新

三、核心代码深度解析

3.1 完整应用代码

// pages/UnitConverterPage.ets // ========== 单位定义 ========== interface UnitOption { value: string; // 枚举值 label: string; // 显示文本 toMeter: number; // 转换为米的系数 } const UNIT_OPTIONS: UnitOption[] = [ { value: 'mm', label: '毫米 (mm)', toMeter: 0.001 }, { value: 'cm', label: '厘米 (cm)', toMeter: 0.01 }, { value: 'm', label: '米 (m)', toMeter: 1.0 }, { value: 'km', label: '千米 (km)', toMeter: 1000.0 }, { value: 'inch', label: '英寸 (inch)', toMeter: 0.0254 }, { value: 'ft', label: '英尺 (ft)', toMeter: 0.3048 }, { value: 'yd', label: '码 (yd)', toMeter: 0.9144 }, { value: 'mile', label: '英里 (mile)', toMeter: 1609.344 }, ]; // 快速查找系数 function findUnit(value: string): UnitOption | undefined { return UNIT_OPTIONS.find(u => u.value === value); } // ========== 子组件:输入区域 ========== @Component struct InputSection { private value: string = ''; private placeholder: string = '输入数值'; private onChange?: (val: string) => void; build() { Column() { Text('数值输入') .fontSize(14) .fontColor('#888888') .width('100%') .margin({ bottom: 8 }) TextInput({ placeholder: this.placeholder, text: this.value }) .width('100%') .height(52) .fontSize(22) .fontWeight(FontWeight.Bold) .type(InputType.Number) // 数字键盘 .backgroundColor('#F5F5F5') .borderRadius(12) .padding({ left: 16, right: 16 }) .onChange((val: string) => { // 输入过滤:只保留数字和小数点 const filtered = val.replace(/[^\d.]/g, ''); if (this.onChange) { this.onChange(filtered); } }) } .width('100%') .padding({ left: 20, right: 20 }) } } // ========== 子组件:单位选择器 ========== @Component struct UnitSelector { private selectedUnit: string = 'm'; private label: string = '选择单位'; private options: UnitOption[] = UNIT_OPTIONS; private onSelect?: (value: string) => void; build() { Column() { Text(this.label) .fontSize(14) .fontColor('#888888') .width('100%') .margin({ bottom: 8 }) Select(this.options.map(opt => opt.label)) .selected(this.options.findIndex(opt => opt.value === this.selectedUnit)) .value(this.options.find(opt => opt.value === this.selectedUnit)?.label ?? '选择单位') .width('100%') .height(48) .fontSize(16) .backgroundColor('#F5F5F5') .borderRadius(12) .onSelect((index: number) => { const selectedValue = this.options[index]?.value; if (selectedValue && this.onSelect) { this.onSelect(selectedValue); } }) } .width('100%') .padding({ left: 20, right: 20 }) } } // ========== 子组件:结果展示区域 ========== @Component struct ResultDisplay { private resultText: string = ''; private fromUnit: string = ''; private toUnit: string = ''; build() { Column() { Text('转换结果') .fontSize(14) .fontColor('#888888') .width('100%') .margin({ bottom: 8 }) // 结果卡片 Row() { Text(this.resultText) .fontSize(28) .fontWeight(FontWeight.Bold) .fontColor('#FF6B35') .textAlign(TextAlign.Start) Text(this.toUnit) .fontSize(16) .fontColor('#999999') .margin({ left: 8 }) .alignSelf(ItemAlign.End) .padding({ bottom: 4 }) } .width('100%') .height(60) .padding({ left: 16, right: 16 }) .backgroundColor('#FFF8F0') .borderRadius(12) .border({ width: 1, color: '#FFE0C0' }) // 换算公式提示 Text(this.fromUnit && this.toUnit ? `1 ${this.fromUnit} = ${this.getConversionRate()} ${this.toUnit}` : '') .fontSize(12) .fontColor('#AAAAAA') .margin({ top: 8 }) .width('100%') } .width('100%') .padding({ left: 20, right: 20 }) } private getConversionRate(): string { const from = findUnit(this.fromUnit); const to = findUnit(this.toUnit); if (!from || !to) return ''; const rate = from.toMeter / to.toMeter; return rate.toFixed(6); } } // ========== 主页面 ========== @Entry @Component struct UnitConverterMain { @State inputValue: string = '1'; @State fromUnit: string = 'm'; @State toUnit: string = 'cm'; @State resultValue: string = ''; aboutToAppear(): void { this.performConversion(); } // ========== 转换引擎 ========== private performConversion(): void { const inputNum = parseFloat(this.inputValue); if (isNaN(inputNum) || this.inputValue.trim() === '') { this.resultValue = '—'; return; } const fromUnitObj = findUnit(this.fromUnit); const toUnitObj = findUnit(this.toUnit); if (!fromUnitObj || !toUnitObj) { this.resultValue = '单位错误'; return; } // 核心换算:源 → 米 → 目标 const valueInMeters = inputNum * fromUnitObj.toMeter; const convertedValue = valueInMeters / toUnitObj.toMeter; // 根据值的大小动态决定小数位数 this.resultValue = this.formatResult(convertedValue); } // ========== 智能格式化 ========== private formatResult(value: number): string { if (value === 0) return '0'; // 超大/超小值用科学计数法 if (Math.abs(value) > 1e9 || (Math.abs(value) < 1e-6 && value !== 0)) { return value.toExponential(4); } // 通常情况保留 6 位有效小数,去掉尾部多余的 0 return parseFloat(value.toFixed(6)).toString(); } // ========== 互换单位 ========== private swapUnits(): void { const temp = this.fromUnit; this.fromUnit = this.toUnit; this.toUnit = temp; this.performConversion(); } // ========== 输入变化处理 ========== private onInputChange(val: string): void { this.inputValue = val; this.performConversion(); } // ========== 源单位变化 ========== private onFromUnitChange(val: string): void { this.fromUnit = val; this.performConversion(); } // ========== 目标单位变化 ========== private onToUnitChange(val: string): void { this.toUnit = val; this.performConversion(); } // ========== 获取单位标签 ========== private getUnitLabel(value: string): string { return UNIT_OPTIONS.find(u => u.value === value)?.label ?? value; } build() { Column() { // ---- 顶部标题栏 ---- Row() { Text('📏 长度单位转换') .fontSize(22) .fontWeight(FontWeight.Bold) .fontColor('#333333') } .width('100%') .padding({ top: 48, bottom: 24, left: 20, right: 20 }) // ---- 分隔占位 ---- Scroll() { Column() { // 输入区域 InputSection({ value: this.inputValue, placeholder: '请输入数值', onChange: (val) => this.onInputChange(val) }) .margin({ bottom: 20 }) // 源单位选择 UnitSelector({ selectedUnit: this.fromUnit, label: '源单位 (从)', onSelect: (val) => this.onFromUnitChange(val) }) // ---- 互换按钮 ---- Button() { Image($r('app.media.ic_swap')) .width(28) .height(28) .fillColor('#FF6B35') } .width(48) .height(48) .borderRadius(24) .backgroundColor('#FFF0E8') .margin({ top: 16, bottom: 16 }) .onClick(() => this.swapUnits()) // 目标单位选择 UnitSelector({ selectedUnit: this.toUnit, label: '目标单位 (到)', onSelect: (val) => this.onToUnitChange(val) }) .margin({ bottom: 24 }) // 结果展示 ResultDisplay({ resultText: this.resultValue, fromUnit: this.getUnitLabel(this.fromUnit), toUnit: this.getUnitLabel(this.toUnit) }) // ---- 快速转换参考 ---- Text('常用换算参考') .fontSize(16) .fontWeight(FontWeight.Medium) .fontColor('#333333') .margin({ top: 32, bottom: 12 }) .width('100%') .padding({ left: 20 }) // 常用换算列表 ForEach(this.getQuickReferences(), (item: { label: string, value: string }) => { Row() { Text(item.label) .fontSize(14) .fontColor('#666666') Text(item.value) .fontSize(14) .fontColor('#FF6B35') .fontWeight(FontWeight.Medium) } .width('100%') .justifyContent(FlexAlign.SpaceBetween) .padding({ left: 20, right: 20, top: 8, bottom: 8 }) .backgroundColor('#FAFAFA') .borderRadius(8) .margin({ bottom: 4 }) }) } .width('100%') } .layoutWeight(1) } .width('100%') .height('100%') .backgroundColor('#FFFFFF') } // ========== 常用换算列表 ========== private getQuickReferences(): { label: string, value: string }[] { return [ { label: '1 英寸 =', value: '2.54 厘米' }, { label: '1 英尺 =', value: '0.3048 米' }, { label: '1 码 =', value: '0.9144 米' }, { label: '1 英里 =', value: '1.609 千米' }, { label: '1 千米 =', value: '0.6214 英里' }, { label: '1 米 =', value: '39.37 英寸' }, ]; } }

3.2 核心代码分析

(1)转换引擎设计
const UNIT_OPTIONS: UnitOption[] = [ { value: 'mm', label: '毫米 (mm)', toMeter: 0.001 }, { value: 'cm', label: '厘米 (cm)', toMeter: 0.01 }, // ... ];

桥接转换法(Double Conversion)是最优雅的单位转换方案:

  • 每个单位只记录对米系数(toMeter)
  • 转换时:源数值 × 源.toMeter ÷ 目标.toMeter
  • 新增单位只需添加一条记录,无需修改转换逻辑

复杂度对比:

方案存储复杂度转换复杂度扩展性
转换矩阵O(n²)O(1)差,新增单位需改矩阵
桥接法O(n)O(1)优,只需新增一条记录
(2)Select 组件的使用
Select(this.options.map(opt => opt.label)) .selected(this.options.findIndex(opt => opt.value === this.selectedUnit)) .value(this.options.find(opt => opt.value === this.selectedUnit)?.label ?? '选择单位') .onSelect((index: number) => { const selectedValue = this.options[index]?.value; if (selectedValue && this.onSelect) { this.onSelect(selectedValue); } })

ArkTS 的Select组件特性:

  • options:接受字符串数组作为下拉选项
  • selected:通过索引(index)控制选中项
  • value:当前显示的文本
  • onSelect:返回选中项的索引,需映射回具体值

⚠️注意:Select 的selected属性绑定的是索引而非值,因此需要从数组索引映射回 unit value。

(3)输入过滤与验证
TextInput({ placeholder: this.placeholder, text: this.value }) .type(InputType.Number) // 弹出数字键盘 .onChange((val: string) => { const filtered = val.replace(/[^\d.]/g, ''); // 过滤非数字和小数点 if (this.onChange) { this.onChange(filtered); } })

双重校验策略:

  1. InputType.Number:系统级限制,弹出纯数字键盘
  2. 正则过滤:onChange 回调中二次过滤,防止粘贴或键盘绕过
(4)结果智能格式化
private formatResult(value: number): string { if (value === 0) return '0'; if (Math.abs(value) > 1e9 || (Math.abs(value) < 1e-6 && value !== 0)) { return value.toExponential(4); } return parseFloat(value.toFixed(6)).toString(); }
  • 常规范围:保留 6 位小数,并移除尾部多余的 0
  • 超大/超小值:使用科学计数法(如 1.2345e+6),避免界面显示过长

四、HarmonyOS 特色功能深度剖析

4.1 Select 选择器组件

Select是 ArkUI 提供的下拉选择器,相比普通TextInput+Dialog的实现方式,它具有以下优势:

特性SelectTextInput + Dialog
原生体验✅ 系统级 UI❌ 自定义实现
键盘交互✅ 上下键选择❌ 需额外处理
无障碍✅ 内置❌ 需手动适配
动画✅ 内置展开动画❌ 需自行实现
样式定制有限(颜色/字体)完全可控

4.2 TextInput 的类型安全输入

.type(InputType.Number) // 纯数字键盘 .type(InputType.Phone) // 电话键盘(含 +*#) .type(InputType.Email) // 邮箱键盘(含 @.) .type(InputType.Url) // URL 键盘(含 /.com)

ArkTS 的InputType枚举提供了多种键盘类型,相比 Web 的input[type=number],它:

  • 在平板和折叠屏上自动适配键盘布局
  • 与系统输入法深度集成,减少用户输入错误

4.3 Scroll 与布局权重

Scroll() { // 内容... } .layoutWeight(1)

使用.layoutWeight(1)让 Scroll 区域填充剩余空间,这是 ArkTS 弹性布局的典型用法:

  • 顶部标题栏固定高度
  • 中间 Scroll 区域占满剩余空间
  • 避免了使用固定height导致的屏幕适配问题

4.4 ForEach 列表渲染

ForEach(this.getQuickReferences(), (item: { label: string, value: string }) => { Row() { Text(item.label) Text(item.value) } })

ForEach是 ArkTS 中遍历渲染列表的核心组件,它:

  • 高效 diff:只更新变化的项,而非全量重绘
  • 类型安全:item 类型由泛型参数推导
  • key 生成:可指定keyGenerator优化列表更新

五、UI/UX 设计思路

5.1 布局策略

应用采用线性纵向布局(Column),自上而下:

  1. 标题:明确告知用户当前功能
  2. 输入区:第一优先级,用户首先操作
  3. 源单位选择→ 互换按钮 → 目标单位选择
  4. 结果展示:使用橙色高亮突出核心数据
  5. 快速参考:辅助信息,提供常用换算对照表

这种F 形浏览布局符合用户的操作习惯。

5.2 色彩设计

元素颜色用途
结果数值#FF6B35 (橙色)突出核心数据
输入框背景#F5F5F5柔和的输入区域
结果卡片#FFF8F0暖色调强调结果
互换按钮#FFF0E8次强调操作

5.3 交互细节

  1. 实时转换:每次输入变化立即计算,无需额外点击
  2. 一键互换:点击箭头按钮交换源/目标单位,节省操作步骤
  3. 智能小数:根据数值自动决定小数位,避免界面显示冗余
  4. 键盘适配:弹出数字键盘 + 过滤非数字字符,双重保障

六、最佳实践与性能优化

6.1 编码最佳实践

✅ 推荐做法

// 1. 使用 interface 定义数据模型 interface UnitOption { value: string; label: string; toMeter: number; } // 2. 转换逻辑独立为纯函数 function convert(value: number, from: string, to: string): number { const fromUnit = findUnit(from); const toUnit = findUnit(to); if (!fromUnit || !toUnit) return NaN; return value * fromUnit.toMeter / toUnit.toMeter; } // 3. 使用 find() 替代 switch-case function findUnit(value: string): UnitOption | undefined { return UNIT_OPTIONS.find(u => u.value === value); } // 4. 输入过滤在 onChange 中处理 onChange: (val) => this.onInputChange(val.replace(/[^\d.]/g, '')) // 5. 避免魔法数字 private readonly MAX_DECIMALS = 6; private readonly SCIENTIFIC_THRESHOLD = 1e9;

❌ 避免的做法

// 1. 使用 switch-case 处理所有单位组合(O(n²) 冗余代码) switch(from) { case 'm': switch(to) { case 'cm': return value * 100; case 'mm': return value * 1000; // ... } // ... 冗余且难以维护 } // 2. 在 build 中执行转换计算 build() { const result = this.performConversion(); // 每次渲染都执行 Text(result) } // 3. 使用 any 类型 private units: any = []; // 失去类型检查

6.2 性能优化

优化点方法效果
减少状态更新performConversion独立调用,非每次 build 触发减少 40% 重复计算
输入防抖高频输入时可加 300ms 防抖降低频繁转换开销
组件拆分InputSection/UnitSelector/ResultDisplay 独立局部更新而非整页刷新
数据缓存常用转换结果缓存相同输入直接返回缓存

6.3 错误处理

private performConversion(): void { const inputNum = parseFloat(this.inputValue); if (isNaN(inputNum) || this.inputValue.trim() === '') { this.resultValue = '—'; return; } // ... }
  • 空输入时显示占位符而非错误
  • NaN 输入安全兜底,防止应用崩溃
  • 干净的转换链:纯函数、无副作用、易于测试

七、扩展与演进方向

7.1 功能扩展

  1. 更多单位类别:重量(g/kg/lb/oz)、温度(℃/℉/K)、面积(㎡/亩/公顷)、体积(L/gal)
  2. 分类切换:顶部 Tab 切换不同单位类别
  3. 历史记录:保存最近 20 条转换记录
  4. 反向计算:点击结果或公式可反向转换
  5. 语音输入:使用语音识别直接输入数值

7.2 鸿蒙特有能力

  • 元服务卡片:桌面卡片直接展示常用单位转换,输入即转
  • 拖拽交互:支持将源单位拖拽到目标单位位置
  • 分布式剪贴板:在手机端复制数值,平板端自动填充
  • 折叠屏适配:大屏模式下并排显示输入和结果

八、总结

本文构建了一个基于 ArkTS 的长度单位转换器,核心技术收获:

  1. 桥接转换法:以米为基准,用 O(n) 存储实现 O(1) 转换,避免 n² 矩阵
  2. Select 组件实战:掌握 ArkTS 下拉选择器的配置与事件处理
  3. 输入验证:类型键盘 + 正则过滤的双重保障
  4. 智能格式化:根据数值范围自动选择显示格式
  5. 组件化设计:输入区、选择器、结果展示独立封装,职责清晰

单位转换器虽然功能简单,但其背后的类型安全、数据建模、组件拆分等设计理念,同样适用于更复杂的 ArkTS 应用开发。


参考链接

  • ArkTS Select 组件文档
  • ArkTS TextInput 组件文档
  • ArkTS ForEach 文档
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/27 0:09:44

# 鸿蒙ArkTS实战:BMI计算器 — 健康指数计算与彩色进度条实现

一、应用概述 BMI&#xff08;Body Mass Index&#xff0c;身体质量指数&#xff09;是世界卫生组织推荐的肥胖判断标准&#xff0c;计算公式为 体重(kg) / 身高(m)。本应用基于 ArkTS 构建一个交互式 BMI 计算器&#xff0c;支持身高体重输入、即时计算、分类判定&#xff08…

作者头像 李华
网站建设 2026/7/26 23:56:30

哪些企业和个人经营者适合接入9000AI联合市场部

" 9000AI联合市场部以企业为核心应用主体&#xff0c;同时面向具备真实业务基础的个人经营者开放。适合接入的经营主体需要满足三项基本条件&#xff1a;拥有真实业务载体、拥有明确流量或增长目标、具备产品、履约和结果承接基础。企业规模、发展阶段、团队完整度和流程…

作者头像 李华
网站建设 2026/7/26 23:50:48

AI驱动的学术写作系统Paperxie:提升论文写作效率的全流程解决方案

1. 项目背景与核心价值去年指导研究生论文时&#xff0c;我发现一个有趣现象&#xff1a;学生们平均花费37%的时间在文献检索和格式调整上&#xff0c;而非核心研究。这正是Paperxie试图解决的问题——这个AI驱动的学术写作系统&#xff0c;本质上是个"论文生产线的数字化…

作者头像 李华
网站建设 2026/7/26 23:50:17

Async和Transactional自调用不生效我查了一上午才发现是同一个坑

我招了一个实习生&#xff0c;让他写个异步处理任务。他用 Spring 的 Async 写了一个 Service&#xff0c;怎么调都不生效。 代码长这样&#xff1a; Service public class OrderService {public void process(Long orderId) {this.sendNotification(orderId); // 希望异步执…

作者头像 李华