单位换算的界面通常不复杂,数值语义却很容易出错。长度、质量、时间、力和速度可以通过比例因子换到基准单位,摄氏度、华氏度与开尔文却带有偏移量;普通生活尺度用固定小数很直观,天文尺度则可能因为数值太大或太小,需要科学计数法和有效数字策略。只要把这几类规则混在一个 factors 数组里,页面看似能算,边界结果却未必可信。
"天体运行模拟"的 UnitConverterPage.ets 已经实现了一个可用的离线换算页:六个量纲组、22 个单位标签、输入值、源单位、目标单位、横向选择器和实时结果。比例型单位先换算到组内基准单位,再换到目标单位;温度则通过摄氏度中转。页面完全由 ArkUI 原生组件构成,没有网络依赖。
本文基于这份真实源码,复核当前算法、交互和精度边界,并给出支持 AU、光年、科学计数法和严格输入校验的演进方案。必须先说明:现有页面还没有 AU、光年或秒差距,也没有自动科学计数法;文章中的相关代码是建议实现,不会被描述成当前已上线能力。

唯一复核标记:CONVERTER-ONE13-SCALE-PRECISION-20260726:比例换算先归一到基准单位,温度使用仿射变换,天文尺度按有效数字格式化。
验证基线与源码实况
本文面向 HarmonyOS 5.0 及以上版本。实际工程应用版本为 1.0.0,targetSdkVersion 为 6.0.2(22),compatibleSdkVersion 为 6.0.1(21),入口模块支持 phone、tablet、2in1。
UnitConverterPage.ets 的静态数据统计如下:
| 量纲组 | 单位数 | 基准单位 |
|---|---|---|
| 长度 | 5 | 米 |
| 质量 | 4 | 千克 |
| 时间 | 4 | 秒 |
| 力 | 3 | 牛顿 |
| 温度 | 3 | 摄氏度中转 |
| 速度 | 3 | m/s |
总计 6 组、22 个单位条目。当前有 5 组使用比例因子,温度组单独处理偏移;结果默认保留 6 位小数后去尾零,温度固定保留 4 位。天文单位数量为 0,科学计数格式分支为 0。
一、UnitGroup 表达了组内归一化规则
源码模型为:
ts
interface UnitGroup {
name: string
units: string[]
factors: number[]
}
例如长度组:
ts
{
name: '长度',
units: [
'米(m)',
'千米(km)',
'厘米(cm)',
'毫米(mm)',
'英尺(ft)'
],
factors: [1, 1000, 0.01, 0.001, 0.3048]
}
每个 factor 表示"1 个该单位等于多少基准单位"。1 km 等于 1000 m,所以因子是 1000;1 cm 等于 0.01 m,所以因子是 0.01。
当前模型依赖两个数组下标完全对齐。若增加单位却忘记增加因子,运行时会得到 undefined 并传播为 NaN。更稳的方式是把名称和因子放进同一个对象。
二、比例型单位的核心公式
源码使用:
ts
const fromFactor = group.factors[this.fromUnit]
const toFactor = group.factors[this.toUnit]
const result = val * fromFactor / toFactor
推导分两步:
text
基准值 = 输入值 × 源单位因子
目标值 = 基准值 ÷ 目标单位因子
以 2 km 转 m 为例:
text
2 × 1000 ÷ 1 = 2000 m
以 500 cm 转 m 为例:
text
500 × 0.01 ÷ 1 = 5 m
只要组内单位都是纯比例关系,这个算法就可以双向换算。
三、温度为什么不能套比例因子
温度组虽然填写了 [1, 1, 1],实际代码检测组名后走专门函数:
ts
if (group.name === '温度') {
return this.convertTemperature(val).toFixed(4)
}
温度换算不是 y = ax,而是 y = ax + b。源码先统一到摄氏度:
ts
let celsius: number = val
if (fromIdx === 1) {
celsius = (val - 32) * 5 / 9
} else if (fromIdx === 2) {
celsius = val - 273.15
}
再从摄氏度输出:
ts
if (toIdx === 0) return celsius
if (toIdx === 1) return celsius * 9 / 5 + 32
return celsius + 273.15
这种"先到基准,再到目标"的结构与比例换算一致,只是每个单位需要缩放和偏移两个参数。
四、用仿射模型统一温度
不应继续依赖 group.name === '温度' 这种显示文本分支。可以为单位定义两个函数:
ts
interface UnitDefinition {
id: string
label: string
toBase: (value: number) => number
fromBase: (value: number) => number
}
温度定义:
ts
const CELSIUS: UnitDefinition = {
id: 'celsius',
label: '摄氏度(°C)',
toBase: (value: number): number => value,
fromBase: (value: number): number => value
}
const FAHRENHEIT: UnitDefinition = {
id: 'fahrenheit',
label: '华氏度(°F)',
toBase: (value: number): number =>
(value - 32) * 5 / 9,
fromBase: (value: number): number =>
value * 9 / 5 + 32
}
换算器只需:
ts
const baseValue = from.toBase(input)
const result = to.fromBase(baseValue)
比例单位也能生成对应函数。算法不再知道"温度"这个中文名称,模型的语义更稳定。
五、当前精度因子有哪些近似
源码中的部分因子是精确或足够精确的:
1 ft = 0.3048 m,定义值精确。1 dyn = 0.00001 N,即10⁻⁵ N。1 ms = 0.001 s。
也有简化值:
1 lb = 0.4536 kg,更常用值为0.45359237 kg。1 km/h = 0.2778 m/s,精确关系是1 / 3.6。1 mph = 0.4472 m/s,常用精确值为0.44704 m/s。
教学速查可以显示四位近似,但中间计算最好使用更精确常量,最后再格式化显示。否则多次往返换算会累计差异。
六、不要把显示精度当计算精度
当前非温度结果:
ts
return result
.toFixed(6)
.replace(/\.?0+$/, '')
toFixed(6) 会先舍入,之后去掉尾零。它适合常见生活尺度,但会让非常小的非零结果变成 0。
例如:
text
1 dyn = 0.00001 N
仍可显示为 0.00001。但若加入更小单位或天文比例,结果小于 0.0000005 时就会被固定六位小数抹平。
计算层应保留原始 number,格式化层根据量级选择普通小数或科学计数法。
七、科学计数法需要按量级切换
推荐格式策略:
ts
function formatNumber(value: number): string {
if (!Number.isFinite(value)) {
return '---'
}
const abs = Math.abs(value)
if (abs === 0) return '0'
if (abs >= 1e9 || abs < 1e-6) {
return value.toExponential(6)
}
return value
.toFixed(6)
.replace(/\.?0+$/, '')
}
阈值不是唯一答案,但必须一致。天文距离经常超过 10⁹ m,微小力或比例也可能小于 10⁻⁶。科学计数法能同时避免超长整数和假零。
若产品面向中文用户,可把 1.496e+11 格式化为 1.496 × 10¹¹,但计算值仍保留普通 number。
八、有效数字比固定小数位更适合天文尺度
固定 6 位小数回答"保留多少小数位",有效数字回答"保留多少有意义的数字"。例如:
text
149 597 870 700 m
保留 6 位有效数字可以显示:
text
1.49598 × 10¹¹ m
而 toFixed(6) 会产生很长的整数加无意义小数。
简单实现:
ts
function formatSignificant(
value: number,
digits: number = 6
): string {
if (!Number.isFinite(value)) return '---'
if (value === 0) return '0'
return Number(value.toPrecision(digits)).toString()
}
若要稳定保留尾零,应返回格式化字符串而不是再转回 Number。

九、加入天文尺度必须先选基准单位
当前长度组以米为基准。可以继续扩展:
ts
const AU_IN_METERS: number = 149_597_870_700
const LIGHT_YEAR_IN_METERS: number =
9_460_730_472_580_800
const PARSEC_IN_METERS: number =
3.085_677_581_491_367e16
然后新增:
ts
{ id: 'au', label: '天文单位(AU)',
factor: AU_IN_METERS }
{ id: 'ly', label: '光年(ly)',
factor: LIGHT_YEAR_IN_METERS }
{ id: 'pc', label: '秒差距(pc)',
factor: PARSEC_IN_METERS }
这些常量适合双精度浮点范围,但不能承诺任意位精确。页面应展示有效数字和近似标识。
十、天文单位与光年分别是什么
天文单位用于太阳系尺度:
text
1 AU = 149 597 870 700 m
光年是光在真空中一个儒略年行进的距离:
text
1 ly ≈ 9.4607304725808 × 10¹⁵ m
秒差距来自视差定义:
text
1 pc ≈ 3.26156 ly
它们都是长度单位,不是时间单位。光年名称包含"年",但分类必须放在长度组,不能放进时间组。
十一、number 能处理到什么程度
ArkTS 的 number 使用双精度浮点语义。它能表示约 10³⁰⁸ 的量级,覆盖常见天文单位换算,但整数精确表示只到 2⁵³ - 1。
这意味着:
9.46 × 10¹⁵可表示,但不是每一米都能精确区分。- 进行比例换算通常够用。
- 不适合要求逐米精确的超大整数审计。
- 多次往返可能出现末位误差。
单位换算页应把结果定位为科学展示与学习辅助,避免显示超过数据能力的虚假小数位。
十二、输入解析不能只靠 parseFloat
源码:
ts
const val = parseFloat(this.inputValue)
if (isNaN(val)) return '---'
parseFloat('12abc') 会得到 12,这可能让用户误以为整个输入被接受。更严格的校验可以使用正则:
ts
const NUMBER_PATTERN: RegExp =
/^[+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?$/
function parseStrictNumber(
input: string
): number | undefined {
const value = input.trim()
if (!NUMBER_PATTERN.test(value)) {
return undefined
}
const parsed = Number(value)
return Number.isFinite(parsed) ? parsed : undefined
}
这个模式支持 -273.15、.5、1.496e11,拒绝 12km 和无限值。
十三、科学计数输入要保留编辑体验
用户输入 1e 时,表达式尚未完成。如果每次 onChange 都立刻判为错误并弹提示,会造成干扰。
可分为:
- 编辑态:保留原字符串,结果暂时显示
---。 - 提交态:失焦或点击换算时显示具体错误。
- 合法态:实时更新结果。
当前页面没有独立提交按钮,采用实时结果。最合适的是对不完整输入静默显示占位符,不要清空用户文本。
十四、绝对零度需要业务校验
温度公式可以计算:
text
-300 °C = -26.85 K
数学结果存在,物理上却低于绝对零度。若页面定位为纯代数换算,可以显示;若定位为物理学习工具,应提示:
ts
if (toKelvin(value, unit) < 0) {
return {
ok: false,
message: '该温度低于绝对零度'
}
}
同样,开尔文输入本身不应小于 0。规则要由产品定位决定,但不能在不同方向表现不一致。
十五、用对象替代平行数组
推荐结构:
ts
type QuantityKind =
| 'length'
| 'mass'
| 'time'
| 'force'
| 'temperature'
| 'speed'
interface UnitDefinition {
id: string
label: string
symbol: string
toBase: (value: number) => number
fromBase: (value: number) => number
}
interface UnitGroup {
id: QuantityKind
name: string
units: UnitDefinition[]
}
优点:
- 标签与换算规则不会错位。
id不受中文显示文案变化影响。- 温度无需特殊组名判断。
- 可为每个单位增加精度、说明和别名。
- 测试可以按单位 ID 枚举。
十六、切换量纲时重置下标是必要的
源码选择新组后:
ts
this.selectedGroup = index
this.fromUnit = 0
this.toUnit = 1
不同组单位数量不同。如果保留旧下标,例如从 5 个长度单位切到 3 个力单位,可能越界。重置到 0 和 1 是正确的防护。
更进一步,模型初始化时应断言每组至少有两个单位,否则 toUnit = 1 仍会越界。
十七、页面状态与结果都是派生关系
页面只保存:
selectedGroupinputValuefromUnittoUnit
结果由 convertValue() 动态计算,没有维护额外 @State result。这避免输入变化后忘记同步结果。
当数据量很小、计算为纯函数时,派生结果比手工状态更可靠。若未来计算需要高精度库或异步服务,再考虑缓存或 ViewModel。
十八、横向选择器保护窄屏布局
量纲组、源单位和目标单位都使用横向 Scroll,因此长标签不会把卡片挤坏。选中源单位使用主色,选中目标单位使用绿色,视觉上区分输入和输出。
应继续验证:
- 系统字体放大。
天文单位(AU)等长标签。- 手机横屏与小窗口。
- 2in1 鼠标滚轮和触控板横向操作。
关闭滚动条后,最好让下一项露出一部分,提示仍可滚动。
十九、结果文本需要约束超长值
当前结果使用 28vp 粗体:
ts
Text(this.convertValue())
.fontSize(28)
.fontWeight(AppFonts.WEIGHT_BOLD)
加入天文尺度后,普通十进制字符串可能超过卡片宽度。可以:
- 默认科学计数法。
- 设置
maxLines(1)。 - 使用
textOverflow。 - 对结果区域提供横向滚动或复制。
- 在平板上扩大结果区,但不依赖超宽布局。
最优先的是正确格式化,而不是缩小字体到不可读。

二十、多设备与安全区
页面根容器已经使用全宽、全高,并加上状态栏和底栏高度:
ts
.width('100%')
.height('100%')
.padding({
top: this.statusBarHeight,
bottom: this.bottomBarHeight
})
手机保持单卡片结构即可。平板和 2in1 可限制卡片最大宽度并居中,避免输入行横跨整屏。软键盘弹出后要保证目标单位和结果仍可到达;当前根页面不是整体滚动容器,较小窗口或大字体下需要实测是否被键盘遮挡。
二十一、关键测试向量
| 输入 | 源单位 | 目标单位 | 预期 |
|---|---|---|---|
| 1 | km | m | 1000 |
| 100 | cm | m | 1 |
| 1 | ft | m | 0.3048 |
| 1 | t | kg | 1000 |
| 60 | min | h | 1 |
| 1 | kN | N | 1000 |
| 1 | dyn | N | 0.00001 |
| 0 | °C | °F | 32 |
| 32 | °F | °C | 0 |
| 0 | °C | K | 273.15 |
| 36 | km/h | m/s | 约 10.0008,受当前近似因子影响 |
abc |
任意 | 任意 | --- |
如果使用精确的 1/3.6,36 km/h 应严格得到 10 m/s。这个对比能直接暴露近似因子带来的误差。
加入天文单位后的新增测试:
text
1 AU -> m = 1.495978707 × 10¹¹
1 ly -> AU ≈ 63241.077
1 pc -> ly ≈ 3.26156
二十二、属性测试比逐例测试更强
对所有比例单位,可以验证往返性质:
ts
const converted = convert(value, from, to)
const restored = convert(converted, to, from)
expectClose(restored, value, tolerance)
还可以验证:
- 同单位换算结果等于输入。
- 零值在比例单位间仍为零。
- 正比例单位保持正负号。
- 所有因子均为有限正数。
- 每组至少两个单位。
- 单位 ID 全局或组内唯一。
浮点结果不要直接使用 ===,应按绝对误差或相对误差比较。
二十三、发布前检查
- 比例型单位统一经过基准单位。
- 温度使用带偏移的仿射变换。
- 单位标签与规则不使用平行数组。
parseFloat不接受尾随垃圾字符。- 支持合法科学计数输入。
- 小量级不会被
toFixed(6)误显示为零。 - 大量级自动使用科学计数法。
- 显示有效数字与计算精度分离。
- AU、光年和秒差距归入长度。
- 精确常量与展示近似值分离。
- 结果长文本不溢出。
- 软键盘和底部安全区不遮挡控件。
- 手机、平板、2in1 完成同一组测试向量。
二十四、总结
当前 UnitConverterPage.ets 已经形成一条清晰的本地换算链路:用户输入字符串,选择量纲和两个单位,比例型单位通过基准因子换算,温度通过摄氏度中转,结果实时派生并在 ArkUI 卡片中显示。切换量纲时重置单位下标、使用横向滚动保护窄屏,都是可靠的首版选择。
面向天体应用继续演进时,核心不是简单追加几个大数字,而是重构单位模型、使用精确常量、严格解析科学计数输入,并按有效数字输出。比例、仿射、格式化和物理边界应分层处理;只有计算值与展示值各自守住职责,单位换算才不会在尺度放大后失去可信度。
说明:本文基于真实 HarmonyOS/ArkTS 源码进行整理,部分文字与示例由 AI 辅助生成;所有现有能力与建议改造已明确区分。