【天体运行模拟|05】HarmonyOS ArkTS 天体模型实战:用强类型结构描述参数和初始条件
2026/8/31 7:00:00 网站建设 项目流程

天体模拟的模型层不应该只是“给列表页准备几段文字”。一个实验对象至少要同时服务首页卡片、分类筛选、收藏页、模拟入口、参数面板和实验记录。如果同一个id在不同页面含义不同,参数顺序靠猜,分类和难度随手写字符串,模型很快就会变成一堆无法验证的配置。

“天体运行模拟”的Experiment.ets已经建立了两个核心接口:ExperimentParam描述参数范围,Experiment描述实验身份、展示信息、分类、图标、收藏标记与参数数组;getAllExperiments()则注册了稳定双体、椭圆与逃逸、三体扰动、双星、黑洞、星系碰撞六个真实入口。

本文从这份真实源码出发,拆解强类型模型如何串起参数和初始条件,同时指出三个必须如实处理的边界:当前id/category/level仍是宽泛字符串,目录中的参数数组尚未真正驱动模拟页通用 Slider,isFavorite的静态默认值也不是持久化收藏状态。目标不是把接口写得更复杂,而是让每个字段都能被编译、运行和页面回读共同验证。

唯一复核标记:MODEL-ONE13-EXPERIMENT-CONTRACT-20260726:实验目录定义身份和参数约束,运行初值必须显式映射,用户状态不能混进静态模型。

验证基线:HarmonyOS 6.0.1(21) 兼容目标

本文面向 HarmonyOS 5.0 及以上版本,实际源码工程给出的构建基线更具体:应用版本为1.0.0targetSdkVersion6.0.2(22)compatibleSdkVersion6.0.1(21),运行系统为 HarmonyOS;入口模块声明支持phonetablet2in1。这些值分别来自AppScope/app.json5、根目录build-profile.json5entry/src/main/module.json5,不是根据界面截图推测出的版本。

本次验证对象固定为以下源码快照:

验证项实际值复核位置
应用版本1.0.0/versionCode 1000000AppScope/app.json5
目标 SDK6.0.2(22)build-profile.json5
最低兼容 SDK6.0.1(21)build-profile.json5
设备类型3 类entry/src/main/module.json5
实验目录6 项Experiment.ets#getAllExperiments()
参数定义12 项每个实验 2 项

Experiment.ets做逐项静态复核后得到:6 个实验 ID 无重复,12 个参数都满足min <= defaultValue <= max,12 个step均大于 0,12 个参数单位均为非空字符串。这里的“通过”只代表模型目录的静态约束成立,不代表目录参数已经驱动模拟页;后文会专门说明这条尚未接通的运行链路。

为了让复核可以落到代码,建议把以下断言放进模型测试或启动期开发校验中:

function validateCatalog(experiments: Experiment[]): string[] { const errors: string[] = [] const ids = new Set<string>() experiments.forEach((experiment: Experiment) => { if (ids.has(experiment.id)) { errors.push(`duplicate experiment id: ${experiment.id}`) } ids.add(experiment.id) experiment.params.forEach((param: ExperimentParam) => { if (param.min > param.defaultValue || param.defaultValue > param.max) { errors.push(`${experiment.id}/${param.name}: default out of range`) } if (param.step <= 0) { errors.push(`${experiment.id}/${param.name}: invalid step`) } if (param.unit.trim().length === 0) { errors.push(`${experiment.id}/${param.name}: empty unit`) } }) }) return errors }

将当前getAllExperiments()的返回值传入后,预期结果为:

experimentCount = 6 parameterCount = 12 duplicateIdCount = 0 invalidDefaultCount = 0 invalidStepCount = 0 emptyUnitCount = 0

一、现有模型解决了什么

参数接口为:

export interface ExperimentParam { name: string unit: string min: number max: number defaultValue: number step: number }

实验接口为:

export interface Experiment { id: string name: string description: string category: string level: string icon: Resource isFavorite: boolean params: ExperimentParam[] }

这两个接口已经比散落常量稳得多。列表页只需读取Experiment[],参数展示可以统一遍历params,资源通过 HarmonyOSResource类型引用,调用方不用为每个实验写一套卡片结构。

字段可以按职责划分:

职责字段
稳定身份id
用户展示namedescriptionicon
内容组织categorylevel
参数约束params
用户状态isFavorite

最后一项值得警惕:收藏不是实验定义,而是用户状态。当前默认值全部为false,真实收藏页会通过DataStore加载 ID 后重新映射。因此,后续应把它从静态目录分离。

二、id是跨页面协议,不只是数组里的键

真实目录包含:

id: 'stable_orbit' id: 'elliptic_escape' id: 'three_body' id: 'binary_star' id: 'black_hole' id: 'galaxy_collision'

这些字符串会进入多个位置:

  • 首页与实验室列表的点击参数。
  • 模拟页resetSystem()的场景分支。
  • 结果页buildAnalysis()的分析分支。
  • 收藏数据中的实验 ID。
  • 实验记录的experimentId

因此,一个拼写错误可能同时影响路由、收藏、记录和结果分析。当前类型是string,编译器不能发现stable_oribt

更稳的方式是收紧为联合类型:

export type ExperimentId = | 'stable_orbit' | 'elliptic_escape' | 'three_body' | 'binary_star' | 'black_hole' | 'galaxy_collision' | 'sandbox'
export interface Experiment { id: ExperimentId // other fields }

sandbox当前由模拟页支持,但不在getAllExperiments()目录中。是否加入正式实验列表应由产品决定;即使不展示,也可以保留在运行场景 ID 类型中,再把“目录实验 ID”和“运行场景 ID”拆成两个类型。

三、分类与难度不应继续使用任意字符串

当前分类包括:

基础认知 轨道探索 多体系统 高级实验

难度包括:

初级任务 中级挑战 高级实验

“高级实验”同时出现在分类和难度中,语义容易混淆。列表页按category过滤,而卡片同时显示categorylevel;如果运营文案稍有变化,筛选条件可能失效。

可以分别定义:

export type ExperimentCategory = | '基础认知' | '轨道探索' | '多体系统' | '极端天体' export type ExperimentLevel = | '初级' | '中级' | '高级'

再让 UI 自己组合“初级任务”“中级挑战”等展示词。这样,分类用于导航,难度用于学习顺序,两者不会共享同一个模糊词。

这属于增强方案。当前真实数据中,黑洞与星系碰撞的categorylevel都是“高级实验”,文章不能声称已经完成语义拆分。

四、参数定义同时描述范围、默认值和交互粒度

稳定双体系统的真实参数:

params: [ { name: '恒星质量', unit: 'M', min: 400, max: 1800, defaultValue: 980, step: 20 }, { name: '行星初速度', unit: 'v', min: 0.4, max: 3.2, defaultValue: 1.25, step: 0.05 } ]

这些字段不是普通表单元数据。它们共同构成参数契约:

  • min/max决定可接受区间。
  • defaultValue决定实验初始状态。
  • step决定用户能否稳定复现某个参数点。
  • unit决定展示和解释方式。

可以在目录初始化时验证:

function isValidParam( param: ExperimentParam ): boolean { return ( Number.isFinite(param.min) && Number.isFinite(param.max) && Number.isFinite(param.defaultValue) && Number.isFinite(param.step) && param.min <= param.defaultValue && param.defaultValue <= param.max && param.step > 0 ) }

这个函数是建议,不是当前源码已有实现。它能阻止默认值落在范围外、步长为零或非有限数。

五、参数名称不适合作为稳定业务键

当前ExperimentParam只有name,例如“恒星质量”“行星初速度”。如果运行页靠名称判断参数含义,一旦文案改成“中心恒星质量”,映射就会失效。

建议增加机器可读键:

export type ExperimentParamKey = | 'primaryMass' | 'orbitalRadius' | 'tangentialSpeed' | 'disturbance' | 'timeScale' | 'binaryDistance' | 'massRatio' | 'approachSpeed' export interface ExperimentParam { key: ExperimentParamKey name: string unit: string min: number max: number defaultValue: number step: number }

机器逻辑读取key,UI 展示name。文案、国际化和产品调整不会破坏参数映射。

六、六组实验真实表达了不同初始条件

目录中的六个实验不是同一参数模板换标题,而是各有目标:

实验参数 1参数 2观察目标
稳定双体恒星质量行星初速度圆轨道与椭圆轨道
椭圆与逃逸轨道半径切向速度束缚与逃逸
三体扰动扰动强度时间倍率初值敏感性
双星系统双星间距互绕速度共同质心与稳定区
黑洞吞噬黑洞质量行星速度强引力与坠落
星系碰撞星系质量比接近速度潮汐拉伸与合并

这说明params已经具备“初始条件目录”的雏形。问题是运行页当前仍定义一套通用参数:

@State paramDefs: ExperimentParam[] = [ { name: '质量', ... }, { name: '半径', ... }, { name: '初速度', ... }, { name: '方向', ... }, { name: '轨道倾角', ... } ]

它没有在aboutToAppear()中读取当前实验的params。因此,目录参数目前主要是描述数据,并未真正驱动模拟页控制器。文章必须明确这个事实。

七、目录参数要通过显式映射进入运行模型

不能简单写:

this.paramDefs = experiment.params

因为模拟页的addSelectedBody()依赖固定数组索引:

this.paramValues[0] // mass this.paramValues[1] // radius this.paramValues[2] // speed this.paramValues[3] // direction this.paramValues[4] // tilt

而目录里的每个实验只有两个参数,含义也不同。若直接替换,paramValues[2]可能为空,双星间距也无法自动映射到mass

更稳的方式是为每种实验建立强类型初值:

interface StableOrbitInitialState { primaryMass: number planetSpeed: number } interface BinaryStarInitialState { starDistance: number orbitSpeed: number } interface GalaxyCollisionInitialState { massRatio: number approachSpeed: number }

再由场景初始化函数消费对应结构,而不是让一个通用数组承担所有含义。

八、使用可判别联合描述不同实验初始条件

ArkTS 中可以用kind建立可判别联合:

interface StableOrbitConfig { kind: 'stable_orbit' primaryMass: number planetSpeed: number } interface ThreeBodyConfig { kind: 'three_body' disturbancePercent: number timeScale: number } interface BlackHoleConfig { kind: 'black_hole' blackHoleMass: number planetSpeed: number } type ExperimentInitialConfig = | StableOrbitConfig | ThreeBodyConfig | BlackHoleConfig

处理时根据kind收窄:

function buildBodies( config: ExperimentInitialConfig ): BodySeed[] { if (config.kind === 'stable_orbit') { return buildStableOrbitBodies(config) } if (config.kind === 'three_body') { return buildThreeBodyBodies(config) } return buildBlackHoleBodies(config) }

这种结构能让编译器知道三体配置没有blackHoleMass,比Record<string, number>number[]更可靠。

这些是演进代码,当前Experiment.ets尚未定义可判别联合。

九、显示元数据与运行配置要分层

目录实验既有名称、描述、图标,又有参数。随着场景复杂度增加,可以拆成:

export interface ExperimentMeta { id: ExperimentId name: string description: string category: ExperimentCategory level: ExperimentLevel icon: Resource } export interface ExperimentDefinition { meta: ExperimentMeta params: ExperimentParam[] }

首页只读取meta,参数面板读取params,模拟服务再把参数解析成ExperimentInitialConfig。这不是为了增加层级,而是避免首页组件依赖运行细节。

当前工程规模只有六项,单个接口仍可维护。只有当参数解析、版本迁移或设备差异开始增长时,拆分才真正有价值。

十、Resource类型让图标引用接受编译期检查

每个实验使用:

icon: $r('app.media.ic_orbit_stable')

接口字段为:

icon: Resource

这比把图片路径保存为字符串更符合 HarmonyOS 资源体系。调用方可以直接:

Image(exp.icon)

资源名、限定目录与深浅色资源仍需在构建和设备上验证,但模型层至少不会把本地文件路径或网络 URL 混进图标字段。

图标还承担实验辨识:稳定轨道、逃逸、三体、双星、黑洞与星系各有独立资源名,符合“一项实验一个视觉入口”的结构。

十一、收藏状态不能固定在实验目录

真实目录中每项都有:

isFavorite: false

但模拟页会:

const ids = await DataStore.loadFavorites() this.isFavorite = ids.includes(this.expId)

收藏页也会加载 ID,再从getAllExperiments()查找实验。这说明真正可信源是本地存储中的 ID 集合,而不是目录的isFavorite

更清晰的模型是删除静态字段:

export interface Experiment { id: ExperimentId name: string description: string category: ExperimentCategory level: ExperimentLevel icon: Resource params: ExperimentParam[] }

页面组合:

interface ExperimentViewState { experiment: Experiment isFavorite: boolean }

这样,实验目录保持不可变,用户状态由 ViewModel 或页面状态拥有。

十二、getAllExperiments()每次都会创建新数组

函数当前直接返回字面量数组。每次调用都会创建新的对象和参数数组:

export function getAllExperiments(): Experiment[] { return [ // definitions ] }

这有一个意外优点:调用方修改isFavorite不会污染全局单例。但代价是首页、收藏、记录与模拟页每次查找都会重新创建目录。

更稳的做法取决于模型是否可变:

  • 若目录保持只读,可定义模块级常量并返回只读引用。
  • 若调用方可能改对象,可返回深拷贝或只读类型。

例如:

const EXPERIMENTS: Experiment[] = [ // definitions ] export function getAllExperiments(): Experiment[] { return EXPERIMENTS.map( (item: Experiment) => ({ ...item, params: [...item.params] }) ) }

当前只有六项,性能压力很小;重点是明确可变性契约,而不是为了少创建对象过早优化。

十三、通过查找函数集中处理不存在的 ID

工程多处写:

getAllExperiments().find( (experiment: Experiment) => experiment.id === this.expId )

可以集中为:

export function findExperiment( id: ExperimentId ): Experiment | undefined { return getAllExperiments().find( (experiment: Experiment) => experiment.id === id ) }

如果某些调用必须有结果,可以提供带回退的函数:

export function getExperimentOrDefault( id: ExperimentId ): Experiment { return ( findExperiment(id) ?? getAllExperiments()[0] ) }

路由入口更适合显式处理undefined,不要静默回退;记录展示可以回退为“已下线实验”。不同调用场景不应共享同一种失败策略。

十四、目录自身需要启动时一致性检查

可以验证:

export function validateExperiments( experiments: Experiment[] ): string[] { const errors: string[] = [] const ids = new Set<string>() for (const experiment of experiments) { if (ids.has(experiment.id)) { errors.push(`重复实验 ID: ${experiment.id}`) } ids.add(experiment.id) for (const param of experiment.params) { if (!isValidParam(param)) { errors.push( `${experiment.id} 参数无效: ${param.name}` ) } } } return errors }

检查项至少包括:

  • ID 唯一。
  • 名称与描述非空。
  • 参数默认值在范围内。
  • 步长为正。
  • 同一实验参数 key 不重复。
  • 图标资源构建可解析。

发布构建不一定要把错误直接展示给用户,但开发与测试阶段应让目录问题尽早暴露。

十五、版本迁移要保护收藏和历史记录

收藏和记录都保存实验 ID。重命名stable_orbit会让旧数据找不到对应实验。模型演进应遵守:

  1. 展示名称可以改,稳定 ID 不轻易改。
  2. 必须改 ID 时提供迁移表。
  3. 已删除实验的历史记录仍可显示基础快照。

迁移表示例:

const EXPERIMENT_ID_MIGRATION: Record<string, ExperimentId> = { orbit_basic: 'stable_orbit', escape_demo: 'elliptic_escape' }

当前版本没有显示迁移需求,但一旦应用已经发布,稳定 ID 就成为数据兼容协议。

十六、把参数值与参数定义分开保存

ExperimentParam是定义,不是用户当前值。不要向其中动态添加value并把同一对象在多个页面共享。

可以定义:

export interface ExperimentParamValue { key: ExperimentParamKey value: number } export interface ExperimentRunDraft { experimentId: ExperimentId values: ExperimentParamValue[] }

定义回答“允许什么”,草稿回答“用户这次选择什么”。恢复草稿时,再用当前定义校验旧值。

这种分离尤其适合多设备:phone 上调好的参数可以在 tablet 界面重新渲染,而不需要持久化 UI 控件状态。

十七、参数映射应返回明确错误

若目录参数无法转换为运行初值,不应悄悄使用默认值。可以定义:

interface ConfigBuildResult { ok: boolean config?: ExperimentInitialConfig message?: string }
function buildInitialConfig( experiment: Experiment, values: ExperimentParamValue[] ): ConfigBuildResult { if (experiment.id === 'stable_orbit') { // validate and build } return { ok: false, message: '当前实验暂不支持参数映射' } }

页面可显示错误状态,而不是进入一个与用户参数无关的默认场景。当前源码尚未接入这层映射,所以文章只把它作为实现边界。

十八、多设备模型应保持与布局无关

phone、tablet 和 2in1 可以采用不同布局,但应消费同一个Experiment

  • phone:单列卡片、底部主导航。
  • tablet:双列卡片、侧栏分类。
  • 2in1:列表与参数详情并排。

模型中不要加入cardWidthcolumnsfontSize。布局策略由页面根据窗口决定,实验定义只描述业务。

Resource图标可以在不同资源限定目录提供适配版本,而无需更改模型中的资源名。

十九、最小验证用例

目录完整性

  1. 调用getAllExperiments()
  2. 确认返回 6 个真实实验。
  3. 确认所有 ID 唯一。
  4. 确认每个实验至少有一个参数。

参数边界

对每个ExperimentParam验证:

min <= defaultValue <= max step > 0 unit 非空

路由一致

  1. 从列表点击每个实验。
  2. 回读模拟页expId
  3. 确认模拟页有对应初始化分支。
  4. 进入结果页确认分析分支一致。

收藏一致

  1. 收藏某实验。
  2. 重新调用getAllExperiments()
  3. 确认收藏状态来自 DataStore,而不是目录默认值。

参数生效

当前版本应明确记录为“待补”:目录params尚未驱动模拟页通用参数面板。增强后再验证修改质量、速度等是否真正影响初始化对象。

二十、常见问题与修复方向

现象根因修复
路由传错 ID 仍能编译id是普通 string使用联合类型
分类筛选漏项分类文案不一致收紧ExperimentCategory
目录参数改了,模拟不变参数尚未映射到运行初值建立显式转换器
收藏状态总是 false读取了静态字段从 DataStore 组合视图状态
修改参数名称后逻辑失效名称被当作业务键增加稳定key
历史记录找不到实验ID 被重命名保持稳定 ID 或迁移
默认值超出 Slider 范围缺少目录校验启动或测试时验证
多页面出现不同实验描述各自复制配置统一从目录读取
大屏需要不同卡片结构模型混入布局字段让页面负责适配

二十一、发布前模型清单

  • 实验 ID 唯一且跨版本稳定。
  • 分类与难度使用独立语义。
  • 参数具有稳定 key,不依赖展示名称。
  • 每个默认值落在合法范围内。
  • 步长大于零且显示精度匹配。
  • 目录参数与运行初值存在显式映射。
  • 用户收藏状态不存入静态目录。
  • 结果页和模拟页识别同一组 ID。
  • 图标资源真实存在且与实验匹配。
  • 历史记录对未知或下线 ID 有回退。
  • 模型不包含 phone/tablet/2in1 布局细节。
  • 未实现的参数驱动能力不写成已生效。

二十二、总结:强类型的价值是把错误提前到模型边界

真实Experiment.ets已经完成重要的一步:六个天文实验拥有统一结构,参数范围、默认值、步长和单位集中定义,页面不必散落复制。这份目录正在被首页、实验室、收藏、模拟记录和模拟页查找共同使用。

下一步不是简单增加更多字段,而是收紧真正稳定的协议:把实验 ID、分类、难度和参数 key 变成受约束类型;把静态目录与收藏状态分开;把参数定义与本次参数值分开;最后用显式转换把目录参数映射为各场景的初始天体结构。

做到这些以后,“恒星质量 980 M”“切向速度 1.75 v”才不只是卡片上的配置,而会沿着 ArkTS 类型系统可靠地进入 Canvas 模拟、结果分析、收藏与历史记录,成为可以验证和迁移的 HarmonyOS 业务模型。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询