【细胞工坊|09】HarmonyOS ArkTS 公式与单位换算实战:处理生物实验计算的精度和异常输入
部分内容由AI辅助生成。
本文面向 HarmonyOS 5.0 及以上版本,基于 细胞工坊 项目真实源码展开,源码根目录为 D:\huawei\one14-9。本文重点复核两个页面:
entry/src/main/ets/views/learning/FormulaPage.etsentry/src/main/ets/views/learning/UnitConverterPage.ets
这两个页面看起来都属于“学习工具”,但工程职责并不相同。FormulaPage 是公式说明页,负责把显微放大、菌落增长、DNA 析出、病毒感染、遗传比例等公式按分类展示出来;UnitConverterPage 是样本换算页,负责体积、质量、时间、浓度、温度和细胞数的本地换算,并对非法输入做兜底。
这篇文章只讨论源码已经实现的能力:公式列表、分类筛选、单位组切换、数值输入、普通因子换算、温度换算、结果格式化、非法输入显示 —。源码中没有表达式解析器、历史记录、远程公式库、收藏、学习进度同步或任意精度小数库,文章不会把这些能力写成已实现。

1. 为什么实验学习工具不能只做一张公式表
在生物实验学习类 HarmonyOS 应用里,公式和单位换算经常被放在同一个入口下。产品层面看,它们都是“工具”;工程层面看,它们有明显边界。
公式页的核心问题是“如何让学生快速找到某类实验原理”。它的输入是分类点击,输出是一组固定知识卡片。换算页的核心问题是“如何把用户输入的一个数值转换成目标单位”。它的输入来自 TextInput,结果随状态实时变化,必须处理非法输入和显示精度。
如果把这两类能力混在一个页面里,常见问题会很快出现:
| 问题 | 在页面里的表现 | 更稳的边界 |
|---|---|---|
| 公式只是说明,却被当成可计算表达式 | 用户以为 M = 物镜 × 目镜 能输入参数计算 |
FormulaPage 只展示说明,不承诺计算 |
| 单位换算没有非法输入兜底 | 输入中文、空字符串、符号后页面显示 NaN |
UnitConverterPage 用 isNaN 返回占位符 |
| 小数位过多 | 0.00100000000002 之类结果污染 UI |
普通单位使用 toFixed(6) 后去尾零 |
| 温度和比例因子混算 | 摄氏度、华氏度、开尔文转换错误 | 温度走独立 convertTemperature() |
当前源码的处理方式是把“说明型公式”和“计算型换算”拆成两个页面。这个拆法朴素,但很实用:公式页没有输入状态,换算页没有公式解释负担,两边的 UI 和错误处理都更清晰。
2. 源码结构:两个页面共用主题,但业务状态分离
先看两个页面共同依赖的内容。它们都导入 router、AppColors、AppFonts 和 Constants:
import { router } from '@kit.ArkUI'
import { AppColors, AppFonts } from '../../common/Theme'
import { Constants } from '../../common/Constants'
这说明页面没有单独发明一套颜色、字体和间距,而是复用项目主题常量。对 AppGallery 审核和后续维护来说,这一点很重要:工具页如果出现独立的深色背景、按钮色或字号,很容易造成阅读页、实验页、学习页之间的视觉割裂。
两个页面也都声明了系统栏相关状态:
@StorageProp('statusBarHeight') statusBarHeight: number = 36
@StorageProp('bottomBarHeight') bottomBarHeight: number = 0
这两个值最终用于页面根容器:
.padding({ top: this.statusBarHeight, bottom: this.bottomBarHeight })
这里的工程意图很直接:顶部导航不压到状态栏,底部内容不贴到系统导航区域。对工具页来说,输入框、单位按钮和结果卡片都属于高频交互控件,底部安全区如果没处理,横屏、小窗或带手势导航的设备上容易出现最后一行控件难点、误触或遮挡。
3. FormulaPage:公式页只负责“分类展示”,不承担计算
FormulaPage 定义了一个局部接口 FormulaItem:
interface FormulaItem {
name: string
formula: string
description: string
category: string
}
四个字段分别对应卡片标题、公式正文、解释说明和分类。当前页面没有把公式拆成可执行表达式,也没有保存参数名、参数单位、输入控件或计算函数。因此它的正确定位是“实验原理公式索引”,不是“公式计算器”。
页面状态也很克制:
@State selectedCategory: number = 0
private categories: string[] = ['全部', '基础', '培养', 'DNA', '病毒', '遗传']
@State formulas: FormulaItem[] = [
{ name: '显微放大倍数', formula: 'M = 物镜 × 目镜', description: '总放大倍数由物镜和目镜共同决定。', category: '基础' },
{ name: '视野亮度', formula: 'B ∝ 光强 / M', description: '放大倍数越高,通常需要更强照明。', category: '基础' },
{ name: '菌落增长趋势', formula: 'N(t) ≈ N₀ × 2ⁿ', description: '条件适宜时,细菌会按世代倍增。', category: '培养' },
{ name: '污染风险', formula: 'R ∝ 暴露时间 × 环境菌量', description: '开盖时间越长,污染风险越高。', category: '培养' }
]
源码里实际数组还包含 DNA、病毒、遗传等分类公式。这里截取部分代码,是为了说明结构:页面把分类和公式数据都放在本地,页面渲染不依赖网络请求,也没有异步加载状态。
这种写法适合当前应用场景:公式数量有限、内容稳定、学习页入口清晰。如果未来要做远程公式库或动态编辑,就需要把 formulas 从页面状态移到 service/repository 层,并增加加载、失败、空态和版本更新逻辑。但当前源码没有这部分,不能把它写成已上线能力。
4. 分类筛选:用索引驱动状态,避免字符串状态到处散落
公式页的筛选函数很短:
private filteredFormulas(): FormulaItem[] {
const cat = this.categories[this.selectedCategory]
if (cat === '全部') {
return this.formulas
}
return this.formulas.filter(f => f.category === cat)
}
这段逻辑有三个明确边界。
第一,页面状态保存的是 selectedCategory: number,也就是分类数组的下标。渲染分类按钮时,ForEach 里的 index 会和这个状态比较,决定高亮样式。
第二,真正筛选时再用下标取分类名称。这样 UI 选中态和筛选逻辑共享同一个状态源,不需要再维护一个 selectedCategoryName。状态少一份,手动同步错误就少一类。
第三,全部 是特殊分类。它不需要额外判断公式数据,只要直接返回完整数组即可。其他分类则走 filter。
从工程角度看,这个函数适合目前的静态列表。如果分类名称来自接口,或者公式数据可能为空,就要补充越界保护:
private safeFilteredFormulas(): FormulaItem[] {
const cat = this.categories[this.selectedCategory] ?? '全部'
if (cat === '全部') {
return this.formulas
}
return this.formulas.filter((item: FormulaItem) => item.category === cat)
}
当前源码没有越界场景,因为分类按钮和下标都来自同一个 categories 数组;但这类保护在后续动态化时值得提前考虑。
5. 公式卡片:公式文本要比普通说明更突出
FormulaPage 的列表结构使用 List 和 ListItem:
List({ space: 10 }) {
ForEach(this.filteredFormulas(), (item: FormulaItem) => {
ListItem() {
Column() {
Text(item.name)
.fontSize(AppFonts.BODY_SIZE)
.fontWeight(AppFonts.WEIGHT_MEDIUM)
.fontColor(AppColors.TEXT_PRIMARY)
Text(item.formula)
.fontSize(22)
.fontWeight(AppFonts.WEIGHT_BOLD)
.fontColor(AppColors.ACCENT_GREEN)
.margin({ top: 8 })
Text(item.description)
.fontSize(AppFonts.CAPTION_SIZE)
.fontColor(AppColors.TEXT_SECONDARY)
.margin({ top: 6 })
Text(item.category)
.fontSize(11)
.fontColor(AppColors.PRIMARY)
.backgroundColor(AppColors.PRIMARY_BG)
.borderRadius(4)
.padding({ left: 6, right: 6, top: 2, bottom: 2 })
.margin({ top: 8 })
}
}
})
}
公式文本用了更大的字号、加粗和绿色强调色,这符合学习工具的阅读优先级。用户进入页面后,真正需要扫到的是 M = 物镜 × 目镜、F ∝ r × rpm²、Aa × Aa → 3:1 这类表达式,而不是先读一大段说明。
这里有一个实际审核风险需要注意:公式中包含上标、希腊字母、箭头、比例符号、微升符号等字符,例如 rpm²、μL、→。这些字符在 ArkTS 字符串中可以正常展示,但在不同字体、不同系统版本和截图压缩后,清晰度可能不一致。发布材料截图时应实际检查公式卡片,不要只看预览里的普通中文。
6. UnitConverterPage:把换算模型压缩成 UnitGroup
单位换算页定义了 UnitGroup:
interface UnitGroup {
name: string
units: string[]
factors: number[]
}
每个单位组有一个名称、一组单位名称和对应的换算因子。当前源码包含:
private unitGroups: UnitGroup[] = [
{ name: '体积', units: ['升(L)', '毫升(mL)', '微升(μL)'], factors: [1, 0.001, 0.000001] },
{ name: '质量', units: ['克(g)', '毫克(mg)', '微克(μg)', '千克(kg)'], factors: [1, 0.001, 0.000001, 1000] },
{ name: '时间', units: ['秒(s)', '分钟(min)', '小时(h)', '毫秒(ms)'], factors: [1, 60, 3600, 0.001] },
{ name: '浓度', units: ['mol/L', 'mmol/L', 'μmol/L'], factors: [1, 0.001, 0.000001] },
{ name: '温度', units: ['摄氏度(°C)', '华氏度(°F)', '开尔文(K)'], factors: [1, 1, 1] },
{ name: '细胞数', units: ['个', '千个', '万个', '百万个'], factors: [1, 1000, 10000, 1000000] }
]
普通单位的换算因子都指向一个组内基准单位。以体积为例,升(L) 是基准,毫升(mL) 的因子是 0.001,微升(μL) 的因子是 0.000001。从任意单位换到任意单位,都可以先换到基准,再除以目标单位因子。
这个模型的优点是实现简单,UI 也容易渲染。缺点也很明确:它只适合线性比例单位。温度不是比例换算,所以源码把温度作为特殊分支处理,factors 在温度组里只是占位。
7. 状态设计:单位组、输入值、源单位和目标单位分开保存
换算页的状态如下:
@State selectedGroup: number = 0
@State inputValue: string = '1'
@State fromUnit: number = 0
@State toUnit: number = 1
这四个状态分别回答四个问题:
| 状态 | 含义 | 为什么不合并 |
|---|---|---|
selectedGroup |
当前单位组 | 切换体积、质量、温度时需要刷新单位列表 |
inputValue |
输入框原始字符串 | 非法输入也要保留在输入框里,不能直接存 number |
fromUnit |
源单位下标 | 和源单位按钮高亮绑定 |
toUnit |
目标单位下标 | 和目标单位按钮高亮绑定 |
inputValue 使用 string 是一个正确选择。用户正在输入时,字符串可能是空、-、.、abc 或中文。如果一开始就强制转成 number,输入框体验会很差,也很容易把非法状态吞掉。当前源码选择在计算时 parseFloat,这让输入层和计算层边界更清晰。
8. 单位组切换:重置 from/to,避免跨组下标错位
单位组按钮的点击逻辑是:
.onClick(() => {
this.selectedGroup = index
this.fromUnit = 0
this.toUnit = 1
})
这段看起来很小,但它避免了一个常见错误。不同单位组的单位数量不同,例如质量有 4 个单位,浓度只有 3 个单位。如果用户先在质量组选择了第 3 个目标单位,再切换到浓度组,旧下标可能还在范围内,也可能越界;即使不越界,也会指向完全不同的单位。
源码在切换组时把源单位重置到第 0 个,把目标单位重置到第 1 个。这样 UI 总是从该组的前两个单位开始,避免跨组残留状态影响换算。
如果后续要做更细的体验,可以在切换组时判断单位数量:
private resetUnitsForGroup(index: number): void {
this.selectedGroup = index
this.fromUnit = 0
this.toUnit = this.unitGroups[index].units.length > 1 ? 1 : 0
}
当前源码的每个单位组都至少有 3 个单位,所以固定 toUnit = 1 是可行的。

9. convertValue:非法输入先兜底,再进入换算逻辑
换算核心在 convertValue():
private convertValue(): string {
const val = parseFloat(this.inputValue)
if (isNaN(val)) return '—'
const group = this.unitGroups[this.selectedGroup]
if (group.name === '温度') {
return this.convertTemperature(val).toFixed(4)
}
const fromFactor = group.factors[this.fromUnit]
const toFactor = group.factors[this.toUnit]
const result = val * fromFactor / toFactor
return result.toFixed(6).replace(/\.?0+$/, '')
}
这里的第一道门是:
const val = parseFloat(this.inputValue)
if (isNaN(val)) return '—'
用户输入无法解析成数字时,页面不抛异常,也不把 NaN 直接显示出来,而是返回一个占位符 —。这对学习工具很重要:学生可能会复制带单位的文本,也可能误输入中文。页面应保持稳定,让用户知道当前值不可换算,而不是让结果区域出现难懂的 JavaScript 结果。
需要注意的是,parseFloat 的行为不是严格数值校验。比如 parseFloat('12abc') 会得到 12,parseFloat('1.2.3') 会得到 1.2。当前源码接受这种宽松解析。文章不能把它写成“严格输入校验”。如果产品希望更严格,需要改成正则校验或 Number() 加完整字符串检查。
一个更严格的版本可以这样写:
private parseStrictNumber(text: string): number | undefined {
const normalized = text.trim()
if (!/^-?\d+(\.\d+)?$/.test(normalized)) {
return undefined
}
const value = Number(normalized)
return Number.isFinite(value) ? value : undefined
}
这是扩展建议,不是当前源码能力。当前页面的已实现能力是 parseFloat 加 isNaN 兜底。
10. 普通单位换算:因子模型让公式统一
普通单位组的计算只有一行:
const result = val * fromFactor / toFactor
以体积为例:
升(L)因子是1毫升(mL)因子是0.001微升(μL)因子是0.000001
如果输入 1,从 毫升(mL) 换到 微升(μL):
result = 1 * 0.001 / 0.000001 = 1000
如果从 微升(μL) 换到 毫升(mL):
result = 1 * 0.000001 / 0.001 = 0.001
这套模型同样适用于质量、时间、浓度和细胞数。换算逻辑不关心单位文字,只关心因子数组。UI 展示单位名称,计算读取同一索引下的因子,两者通过数组下标绑定。
这里的维护风险是 units 和 factors 必须长度一致,而且顺序必须一致。当前源码由开发者手写数组,数量不大,风险可控。若后续单位组增多,建议在初始化时做一次自检:
private isValidUnitGroup(group: UnitGroup): boolean {
return group.units.length === group.factors.length && group.units.length > 0
}
如果这个校验不通过,就不要渲染该单位组,避免点击后出现 undefined 参与计算。
11. 结果精度:普通单位保留 6 位再去掉尾零
普通单位的输出处理是:
return result.toFixed(6).replace(/\.?0+$/, '')
这里分两步。
第一步,toFixed(6) 把结果固定到 6 位小数。这样可以压住浮点运算带来的长尾,比如某些乘除组合可能产生很多小数位。
第二步,正则去掉末尾多余的零。如果结果是 1000.000000,最终显示 1000;如果结果是 0.001000,最终显示 0.001。
这个处理很适合学习型换算页。用户需要的是可读的实验量级,而不是 IEEE 754 浮点细节。它不是任意精度计算,也不适合严肃实验记录的审计级数值。如果要处理科研级精度,需要引入小数库或把数值存成整数基准单位后再格式化。
当前源码没有引入第三方精度库,所以文章只把它定义为“显示精度控制”,而不是“高精度计算”。
12. 温度换算:不能用比例因子,必须单独处理
温度组在 convertValue() 中走特殊分支:
if (group.name === '温度') {
return this.convertTemperature(val).toFixed(4)
}
真正转换在 convertTemperature():
private convertTemperature(val: number): number {
const fromIdx = this.fromUnit
const toIdx = this.toUnit
let celsius: number = val
if (fromIdx === 1) celsius = (val - 32) * 5 / 9
else if (fromIdx === 2) celsius = val - 273.15
if (toIdx === 0) return celsius
if (toIdx === 1) return celsius * 9 / 5 + 32
return celsius + 273.15
}
这段逻辑先把源单位转换成摄氏度,再从摄氏度转换到目标单位。索引约定来自温度组:
{ name: '温度', units: ['摄氏度(°C)', '华氏度(°F)', '开尔文(K)'], factors: [1, 1, 1] }
索引 0 是摄氏度,1 是华氏度,2 是开尔文。这样的写法很短,但依赖数组顺序。如果将来调整温度单位顺序,就必须同步修改 convertTemperature()。更稳的做法是给单位加稳定 id:
interface UnitOption {
id: string
label: string
factor?: number
}
然后温度转换按 id 判断,而不是按下标判断。当前源码采用下标判断,数量小、可读性高,但后续扩展时要小心。
13. TextInput:保留原始输入,结果区实时响应
输入框代码如下:
TextInput({ text: this.inputValue, placeholder: '输入数值' })
.fontSize(24)
.fontWeight(AppFonts.WEIGHT_BOLD)
.layoutWeight(1)
.backgroundColor('transparent')
.onChange((value: string) => { this.inputValue = value })
这里没有直接限制键盘类型,也没有正则拦截输入。用户每次输入变化,inputValue 更新,结果区里的 Text(this.convertValue()) 会随着状态重新计算。
这种实现成本低,交互也直接。缺点是输入阶段不会阻止非法字符,只在结果区显示 —。对学习工具来说,这个容错方式通常比强制弹错误提示更轻。它让用户可以继续编辑,不打断输入。
如果产品希望减少非法输入,可以进一步增加输入类型或校验提示。但要避免两个极端:
- 不要在用户刚输入
.或-时立即清空输入框; - 不要用弹窗打断每一次非法字符输入。
当前源码选择的是轻量兜底,适合这个工具页。
14. 单位按钮:源单位和目标单位样式区分
换算页有两组单位按钮,一组用于“从”,一组用于“到”。源单位选中态用 AppColors.PRIMARY,目标单位选中态用 AppColors.ACCENT_GREEN:
Text(unit)
.fontSize(12)
.fontColor(this.fromUnit === index ? AppColors.TEXT_WHITE : AppColors.TEXT_SECONDARY)
.backgroundColor(this.fromUnit === index ? AppColors.PRIMARY : '#111827')
.border({ width: 1, color: this.fromUnit === index ? AppColors.PRIMARY : '#1F3B4D' })
.borderRadius(12)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.onClick(() => { this.fromUnit = index })
目标单位按钮结构相同,只是选中色换成 AppColors.ACCENT_GREEN。这样做能让用户在视觉上区分“输入单位”和“输出单位”。尤其是体积和质量单位都比较短,如果两组按钮样式完全一样,用户很容易看错当前选中的是哪一侧。
按钮外层使用横向 Scroll:
Scroll() {
Row({ space: 6 }) {
ForEach(this.unitGroups[this.selectedGroup].units, (unit: string, index: number) => {
// unit chip
})
}
}
.scrollable(ScrollDirection.Horizontal)
.scrollBar(BarState.Off)
这个选择对小屏幕有价值。像 摄氏度(°C)、华氏度(°F)、开尔文(K) 这类标签不一定都能在窄屏一行放下。横向滚动比强行缩小字体更可靠。

15. 页面布局:工具页要让输入、选择和结果保持同一视觉块
换算页把输入、源单位选择、箭头、目标单位选择和结果放在一个卡片里:
Column({ space: 16 }) {
// 输入
// 单位选择 From
// 箭头
// 单位选择 To
// 结果
}
.width('100%')
.padding(20)
.margin({
left: Constants.PAGE_PADDING,
right: Constants.PAGE_PADDING,
top: 20
})
.backgroundColor(AppColors.CARD_BG)
.borderRadius(Constants.CARD_RADIUS)
.shadow({ radius: 4, color: '#0A000000', offsetY: 2 })
这比把输入框、按钮和结果分散在页面不同区域更稳。工具页的用户任务是连续的:选组、输入、选单位、看结果。把这些控件放在同一视觉块里,可以降低视线移动和误操作。
需要注意的是,当前页面根布局是 Column,换算卡片外没有全页 Scroll。在普通手机竖屏下问题不大;如果后续增加更多说明、历史记录或错误提示,就应把主体内容放入 Scroll,避免横屏、小窗或字体放大后底部结果不可达。
16. 真实能力边界:不要把当前实现写成完整实验计算平台
基于源码,当前能力边界可以明确写成下面这张表:
| 能力 | 当前是否实现 | 源码证据 |
|---|---|---|
| 公式分类展示 | 已实现 | FormulaPage.categories 与 filteredFormulas() |
| 公式说明卡片 | 已实现 | FormulaItem.name/formula/description/category |
| 公式参数输入计算 | 未实现 | FormulaPage 没有输入控件和计算函数 |
| 普通单位比例换算 | 已实现 | val * fromFactor / toFactor |
| 温度换算 | 已实现 | convertTemperature() |
| 非法输入兜底 | 已实现 | parseFloat + isNaN 返回 — |
| 结果显示精度控制 | 已实现 | 普通单位 toFixed(6),温度 toFixed(4) |
| 历史记录/收藏 | 未实现 | 页面没有持久化和记录数组 |
| 远程公式库 | 未实现 | 页面无网络请求 |
| 任意精度小数 | 未实现 | 页面未引入 Decimal 类库 |
这张表对技术文章很关键。读者真正需要的是“可以从项目里学到什么”,不是泛泛而谈一个不存在的实验计算平台。把边界讲清楚,反而能让文章更可信,也方便后续迭代。
17. 可迁移写法:把计算逻辑抽到纯函数会更容易测试
当前源码把 convertValue() 和 convertTemperature() 放在页面组件内部。对页面规模来说,这样写完全可以。但如果后续要增加更多单位、自动化测试或复用到其他页面,把计算逻辑抽成纯函数会更稳。
例如可以抽成:
export interface UnitGroupModel {
name: string
units: string[]
factors: number[]
}
export function convertLinearUnit(value: number, group: UnitGroupModel, from: number, to: number): string {
const fromFactor = group.factors[from]
const toFactor = group.factors[to]
const result = value * fromFactor / toFactor
return result.toFixed(6).replace(/\.?0+$/, '')
}
export function convertTemperatureValue(value: number, from: number, to: number): string {
let celsius = value
if (from === 1) {
celsius = (value - 32) * 5 / 9
} else if (from === 2) {
celsius = value - 273.15
}
if (to === 0) return celsius.toFixed(4)
if (to === 1) return (celsius * 9 / 5 + 32).toFixed(4)
return (celsius + 273.15).toFixed(4)
}
这样做的收益是:
- 页面只负责输入、按钮和结果展示;
- 计算函数可以用普通 TypeScript/ArkTS 单元测试覆盖;
- 后续换算逻辑变化时,不需要在 ArkUI 树里找业务代码;
- 多个工具页复用同一套计算规则时,不会复制粘贴。
当前项目源码还没有这样拆分,所以这属于后续工程化建议,不是现状描述。
18. 验证清单:从页面状态到结果输出逐项检查
验证这个页面不能只点一次默认值。至少应覆盖这些路径:
| 验证项 | 操作 | 预期 |
|---|---|---|
| 默认换算 | 打开样本换算页 | 默认输入 1,默认单位组为体积 |
| 单位组切换 | 从体积切到质量、时间、温度 | fromUnit 回到第 0 项,toUnit 回到第 1 项 |
| 普通单位换算 | 输入 1,从 mL 到 μL |
显示 1000,没有多余尾零 |
| 小数结果 | 从 μL 到 mL | 显示小数,尾零被清理 |
| 非法输入 | 输入中文或空字符串 | 结果显示 —,页面不崩溃 |
| 温度换算 | 从摄氏度到华氏度 | 走 convertTemperature(),不是比例因子 |
| 横向滚动 | 在小屏幕查看单位按钮 | 单位按钮可横向滑动,不挤压到不可读 |
| 返回按钮 | 点击顶部返回 | router.back() 正常返回上一页 |
如果要验证公式页,则重点看:
| 验证项 | 操作 | 预期 |
|---|---|---|
| 分类全部 | 打开实验原理页 | 显示所有公式卡片 |
| 分类切换 | 点击基础、培养、DNA、病毒、遗传 | 列表按 category 筛选 |
| 公式字符 | 查看带上标、箭头、微符号的公式 | 字符显示清晰,不乱码 |
| 卡片阅读 | 长说明在小屏幕展示 | 文本不与分类标签重叠 |
这些检查能直接对应源码状态和 UI 结构,比只看页面截图更可靠。
19. 常见问题与修复方向
| 问题 | 可能原因 | 修复方向 |
|---|---|---|
输入 abc 后显示 NaN |
没有在结果前判断 isNaN |
保留 if (isNaN(val)) return '—' |
| 温度换算结果明显错误 | 把温度当成普通比例单位 | 温度单独走摄氏度中转函数 |
| 切换单位组后结果异常 | fromUnit/toUnit 没有重置 |
切换组时重置为 0 和 1 |
| 结果小数位很长 | 直接显示浮点计算结果 | 用 toFixed 控制显示精度 |
| 公式页被误认为可输入计算 | UI 文案或文章描述过度承诺 | 明确 FormulaPage 是说明列表 |
| 小屏幕单位按钮拥挤 | 单位按钮全部固定在一行 | 外层使用横向 Scroll |
| 新增单位后结果不对 | units 与 factors 顺序不一致 |
增加初始化校验,保证数组长度和顺序一致 |
这张表也能作为后续重构前的回归清单。只要换算规则、单位组或输入控件发生变化,就应重新跑这些检查。
20. 小结:公式页讲清原理,换算页控制结果
05-09 公式与单位换算 这部分源码的价值不在于复杂算法,而在于边界清楚。
FormulaPage 把公式当作学习内容:按分类筛选、卡片展示、突出公式文本。它不处理输入,不解析表达式,也不承诺实时计算。
UnitConverterPage 把换算当作交互工具:用 UnitGroup 管理单位和因子,用 inputValue 保留原始字符串,用 parseFloat/isNaN 处理非法输入,用 toFixed 控制显示精度,用独立函数处理温度。
对 HarmonyOS ArkTS 项目来说,这种拆法很适合学习工具类页面:展示职责和计算职责分开,页面状态少,错误边界清楚,后续要抽 service 或加测试也有明确落点。当前源码支持静态公式列表和本地单位换算;没有表达式解析、历史记录、远程公式库或高精度小数库。
更多推荐


所有评论(0)