【细胞工坊|05】HarmonyOS ArkTS 实验结果页实战:展示关键指标并支持重新实验
部分内容由AI辅助生成。本文面向 HarmonyOS 5.0 及以上版本,基于
细胞工坊项目真实源码展开,源码根目录为D:\huawei\one14-9。本文重点复核entry/src/main/ets/views/experiment/ExperimentResultPage.ets,并结合ExperimentFeedback.ets的表头和结论模型,不虚构源码里不存在的数据上报、云端分析或 AI 诊断能力。
实验模拟类应用的结果页不能只显示一个“完成”按钮。用户完成显微观察、DNA 提取、PCR 扩增或无菌操作后,真正关心的是三个问题:这次实验关键指标是什么,为什么得到这个结论,能不能带着这次反馈回去重新调整参数。结果页如果只做成静态总结卡片,就会把实验过程和下一轮操作断开。
细胞工坊 的 ExperimentResultPage 做的是一个前端闭环:从路由读取完成时传入的结果快照,构建表格数据,绘制反馈曲线,调用反馈模型生成结论,支持系统分享,并通过返回按钮回到实验页重新操作。它没有在结果页重新运行实验,也没有把结果上传到服务器;所有展示都来自本地快照和页面内计算,这符合一个离线教学实验应用的边界。

本文会解决四个具体问题:
- 如何用路由参数把实验完成时的关键指标传入结果页。
- 如何把
successRate、activity、contamination等最终值转成可读表格和曲线。 - 如何让结果页结论、分享文本和表格表头使用同一套反馈模型。
- 如何通过返回实验按钮形成“查看结果 -> 调整参数 -> 重新实验”的闭环。


一、结果页只消费完成快照,不重新计算实验来源
实际项目里,结果页最容易犯的错误是“再算一次”。模拟页已经根据实验参数和进度算出了成功率、活性、污染指数和温度,结果页如果又根据自己的逻辑重新计算,页面、分享内容和历史记录就可能出现不一致。
ExperimentResultPage 的入口参数很明确:
interface ResultRouterParams {
expId?: string
expName?: string
category?: string
progress?: number
successRate?: number
activity?: number
contamination?: number
temperature?: number
stage?: string
paramSummary?: string
}
这些字段来自上一页完成实验时构造的结果快照。结果页并不关心模拟页内部用了什么公式,也不反向读取实验页状态。它只需要知道这次实验是什么、完成到哪个阶段、关键指标是多少、参数摘要是什么。
这个设计能防止两类问题:
| 问题 | 如果结果页重算 | 当前快照方案 |
|---|---|---|
| 指标来源不一致 | 页面显示和分享文本可能不一致 | 所有展示读取同一组状态 |
| 回退后参数变化 | 结果页可能跟着旧组件状态变化 | 结果页持有完成时快照 |
| 新实验扩展 | 每页都要知道实验公式 | 结果页只处理通用展示 |
结果页不是实验引擎,它是完成结果的展示层。这个边界越清楚,后续扩展越稳。
二、aboutToAppear() 是快照落地的第一道边界
结果页在 aboutToAppear() 中读取路由参数,并把合法字段写入本页状态:
aboutToAppear(): void {
const params = router.getParams() as ResultRouterParams | undefined
if (params?.expId) this.expId = params.expId
if (params?.expName) this.expName = params.expName
if (params?.category) this.category = params.category
if (typeof params?.progress === 'number') this.progress = params.progress
if (typeof params?.successRate === 'number') this.successRate = params.successRate
if (typeof params?.activity === 'number') this.activity = params.activity
if (typeof params?.contamination === 'number') this.contamination = params.contamination
if (typeof params?.temperature === 'number') this.temperature = params.temperature
if (params?.stage) this.stage = params.stage
if (params?.paramSummary) this.paramSummary = params.paramSummary
this.tableData = this.buildTableData()
}
这里有两个细节值得保留。
第一,数值字段使用 typeof params?.xxx === 'number' 判断。这样可以避免字符串、空值或未传字段覆盖默认值。HarmonyOS 页面路由在实际项目中经常会遇到参数缺失,结果页不应该因为某个字段缺失就直接白屏。
第二,状态落地后立即调用 buildTableData()。这意味着表格和曲线基于同一组页面状态生成,而不是等到渲染时临时拼凑。后续如果要增加加载态或异常态,也能围绕这一处做守卫。
三、关键指标用页面状态保存,便于表格、曲线和分享复用
源码中结果页为关键指标提供了默认值:
@State expId: string = 'microscope_observation'
@State expName: string = '显微镜观察洋葱表皮'
@State category: string = '基础'
@State progress: number = 100
@State successRate: number = 96
@State activity: number = 94
@State contamination: number = 20
@State temperature: number = 37
@State stage: string = '实验完成'
@State paramSummary: string = '默认实验条件'
这些字段不是随便展示的文案,而是结果页的核心数据源:
| 字段 | 页面用途 |
|---|---|
expId |
选择表头、结论模板和曲线形状 |
expName |
页面标题、图表标题、分享标题 |
category |
分享文本中的实验分类 |
progress |
表格进度和结论进度 |
successRate |
最终评分、结论判断 |
activity |
曲线第一指标和实验表现 |
contamination |
曲线第二指标和风险判断 |
temperature |
结论和分享中的环境/实验条件 |
paramSummary |
解释这次结果对应的参数组合 |
结果页的核心不是卡片数量,而是这些字段能否被一致地复用。页面标题、表格、曲线、结论和分享文本都读取这些状态,用户看到的结果就不会分裂。
四、表格数据不是直接写死,而是根据最终值生成过程点
结果页默认表格有 6 行,从 0% 到 100%:
@State tableData: number[][] = [
[0, 62, 8, 74],
[20, 70, 10, 80],
[40, 78, 12, 86],
[60, 84, 15, 89],
[80, 90, 18, 93],
[100, 94, 20, 96]
]
但进入页面后,tableData 会被 buildTableData() 重新构建。这个函数读取最终 activity、contamination、successRate,按进度点生成过程数据:
private buildTableData(): number[][] {
const result: number[][] = []
const a = this.activity
const c = this.contamination
const s = this.successRate
for (let i = 0; i <= 5; i++) {
const progress = i * 20
const t = progress / 100
let v1 = a * (0.58 + t * 0.42)
let v2 = c * (0.36 + t * 0.64)
let v3 = s * (0.74 + t * 0.26)
result.push([
progress,
Math.round(this.clamp(v1, 0, 100)),
Math.round(this.clamp(v2, 0, 100)),
Math.round(this.clamp(v3, 0, 100))
])
}
return result
}
这段默认逻辑的意义是:即使某个实验没有专属曲线分支,结果页也能展示一条基本可读的指标走势。它不是科学实验真实采样点,而是教学模拟中的过程展示。
更重要的是,源码为不同 expId 加了曲线分支。显微观察、细菌培养、无菌操作、PCR、DNA 分层、细胞周期等实验有不同曲线形状,这让结果页不会把所有实验都画成同一种线。
五、曲线形状表达实验过程,不只是装饰图
结果页用 shape() 把 0-1 的进度映射成不同曲线:
private shape(kind: string, t: number): number {
if (t <= 0) return 0
if (t >= 1) return 1
if (kind === 'linear') return t
if (kind === 'sqrt') return Math.sqrt(t)
if (kind === 'square') return t * t
if (kind === 'cube') return t * t * t
if (kind === 's') return 1 / (1 + Math.exp(-(t - 0.5) * 8))
if (kind === 'peak') return Math.sin(t * Math.PI)
if (kind === 'lateRise') return t < 0.3 ? t * 0.4 : 0.12 + (t - 0.3) / 0.7 * 0.88
if (kind === 'platL') return 1 - Math.exp(-t * 3)
if (kind === 'invSquare') return 1 - (1 - t) * (1 - t)
return t
}
这段逻辑让结果页的曲线具备业务含义。例如:
| 曲线类型 | 代码含义 | 典型实验 |
|---|---|---|
s |
S 型增长 | 细菌培养 |
cube |
后期快速上升 | PCR 扩增、污染后期暴涨 |
peak |
中段峰值 | 有丝分裂、细胞周期 |
lateRise |
滞后后拉升 | 叶绿素层析 |
platL |
快升后平台 | 发酵、DNA 分层离心 |
invSquare |
收敛下降 | 混浊、误识别、杂光下降 |
结果页并没有保存真实过程采样数组,所以曲线不能被描述为真实实验数据。更准确的说法是:它根据最终指标和实验类型生成教学模拟曲线。这个边界必须说清楚,否则文章会夸大源码能力。
六、Canvas 曲线绘制读取同一份 tableData
反馈曲线用 Canvas 绘制,核心输入仍然是 tableData:
private drawChart(): void {
if (this.chartW === 0) return
const ctx = this.chartCtx
const w = this.chartW
const h = this.chartH
const padL = 46
const padR = 20
const padT = 34
const padB = 38
const plotW = w - padL - padR
const plotH = h - padT - padB
ctx.clearRect(0, 0, w, h)
ctx.fillStyle = '#111827'
ctx.fillRect(0, 0, w, h)
}
随后它绘制坐标轴、横向网格、折线和节点:
ctx.strokeStyle = AppColors.ACCENT_GREEN
ctx.lineWidth = 3
ctx.beginPath()
for (let i = 0; i < this.tableData.length; i++) {
const row = this.tableData[i]
const x = padL + (row[0] / 100) * plotW
const y = padT + plotH - (row[1] / 100) * plotH
if (i === 0) ctx.moveTo(x, y)
else ctx.lineTo(x, y)
}
ctx.stroke()
当前源码只绘制了 row[1] 这一条主曲线,也就是第一业务指标。表格里仍然保留 row[2] 和 row[3],用于展示风险指标和最终评分。
这有一个现实取舍:单条曲线更清晰,不会在手机屏幕上把三条线挤在一起;但如果后续要做专业数据分析,可以增加图例,并把 row[2]、row[3] 也画出来。
七、数据表格和反馈曲线用 Tab 分流,减少一屏信息压力
结果页通过 activeTab 在“数据表格”和“反馈曲线”之间切换:
@State activeTab: number = 1
UI 里两个文本按钮分别设置 activeTab:
Text('数据表格')
.fontColor(this.activeTab === 0 ? '#07111F' : AppColors.PRIMARY)
.backgroundColor(this.activeTab === 0 ? AppColors.ACCENT_GREEN : AppColors.CARD_BG)
.onClick(() => { this.activeTab = 0 })
Text('反馈曲线')
.fontColor(this.activeTab === 1 ? '#07111F' : AppColors.PRIMARY)
.backgroundColor(this.activeTab === 1 ? AppColors.ACCENT_GREEN : AppColors.CARD_BG)
.onClick(() => { this.activeTab = 1 })
这是一个适合移动端的折中。表格适合复查具体数字,曲线适合观察趋势。如果强行把表格、曲线、结论、分享、返回按钮都塞在同一个首屏,结果页会变成信息堆叠。
对 HarmonyOS 多设备适配来说,后续可以在平板或 2in1 上把表格和曲线并排显示,但手机端保持 Tab 更稳。
八、表头来自反馈模型,避免结果页写死列名
结果页没有固定写“进度、活性、污染、评分”,而是调用反馈模型:
private currentSchema(): FeedbackSchema {
return getFeedbackSchema(this.expId)
}
private formatCell(value: number, unit: string): string {
return `${value.toFixed(0)}${unit}`
}
表格头部渲染:
ForEach(this.currentSchema().headers, (h: string) => {
Text(h)
.fontSize(12)
.fontWeight(AppFonts.WEIGHT_MEDIUM)
.fontColor(AppColors.TEXT_SECONDARY)
.layoutWeight(1)
.textAlign(TextAlign.Center)
})
这让结果页可以复用到不同实验。显微观察显示“视野、清晰度、杂光、识别率”,DNA 分层显示“转速段、分层度、混浊、纯度”,PCR 显示“循环、扩增量、非特异、纯度”。
如果新实验接入后表头不对,问题通常不在结果页,而在 ExperimentFeedback.ets 没有为该 expId 配置 schema。这就是职责分离的价值。
九、实验结论复用反馈模型,分享文本也复用同一结论
结果页生成结论时,会构造 ConclusionContext:
private getConclusion(): string {
const ctx: ConclusionContext = {
expName: this.expName,
stage: this.stage,
paramSummary: this.paramSummary,
progress: this.progress,
successRate: this.successRate,
activity: this.activity,
contamination: this.contamination,
temperature: this.temperature
}
return getConclusion(this.expId, ctx)
}
分享文本也调用同一个 getConclusion():
private buildShareText(): string {
return [
`细胞工坊 - ${this.expName}`,
`实验分类:${this.category}`,
`实验参数:${this.paramSummary}`,
`流程进度:${this.progress}%`,
`成功率:${this.successRate}%`,
`样本活性:${this.activity}%`,
`污染指数:${this.contamination}%`,
`温度:${this.temperature}℃`,
`结果分析:${this.getConclusion()}`
].join('\n')
}
这点很重要。页面结论、分享描述和分享正文使用同一套结论来源,用户不会看到“页面提示污染偏高,分享文本却说实验稳定”的冲突。
结果类页面经常被用户截图、分享或复制,因此所有出口都应复用同一份结果解释。
十、系统分享失败必须有用户提示和日志
结果页使用 HarmonyOS ShareKit:
const record: systemShare.SharedRecord = {
utd: uniformTypeDescriptor.UniformDataType.PLAIN_TEXT,
title: `${this.expName}结果分析`,
description: this.getConclusion(),
content: this.buildShareText()
}
const data = new systemShare.SharedData(record)
const controller = new systemShare.ShareController(data)
如果宿主上下文为空,或系统分享面板失败,页面不会静默吞掉错误:
private showShareFailureToast(): void {
try {
this.getUIContext().getPromptAction().showToast({ message: '系统分享暂不可用,请稍后再试' })
} catch (err) {
const message = err instanceof Error ? err.message : JSON.stringify(err)
hilog.error(LOG_DOMAIN, LOG_TAG, 'Show share failure toast failed: %{public}s', message)
}
}
这符合结果页的用户体验要求:分享不是核心实验流程,但失败时也要让用户知道发生了什么。日志使用 hilog.error,不会把错误直接暴露给用户。
十一、重新实验靠返回链路,不在结果页复制实验逻辑
结果页底部有两个按钮:
Button('保存记录')
.fontSize(14)
.fontColor(AppColors.PRIMARY)
.backgroundColor(AppColors.CARD_BG)
Button('返回实验')
.fontSize(14)
.fontColor('#07111F')
.backgroundColor(AppColors.ACCENT_GREEN)
.onClick(() => {
router.back()
})
返回实验 的实现非常克制:只调用 router.back()。它不在结果页重新创建实验,不复制参数调整 UI,也不试图重置模拟页。结果页的职责是告诉用户本次结果;如果用户要重新实验,就回到实验页重新操作。
这能避免两类重复:
- 结果页不需要知道实验页的按钮、滑块、Canvas 和计时器状态。
- 实验页仍然是唯一的参数调整和运行入口。
需要注意的是,源码里的 保存记录 按钮目前只有样式,没有绑定保存动作。不能在文章中声称它已经保存结果;当前真实可复核的是“按钮存在”,保存记录逻辑实际在模拟页完成时已经通过 persistRecord() 触发。
十二、结果页适配与可读性的检查点
结果页由标题栏、Tab、Canvas 或 List、结论卡片、底部按钮组成。上线前至少检查这些点:
| 检查项 | 操作 | 期望结果 |
|---|---|---|
| 参数缺失 | 直接进入结果页或缺字段路由 | 使用默认值,不白屏 |
| 表格 Tab | 点击数据表格 | 四列表头与当前实验语义一致 |
| 曲线 Tab | 点击反馈曲线 | Canvas 成功绘制主指标折线 |
| 长实验名称 | 使用较长 expName |
标题居中且不遮挡分享按钮 |
| 长结论 | 污染偏高或 PCR 非特异场景 | 结论卡片文字可读,不压底部按钮 |
| 分享失败 | 模拟 host context 为空 | toast 显示“系统分享暂不可用,请稍后再试” |
| 返回实验 | 点击返回实验 | 返回上一页,不在结果页重建实验 |
如果只做一次快速验证,优先覆盖“完成实验 -> 查看结果 -> 切换表格/曲线 -> 分享 -> 返回实验”。这条路径覆盖了结果页的主要职责。
十三、常见问题与修复建议
| 现象 | 首查位置 | 原因 | 修复 |
|---|---|---|---|
| 结果页指标是默认值 | 路由传参 | 模拟页没有传入对应字段 | 检查 buildResultParams() 和 ResultRouterParams 字段一致性 |
| 表格列名不符合实验 | getFeedbackSchema() |
新实验没有专属 schema | 为 expId 配置表头和单位 |
| 曲线没有显示 | chartW、onReady() |
Canvas 未 ready 或尺寸为 0 | 保留 if (this.chartW === 0) return,检查容器高度 |
| 分享点击没反应 | shareResult() |
host context 为空或系统分享失败 | 保留 toast 和 hilog.error |
| 返回后状态异常 | 路由栈 | 结果页不是从实验页进入 | 确认入口路径,必要时提供显式重开实验路由 |
| 保存记录按钮无效果 | 当前按钮未绑定 | 源码只有 UI 样式 | 不在结果页声明已保存;如要支持,补具体事件 |
排查时先确认结果快照。快照正确,表格和结论才有意义;快照错误,页面样式再好也只是展示了错误结果。
总结
细胞工坊 的实验结果页实现了一条清晰的 HarmonyOS ArkTS 前端链路:从路由读取完成快照,把关键指标落到状态,生成表格数据,使用 Canvas 绘制反馈曲线,通过反馈模型输出结论,支持 ShareKit 分享,并通过 router.back() 返回实验页重新操作。
这套实现的重点不是复杂,而是边界清楚。结果页不重新运行实验,不夸大数据来源,不把“保存记录”按钮说成已经实现的业务动作。它只负责把已经完成的实验结果展示清楚,并提供下一轮操作入口。对教学实验类 HarmonyOS 应用来说,这比堆一个静态总结页更可靠,也更容易维护。
更多推荐


所有评论(0)