【知律|03】HarmonyOS ArkTS 普法练习实战:提交答案后同步统计与下一题

答题页面看起来只是"选一个答案,再点下一题",实际却是一段很容易重复结算的状态流程:选项只能提交一次,解析出现后不能改答案,错题要写入本地,错题练习答对后要移除,自动下一题不能越过用户手动操作,最后一题还要把题库与章节统计同步保存。进入模拟考试后,手动交卷和倒计时自动交卷又可能在同一秒发生。

本文基于知律项目 D:\huawei\one19-11、包名 com.jiaweikang.one19 的真实源码,复核 PracticePage.etsQuestionUtilsUserDataManager.ets 和题库生成逻辑。当前实现已经用 showAnalysis 挡住同一题的重复选择,并通过 @StorageLink 回流收藏、错题、笔记和学习进度;但会话结束没有统一幂等锁,手动完成、重复点击与考试倒计时存在重复写入风险。文章会先还原现状,再给出可迁移的修正方案,不把建议写成已上线能力。

一、练习页同时支持五种模式

路由参数中的 mode 支持章节练习、随机练习、模拟考试、错题练习和错题解析:

ts 复制代码
interface PracticeParams {
  bankId: string
  chapterId?: string
  mode: string
  records?: string
}

五种模式共用一套题目 UI,却不共享完全相同的写入规则。章节练习需要同步章节统计;错题练习答对后移除错题;错题解析只读历史答案;考试模式限制 20 题并在超时后交卷。把这些差异散落在多个点击回调里,会让结算边界越来越难验证。

二、题目加载顺序决定真实题集

aboutToAppear 根据模式建立 questions

ts 复制代码
if (this.mode === 'wrongAnalysis') {
  this.loadWrongAnalysis(params)
} else if (this.mode === 'wrong') {
  // 从本地错题记录还原问题
} else if (params.chapterId) {
  this.questions = getQuestionsByChapter(params.bankId, params.chapterId)
} else {
  this.questions = getQuestions(params.bankId)
}

如果章节筛选结果为空,非错题解析模式会回退到整个题库。这个回退避免空页面,却会改变用户预期:用户从某章节进入,最终可能练到全库题目。更稳的做法是显示章节空态并让用户主动切换,不要静默扩大范围。

三、随机练习与考试抽题不同

随机模式直接使用 QuestionUtils.shuffle;考试模式调用 pickHourlyExamQuestions,按题库 ID 和当前 UTC 小时构造种子,再执行 Fisher-Yates 洗牌,最多取 20 题。同一小时内题目顺序稳定,跨小时整体轮换。

这是一种可复核的确定性抽题,并非服务器下发试卷。文章或产品文案可以写"按小时轮换的本地模拟题",不能写成"全国统一题库"或"实时考试数据"。

四、单题提交的第一道防线是 showAnalysis

核心逻辑先检查只读模式和解析状态:

ts 复制代码
private selectOption(key: string): void {
  if (this.mode === 'wrongAnalysis') return
  if (this.showAnalysis) return
  this.selectedKey = key
  this.showAnalysis = true
  // 记录答案
}

第一次点击后立即把 showAnalysis 设为 true,后续选项点击直接返回。这能在同一页面生命周期内阻止同题重复提交,也让选项颜色切换到正确、错误和未选状态。

五、答案记录只保存会话事实

提交后向 records 写入:

ts 复制代码
const correct = key === q.answer
this.records.push({
  questionId: q.id,
  selected: key,
  correct
})

AnswerRecord 只包含题目 ID、选择键和对错,不复制题干或解析,保持了会话记录轻量。后续成绩计算通过 records 完成,错题解析再按 questionId 回查题库。前提是题目 ID 在内容升级后保持稳定,否则历史记录会找不到题目。

六、错题写入按 questionId 去重

答错时调用 addWrong,它先过滤同一题再把新记录放到数组开头:

ts 复制代码
static addWrong(
  records: WrongRecord[],
  questionId: string,
  bankId: string
): WrongRecord[] {
  const filtered = records.filter(r => r.questionId !== questionId)
  const result = [{ questionId, bankId, wrongAt: nowStr() }, ...filtered]
  UserDataManager.persist(UserDataManager.K_WRONG, result)
  return result
}

所以同一题多次答错不会让错题数量无限增加,但 wrongAt 会刷新为最近一次时间。这个数表示"当前去重错题数",不是累计答错次数。

七、错题练习答对会移除

mode === 'wrong' 且答案正确,页面调用 removeWrong。普通章节或随机练习答对不会主动清理旧错题,这意味着一题曾经答错后,只有进入错题模式并答对才会移除。

这是明确的产品规则,应在 UI 中说明。若期望任何模式答对都消除错题,需要修改条件并补回归测试,不能仅改变文案。

八、解析状态驱动选项颜色

optionBgoptionBorder 在提交前后使用不同规则:

ts 复制代码
if (!this.showAnalysis) {
  return this.selectedKey === key
    ? Colors.OPTION_SELECTED_BG
    : Colors.OPTION_BG
}
if (key === q.answer) return Colors.OPTION_CORRECT_BG
if (key === this.selectedKey) return Colors.OPTION_WRONG_BG
return Colors.OPTION_BG

正确答案始终显示正确色,错误选择显示错误色,其余选项保持普通状态。颜色之外还应为屏幕朗读补充"正确答案""你的选择错误"等语义,避免只靠红绿区分结果。

九、自动下一题有二次状态检查

设置开启后,非考试模式延迟 800ms:

ts 复制代码
if (this.autoNextQuestion && this.mode !== 'exam') {
  setTimeout(() => {
    if (this.showAnalysis) {
      this.goNext()
    }
  }, 800)
}

用户若在 800ms 内手动点击下一题,goNext 会把新题的 showAnalysis 重置为 false,延迟回调随后不会再次前进。这一二次检查有效避免常见的"自动与手动各跳一次"问题。

十、答题卡可以回看已答题

点击答题卡题号后,页面按 questionId 查找历史记录:

ts 复制代码
const record = this.records.find(
  r => r.questionId === this.questions[idx].id
)
if (record) {
  this.selectedKey = record.selected
  this.showAnalysis = true
} else {
  this.selectedKey = ''
  this.showAnalysis = false
}

已答题恢复锁定与解析状态,未答题可继续选择。由于 records 以题目 ID 查询,即使题目顺序经过洗牌也能正确恢复;不要改成用数组下标绑定答案。

十一、错题解析模式是只读会话

loadWrongAnalysis 从路由中的 JSON 解析 AnswerRecord,只保留错误项,再回查问题。applyAnalysisState 自动展示用户当时的选择和解析,selectOption 在该模式直接返回。

路由 JSON 属于不可信输入,当前解析失败时回退空数组是合理降级;进一步可以校验每条记录的 questionId、selected 与 correct 类型,防止异常对象进入 UI。

十二、题库统计在整场结束时才写

普通练习不是每答一题就写 BankProgress,而是在最后一次 goNext 中按本次 records 汇总:

ts 复制代码
const correctCount = this.records.filter(r => r.correct).length
this.progressList = UserDataManager.updateProgress(
  this.progressList,
  this.bankId,
  this.records.length,
  correctCount,
  this.chapterId
)

这种批量结算减少 Preferences 写入次数,但用户中途返回会丢失本场题库进度;错题和收藏因为即时保存仍会保留。产品需要明确选择"完成后结算"还是"逐题结算",两者不能混用。

十三、章节统计只在 chapterId 非空时写入

ts 复制代码
if (this.chapterId.length > 0) {
  this.chapterProgressList =
    UserDataManager.updateChapterProgress(
      this.chapterProgressList,
      this.bankId,
      this.chapterId,
      this.records.length,
      correctCount
    )
}

随机练习、考试和全库练习不会增加章节统计,即使题目本身带 chapterId。这是当前源码事实。若希望所有模式都按题目分摊章节进度,需要按 records 对题目分组,而不是把路由 chapterId 作为唯一依据。

十四、finished 是累计次数而非覆盖题数

updateProgressold.finished + addFinished,没有按 questionId 去重。重复完成同一套题会继续增加 finished,详情页再用 finished / totalCount 并限制到 100%。

因此它表达累计答题量,不是唯一题目覆盖率。对外展示"完成度"时需要谨慎;要表达覆盖率,应另存已答 questionId 集合。

十五、考试超时走独立结算入口

计时器每秒增加 timerSec,剩余时间归零时调用 autoSubmitExam。该方法清除计时器、计算正确数和分数、更新题库进度,再用 router.replaceUrl 进入结果页。

它没有更新章节统计,这与考试通常跨章节相符;但也没有在当前方法中调用 addExamHistory。除非结果页另有真实写入,否则不能宣称"考试历史已同步保存"。

十六、手动交卷和自动交卷存在竞争窗口

最后一题手动点击"交卷"会走 goNext,其中先更新题库进度再导航;与此同时,计时器仍可能触发 autoSubmitExam。当前代码没有 isFinishingsubmittedAt 或 sessionId 防重。

在页面销毁前的短窗口内,两条路径可能都执行 updateProgress,造成同一场考试重复累计。普通练习最后一题快速重复点击,也可能在路由离开前多次进入完成分支。

十七、用会话级幂等锁统一完成

最小修正是新增一个不参与 UI 的完成锁:

ts 复制代码
private isFinishing: boolean = false

private beginFinish(): boolean {
  if (this.isFinishing) return false
  this.isFinishing = true
  if (this.timerId !== -1) {
    clearInterval(this.timerId)
    this.timerId = -1
  }
  return true
}

所有完成路径第一行调用 beginFinish(),返回 false 时立即退出。锁必须在任何持久化和路由之前设置,才能覆盖同步重复点击与计时器回调。

十八、结算逻辑应收敛到一个方法

ts 复制代码
private finishSession(reason: 'manual' | 'timeout'): void {
  if (!this.beginFinish()) return

  const uniqueRecords = this.uniqueAnswerRecords(this.records)
  const correctCount = uniqueRecords.filter(r => r.correct).length

  this.progressList = UserDataManager.updateProgress(
    this.progressList,
    this.bankId,
    uniqueRecords.length,
    correctCount,
    this.chapterId
  )

  this.navigateAfterFinish(uniqueRecords, correctCount, reason)
}

手动完成和超时完成只传不同 reason,不各自复制统计与路由。这样测试可以直接验证"一次会话只结算一次"。

十九、结算前再按 questionId 去重

虽然 showAnalysis 已经防止常规重复提交,结算层仍可以做最后一道防线:

ts 复制代码
private uniqueAnswerRecords(records: AnswerRecord[]): AnswerRecord[] {
  const seen: Set<string> = new Set<string>()
  const result: AnswerRecord[] = []
  for (const record of records) {
    if (seen.has(record.questionId)) continue
    seen.add(record.questionId)
    result.push(record)
  }
  return result
}

它防止未来改动、异常路由数据或状态恢复把重复题目带入结算。去重规则要固定:通常保留第一次正式提交,不能在结算时偷偷选择最后一次或正确答案。

二十、持久化结果需要反馈

UserDataManager.persist 当前捕获异常后静默返回,页面无法知道写入是否成功。内存里的 StorageLink 先变化,重启后却可能回退。

更可靠的接口应返回 boolean 或结果对象;结算失败时不要立即导航,可以提示"学习进度未保存",允许重试或明确继续离开。尤其是考试结果,不能只依赖界面瞬时显示。

二十一、考试历史需要明确写入点

公共管理器已有 addExamHistory,但当前 PracticePage 的手动和超时完成代码都未调用它。建议在统一 finishSession 成功保存题库进度后写一次考试历史:

ts 复制代码
if (this.mode === 'exam') {
  this.examHistory = UserDataManager.addExamHistory(
    this.examHistory,
    this.bankId,
    score,
    uniqueRecords.length,
    correctCount,
    this.timerSec
  )
}

这需要页面增加对应 StorageLink,并确认结果页不会再次写入。一个实体只能有一个明确所有者。

二十二、中途返回的产品语义要写清

当前用户直接返回时,计时器和 TTS 会释放,但本场 records 不会同步到题库或章节进度。错题、收藏与笔记已经即时持久化,形成"部分数据保留、汇总进度不保留"的行为。

可选方案有两个:坚持完成后结算并在返回前提示,或逐题写入并使用 sessionId 防重。不要在页面消失时无条件补写,因为正常完成导航也会触发消失,容易再次结算。

二十三、自动下一题要给解析阅读时间

800ms 足以看到颜色变化,却可能来不及阅读较长法律解析。设置项虽然允许用户关闭自动下一题,但更稳的设计是提供 2 至 5 秒可选延迟,或在解析卡片上显示倒计时与取消按钮。

法律学习强调理解,不应为了节奏把解析一闪而过。此处属于产品优化建议,不是现有源码能力。

二十四、空题状态与考试除零

页面为空时显示"暂无可做的题目"并提供返回。考试分数计算需要继续保证 questions.length > 0;当前加载逻辑通常回退全库,但防御层仍应处理空数组,避免 correctCount / 0

统一结算方法可以在开头返回错误状态,禁止写入零题考试记录。

二十五、TTS 与答题状态应解耦

听音逻辑优先创建离线引擎,失败后尝试在线引擎,再失败则保留文字弹窗。它不会阻止选项提交,这是合理的能力降级。

在线 TTS 可能引入网络和隐私披露要求,发布材料必须与真实 SDK 行为一致。文章只确认源码存在在线回退,不推断服务一定可用,也不输出任何账号或鉴权信息。

二十六、多设备安全区已经进入页面

顶部和底部间距分别从 topAvoidAreaHeightPxnavigationIndicatorHeightPx 计算,底部工具栏高度包含避让值。答题卡浮层、笔记弹窗和听音弹窗也把底部安全距离加入 padding。

仍需在手机横屏、平板和 2in1 小窗口验证长题干、四个长选项与固定工具栏不会互相遮挡。答题卡高度为 86%,内部 Scroll 保证题号可达。

二十七、推荐的状态枚举

多个布尔值可以收敛为明确阶段:

ts 复制代码
type PracticePhase =
  | 'loading'
  | 'answering'
  | 'reviewing'
  | 'finishing'
  | 'finished'
  | 'error'

answering 才接受选项,reviewing 才允许下一题,finishing 禁止所有点击,finished 只等待路由离开。相比 showAnalysis + timerId + mode 的隐式组合,状态枚举更容易测试非法转换。

二十八、故障排查表

现象 优先检查 修复方向
同一题出现两条记录 showAnalysis 设置时机 提交前立即锁定
自动跳过两题 延迟回调是否复查状态 保留二次检查
考试进度翻倍 手动与超时是否并发结算 增加 isFinishing
章节进度不变 路由 chapterId 是否为空 明确模式统计规则
错题数量不增加 addWrong 是否按 ID 去重 区分当前错题与累计错误
重启后统计回退 Preferences flush 是否失败 返回保存结果
历史考试为空 是否调用 addExamHistory 统一结算写一次
中途返回无完成度 当前采用完成后结算 提示或改逐题结算

二十九、回归测试清单

  • 同一选项连续点击只产生一条 AnswerRecord;
  • 解析出现后点击其他选项不改变答案;
  • 自动下一题开启时,手动先点不会多跳一题;
  • 答题卡回到已答题时恢复选择和解析;
  • 普通模式答错添加一条去重错题;
  • 错题模式答对后移除该题;
  • 完成章节练习只结算一次题库和章节统计;
  • 随机练习不误写固定章节;
  • 手动交卷与倒计时同秒发生只结算一次;
  • 连续点击"完成/交卷"只导航一次;
  • 空题集不计算分数、不写进度;
  • 中途返回行为与页面提示一致;
  • Preferences 写入失败有可见反馈;
  • TTS 不可用时文字弹窗仍可使用;
  • 深浅色、横屏、小窗口和底部手势区无覆盖;
  • 屏幕朗读能识别选中、正确和错误状态。

三十、源码事实与能力边界

可以确认:页面支持五种模式;单题通过 showAnalysis 防止重复选择;错题按 questionId 去重;自动下一题有延迟后的状态复查;题库和章节进度在正常完成时累计;考试超时会自动进入结果页;收藏、笔记和错题通过 Preferences 即时保存。

必须谨慎:finished 是累计答题次数,不是唯一覆盖数;章节统计只在路由 chapterId 存在时更新;中途退出不结算本场汇总;考试历史在当前页面没有写入;完成阶段缺少幂等锁,存在重复结算窗口。以上边界都来自源码,不以 UI 看起来正常为依据。

三十一、结语

普法练习页的可靠性不在于按钮多,而在于状态转换能否回答三个问题:这一题是否已经提交,这一场是否已经结算,这一份统计是否真的保存成功。知律当前的单题锁定、错题去重和自动下一题复查已经形成了清晰基础,但整场结束仍需统一幂等入口。

把选题、提交、解析、下一题和结算拆成稳定状态,再让题库统计、章节统计、错题与考试历史各有唯一写入所有者,HarmonyOS 5.0 及以上的 ArkUI 页面才能在快速点击、倒计时、多模式切换和生命周期变化下保持一致。

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

相关推荐
梦想不只是梦与想8 小时前
鸿蒙 测试工具:DevEco Testing(一)
测试工具·harmonyos·testing
大锅盖110 小时前
Web 工单要调用相机,第一步不是打开取景框,而是建立能力门禁
前端·数码相机·harmonyos
贾伟康14 小时前
【华夏二十四节气|07】HarmonyOS 6.0.2(22) ArkTS 节气搜索实战:多字段匹配与四态闭环
移动开发·harmonyos·arkts·arkui·本地搜索
大龄秃头程序员15 小时前
Flutter 项目鸿蒙适配实战:从环境搭建到多环境打包全指南
harmonyos
贾伟康17 小时前
【万能转换器|18】HarmonyOS ArkTS 权限与隐私实战:让 module.json5、功能说明和拒绝路径一致
移动开发·harmonyos·arkts·权限管理·隐私合规
贾伟康17 小时前
【万能转换器|19】HarmonyOS ArkTS 回归测试实战:覆盖启动、空数据、异常输入和重复点击
软件测试·移动开发·harmonyos·arkts·回归测试
UnicornIT18 小时前
【HarmonyOS】时间管理类APP:做成“自适应“
ui·华为·harmonyos·鸿蒙
less_1213818 小时前
HarmonyOS WPS Open SDK:不落地、水印与功能开关的合规打开策略
华为·harmonyos·wps
贾伟康19 小时前
【万能转换器|20】HarmonyOS ArkTS AppGallery 发布复查实战:核对包名、版本、设备、素材和离线声明
移动开发·harmonyos·arkts·appgallery·应用发布
OH_TPC1 天前
HarmonyOS APP开发---“滤镜大师“图像处理App,需要用到这个库
java·图像处理·华为·harmonyos·鸿蒙