【细胞工坊|04】HarmonyOS ArkTS 实验反馈模型实战:区分成功、警告和错误操作提示
部分内容由AI辅助生成。本文面向 HarmonyOS 5.0 及以上版本,基于
细胞工坊项目真实源码展开,源码根目录为D:\huawei\one14-9。重点复核entry/src/main/ets/model/ExperimentFeedback.ets与entry/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,就必须问一句,它的三列业务含义到底是什么。
三、结果页只消费快照,不重新发明结果
ExperimentResultPage 在 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()
}
这段代码的关键点是“结果页是消费方”。模拟页已经计算好 successRate、activity、contamination 等指标,结果页只把它们用于展示、曲线生成、结论生成和分享内容。
如果结果页重新计算成功率,会有两个风险:
- 同一轮实验的结果可能因为页面逻辑差异而变化。
- 分享文本、曲线和结论可能使用不同的指标来源。
当前做法把快照作为可信输入,buildTableData() 只根据最终值补出过程曲线,getConclusion() 只根据快照输出建议,边界更清楚。
四、用曲线形状表达过程,不把最终值硬铺成直线
反馈页除了表格,还提供“反馈曲线”。源码里的 buildTableData() 会把最终的 activity、contamination、successRate 映射成 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 都很重要。如果把它做成红色错误弹窗,用户会误以为系统不能继续使用;如果完全不提示,用户又无法理解为什么纯度低。
七、错误操作提示:源码中真实存在的是操作失误建议和系统失败兜底
本篇标题里提到“错误操作提示”,需要严格按源码边界表达。当前项目没有独立的 ErrorType、FeedbackLevel 或错误码表,也没有把结论分成 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() |
新实验仍使用默认线性趋势 | 为该实验选择 s、cube、peak 等曲线形状 |
| 风险场景仍显示成功 | 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 给出表头和结论,曲线函数补出过程趋势,分享逻辑复用同一结论并处理系统失败。
真正值得借鉴的是边界划分。成功提示来自达标指标,警告提示来自污染、活性、温度等风险阈值,错误操作提示来自具体操作风险或系统能力失败兜底。只要这三类反馈都由真实源码中的状态和指标触发,实验应用就不会把“完成了流程”误写成“结果一定正确”。
更多推荐




所有评论(0)