【知律|04】HarmonyOS ArkTS 答题结果实战:展示错因、知识点和复习入口
考试结果页是用户最容易相信数字的地方也是最不应该用占位数据的地方。一个分数如果来自未经校验的路由参数一条历史如果页面每次出现都重复保存一张答题网格如果把“提交顺序”误当成“题目顺序”界面再漂亮也无法成为可信学习记录。本文基于知律项目D:\huawei\one19-11、包名com.jiaweikang.one19的真实源码复核ExamResultPage.ets、PracticePage.ets、QuestionUtils、UserDataManager.ets和分数等级配置。当前结果页真实实现了分数卡、正确率、用时、答题网格、考试历史保存、错题解析入口与再考一次但排名236/1258是静态文本错因和知识点并不在结果页直接展示路由载荷也没有校验或会话级去重。文章将围绕这些可复核事实给出修正方案。一、先确认结果页真正展示了什么页面包含四组内容分数卡和由分数映射的印章正确率、用时和静态排名三宫格正确、错误、未答三态答题网格“错题解析”和“再考一次”两个入口。它没有在当前页面展开每道错题的原因也没有独立知识点模型。错因与题目解析要点击“错题解析”后由PracticePage的wrongAnalysis模式展示。因此标题中的“展示错因、知识点”应理解为结果到复习的完整链路不能宣称结果页已经生成结构化知识点报告。二、结果参数全部来自路由interface ExamResultParams { bankId: string score: number total: number correct: number durationSec: number records: string }练习页在交卷时把这些值通过replaceUrl传入。结果页没有重新从题目与答案计算而是直接赋值。这让页面实现简单但也意味着参数是唯一事实来源一旦重复、缺失或越界历史和 UI 会一起出错。三、aboutToAppear 无条件保存考试历史aboutToAppear(): void { const params router.getParams() as ExamResultParams | undefined if (params) { this.bankId params.bankId this.score params.score this.total params.total this.correct params.correct this.durationSec params.durationSec try { this.records JSON.parse(params.records) as AnswerRecord[] } catch (_) {} } this.rankInfo getRankByScore(this.score) this.examHistory UserDataManager.addExamHistory( this.examHistory, this.bankId, this.score, this.total, this.correct, this.durationSec ) }即使 params 不存在方法仍会保存默认的空 bankId、0 分、0 题和 0 秒记录。页面被异常直达时历史会出现零值条目从错题解析返回导致页面再次出现时也存在重复写入同一场考试的可能。四、历史模型缺少 sessionId当前ExamHistory只有interface ExamHistory { bankId: string score: number total: number correct: number durationSec: number finishedAt: string }它无法判断两条相同分数记录是两次考试还是同一页面重复保存。finishedAt在写入时重新生成反而会把重复记录伪装成不同时间。五、为每场考试增加稳定标识更可靠的做法是在练习会话开始时生成sessionId交卷与结果页全程传递interface ExamResultPayload { sessionId: string bankId: string questionIds: string[] records: AnswerRecord[] durationSec: number finishReason: manual | timeout }分数、正确数和总题数可以从 payload 重新计算避免同一事实有多份独立参数。历史保存以 sessionId 去重同一会话无论页面出现几次都只落库一次。六、先验证再更新 ArkUI 状态interface ResultState { status: ready | invalid payload?: ExamResultPayload message: string } function validatePayload(payload: ExamResultPayload): ResultState { if (!payload.sessionId || !payload.bankId) { return { status: invalid, message: 考试标识不完整 } } if (payload.questionIds.length 0) { return { status: invalid, message: 试卷为空 } } const questionSet new Set(payload.questionIds) const valid payload.records.every( record questionSet.has(record.questionId) ) return valid ? { status: ready, payload, message: } : { status: invalid, message: 答案记录与试卷不匹配 } }页面收到 invalid 时显示错误态并禁止保存历史。不要先把数值写入状态再在渲染阶段逐个修补。七、分数应由同一份记录计算当前手动交卷使用Math.round(correctCount / questions.length * 100)超时交卷调用QuestionUtils.calcScore。结果页又接收 score、total、correct 三个值。统一 payload 后可以在结果层派生const total payload.questionIds.length const correct payload.records.filter(r r.correct).length const score total 0 ? Math.round(correct / total * 100) : 0一份原始记录派生全部指标避免 score 与 correct/total 不一致。八、正确率已经处理 total 为零页面计算正确率时使用Math.round(this.correct / Math.max(this.total, 1) * 100)它避免除零但不能证明零题考试是合法结果。零题应该进入错误态而不是显示 0% 后写入历史。九、分数等级是本地阈值getRankByScore按分数映射优秀、良好、及格等本地等级并返回印章资源。这是本地 UI 分类不是用户在某个平台中的真实排名。等级阈值可以在文章中说明但不能写成权威考试认定或官方资格结果。十、236/1258 是静态占位数字结果页直接渲染this.MetricItem(${236}/${1258}, 排名)源码没有排行榜服务、用户总量、匿名统计或网络请求。这个数字不能当作真实排名更不能记录为平台数据、PV 或用户规模。发布前应改为“本地等级”或直接移除排名卡。十一、答题网格假设 records 与题号同序for (let i 0; i this.total; i) { let state: string empty if (i this.records.length) { state this.records[i].correct ? correct : wrong } result.push({ index: i 1, state }) }这段代码按 records 数组位置决定题号状态。但练习页允许通过答题卡跳题records 按提交顺序 push。用户先回答第 5 题时它会成为 records[0]结果网格却把第 1 题标成已答。十二、网格必须按 questionId 映射结果 payload 需要携带试卷顺序questionIdsprivate makeAnswerGrid(payload: ExamResultPayload): GridItem[] { const answerMap new Mapstring, AnswerRecord() for (const record of payload.records) { answerMap.set(record.questionId, record) } return payload.questionIds.map((questionId, index) { const record answerMap.get(questionId) return { index: index 1, state: !record ? empty : (record.correct ? correct : wrong) } }) }题号由试卷顺序决定答案状态由 questionId 查询提交顺序不再影响结果。十三、AnswerRecord JSON 需要逐项校验当前 JSON.parse 成功就断言为AnswerRecord[]并未验证数组、字段类型和 selected 范围。建议检查外层必须是数组questionId 与 selected 必须是字符串correct 必须是 boolean同一 questionId 只能出现一次selected 必须属于该题可选键记录必须属于当前试卷。解析失败不应默默显示空网格并保存一条完整历史。十四、考试历史保存要幂等static addExamHistoryOnce( records: ExamHistory[], next: ExamHistory ): ExamHistory[] { if (records.some(item item.sessionId next.sessionId)) { return records } const result [next, ...records] UserDataManager.persistExamHistory(result) return result }这个方法把防重放在持久化边界页面重复出现、手动与超时竞争、恢复导航都不会制造第二条记录。十五、结果页不是最理想的历史写入所有者更稳的所有权是练习页统一完成方法计算并保存结果成功后把 sessionId 导航到结果页结果页只读取已保存记录。这样页面展示不会产生写副作用。如果暂时保留结果页写入也必须先验证 payload并使用 sessionId 幂等保存。十六、错题解析入口传的是全部 records点击“错题解析”把整个 records JSON 传回 PracticePagerouter.pushUrl({ url: pages/PracticePage, params: { bankId: this.bankId, mode: wrongAnalysis, records: JSON.stringify(this.records) } })PracticePage 再过滤!record.correct根据 questionId 回查题目并展示解析。所以结果页只是复习入口不直接拥有错因内容。十七、知识点当前没有独立模型Question 只有 type、stem、options、answer、analysis 和 example没有 knowledgePointIds。结果页无法统计“合同法错 3 题”之类结构化知识点。如需知识点报告应为题目增加稳定标签或知识点 ID并在结果层按 ID 聚合。不能从 analysis 文本临时分词后宣称得到准确知识图谱。十八、空错题时应禁用解析入口当前无论是否存在错误记录按钮都可以进入 wrongAnalysis。PracticePage 对零错题有标题和空态处理但结果页提前计算wrongCount并禁用按钮会更清晰。private wrongCount(): number { return this.records.filter(record !record.correct).length }全对时可将按钮改为“查看答题卡”或直接隐藏。十九、再考一次使用 replaceUrl“再考一次”通过 replaceUrl 回到 PracticePage避免结果页不断堆叠在路由栈中。这是合理的返回路径。新的考试必须创建新的 sessionId若复用旧 ID幂等历史会把新考试误判为重复。二十、错题解析使用 pushUrl 的返回语义错题解析用 pushUrl用户查看后返回结果页。若结果页 aboutToAppear 再次保存历史就暴露了当前副作用问题。引入幂等后这条路由结构才安全。二十一、Preferences 适合轻量历史但要设上限ExamHistory 以 JSON 数组保存在 Preferences每次新记录放在最前。长期考试会让数组不断增长。可以只保留最近 50 或 100 场或改用关系型存储支持分页与统计。具体阈值应由产品需求决定但必须避免无限增长。二十二、保存失败不能吞掉当前 UserDataManager.persist 捕获异常后静默结束。结果页仍会显示历史已保存的样子重启却可能丢失。保存接口应返回成功或失败失败时给出“结果可查看但未保存到本机”的状态。不要将 AppStorage 内存变化等同于持久化成功。二十三、用时也需要范围校验durationSec 直接格式化显示。负数、超大值或非有限数字都应判为无效。考试倒计时场景下用时还应限制在 0 到考试时长之间。这不是为了修饰 UI而是防止异常载荷进入历史统计。二十四、状态颜色需要文字语义网格用绿、红、灰区分正确、错误、未答图例提供文字说明。对屏幕朗读每个格子还应设置如“第 3 题回答错误”的 accessibilityText。不能只依赖颜色深浅色模式也要验证正文、图例和印章对比度。二十五、底部安全区已正确纳入按钮区 bottom padding 使用导航指示区高度与最小间距的较大值能避让系统手势区域。长网格放在 Scroll 内底部按钮保持可达。仍需验证小窗口、横屏和平板下三宫格不会挤压静态排名移除后可改为两列指标或自适应布局。二十六、推荐的结果页状态type ExamResultViewState | { status: loading } | { status: invalid; message: string } | { status: ready; result: ValidatedExamResult } | { status: save_failed; result: ValidatedExamResult }ready 才显示完整结果invalid 不保存历史save_failed 允许查看本场结果但明确告知没有落库。二十七、故障排查表现象优先检查修复方向历史出现 0 分 0 题params 是否缺失无效载荷禁止保存返回后历史重复aboutToAppear 副作用sessionId 幂等跳题后网格错位records 是否按提交序按 questionIds 映射分数与正确率矛盾多份参数是否独立从 records 统一派生排名始终相同是否静态 236/1258移除或接真实服务全对仍能进错题解析wrongCount 是否为 0禁用入口重启历史丢失Preferences flush 结果展示保存失败错题解析找不到题questionId 是否稳定内容迁移保持 ID二十八、回归测试清单缺少路由参数时显示错误态且不写历史total 为 0、correct 越界、durationSec 非法时拒绝载荷同一 sessionId 多次出现只保存一条新一次考试使用新 sessionId任意顺序答题后网格仍对应正确题号未答题显示灰色且不计入正确数分数、正确数、总题数由同一 records 派生静态排名不再显示为真实排名全对时错题解析入口禁用或隐藏错题解析只展示错误记录再考一次替换结果页且不复用会话 IDPreferences 保存失败有可见提示历史数量达到上限后按规则裁剪手机、横屏、平板和 2in1 布局无截断屏幕朗读能播报每个题号状态。二十九、源码事实与产品表述边界可以确认结果页显示本地分数、正确率、用时和答题网格分数印章来自本地阈值考试历史通过 Preferences 保存错题解析入口把 records 传给 PracticePage再考一次使用 replaceUrl。必须谨慎236/1258是静态数字不是真实排名结果页不直接展示错因和结构化知识点网格当前按提交顺序着色历史缺少 sessionId 去重路由参数未校验无参数时仍可能保存零值历史。这些问题不能用“仅供参考”掩盖应从数据契约修正。三十、结语可信的答题结果页不是把几个数字排成卡片而是保证整场考试有唯一身份、所有指标来自同一份答案记录、网格能还原真实题号、历史只保存一次、复习入口能追溯到具体题目。知律当前已经打通结果展示、历史保存和错题解析路由但还需要移除静态排名、验证结果载荷、按题目顺序映射网格并为历史增加幂等键。完成这些改造后HarmonyOS 5.0 及以上的 ArkUI 结果页才能成为可复核的本地学习记录而不是一次性的视觉总结。本文部分内容由 AI 辅助整理所有源码事实、数据口径与能力边界均以知律项目实际代码复核为准题目中的法律内容仅用于软件工程分析不构成法律意见。