部分内容由AI辅助生成。

本文面向 HarmonyOS 5.0 及以上版本,基于 细胞工坊 项目真实源码展开,源码根目录为 D:\huawei\one14-9。本文重点复核两个页面:

  • entry/src/main/ets/views/learning/FormulaPage.ets
  • entry/src/main/ets/views/learning/UnitConverterPage.ets

这两个页面看起来都属于“学习工具”,但工程职责并不相同。FormulaPage 是公式说明页,负责把显微放大、菌落增长、DNA 析出、病毒感染、遗传比例等公式按分类展示出来;UnitConverterPage 是样本换算页,负责体积、质量、时间、浓度、温度和细胞数的本地换算,并对非法输入做兜底。

这篇文章只讨论源码已经实现的能力:公式列表、分类筛选、单位组切换、数值输入、普通因子换算、温度换算、结果格式化、非法输入显示 。源码中没有表达式解析器、历史记录、远程公式库、收藏、学习进度同步或任意精度小数库,文章不会把这些能力写成已实现。

封面图

1. 为什么实验学习工具不能只做一张公式表

在生物实验学习类 HarmonyOS 应用里,公式和单位换算经常被放在同一个入口下。产品层面看,它们都是“工具”;工程层面看,它们有明显边界。

公式页的核心问题是“如何让学生快速找到某类实验原理”。它的输入是分类点击,输出是一组固定知识卡片。换算页的核心问题是“如何把用户输入的一个数值转换成目标单位”。它的输入来自 TextInput,结果随状态实时变化,必须处理非法输入和显示精度。

如果把这两类能力混在一个页面里,常见问题会很快出现:

问题 在页面里的表现 更稳的边界
公式只是说明,却被当成可计算表达式 用户以为 M = 物镜 × 目镜 能输入参数计算 FormulaPage 只展示说明,不承诺计算
单位换算没有非法输入兜底 输入中文、空字符串、符号后页面显示 NaN UnitConverterPageisNaN 返回占位符
小数位过多 0.00100000000002 之类结果污染 UI 普通单位使用 toFixed(6) 后去尾零
温度和比例因子混算 摄氏度、华氏度、开尔文转换错误 温度走独立 convertTemperature()

当前源码的处理方式是把“说明型公式”和“计算型换算”拆成两个页面。这个拆法朴素,但很实用:公式页没有输入状态,换算页没有公式解释负担,两边的 UI 和错误处理都更清晰。

2. 源码结构:两个页面共用主题,但业务状态分离

先看两个页面共同依赖的内容。它们都导入 routerAppColorsAppFontsConstants

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 的列表结构使用 ListListItem

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') 会得到 12parseFloat('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
}

这是扩展建议,不是当前源码能力。当前页面的已实现能力是 parseFloatisNaN 兜底。

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 展示单位名称,计算读取同一索引下的因子,两者通过数组下标绑定。

这里的维护风险是 unitsfactors 必须长度一致,而且顺序必须一致。当前源码由开发者手写数组,数量不大,风险可控。若后续单位组增多,建议在初始化时做一次自检:

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.categoriesfilteredFormulas()
公式说明卡片 已实现 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 没有重置 切换组时重置为 01
结果小数位很长 直接显示浮点计算结果 toFixed 控制显示精度
公式页被误认为可输入计算 UI 文案或文章描述过度承诺 明确 FormulaPage 是说明列表
小屏幕单位按钮拥挤 单位按钮全部固定在一行 外层使用横向 Scroll
新增单位后结果不对 unitsfactors 顺序不一致 增加初始化校验,保证数组长度和顺序一致

这张表也能作为后续重构前的回归清单。只要换算规则、单位组或输入控件发生变化,就应重新跑这些检查。

20. 小结:公式页讲清原理,换算页控制结果

05-09 公式与单位换算 这部分源码的价值不在于复杂算法,而在于边界清楚。

FormulaPage 把公式当作学习内容:按分类筛选、卡片展示、突出公式文本。它不处理输入,不解析表达式,也不承诺实时计算。

UnitConverterPage 把换算当作交互工具:用 UnitGroup 管理单位和因子,用 inputValue 保留原始字符串,用 parseFloat/isNaN 处理非法输入,用 toFixed 控制显示精度,用独立函数处理温度。

对 HarmonyOS ArkTS 项目来说,这种拆法很适合学习工具类页面:展示职责和计算职责分开,页面状态少,错误边界清楚,后续要抽 service 或加测试也有明确落点。当前源码支持静态公式列表和本地单位换算;没有表达式解析、历史记录、远程公式库或高精度小数库。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐