尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

【天体运行模拟|05】HarmonyOS ArkTS 天体模型实战:用强类型结构描述参数和初始条件

【天体运行模拟|05】HarmonyOS ArkTS 天体模型实战:用强类型结构描述参数和初始条件 天体模拟的模型层不应该只是“给列表页准备几段文字”。一个实验对象至少要同时服务首页卡片、分类筛选、收藏页、模拟入口、参数面板和实验记录。如果同一个id在不同页面含义不同参数顺序靠猜分类和难度随手写字符串模型很快就会变成一堆无法验证的配置。“天体运行模拟”的Experiment.ets已经建立了两个核心接口ExperimentParam描述参数范围Experiment描述实验身份、展示信息、分类、图标、收藏标记与参数数组getAllExperiments()则注册了稳定双体、椭圆与逃逸、三体扰动、双星、黑洞、星系碰撞六个真实入口。本文从这份真实源码出发拆解强类型模型如何串起参数和初始条件同时指出三个必须如实处理的边界当前id/category/level仍是宽泛字符串目录中的参数数组尚未真正驱动模拟页通用 SliderisFavorite的静态默认值也不是持久化收藏状态。目标不是把接口写得更复杂而是让每个字段都能被编译、运行和页面回读共同验证。唯一复核标记MODEL-ONE13-EXPERIMENT-CONTRACT-20260726实验目录定义身份和参数约束运行初值必须显式映射用户状态不能混进静态模型。验证基线HarmonyOS 6.0.1(21) 兼容目标本文面向 HarmonyOS 5.0 及以上版本实际源码工程给出的构建基线更具体应用版本为1.0.0targetSdkVersion为6.0.2(22)compatibleSdkVersion为6.0.1(21)运行系统为 HarmonyOS入口模块声明支持phone、tablet、2in1。这些值分别来自AppScope/app.json5、根目录build-profile.json5和entry/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 max12 个step均大于 012 个参数单位均为非空字符串。这里的“通过”只代表模型目录的静态约束成立不代表目录参数已经驱动模拟页后文会专门说明这条尚未接通的运行链路。为了让复核可以落到代码建议把以下断言放进模型测试或启动期开发校验中function validateCatalog(experiments: Experiment[]): string[] { const errors: string[] [] const ids new Setstring() 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用户展示name、description、icon内容组织category、level参数约束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 | sandboxexport interface Experiment { id: ExperimentId // other fields }sandbox当前由模拟页支持但不在getAllExperiments()目录中。是否加入正式实验列表应由产品决定即使不展示也可以保留在运行场景 ID 类型中再把“目录实验 ID”和“运行场景 ID”拆成两个类型。三、分类与难度不应继续使用任意字符串当前分类包括基础认知 轨道探索 多体系统 高级实验难度包括初级任务 中级挑战 高级实验“高级实验”同时出现在分类和难度中语义容易混淆。列表页按category过滤而卡片同时显示category和level如果运营文案稍有变化筛选条件可能失效。可以分别定义export type ExperimentCategory | 基础认知 | 轨道探索 | 多体系统 | 极端天体 export type ExperimentLevel | 初级 | 中级 | 高级再让 UI 自己组合“初级任务”“中级挑战”等展示词。这样分类用于导航难度用于学习顺序两者不会共享同一个模糊词。这属于增强方案。当前真实数据中黑洞与星系碰撞的category和level都是“高级实验”文章不能声称已经完成语义拆分。四、参数定义同时描述范围、默认值和交互粒度稳定双体系统的真实参数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 }机器逻辑读取keyUI 展示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比Recordstring, 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 Setstring() 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会让旧数据找不到对应实验。模型演进应遵守展示名称可以改稳定 ID 不轻易改。必须改 ID 时提供迁移表。已删除实验的历史记录仍可显示基础快照。迁移表示例const EXPERIMENT_ID_MIGRATION: Recordstring, 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 可以采用不同布局但应消费同一个Experimentphone单列卡片、底部主导航。tablet双列卡片、侧栏分类。2in1列表与参数详情并排。模型中不要加入cardWidth、columns或fontSize。布局策略由页面根据窗口决定实验定义只描述业务。Resource图标可以在不同资源限定目录提供适配版本而无需更改模型中的资源名。十九、最小验证用例目录完整性调用getAllExperiments()。确认返回 6 个真实实验。确认所有 ID 唯一。确认每个实验至少有一个参数。参数边界对每个ExperimentParam验证min defaultValue max step 0 unit 非空路由一致从列表点击每个实验。回读模拟页expId。确认模拟页有对应初始化分支。进入结果页确认分析分支一致。收藏一致收藏某实验。重新调用getAllExperiments()。确认收藏状态来自 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 业务模型。
返回列表