一、引言单位换算是每个人都需要的工具。美国人用英里和英尺欧洲人用公里和米厨师用盎司和磅健身者用千克和克科学家用开尔文和摄氏度普通人用华氏度和摄氏度。这些单位系统各自存在了数百年的历史原因已经不可能在全球范围内统一。从技术角度看单位换算的核心挑战有三个第一不同类别的单位有不同的换算规则。长度和重量是线性换算——只要乘以一个系数即可如 1 千克 1000 克。温度是非线性换算——摄氏度转华氏度的公式是°F °C × 9/5 32包含了乘法和加法不能简单地乘以一个系数。第二单位在不同屏幕上的呈现。4 个单位按钮排成一行在小屏幕上可能放不下需要支持自动折行。而 3 个分类标签需要水平排列空间富裕时用 Row窄屏时需要考虑适配。第三输入和输出的双向性。用户可能输入任意一端的值需要实时计算另一端的值。而且用户可能随时切换哪一端是输入需要一个快捷的交换操作。本文将用 ArkUI 从零构建一个单位换算器支持三类单位长度 / 重量 / 温度每类 3-4 种单位实时双向换算一键交换输入和输出。阅读完本文你将能够设计分层单位换算架构分类层 单位层 换算函数层使用基准单位模式统一处理同类别的线性换算处理温度的非线性换算公式而非系数实现输入和输出的双向实时计算用Flex({ wrap: FlexWrap.Wrap })构建自适应的单位选择按钮组二、数据模型与换算架构2.1 分类与单位的分层定义换算器的数据结构分为两层分类Category包含多个单位UnitinterfaceUnitDef{symbol:string;// 单位符号如 kg, °Fname:string;// 中文名称如 千克, 华氏度}interfaceCategoryDef{name:string;// 分类名如 长度icon:string;// 图标如 units:UnitDef[];// 该分类下的单位列表}三层嵌套结构的根是分类数组constCATEGORIES:CategoryDef[][{name:长度,icon:,units:[{symbol:m,name:米},{symbol:km,name:千米},{symbol:mi,name:英里},{symbol:ft,name:英尺},],},{name:重量,icon:⚖️,units:[{symbol:kg,name:千克},{symbol:g,name:克},{symbol:lb,name:磅},{symbol:oz,name:盎司},],},{name:温度,icon:️,units:[{symbol:°C,name:摄氏度},{symbol:°F,name:华氏度},{symbol:K,name:开尔文},],},];分类用 icon name 的组合呈现为可点击的标签单位用 symbol 呈现为可选的按钮。symbol 而非 name 作为按钮文字有两个原因第一symbol 更短如 “km” vs “千米”在手机上更容易在一行内放下多个按钮第二熟悉国际单位制的用户目标用户群的主要部分看到 symbol 能比中文名更快地定位。2.2 基准单位换算模式对于长度和重量的线性换算我们使用基准单位模式选每个类别中的一个单位作为基准长度选米重量选克将任意单位的值转换为基准单位再从基准单位转换为目标单位输入值 → toBase() → 基准值 → fromBase() → 输出值长度类所有单位先转换为米再从米转换为目标单位functiontoBase(value:number,unit:string,catIndex:number):number{if(catIndex0){// 长度 → 米if(unitkm)returnvalue*1000;if(unitmi)returnvalue*1609.344;if(unitft)returnvalue*0.3048;returnvalue;}// ...}重量类所有单位先转换为克再从克转换为目标单位if(catIndex1){// 重量 → 克if(unitkg)returnvalue*1000;if(unitlb)returnvalue*453.592;if(unitoz)returnvalue*28.3495;returnvalue;}注意磅和盎司的系数不是整数——453.592克每磅28.3495克每盎司。这些是国际标准转换系数精确到足够日常使用。不要简化成 454 或 28因为累积误差在多次换算后会变得明显。基准单位模式的好处是扩展性——如果要新增一个单位比如长度类增加英寸只需在toBase和fromBase中各加一个if分支不需要修改任何其他逻辑。2.3 温度的非线性换算温度不能用简单的系数乘法因为三个温标有不同的零点。摄氏度、华氏度、开尔文之间的转换公式如下°C → °F: F C × 9/5 32 °F → °C: C (F - 32) × 5/9 °C → K: K C 273.15 K → °C: C K - 273.15温度的处理方式与长度/重量不同——先用专门的转换函数转到摄氏度作为温度体系的基准再从摄氏度转到目标单位functiontoCelsius(value:number,unit:string):number{if(unit°F)return(value-32)*5/9;if(unitK)returnvalue-273.15;returnvalue;// 已经是 °C}functionfromCelsius(celsius:number,unit:string):number{if(unit°F)returncelsius*9/532;if(unitK)returncelsius273.15;returncelsius;}虽然温度的逻辑与长度/重量不同但它们共享相同的接口——toBase()和fromBase()函数在catIndex 2温度时调用toCelsius和fromCelsius对外暴露了一致的调用方式。2.4 统一的 convert 函数有了toBase和fromBase换算过程变为清晰的两次调用functionconvert(value:number,from:string,to:string,catIndex:number):number{constbasetoBase(value,from,catIndex);returnfromBase(base,to,catIndex);}这是整个换算系统的核心——一行代码完成任何同类单位之间的换算。from和to可以是相同类别中的任意两个单位包括同单位同单位换算结果等于原值catIndex告知系统使用哪套换算规则。三、双向实时计算3.1 输入驱动的计算换算器采用输入驱动模式——用户修改输入值时结果自动更新TextInput({text:this.inputText,placeholder:输入数值}).type(InputType.Number).onChange((value:string){this.inputTextvalue;this.updateResult();})onChange回调中的updateResult()是关键——每次输入变化都触发重新计算。这比输入完后点换算按钮的模式更即时用户可以连续输入多位数字看到结果随之变化。3.2 updateResult 的逻辑updateResult():void{constvalparseFloat(this.inputText);if(isNaN(val)||this.inputText.trim()){this.resultText—;return;}constrconvert(val,this.fromUnit,this.toUnit,this.catIndex);this.resultTextformatResult(r);}三种情况输入为空或非数字显示—表示等待有效输入有效输入调用convert()→formatResult()→ 更新resultText源单位或目标单位变化同样调用updateResult()在单位按钮的onClick中触发结果区域使用Text而非TextInput展示确保用户不能编辑计算结果——它只反映输入 → 换算的输出。3.3 单位交换用户点击中间的交换按钮⇅时输入和输出两端交换swapUnits():void{consttmpthis.fromUnit;this.fromUnitthis.toUnit;this.toUnittmp;constvalparseFloat(this.inputText);if(!isNaN(val)){constrconvert(val,this.fromUnit,this.toUnit,this.catIndex);this.inputTextformatResult(r);}this.updateResult();}交换的不仅是单位标签还包括数值——输入框的值被替换为当前换算结果的数值。例如用户输入 “1 km → 0.621 mi”点击交换后变为 “0.621 mi → 1 km”。这保持了换算的一致性——交换前后表示的是同一段距离只是视角反过来了。3.4 结果格式化浮点运算会产生类似0.621371192这样的长尾数。formatResult()负责将其格式化为可读的字符串functionformatResult(n:number):string{if(isNaN(n))return—;if(!isFinite(n))return—;constabsMath.abs(n);if(abs0)return0;if(abs0.000001||abs1000000)returnn.toExponential(6);if(abs1)returnn.toFixed(6).replace(/0$/,).replace(/\.$/,);returnn.toFixed(4).replace(/0$/,).replace(/\.$/,);}五个段落处理不同的数值范围范围策略示例NaN / Infinity返回 “—”无效输入0直接返回 “0”0 ≠ 0.0000 0.000001科学计数法6 位有效数字4.2e-7 1小数点后 6 位去掉末尾零0.5而非0.500000≥ 1 且 1000000小数点后 4 位去掉末尾零3.1416≥ 1000000科学计数法1.23e6replace(/0$/, ).replace(/\.$/, )去掉小数点后的末尾零和可能残留的小数点。例如1.5000→1.52.0000→2。科学计数法toExponential(6)用于极小或极大的数值保持显示屏上内容的可读性——6.022e23比602200000000000000000000好读一千倍。四、UI 设计4.1 整体布局ConverterPage ├── 深色标题栏 单位换算 ├── 分类标签行长度 | 重量 | 温度圆角胶囊按钮 ├── 输入卡片白色圆角 │ ├── 输入 标签 │ ├── TextInput28sp 加粗数字键盘 │ └── 源单位选择Flex wrap 按钮组 ├── 交换按钮⇅圆形居中带阴影 ├── 结果卡片白色圆角 │ ├── 结果 标签 │ ├── Text 显示区28sp 加粗只读 │ └── 目标单位选择Flex wrap 按钮组4.2 分类标签的设计三个分类标签使用圆角胶囊BorderRadius.FULL样式当前选中的分类用蓝色填充背景 蓝色文字选中态蓝色浅底 蓝色文字 加粗 未选中态灰色浅底 灰色文字 常规分类切换时单位会被重置为该分类的第一个和最后一个单位如米和英尺输入值重置为 1switchCategory(idx:number):void{this.catIndexidx;this.fromUnitCATEGORIES[idx].units[0].symbol;this.toUnitCATEGORIES[idx].units[CATEGORIES[idx].units.length-1].symbol;this.inputText1;this.updateResult();}选择第一个和最后一个单位作为初始的源/目标单位是为了给用户展示一个有意义的换算而非 “1 m 1 m”。例如切换到重量时看到 “1 kg 35.274 oz”立刻就知道换算功能在工作。4.3 单位按钮的双色系统源单位和目标单位使用不同的选中颜色——这是本文在视觉设计上的一个重要决策源单位选中蓝色实心 白色文字#1677FF目标单位选中绿色实心 白色文字#52C41A蓝 vs 绿的区分让用户在界面上能立即分辨出哪边是输入哪边是输出。在交换操作时两种颜色对调视觉上强化了方向变了的信号。如果源和目标单位使用相同的选中色用户容易混淆当前的操作方向。源单位蓝色和文本输入框在同一个白色卡片中目标单位绿色和结果显示在另一个白色卡片中。两个卡片形成对称的结构视觉上暗示了输入 → 输出的方向性。4.4 Flex wrap 适配窄屏单位按钮使用Flex({ wrap: FlexWrap.Wrap })让按钮在屏幕宽度不够时自动折行Flex({wrap:FlexWrap.Wrap}){ForEach(CATEGORIES[this.catIndex].units,(unit:UnitDef){Text(unit.symbol).fontSize(FontSize.BODY).fontColor(this.fromUnitunit.symbol?#FFFFFF:#888899).fontWeight(FontWeight.Bold).padding({left:14,right:14,top:8,bottom:8}).borderRadius(BorderRadius.FULL).backgroundColor(this.fromUnitunit.symbol?#1677FF:#F0F0F5).margin({right:Spacing.SM,bottom:Spacing.SM}).onClick((){this.fromUnitunit.symbol;this.updateResult();})})}.width(100%)四个关键设计细节.margin({ bottom: Spacing.SM })提供折行后的行间距。没有这个 margin 的话折到下一行的按钮会和上一行紧贴。Flex而非Row——Row不支持 wrap按钮会溢出屏幕不可见。每个单位按钮使用BorderRadius.FULL完全圆角与分类标签的圆角胶囊样式保持一致。未选中按钮灰色#F0F0F5选中按钮根据位置不同使用蓝色或绿色——这是单位换算器最独特的视觉元素。五、完整代码结构ConverterPage ├── Column根布局 │ ├── Row标题栏 单位换算 │ ├── Row分类标签长度 | ⚖️重量 | ️温度 │ ├── Column输入卡片 │ │ ├── Text输入标签 │ │ ├── TextInput数值输入数字键盘 │ │ └── Flex wrap源单位按钮组选中蓝色 │ ├── Row交换按钮 ⇅居中带阴影 │ ├── Column结果卡片 │ │ ├── Text结果标签 │ │ ├── Text结果展示只读加粗 │ │ └── Flex wrap目标单位按钮组选中绿色 │ └── 空白区域layoutWeight 填充 └── 全局函数 ├── toBase() — 任意单位 → 基准单位 ├── fromBase() — 基准单位 → 任意单位 ├── convert() — 完整换算toBase fromBase ├── toCelsius() / fromCelsius() — 温度特殊处理 └── formatResult() — 结果格式化六、总结本文从零构建了一个单位换算器。与前七篇的应用不同单位换算器的核心在于分层换算架构 双向实时计算 非线性单位的特殊处理。核心要点回顾分层数据模型分类层CategoryDef定义单位组和换算规则单位层UnitDef定义每个单位的符号和名称。三层嵌套数分类数组 → 单位数组 → 单位对象清晰表达了数据的层级关系。基准单位模式所有同类单位先通过toBase()转换到基准单位长度→米重量→克温度→摄氏度再通过fromBase()从基准单位转换到目标单位。新增单位只需在toBase和fromBase中各加一个分支其他逻辑不受影响。温度的非线性处理°F C × 9/5 32包含乘法和位移不能简化为系数乘法。toCelsius()和fromCelsius()专门处理这三个温标之间的转换通过catIndex与线性换算共享同一调用接口。输入驱动的实时计算TextInput.onChange→updateResult()→convert()→formatResult()→ 显示结果。用户每输入一个数字结果即时更新。交换按钮双向互换单位和数值保持换算一致性。源/目标双色区分源单位选中蓝色#1677FF目标单位选中绿色#52C41A。颜色差异让用户立即分辨输入和输出交换操作时颜色对调提供视觉反馈。这是本文在 UI 设计上最重要的创新——用颜色编码来区分信息流向。结果格式化的数值范围策略0 直接输出、小数去掉末尾零、极小/极大值用科学计数法。replace(/0$/, ).replace(/\.$/, )是高性价比的显示优化——一行正则免去了手动判断小数位的麻烦。单位换算器是一个看起来简单但设计细节丰富的工具——分类管理、换算公式、输入处理、结果格式化每个环节都有多个边界情况需要处理。它是实用型 App 的典型代表功能不花哨但每个细节都影响用户的实际使用体验。