HarmonyOS ArkTS 单位换算器的计算逻辑与交互设计

发布时间:2026/7/20 19:24:50
HarmonyOS ArkTS 单位换算器的计算逻辑与交互设计 引子换算器看起来简单但有坑单位换算器是工具箱里最数学的工具——输入一个数选两个单位得到结果。逻辑清晰代码也不多。但做起来发现两个问题温度换算和其他单位的换算逻辑不一样数字格式化要考虑极端情况。完整效果数据模型Record interface 的组合interfaceUnitItem{name:string;// 单位名称factor:number;// 换算系数offset?:number;// 偏移量仅温度用}constCATEGORIES:Recordstring,UnitItem[]{长度:[{name:米 (m),factor:1},{name:千米 (km),factor:1000},{name:厘米 (cm),factor:0.01},// ...],重量:[...],温度:[...],面积:[...],数据:[...],};为什么用 Record 而不是 classRecordstring, UnitItem[]是 TypeScript 的工具类型表示键是字符串值是 UnitItem 数组。用它比定义一个Categoryclass 更简洁——不需要写构造函数不需要实例化。offset 字段的可选性offset?: number的?表示这个字段可选。温度换算需要偏移量比如摄氏度和华氏度之间有 32 的偏移但长度、重量等不需要。可选字段让接口更灵活。为什么温度的 factor 都是 1温度的三个单位摄氏度、华氏度、开尔文的 factor 都是 1因为温度换算不是简单的乘法。摄氏度转华氏度是C × 9/5 32有乘法有加法不能用单一 factor 表示。所以温度换算单独处理不用 factor 公式。换算算法两套逻辑convert():void{constvparseFloat(this.input);if(isNaN(v)){this.result—;return;}if(this.curCatTEMP_MODE){// 温度先转摄氏度再转目标单位letcv;constfromthis.units[this.fromIdx].name;consttothis.units[this.toIdx].name;if(from.includes(华氏))c(v-32)*5/9;elseif(from.includes(开尔文))cv-273.15;if(to.includes(华氏))cc*9/532;elseif(to.includes(开尔文))cc273.15;this.resultthis.fmt(c);}else{// 其他factor 公式constbasev*this.units[this.fromIdx].factor;this.resultthis.fmt(base/this.units[this.toIdx].factor);}}factor 公式的原理长度、重量、面积、数据的换算都基于基本单位结果 输入 × 源单位factor ÷ 目标单位factor比如 1 千米转厘米1 × 1000 (km→m) ÷ 0.01 (m→cm) 100000 cm每个单位的 factor 表示1 个该单位等于多少基本单位。通过先转基本单位再转目标单位实现了任意两个单位之间的换算。温度的特殊处理温度不能用 factor 公式因为摄氏度和华氏度之间有偏移°F °C × 9/5 32 °C (°F - 32) × 5/9 K °C 273.15代码的策略是先转摄氏度再转目标单位如果源单位是华氏度c (v - 32) × 5/9如果源单位是开尔文c v - 273.15如果目标单位是华氏度c c × 9/5 32如果目标单位是开尔文c c 273.15用name.includes(华氏)判断单位类型而不是用索引或枚举。这样即使以后加新的温度单位比如兰氏度只需要加对应的 if 分支。isNaN 的防御if(isNaN(v)){this.result—;return;}用户可能输入非数字比如空字符串、字母parseFloat会返回NaN。不处理的话后续计算会得到NaN显示出来很难看。提前判断显示 ‘—’ 作为占位符。数字格式化fmt 方法fmt(v:number):string{if(Math.abs(v)1e-9)return0;if(Math.abs(v)1e12||Math.abs(v)1e-9)returnv.toExponential(6);returnparseFloat(v.toFixed(8)).toString();}三种情况的处理接近零|v| 1e-9直接返回 ‘0’避免显示0.0000000001这种无意义的精度极端大或极端小用科学计数法toExponential(6)比如1e15或1e-12正常范围toFixed(8)保留 8 位小数再去掉末尾的零为什么用 1e-9 而不是 0浮点数计算有精度误差0.1 0.2不等于0.3而是0.30000000000000004。所以不能直接判断v 0要用Math.abs(v) 1e-9判断足够接近零。toFixed(8) 的选择8 位小数是精度和可读性的平衡。太少比如 2 位会丢失精度太多比如 15 位会让结果看起来很乱。去掉末尾的零parseFloat(v.toFixed(8))会去掉末尾的零。比如3.14000000会变成3.14100.00000000会变成100。状态管理六个 State 变量Statecats:string[]Object.keys(CATEGORIES);// 分类列表StatecurCat:string长度;// 当前分类Stateunits:UnitItem[]CATEGORIES[长度];// 当前单位列表StatefromIdx:number0;// 源单位索引StatetoIdx:number1;// 目标单位索引Stateinput:string1;// 输入值Stateresult:string;// 结果为什么 units 也要是 Stateunits是从CATEGORIES[curCat]取出来的。如果不设为 State切换分类时units不会更新UI 还是显示旧的单位列表。fromIdx 和 toIdx 的初始值初始值是 0 和 1第一个和第二个单位。切换分类时重置为 0 和 1确保不会出现索引越界的问题。交互逻辑三个核心方法swap交换源和目标swap():void{consttthis.fromIdx;this.fromIdxthis.toIdx;this.toIdxt;this.convert();}交换两个索引然后重新计算。用户可以快速反转换算方向比如从千米→米变成米→千米。selCat切换分类selCat(cat:string):void{this.curCatcat;this.unitsCATEGORIES[cat];this.fromIdx0;this.toIdx1;this.convert();}切换分类时需要更新curCat当前分类名更新units当前单位列表重置fromIdx和toIdx避免索引越界重新计算更新结果点击单位切换.onClick((){this.fromIdx(this.fromIdx1)%this.units.length;this.convert();})点击从区域索引循环加 1。比如当前是米点击后变成千米再点击变成厘米。用取余实现循环。UI 结构五个区域┌─────────────────────────────┐ │ ← 单位换算 │ ← 导航栏 ├─────────────────────────────┤ │ [长度] [重量] [温度] ... │ ← 分类标签 ├─────────────────────────────┤ │ 输入数值 │ │ ┌─────────────────────┐ │ │ │ 1 │ │ ← 输入框 │ └─────────────────────┘ │ ├─────────────────────────────┤ │ 米 (m) [切换] 千米(km)│ ← 单位选择 │ 从 到 │ ├─────────────────────────────┤ │ 1000 │ ← 结果 │ 千米 (km) │ └─────────────────────────────┘分类标签用ScrollRowForEach实现水平滚动的标签栏。选中的标签用绿色背景#34C759未选中的用深色背景#2A2A3E。输入框TextInput组件onChange事件实时触发换算。用户每输入一个字符结果都会更新。单位选择两个Column分别显示从和到的单位。点击整个区域会切换到下一个单位循环切换。中间的切换按钮交换两个单位。结果显示结果用 40px 的大字体显示绿色#34C759让用户一眼就能看到。maxLines(2)限制最多两行避免极端数字撑开布局。踩坑记录坑 1温度换算的顺序如果先判断目标单位再判断源单位可能会出错。比如华氏度→开尔文错误先转目标开尔文再转源华氏度 正确先转源华氏度→摄氏度再转目标摄氏度→开尔文必须先归一到摄氏度再从摄氏度转到目标。坑 2fromIdx 越界切换分类后如果fromIdx没有重置可能会超过新单位列表的长度。比如从数据6 个单位切到温度3 个单位fromIdx如果是 5就会越界。坑 3输入非数字用户可能输入abc或空字符串。parseFloat返回NaN后续计算会出错。必须提前判断。坑 4科学计数法的显示toExponential(6)会显示类似1.234567e12的格式。普通用户可能看不懂。如果要更友好的显示可以加千位分隔符。坑 5浮点数精度0.1 0.2 0.30000000000000004不是0.3。fmt方法用1e-9判断接近零避免了这个问题。代码改进建议1. 单位选择用 Picker当前点击区域循环切换单位用户不知道下一个是什么。可以用Picker组件让用户从列表中选择Picker({range:this.units.map(uu.name),selected:this.fromIdx}).onChange((idx:number){this.fromIdxidx;this.convert();})2. 历史记录保存最近的换算记录让用户可以快速查看。可以用 Preferences API 存储。3. 复制结果长按结果可以复制到剪贴板。可以用clipboard模块import{clipboard}fromkit.BasicServicesKit;clipboard.setData(this.result);4. 更多分类当前只有 5 个分类。可以扩展更多比如速度、压力、时间。5. 自定义单位让用户自己添加单位比如公司内部的换算比例。总结单位换算器的核心是两套算法——长度、重量等用 factor 公式温度用偏移公式。数据模型用Recordstring, UnitItem[]组织UI 用State驱动更新。数字格式化处理了极端情况交互逻辑覆盖了切换、交换、输入三个场景。适用边界这个部分适合用作 ArkUI 数据驱动计算的学习案例涵盖了 Record 类型、可选字段、条件分支、数字格式化、实时计算等核心知识点。但如果要上架应用商店还需要补充 Picker 组件、历史记录、复制功能、更多分类、自定义单位等内容。说白了换算器就是把公式套对。factor 对了温度的特殊情况处理了结果就对了。