【细胞工坊|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 }这段逻辑让结果页的曲线具备业务含义。例如曲线类型代码含义典型实验sS 型增长细菌培养cube后期快速上升PCR 扩增、污染后期暴涨peak中段峰值有丝分裂、细胞周期lateRise滞后后拉升叶绿素层析platL快升后平台发酵、DNA 分层离心invSquare收敛下降混浊、误识别、杂光下降结果页并没有保存真实过程采样数组所以曲线不能被描述为真实实验数据。更准确的说法是它根据最终指标和实验类型生成教学模拟曲线。这个边界必须说清楚否则文章会夸大源码能力。六、Canvas 曲线绘制读取同一份tableData反馈曲线用 Canvas 绘制核心输入仍然是tableDataprivate 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 1UI 里两个文本按钮分别设置activeTabText(数据表格) .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。这就是职责分离的价值。九、实验结论复用反馈模型分享文本也复用同一结论结果页生成结论时会构造ConclusionContextprivate 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) }这点很重要。页面结论、分享描述和分享正文使用同一套结论来源用户不会看到“页面提示污染偏高分享文本却说实验稳定”的冲突。结果类页面经常被用户截图、分享或复制因此所有出口都应复用同一份结果解释。十、系统分享失败必须有用户提示和日志结果页使用 HarmonyOSShareKitconst 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 应用来说这比堆一个静态总结页更可靠也更容易维护。