【细胞工坊|03】HarmonyOS ArkTS DNA 提取流程实战:用步骤状态避免跳步和错误结果
部分内容由AI辅助生成。本文面向 HarmonyOS 5.0 及以上版本,基于
细胞工坊项目真实源码展开,源码根目录为D:\huawei\one14-9。文中示例围绕entry/src/main/ets/views/experiment/ExperimentSimPage.ets、entry/src/main/ets/model/Experiment.ets和entry/src/main/ets/model/ExperimentFeedback.ets中已经存在的 DNA 实验流程,不虚构设备能力或后台服务。
DNA 提取类实验页最容易出错的地方,不是某个按钮没有响应,而是步骤、参数、动画和结果反馈没有被同一套状态约束。用户可能先调裂解液浓度,再启动水果 DNA 提取;也可能切到 DNA 分层离心或 PCR 扩增。如果页面只把这些流程当作三张静态说明图,结果页很容易出现两个问题:一个是跳步后仍然显示完成结果,另一个是反馈字段沿用上一类实验的语义,比如把 PCR 的非特异条带写成离心混浊。
细胞工坊 的做法更接近一个小型实验引擎:实验模型定义参数边界,模拟页持有运行状态,进度驱动阶段变化,指标函数按 expId 分支计算,Canvas 按同一个 expId 绘制不同场景,最后再把结果参数交给反馈页。本文拆解这条链路,重点回答一个实际工程问题:在 HarmonyOS ArkTS 应用里,如何让 DNA 提取流程既可交互,又不因为跳步、参数错配、反馈复用而产生错误结果。

本文会解决四个具体问题:
- 如何在
Experiment.ets中把水果 DNA 提取、DNA 分层离心、PCR 扩增定义成可复核的模型。 - 如何用
progress、isRunning、isFinished、paramValues约束页面步骤,避免运行中修改参数或未完成就生成结果。 - 如何让温度、活性、污染、成功率这些指标随不同 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_layering |
DNA分层离心 | 离心转速 1000-8000 rpm | 离心时间 1-12 min | 控制分层度、混浊和纯度 |
pcr_amplification |
PCR基因扩增 | 循环次数 10-40 次 | 退火温度 45℃-70℃ | 控制扩增量、非特异条带和纯度 |
这里最关键的不是参数多,而是参数名称进入了后续查询逻辑。例如 ExperimentSimPage 里的 getParamValue('转速', 4200)、getParamValue('循环', 25) 都依赖参数名称中包含对应关键词。如果后续改名,需要同步评估这些查询点,否则 UI 仍能显示滑块,但模拟场景会读不到真实参数。
二、路由进入后先初始化实验,避免页面带着旧状态运行
实验页从路由参数进入,aboutToAppear() 会读取 expId、expName、知识点标题和摘要,然后调用 initExperiment()、resetExperiment()、loadFavoriteState()。这个顺序能解决一个常见问题:同一个页面组件复用时,旧实验的 paramDefs 或 progress 不能带到新实验里。
源码中的关键链路可以简化为:
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 分层离心则更像低温离心场景。ExperimentSimPage 的 getTemperature() 先尝试读取包含“温度”的显式参数;如果没有,再按 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()
}
}
这里的字段可以分成三层:
| 字段 | 来源 | 作用 |
|---|---|---|
expId、expName、category |
实验模型和页面状态 | 决定反馈模板和展示归属 |
progress、stage |
步骤状态 | 表达实验是否完整推进 |
successRate、activity、contamination、temperature、paramSummary |
指标函数与参数摘要 | 形成反馈页可解释结果 |
这比在反馈页重新读取参数更稳定。结果页只消费已经完成的快照,不参与运行时计算,能减少“进入结果页后参数又变了”的问题。
八、反馈表头和结论模板也必须按 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 中对应 id 的 params |
确认路由传入的 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 应用来说,这套方法比堆叠说明卡片更可靠。只要新增实验时同步检查模型、指标、绘制、反馈和记录五个点,就能避免大多数跳步、串台和错误结果问题。
更多推荐



所有评论(0)