部分内容由AI辅助生成。本文面向 HarmonyOS 5.0 及以上版本,基于 细胞工坊 项目真实源码展开,源码根目录为 D:\huawei\one14-9。重点复核 entry/src/main/ets/model/ExperimentFeedback.etsentry/src/main/ets/views/experiment/ExperimentResultPage.ets,不虚构项目中不存在的后台分析服务、AI 诊断能力或实验硬件能力。

实验类应用的反馈页很容易写成“恭喜完成实验”的静态结果页。这样做的问题是:用户看不到操作哪里稳定、哪里有风险,也不知道下一次应该调整哪个参数。更严重的是,不同实验的结果语义并不一样。同样一个 activity 数值,在显微观察里可以理解为清晰度,在 DNA 提取里是絮状量,在 PCR 里又接近扩增量。如果反馈页只显示固定列名,结果不会报错,但会误导用户。

细胞工坊 的源码把反馈模型拆成两层:ExperimentFeedback.ets 负责不同实验的表头和结论模板,ExperimentResultPage.ets 负责读取结果快照、生成曲线数据、渲染表格/图表以及处理系统分享失败。它没有实现一个独立的“错误枚举系统”,但已经通过污染、活性、成功率、温度和系统调用失败建立了真实可复核的成功、警告和错误提示边界。

封面:实验反馈模型

本文解决四个实际问题:

  • 怎样用 FeedbackSchema 避免不同实验共用错误表头。
  • 怎样用 ConclusionContext 把结果快照传给结论模板。
  • 怎样用阈值分支区分成功建议、风险警告和错误操作提示。
  • 怎样在结果页处理分享失败,避免系统能力异常时静默失败。

流程图:结果快照到反馈输出

结构图:反馈模型职责

一、反馈模型的边界:业务语义不放在页面表格里

结果页通常有两种写法。一种是在页面里直接写表头和结论,短期很快,但一旦实验数量增多,页面会充满 if expId === ...。另一种是把业务语义抽到模型层,页面只负责渲染。细胞工坊 采用的是后者。

ExperimentFeedback.ets 里的核心接口很小:

export interface FeedbackSchema {
  headers: string[]
  unitOverrides: string[]
}

export interface ConclusionContext {
  expName: string
  stage: string
  paramSummary: string
  progress: number
  successRate: number
  activity: number
  contamination: number
  temperature: number
}

FeedbackSchema 只描述表格列名和单位,不关心 UI 的颜色、Tab 或 Canvas。ConclusionContext 则是结果页传入的业务快照,包含实验名称、阶段、参数摘要、进度、成功率、活性、污染指数和温度。

这个边界有一个明显好处:页面不会知道“PCR 的第二列应该叫扩增量”。页面只调用 getFeedbackSchema(expId),拿到什么表头就渲染什么表头。反馈模型也不会关心页面是用表格还是图表展示,它只负责业务语义。

二、默认表头是兜底,不是所有实验的真实语义

源码里有一组默认表头:

const DEFAULT_HEADERS: string[] = ['进度', '活性', '污染', '评分']
const DEFAULT_UNITS: string[] = ['%', '%', '%', '']

默认值的作用是兜底,而不是替代所有实验。真正上线时,已经配置的实验应优先走专属分支。例如:

export function getFeedbackSchema(expId: string): FeedbackSchema {
  switch (expId) {
    case 'microscope_observation':
      return { headers: ['视野', '清晰度', '杂光', '识别率'], unitOverrides: ['%', '%', '%', ''] }
    case 'sterile_operation':
      return { headers: ['步骤', '规范度', '污染', '总评'], unitOverrides: ['%', '%', '%', ''] }
    case 'pcr_amplification':
      return { headers: ['循环', '扩增量', '非特异', '纯度'], unitOverrides: ['%', '%', '%', ''] }
    default:
      return { headers: DEFAULT_HEADERS, unitOverrides: DEFAULT_UNITS }
  }
}

这段代码防止了一个常见错配:数据列仍然是 [progress, row1, row2, row3],但业务名字跟实验不一致。PCR 如果显示“活性、污染、评分”,用户很难知道非特异条带是否偏高;无菌操作如果显示“活性”,也不如“规范度”准确。

可以把表头分为三类:

实验类型 第二列 第三列 第四列
显微观察 清晰度 杂光 识别率
无菌操作 规范度 污染 总评
DNA 提取 絮状量 杂质 产率
PCR 扩增 扩增量 非特异 纯度
血型鉴定 凝集度 反向干扰 判定置信

表头本质上是用户理解结果的第一层提示。把表头放进模型层,能让新实验接入时有明确检查点:如果新增了实验 ID,就必须问一句,它的三列业务含义到底是什么。

三、结果页只消费快照,不重新发明结果

ExperimentResultPageaboutToAppear() 中读取路由参数。它不会重新跑实验,也不会回到模拟页取状态,而是把完成时传来的快照变成页面状态。

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()
}

这段代码的关键点是“结果页是消费方”。模拟页已经计算好 successRateactivitycontamination 等指标,结果页只把它们用于展示、曲线生成、结论生成和分享内容。

如果结果页重新计算成功率,会有两个风险:

  • 同一轮实验的结果可能因为页面逻辑差异而变化。
  • 分享文本、曲线和结论可能使用不同的指标来源。

当前做法把快照作为可信输入,buildTableData() 只根据最终值补出过程曲线,getConclusion() 只根据快照输出建议,边界更清楚。

四、用曲线形状表达过程,不把最终值硬铺成直线

反馈页除了表格,还提供“反馈曲线”。源码里的 buildTableData() 会把最终的 activitycontaminationsuccessRate 映射成 0%、20%、40%、60%、80%、100% 六个进度点。更重要的是,它为不同实验使用不同的曲线形状。

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)
  if (kind === 'inv') return 1 - t
  if (kind === 'invSqrt') return 1 - Math.sqrt(1 - t)
  return t
}

这不是数学炫技,而是让反馈更贴近实验过程。例如:

曲线类型 源码含义 适合场景
s S 型增长 细菌培养
cube 后期快速上升 PCR 扩增、污染后期暴涨
peak 中段峰值 有丝分裂、细胞周期
lateRise 滞后后拉升 叶绿素层析
platL 快升后平台 发酵、离心分层
invSquare 快速收敛下降 杂光、混浊、误识别下降

反馈模型不是只有一句结论。过程曲线能告诉用户:这个指标是从头到尾稳定上升,还是中段出现峰值,或者后期才突然恶化。对教学应用来说,这比只给一个最终分更有解释力。

五、成功提示来自结果达标,而不是固定祝贺文案

源码里的成功提示通常出现在指标达到较好状态时。例如孟德尔配对实验:

case 'mendel_pairing':
  return `共统计${p}%子代样本,显性比${a}%、隐性比${c}%,与孟德尔预期吻合度${s}%。${s >= 80 ? '配对结果接近 3:1 经典比例。' : '当前样本量偏小,建议扩大样本数量重做统计。'}`

这里的成功不是“实验完成”本身,而是 s >= 80。这比固定写“实验成功”更合理:用户可能完成了流程,但样本量不足、污染较高或参数偏离目标,结论就不能只报喜。

再看 DNA 分层离心:

case 'dna_layering':
  return `离心至${p}%阶段,分层清晰度${a}%、混浊${c}%,最终样本纯度${s}%。${c >= 30 ? '混浊偏高,建议延长离心时间或提高转速。' : '分层界面清晰,适合下一步取样分析。'}`

当混浊不高时,系统输出“分层界面清晰,适合下一步取样分析”。这就是成功提示。它依赖真实指标 contamination,而不是点击完成按钮后无条件显示。

工程上可以把成功提示理解为:

条件 提示类型 用户看到的价值
成功率达到阈值 成功 当前参数组合有效
污染低于阈值 成功 操作风险可控
活性或扩增量足够 成功 关键实验现象成立

六、警告提示来自风险阈值,不应该被错误提示替代

警告和错误的区别在于:警告代表实验仍有结果,但风险已经影响解释。源码中大量结论使用污染、活性、温度等阈值输出警告。例如细胞增长周期:

case 'cell_growth_cycle':
  return `培养时长达到${p}%,分裂率${a}%、凋亡率${c}%,整体活力${s}%。${c >= 30 ? '环境压力过大导致凋亡上升,建议降低压力或补充营养。' : '当前营养水平能稳定维持细胞分裂节律。'}`

这里 c >= 30 不表示应用运行错误,也不表示页面失败,而是实验条件的风险警告。用户需要调整环境压力或营养水平。

PCR 扩增也类似:

case 'pcr_amplification':
  return `循环推进到${p}%,目标片段扩增量${a}%、非特异条带${c}%,扩增产物纯度${s}%。${c >= 30 ? '退火温度偏低产生非特异扩增,建议提高2-4℃。' : 'PCR反应特异性良好,可减少循环次数避免引物二聚体。'}`

非特异条带偏高是实验质量警告,不是程序错误。这个区分对文案和 UI 都很重要。如果把它做成红色错误弹窗,用户会误以为系统不能继续使用;如果完全不提示,用户又无法理解为什么纯度低。

七、错误操作提示:源码中真实存在的是操作失误建议和系统失败兜底

本篇标题里提到“错误操作提示”,需要严格按源码边界表达。当前项目没有独立的 ErrorTypeFeedbackLevel 或错误码表,也没有把结论分成 success/warning/error 三个显式枚举。真实存在的是两类错误相关逻辑。

第一类是实验操作错误建议。例如无菌操作:

case 'sterile_operation':
  return `完成${p}%操作步骤,规范度${a}%、污染${c}%,总评${s}分。${c >= 25 ? '开盖时长过长导致污染,建议缩短开盖时间并加强消毒覆盖。' : '操作链路稳定,可挑战更短开盖时长以提升等级。'}`

“开盖时长过长导致污染”不是系统异常,而是用户操作链路中的错误倾向。它由污染指数触发,能指导用户下一次缩短开盖时间、加强消毒覆盖。

第二类是系统能力失败兜底。结果页分享使用 ShareKit,如果拿不到宿主上下文,或系统分享面板调用失败,会显示 toast:

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 记录失败原因。对 HarmonyOS 应用来说,这个处理方式比静默失败更适合上架审查,也更利于调试。

八、结果分享复用同一个结论,避免分享内容与页面不一致

结果页的分享文本不是重新写一套结论,而是调用 this.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')
}

这段代码保证了页面结论和分享结论一致。用户在页面看到的“污染偏高,建议核查培养箱密封性与无菌指数”,分享到外部应用时仍然是同一个判断。

如果分享内容独立拼接,很容易出现页面显示警告、分享文本却显示成功的情况。反馈类功能尤其要避免这种分叉,因为用户后续复盘可能依赖分享记录。

九、表格和曲线共用同一份 tableData

ExperimentResultPage 有两个 Tab:数据表格和反馈曲线。表格通过 currentSchema().headers 渲染列名,通过 tableData 渲染行;曲线也读取 tableData 中的同一组数据。

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)
})

ForEach(this.tableData, (row: number[]) => {
  Text(this.formatCell(row[0], this.currentSchema().unitOverrides[0]))
  Text(this.formatCell(row[1], this.currentSchema().unitOverrides[1]))
  Text(this.formatCell(row[2], this.currentSchema().unitOverrides[2]))
  Text(this.formatCell(row[3], this.currentSchema().unitOverrides[3]))
})

这有两个好处。

第一,表格列名和单位来自同一个 schema,新增实验时不用改表格 UI。第二,表格和曲线数据同源,用户在曲线中看到的走势能在数据表格中找到对应点。

需要注意的是,目前单位都以百分比为主,第四列为空单位。如果后续加入“分钟”“rpm”“℃”这类非百分比列,可以继续使用 unitOverrides 扩展,不需要重写表格组件。

十、一个反馈模型接入新实验时的检查清单

新增实验时,不能只在实验列表里加一个卡片。为了让反馈模型正确工作,至少检查这些点:

检查点 文件/函数 目标
路由快照 ExperimentSimPage.buildResultParams() 确认新实验能传出进度、成功率、活性、污染、温度和参数摘要
表头语义 getFeedbackSchema(expId) 确认第二到第四列不是默认泛化文案
结论模板 getConclusion(expId, ctx) 确认成功和风险建议与实验参数一致
曲线形状 buildTableData() 确认趋势符合实验过程,不是所有指标线性增长
分享文本 buildShareText() 确认分享复用页面结论
失败兜底 shareResult() 确认系统分享失败时有 toast 和日志

这张表能直接用于代码评审。尤其是“表头语义”和“结论模板”,它们决定用户是否能理解反馈。

十一、常见问题与排查方法

现象 首查位置 原因 修复
结果页表头仍是活性/污染/评分 getFeedbackSchema() 新实验没有专属 schema 为该 expId 增加表头和单位
页面结论和分享文本不一致 buildShareText() 分享文案没有复用 getConclusion() 分享文本统一调用结果页结论
曲线看起来不符合实验过程 buildTableData() 新实验仍使用默认线性趋势 为该实验选择 scubepeak 等曲线形状
风险场景仍显示成功 getConclusion() 阈值条件不准确 按污染、活性、成功率或温度增加分支
分享按钮点击无反馈 shareResult() host context 为空或系统分享失败 保留 toast,使用 hilog.error 记录失败原因
第四列单位错误 unitOverrides 默认单位不适配 为对应列指定空单位或业务单位

排查时先不要改样式。反馈页的错乱通常来自输入快照、schema、结论模板、曲线数据四处不一致。样式只能改善展示,不能修复业务语义。

十二、可以进一步演进,但不要越过源码边界

如果后续要把成功、警告和错误操作提示做得更清晰,可以在现有基础上演进出显式等级:

type FeedbackLevel = 'success' | 'warning' | 'error'

interface FeedbackConclusion {
  level: FeedbackLevel
  message: string
  action: string
}

但这只是后续重构方向,当前源码还没有这样的类型。把它加入项目时,需要同步修改 getConclusion() 的返回值、结果页 UI、分享文本和测试用例。否则文章或文档里提前宣称已有“三级反馈系统”,就会和源码不一致。

更稳的演进路径是:

  • 先保留现有字符串结论。
  • 新增一个内部函数,根据污染、活性、成功率推导等级。
  • UI 先使用等级控制颜色和图标。
  • 分享文本仍复用最终结论,避免页面与分享分叉。

这样既能提升用户感知,又不破坏现有结果页。

总结

细胞工坊 的实验反馈模型不是复杂的后端诊断系统,而是一套清晰的 ArkTS 前端反馈契约:结果页读取完成后的快照,模型层按 expId 给出表头和结论,曲线函数补出过程趋势,分享逻辑复用同一结论并处理系统失败。

真正值得借鉴的是边界划分。成功提示来自达标指标,警告提示来自污染、活性、温度等风险阈值,错误操作提示来自具体操作风险或系统能力失败兜底。只要这三类反馈都由真实源码中的状态和指标触发,实验应用就不会把“完成了流程”误写成“结果一定正确”。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐