本文面向 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 或加测试也有明确落点。当前源码支持静态公式列表和本地单位换算;没有表达式解析、历史记录、远程公式库或高精度小数库。