【细胞工坊|03】HarmonyOS ArkTS DNA 提取流程实战:用步骤状态避免跳步和错误结果

【细胞工坊|03】HarmonyOS ArkTS DNA 提取流程实战:用步骤状态避免跳步和错误结果

部分内容由AI辅助生成。本文面向 HarmonyOS 5.0 及以上版本,基于细胞工坊项目真实源码展开,源码根目录为D:\huawei\one14-9。文中示例围绕entry/src/main/ets/views/experiment/ExperimentSimPage.etsentry/src/main/ets/model/Experiment.etsentry/src/main/ets/model/ExperimentFeedback.ets中已经存在的 DNA 实验流程,不虚构设备能力或后台服务。

DNA 提取类实验页最容易出错的地方,不是某个按钮没有响应,而是步骤、参数、动画和结果反馈没有被同一套状态约束。用户可能先调裂解液浓度,再启动水果 DNA 提取;也可能切到 DNA 分层离心或 PCR 扩增。如果页面只把这些流程当作三张静态说明图,结果页很容易出现两个问题:一个是跳步后仍然显示完成结果,另一个是反馈字段沿用上一类实验的语义,比如把 PCR 的非特异条带写成离心混浊。

细胞工坊的做法更接近一个小型实验引擎:实验模型定义参数边界,模拟页持有运行状态,进度驱动阶段变化,指标函数按expId分支计算,Canvas 按同一个expId绘制不同场景,最后再把结果参数交给反馈页。本文拆解这条链路,重点回答一个实际工程问题:在 HarmonyOS ArkTS 应用里,如何让 DNA 提取流程既可交互,又不因为跳步、参数错配、反馈复用而产生错误结果。

本文会解决四个具体问题:

  • 如何在Experiment.ets中把水果 DNA 提取、DNA 分层离心、PCR 扩增定义成可复核的模型。
  • 如何用progressisRunningisFinishedparamValues约束页面步骤,避免运行中修改参数或未完成就生成结果。
  • 如何让温度、活性、污染、成功率这些指标随不同 DNA 实验语义变化,而不是复用同一套通用公式。
  • 如何让 Canvas 场景、反馈表头、结论模板和本地记录都跟随同一个expId,保证结果页可追溯。

一、先把 DNA 流程定义成模型,而不是写死在页面里

如果把“提取水果 DNA”的名称、参数滑块范围、图标和分类直接写在页面组件中,后面增加 DNA 分层离心、PCR 扩增时就会出现大量条件判断。细胞工坊把实验入口集中放在Experiment.ets,每个实验都有稳定的id、展示名称、说明、分类、等级、图标和参数数组。

DNA 相关的三组模型可以抽象成下面的结构:

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[] }

这段模型的边界很清楚:它只负责描述实验,不负责运行实验。页面拿到params后生成滑块,指标函数通过paramValues读取当前值,反馈页通过expId决定表头和结论。这样做的好处是,一个 DNA 实验的参数含义从入口到结果页都可以被同一个模型追踪。

当前源码里至少有三类 DNA 流程:

实验 ID名称参数 1参数 2工程含义
fruit_dna_extraction提取水果DNA裂解液浓度 20%-100%酒精温度 -10℃-15℃控制絮状物析出、杂质和产率
dna_layeringDNA分层离心离心转速 1000-8000 rpm离心时间 1-12 min控制分层度、混浊和纯度
pcr_amplificationPCR基因扩增循环次数 10-40 次退火温度 45℃-70℃控制扩增量、非特异条带和纯度

这里最关键的不是参数多,而是参数名称进入了后续查询逻辑。例如ExperimentSimPage里的getParamValue('转速', 4200)getParamValue('循环', 25)都依赖参数名称中包含对应关键词。如果后续改名,需要同步评估这些查询点,否则 UI 仍能显示滑块,但模拟场景会读不到真实参数。

二、路由进入后先初始化实验,避免页面带着旧状态运行

实验页从路由参数进入,aboutToAppear()会读取expIdexpName、知识点标题和摘要,然后调用initExperiment()resetExperiment()loadFavoriteState()。这个顺序能解决一个常见问题:同一个页面组件复用时,旧实验的paramDefsprogress不能带到新实验里。

源码中的关键链路可以简化为:

aboutToAppear(): void { const params = router.getParams() as SimRouterParams | undefined if (params?.expId) { this.expId = params.expId } if (params?.expName) { this.title = params.expName } this.initExperiment() this.resetExperiment() this.loadFavoriteState() } private currentExperiment(): Experiment { const found = getAllExperiments().find((item: Experiment) => item.id === this.expId) return found ? found : getAllExperiments()[0] } private initExperiment(): void { const exp = this.currentExperiment() this.title = exp.name this.paramDefs = exp.params this.paramValues = this.paramDefs.map((p: ExperimentParam) => p.defaultValue) }

这段代码有三个工程意图。

第一,expId是主键。页面标题可以展示,分类可以参与结果页,但真正决定参数、指标、动画、反馈模板的是expId

第二,参数值不是随便创建的数组,而是从当前实验的paramDefs映射出默认值。水果 DNA 提取的默认值是裂解液浓度 70%、酒精温度 0℃;DNA 分层离心默认是 4200 rpm、5 min;PCR 默认是 25 次循环、58℃退火温度。

第三,currentExperiment()提供了兜底。如果路由传入未知expId,页面不会直接崩溃,而是回到第一个实验。这个兜底对上线应用很实用,但也意味着测试时要覆盖非法路由参数,否则可能把路由错误掩盖成默认实验。

三、步骤状态用progress表达,按钮只改变运行态

DNA 提取流程在 UI 上看起来像“器材准备、样本处理、动态观察、自动检测、实验完成”几个阶段,但源码没有把它拆成多个页面,而是用progress统一驱动。startTimer()每 16ms 推进一次进度,getStageLabel()按进度区间返回阶段文案。

private startTimer(): void { this.stopTimer() this.timerId = setInterval(() => { this.progress = Math.min(1, this.progress + 0.0045 * this.speed) this.updateDisplay() this.drawCanvas() this.persistLearningTime(false) if (this.progress >= 1) { this.isRunning = false this.isFinished = true this.stopTimer() this.persistLearningTime() DataStore.incrementExperimentCount() this.persistRecord() } }, 16) } private getStageLabel(): string { if (this.progress < 0.18) return '器材准备' if (this.progress < 0.42) return '样本处理' if (this.progress < 0.72) return '动态观察' if (this.progress < 1) return '自动检测' return '实验完成' }

这套状态设计比单独的“下一步”按钮更稳,原因是所有下游逻辑都读同一个progress

状态字段作用防止的问题
progress表示实验推进程度和阶段防止动画、指标、结果页各自维护进度
isRunning控制运行中状态防止运行中随意改参数
isFinished控制结果入口展示防止未完成就进入反馈页
speed控制推进倍率允许加速演示,但不破坏阶段边界

对 DNA 提取这类流程来说,这一点很重要。裂解液浓度或退火温度应在实验开始前确定;一旦进入运行中,用户再改参数会导致指标曲线和最终反馈难以解释。源码里的 Slider 使用.enabled(!this.isRunning),正是用运行态限制参数改动。

四、温度不是公共字段,而是按实验语义计算

在生物实验模拟里,“温度”不一定来自同一个参数。水果 DNA 提取关心酒精温度,PCR 关心退火温度,DNA 分层离心则更像低温离心场景。ExperimentSimPagegetTemperature()先尝试读取包含“温度”的显式参数;如果没有,再按expId给出业务默认值。

private getTemperature(): number { const explicit = this.getParamValue('温度', -999) if (explicit > -100) return explicit switch (this.expId) { case 'fruit_dna_extraction': return this.rawParam(1, 0) case 'pcr_amplification': return this.rawParam(1, 58) case 'dna_layering': return 4 default: return 37 } }

这段逻辑看起来简单,但能避免一个非常具体的错误:如果统一把第二个参数当“温度”,DNA 分层离心的第二个参数是“离心时间”,结果页就会显示离心时间对应的温度。源码通过case 'dna_layering': return 4明确把分层离心固定成 4℃,避免了参数语义错配。

更稳的扩展方式,是把“展示温度”看成计算指标,而不是模型的固定字段。不同实验如果没有温度概念,可以返回环境默认值;如果有,就从明确的参数中读取。这样页面展示的“温度”不会阻止新增实验,也不会要求所有实验都有同名参数。

五、污染、活性、成功率要分支计算,不能复用一个公式

DNA 提取流程的反馈质量取决于三类指标:活性、污染和成功率。源码没有把所有实验套进同一公式,而是先用normParam()把参数归一化到 0-1,再按实验 ID 分支。

水果 DNA 提取的污染和活性逻辑可以简化为:

case 'fruit_dna_extraction': contamination = clamp(15 + (1 - p0) * 20 + p1 * 18 + progress * 8, 0, 100) activity = clamp(35 + p0 * 35 + (1 - p1) * 22 + progress * 20, 0, 100) successRate = clamp(40 + p0 * 26 + (1 - p1) * 18 + progress * 14, 0, 100)

其中p0对应裂解液浓度,p1对应酒精温度。浓度越高,絮状物析出表现越好;酒精温度越低,DNA 析出更稳定。这和反馈模板中的建议一致:如果絮状量偏低,就提示提高裂解液浓度或降低酒精温度。

DNA 分层离心的逻辑不同:

case 'dna_layering': contamination = clamp((1 - p0) * 40 + (1 - p1) * 18 + progress * 10, 0, 100) activity = clamp(30 + p0 * 42 + p1 * 18 + progress * 15, 0, 100) successRate = clamp(35 + p0 * 32 + p1 * 18 + progress * 12, 0, 100)

这里p0是离心转速,p1是离心时间。转速低、时间短,混浊更高;转速和时间提升后,分层度和纯度提高。注意这不是严格生化模型,而是教学模拟中的交互映射,适合用来让学生理解参数变化对结果趋势的影响。

PCR 扩增又是另一类约束:

case 'pcr_amplification': contamination = clamp((1 - Math.abs(p1 - 0.52)) * -20 + 35 + p0 * 12 + progress * 8, 0, 100) activity = clamp(25 + p0 * 50 + (1 - Math.abs(p1 - 0.52)) * 22 + progress * 12, 0, 100) successRate = clamp(40 + (1 - Math.abs(p1 - 0.52)) * 24 + p0 * 18 - Math.max(0, p0 - 0.7) * 22 + progress * 12, 0, 100)

PCR 的核心是循环次数和退火温度。循环次数提升会增加扩增量,但过高循环也可能拉低最终纯度;退火温度偏离合适区间时,非特异条带会上升。源码用Math.abs(p1 - 0.52)表达退火温度接近目标区间时结果更好,再通过Math.max(0, p0 - 0.7)限制过多循环。

六、Canvas 场景跟随expId,不要让三个 DNA 流程共用一张动画

DNA 类实验如果只换标题不换场景,用户很难判断自己正在做的是提取、离心还是扩增。源码在drawCanvas()里按expId分发绘制函数:

switch (this.expId) { case 'fruit_dna_extraction': this.drawDnaScene(ctx, w, h) break case 'dna_layering': this.drawCentrifugeScene(ctx, w, h) break case 'pcr_amplification': this.drawPcrScene(ctx, w, h) break }

drawDnaScene()绘制两条随进度摆动的 DNA 曲线和底部析出进度条,适合表达水果 DNA 提取中絮状物逐步析出。drawCentrifugeScene()绘制离心机和四个旋转样品管,并在底部显示当前转速。drawPcrScene()则根据循环数推导拷贝数量,用多个 DNA 小片段表达扩增过程。

这类分发函数适合放在页面内部,因为它依赖 Canvas 上下文、尺寸和当前进度;但业务含义不能散落。建议扩展时遵守一个规则:新增实验 ID 后,同时检查四个地方是否同步更新。

更新点文件缺失后的表现
实验模型Experiment.ets列表无法进入或参数为空
指标分支ExperimentSimPage.ets成功率、污染、活性不符合实验语义
绘制分支ExperimentSimPage.ets页面显示默认或错误场景
反馈模板ExperimentFeedback.ets结果页字段和建议不匹配

这个表很适合放进代码评审清单。因为 DNA 实验不是单一页面,它是一条横跨模型、页面、绘制、反馈和存储的链路。

七、结果参数统一从buildResultParams()生成

实验完成后,页面才显示“查看实验反馈”按钮。点击时不是直接把页面上的文本拼进结果页,而是调用buildResultParams()生成稳定对象,再通过路由传给ExperimentResultPage

private buildResultParams(): ResultRouterParams { const exp = this.currentExperiment() return { expId: this.expId, expName: this.title, category: exp.category, progress: Math.round(this.progress * 100), successRate: Math.round(this.getSuccessRate()), activity: Math.round(this.getActivity()), contamination: Math.round(this.getContamination()), temperature: Math.round(this.getTemperature() * 10) / 10, stage: this.getStageLabel(), paramSummary: this.getParamSummary() } }

这里的字段可以分成三层:

字段来源作用
expIdexpNamecategory实验模型和页面状态决定反馈模板和展示归属
progressstage步骤状态表达实验是否完整推进
successRateactivitycontaminationtemperatureparamSummary指标函数与参数摘要形成反馈页可解释结果

这比在反馈页重新读取参数更稳定。结果页只消费已经完成的快照,不参与运行时计算,能减少“进入结果页后参数又变了”的问题。

八、反馈表头和结论模板也必须按 DNA 实验区分

ExperimentFeedback.ets做了一件很关键的事:同样是四列反馈表,它没有用“活性、污染、评分”覆盖所有实验,而是按expId返回不同表头。

export function getFeedbackSchema(expId: string): FeedbackSchema { switch (expId) { case 'fruit_dna_extraction': return { headers: ['进度', '絮状量', '杂质', '产率'], unitOverrides: ['%', '%', '%', ''] } case 'dna_layering': return { headers: ['转速段', '分层度', '混浊', '纯度'], unitOverrides: ['%', '%', '%', ''] } case 'pcr_amplification': return { headers: ['循环', '扩增量', '非特异', '纯度'], unitOverrides: ['%', '%', '%', ''] } default: return { headers: ['进度', '活性', '污染', '评分'], unitOverrides: ['%', '%', '%', ''] } } }

这一层防止了结果语义串台。举例来说,activity在水果 DNA 提取里可解释为絮状物析出量,在 DNA 分层离心里可解释为分层度,在 PCR 中则对应扩增量。如果反馈页只显示“活性”,用户能看到数字,却无法理解这个数字代表什么。

结论模板同样按实验区分:

case 'fruit_dna_extraction': return `在${ctx.paramSummary}下完成${p}%流程,絮状物析出量${a}%,杂质残留${c}%,DNA产率${s}%。${a < 70 ? '建议提高裂解液浓度或降低酒精温度,加快DNA析出。' : 'DNA絮状物清晰可见,可继续比较不同水果之间的产率差异。'}` case 'dna_layering': return `离心至${p}%阶段,分层清晰度${a}%、混浊${c}%,最终样本纯度${s}%。${c >= 30 ? '混浊偏高,建议延长离心时间或提高转速。' : '分层界面清晰,适合下一步取样分析。'}` case 'pcr_amplification': return `循环推进到${p}%,目标片段扩增量${a}%、非特异条带${c}%,扩增产物纯度${s}%。${c >= 30 ? '退火温度偏低产生非特异扩增,建议提高2-4℃。' : 'PCR反应特异性良好,可减少循环次数避免引物二聚体。'}`

这段模板有一个值得保留的设计:建议不是固定文案,而是由指标触发。絮状量低就建议调整裂解液或酒精温度,混浊偏高就建议延长离心时间或提高转速,PCR 非特异偏高就建议提高退火温度。这样的反馈才像实验系统,而不是简单的完成页。

九、本地记录保存参数摘要,便于复盘

实验完成时,startTimer()会调用DataStore.incrementExperimentCount()persistRecord()。记录里保存了实验 ID、标题、分类和参数摘要。

private persistRecord(): void { const exp = this.currentExperiment() const parts: string[] = [] for (let i = 0; i < this.paramDefs.length; i++) { const def = this.paramDefs[i] const val = this.paramValues[i] ?? def.defaultValue const unit = def.unit ? ' ' + def.unit : '' parts.push(`${def.name}:${val}${unit}`) } const summary = parts.length > 0 ? parts.join(';') : '默认实验条件' const rec: StoredExperimentRecord = buildRecord(this.expId, this.title, exp.category, summary) DataStore.appendRecord<StoredExperimentRecord>(rec) }

这段逻辑对 DNA 实验尤其有用。一次水果 DNA 提取如果只保存“完成”,复盘价值很低;如果保存“裂解液浓度:70%;酒精温度:0℃”,下次就能比较不同条件下的产率差异。PCR 也一样,循环次数和退火温度必须被记录下来,否则“非特异条带偏高”的建议没有可追溯依据。

在 HarmonyOS 应用里,类似记录不建议由 UI 直接拼成散乱字符串到处传。当前源码把记录构造收敛在persistRecord(),已经比每个按钮事件里直接写存储更容易维护。后续如果要做统计页,可以再把paramSummary拆成结构化字段或单独的ExperimentRecord模型版本。

十、运行中禁用参数滑块,避免同一轮实验出现两个条件

DNA 提取流程中,最容易被忽略的是运行中参数变更。源码中的 Slider 绑定了paramValues[index],并通过.enabled(!this.isRunning)限制运行状态。

Slider({ value: this.paramValues[index], min: param.min, max: param.max, step: param.step, style: SliderStyle.OutSet }) .onChange((value: number) => { this.paramValues[index] = Math.round(value * 100) / 100 this.paramValues = [...this.paramValues] if (!this.isRunning) { this.updateDisplay() this.drawCanvas() } }) .enabled(!this.isRunning)

这段代码同时做了两件事:

  • 参数变化后通过this.paramValues = [...this.paramValues]触发 ArkUI 状态刷新。
  • 只有未运行时才立即刷新指标和画布,运行中则不允许继续调整参数。

如果放开运行中修改,就会出现同一轮实验前半段使用 70% 裂解液、后半段使用 100% 裂解液,最终反馈却只展示最后一个参数摘要的情况。教学类实验可以简化真实过程,但不能让结果解释失去一致性。

十一、验证清单:确认 DNA 流程没有串台

开发或改造这类实验页时,可以按下面清单做最小验证:

验证项操作期望结果
模型加载从列表进入fruit_dna_extraction页面标题为提取水果DNA,参数为裂解液浓度和酒精温度
阶段推进点击开始并观察指标阶段从器材准备推进到实验完成
运行中锁定运行中查看参数滑块滑块不可编辑
场景分发分别进入三个 DNA 实验分别看到 DNA 双链、离心机、PCR 扩增场景
结果快照完成后进入反馈页参数摘要、成功率、污染、温度和阶段来自buildResultParams()
反馈语义查看三类 DNA 反馈表头分别为絮状量/分层度/扩增量等业务字段
记录保存完成实验后查看记录记录包含实验名称、分类和参数摘要

如果只能做一次快速回归,优先覆盖“进入实验、开始、完成、进入反馈页”这条主路径。因为这条路径同时经过模型、状态、Canvas、指标、路由、反馈和存储。

十二、常见问题与排查

问题优先检查点修复建议
页面显示了错误参数Experiment.ets中对应idparams确认路由传入的expId正确,参数名称不要和指标查询关键词脱节
温度显示不符合实验getTemperature()分支对没有显式温度参数的实验增加固定业务温度或环境默认值
DNA 分层离心显示了 DNA 双链动画drawCanvas()switch检查dna_layering是否分发到drawCentrifugeScene()
PCR 反馈仍然显示活性/污染/评分getFeedbackSchema()pcr_amplification增加专属表头
反馈建议和参数不一致getConclusion()与指标函数保证建议条件引用的是同一组业务指标,例如非特异条带、混浊、絮状量
运行中修改参数导致结果难解释Slider.enabled(!this.isRunning)运行时锁定参数;如需动态调参,要记录调参时间线
完成后没有记录startTimer()persistRecord()检查progress >= 1分支是否执行,以及 DataStore 写入是否成功

排查时不要先改 UI 文案。DNA 流程的串台问题通常来自expId、参数名、指标分支和反馈模板不一致。先把这四处对齐,再看视觉和交互。

十三、可继续优化的方向

当前源码已经把 DNA 三类流程串成了可运行链路。如果后续要继续增强,可以考虑三点。

第一,把指标公式从页面中抽离到独立的ExperimentMetricService。现在getContamination()getActivity()getSuccessRate()都在页面组件内,阅读上比较集中,但随着实验数量增加,页面会越来越重。抽离后可以单独写单元测试,验证不同参数下的趋势是否符合预期。

第二,把paramSummary从字符串升级为结构化数组。当前字符串适合展示和记录,但如果后面要做“不同退火温度下 PCR 纯度对比”,结构化数据会更方便。

第三,为 DNA 实验增加更细的阶段标签。例如水果 DNA 提取可以拆成裂解、过滤、酒精沉淀、观察;PCR 可以拆成变性、退火、延伸。即使 UI 仍然只用一个progress,内部也能按实验类型返回更贴近业务的阶段文案。

总结

这篇文章拆解的不是 DNA 提取的理论步骤,而是 HarmonyOS ArkTS 应用中让实验流程可交互、可解释、可复盘的状态设计。细胞工坊的关键做法是把expId作为贯穿链路的主键:模型定义参数,页面初始化状态,进度驱动阶段,指标函数按实验语义计算,Canvas 场景跟随实验切换,反馈模板输出专属表头和建议,最后再把参数摘要写入本地记录。

对教学模拟类 HarmonyOS 应用来说,这套方法比堆叠说明卡片更可靠。只要新增实验时同步检查模型、指标、绘制、反馈和记录五个点,就能避免大多数跳步、串台和错误结果问题。