【知律|04】HarmonyOS ArkTS 答题结果实战:展示错因、知识点和复习入口
2026/8/14 13:48:56 网站建设 项目流程

考试结果页是用户最容易相信数字的地方,也是最不应该用占位数据的地方。一个分数如果来自未经校验的路由参数,一条历史如果页面每次出现都重复保存,一张答题网格如果把“提交顺序”误当成“题目顺序”,界面再漂亮也无法成为可信学习记录。

本文基于知律项目D:\huawei\one19-11、包名com.jiaweikang.one19的真实源码,复核ExamResultPage.etsPracticePage.etsQuestionUtilsUserDataManager.ets和分数等级配置。当前结果页真实实现了分数卡、正确率、用时、答题网格、考试历史保存、错题解析入口与再考一次;但排名236/1258是静态文本,错因和知识点并不在结果页直接展示,路由载荷也没有校验或会话级去重。文章将围绕这些可复核事实给出修正方案。

一、先确认结果页真正展示了什么

页面包含四组内容:

  1. 分数卡和由分数映射的印章;
  2. 正确率、用时和静态排名三宫格;
  3. 正确、错误、未答三态答题网格;
  4. “错题解析”和“再考一次”两个入口。

它没有在当前页面展开每道错题的原因,也没有独立知识点模型。错因与题目解析要点击“错题解析”后,由PracticePagewrongAnalysis模式展示。因此标题中的“展示错因、知识点”应理解为结果到复习的完整链路,不能宣称结果页已经生成结构化知识点报告。

二、结果参数全部来自路由

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 需要携带试卷顺序questionIds

private makeAnswerGrid(payload: ExamResultPayload): GridItem[] { const answerMap = new Map<string, 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 传回 PracticePage:

router.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 派生;
  • 静态排名不再显示为真实排名;
  • 全对时错题解析入口禁用或隐藏;
  • 错题解析只展示错误记录;
  • 再考一次替换结果页且不复用会话 ID;
  • Preferences 保存失败有可见提示;
  • 历史数量达到上限后按规则裁剪;
  • 手机、横屏、平板和 2in1 布局无截断;
  • 屏幕朗读能播报每个题号状态。

二十九、源码事实与产品表述边界

可以确认:结果页显示本地分数、正确率、用时和答题网格;分数印章来自本地阈值;考试历史通过 Preferences 保存;错题解析入口把 records 传给 PracticePage;再考一次使用 replaceUrl。

必须谨慎:236/1258是静态数字,不是真实排名;结果页不直接展示错因和结构化知识点;网格当前按提交顺序着色;历史缺少 sessionId 去重;路由参数未校验;无参数时仍可能保存零值历史。这些问题不能用“仅供参考”掩盖,应从数据契约修正。

三十、结语

可信的答题结果页不是把几个数字排成卡片,而是保证整场考试有唯一身份、所有指标来自同一份答案记录、网格能还原真实题号、历史只保存一次、复习入口能追溯到具体题目。

知律当前已经打通结果展示、历史保存和错题解析路由,但还需要移除静态排名、验证结果载荷、按题目顺序映射网格,并为历史增加幂等键。完成这些改造后,HarmonyOS 5.0 及以上的 ArkUI 结果页才能成为可复核的本地学习记录,而不是一次性的视觉总结。

本文部分内容由 AI 辅助整理,所有源码事实、数据口径与能力边界均以知律项目实际代码复核为准;题目中的法律内容仅用于软件工程分析,不构成法律意见。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询