乒乓球口袋教练 HarmonyOS 学习应用(04):测验题库与动作判断反馈
一、动作判断反馈需要保存哪些上下文测验不是把选项染成对错颜色就结束。为了让动作判断有学习价值结果页需要保留模块、题目、选择、正确答案和解释的上下文否则用户只得到一个分数不知道是发球落点、接发判断还是相持节奏出现偏差。export interface QuizQuestion { id: string; moduleId: string; question: string; options: QuizOption[]; correctLabel: string; explanation: string; } function option(label: string, text: string): QuizOption { return { label, text } as QuizOption; } function question( id: string, moduleId: string, title: string, a: string, b: string, c: string, d: string, correctLabel: string, explanation: string ): QuizQuestion { return { id, moduleId, question: title, options: [ option(A, a), option(B, b), option(C, c), option(D, d) ],二、题库为空时为什么不能继续提交QuizPage 先根据 moduleId 获取题目。若当前模块没有题目页面应显示空状态并停止进入结果页提交空集合会得到没有意义的 0/0 记录还可能把“历史最佳”污染成无法解释的数字。事实唯一来源页面职责正常结果模型或服务的回读渲染可观察状态边界结果明确的缺项或失败分支说明原因并保留入口重进结果持久化或模型再次解析不复用旧页面变量Column() { TopBar({ title: this.moduleTitle · 测验 }); if (this.questions.length 0) { Column() { Text(该模块暂无测验题目) .fontSize(AppText.fsSubtitle) .fontColor(this.palette.textSecondary); } .layoutWeight(1) .justifyContent(FlexAlign.Center) .width(100%); } else { Column() { // 进度条 Row({ space: AppSizes.s3 }) { Text((this.currentIndex 1).toString() / this.questions.length.toString()) .fontSize(AppText.fsBody) .fontWeight(AppText.fwSemi) .fontColor(this.palette.primary); Stack() { Column() .width(100%).height(4) .borderRadius(2) .backgroundColor(this.palette.bgElevated); Row() { Column() .width(100%).height(4) .borderRadius(2) .backgroundColor(this.palette.primary); } .width(((this.currentIndex 1) / this.questions.length * 100).toString() %) .height(4); } .layoutWeight(1) .height(4);三、结果页怎样只写入一次成绩结果页以一次进入标记保护 submitResult。组件回到前台或重新渲染时可以显示先前结果和解释却不能再次把同一成绩追加到结果列表。保存记录包含 moduleId、score、total 和时间后续统计有足够信息区分不同模块。if (params ! undefined params ! null) { this.moduleId params.moduleId; this.score params.score; this.total params.total; const m: LearnModule | undefined findModule(this.moduleId); this.moduleTitle m ! undefined ? m.title : 测验; if (!this.saved) { QuizService.submitResult(this.moduleId, this.score, this.total); this.saved true; } } } build() { Column() { TopBar({ title: 测验结果 }); Scroll() { Column({ space: AppSizes.s5 }) { Column().height(AppSizes.s5); // 分数圆环 Stack({ alignContent: Alignment.Center }) { Column() .width(160).height(160) .borderRadius(80) .border({ width: 8, color: this.palette.bgElevated }); Column() .width(160).height(160) .borderRadius(80) .border({ width: 8, color: this.ringColor() }) .opacity(1); Column({ space: AppSizes.s1 }) { Text(this.score.toString() / this.total.toString())四、历史最佳如何限制在当前模块历史最佳只在相同 moduleId 的结果中比较。不同专项题量和难度不同把所有测验混在一起会让一个小模块的满分覆盖另一个模块的练习轨迹。模块边界同时是题库加载、结果保存和最佳分展示的共同边界。风险不能采用的做法当前链路的处理数据不存在用默认对象继续渲染停在可解释的空或失败状态页面重进复用上一次内存字段重新查询模型或服务重复动作追加同一业务事实通过稳定 id 或进入标记收敛} static bestScore(moduleId: string): number { const results: QuizResult[] QuizService.allResults(); let best: number 0; for (const r of results) { if (r.moduleId moduleId r.score best) { best r.score; } } return best; } static hasCompleted(moduleId: string): boolean { return QuizService.allResults().some(r r.moduleId moduleId); } static completedModuleCount(): number { const done: Setstring new Setstring(); for (const r of QuizService.allResults()) { done.add(r.moduleId); } return done.size; } static hasPerfect(): boolean { return QuizService.allResults().some(r r.score r.total); }五、从答题到结果页的验收验收时完成一个有题模块的答题并进入结果页回读本次得分与解释返回再进确认不会多写一次随后切换另一模块完成答题确认两个模块的历史最佳分别保留。也应打开空题模块确认没有生成伪成绩。验收阶段操作应观察到的结果建立事实完成一次与主题直接对应的动作服务或模型能回读该事实页面回读进入目标页或结果页标题、数据和状态相互一致重进验证返回、重启或切换模块后再次进入结果不依赖上一页残留六、取舍与后续维护当前实现把业务事实集中到模型与服务页面只负责呈现和触发操作。这样做会多出类型、查询和回读代码却能避免同一份统计或记录在多个页面分叉。需要扩展新课程、新资源或新题目时应先补齐稳定标识与服务合同再补页面入口。ArkTS 的响应式状态机制可参考 HarmonyOS ArkTS 状态管理文档。本文的代码和验收围绕同一个原则可见结果必须能回到对应模型、服务或持久化事实不以邻近页面和控件文字代替主题结果。七、围绕事实来源继续演进乒乓球口袋教练 HarmonyOS 学习应用04测验题库与动作判断反馈 关注的不是给页面再增加一层显示逻辑而是让“动作判断反馈需要保存哪些上下文”始终能回到唯一的业务事实。模型字段、服务返回值和页面展示应当保持单向关系事实先发生页面随后回读页面不保存第二份能够被误认为真实数据的临时副本。处理“题库为空时为什么不能继续提交”时新增条件必须先归属到课程模型、资源服务、日志事实或测验结果之一。把条件散落在多个组件里短期看起来省事后续就会出现入口不同、重进不同、统计不同的问题。把规则放回稳定入口页面只根据结果改变可见状态能让同一结论在不同入口被复查。“结果页怎样只写入一次成绩”说明当前实现选择了页面重建后重新查询而不是把对象和统计快照长期塞进路由参数。读取会增加少量成本却避免课程改名、资源替换、日志编辑或题库切换后继续显示旧内容。学习应用的结果需要能解释不能只追求一次渲染恰好看起来正确。异常分支也应保留语义历史最佳只在相同 moduleId 的结果中比较。不同专项题量和难度不同把所有测验混在一起会让一个小模块的满分覆盖另一个模块的练习轨迹。模块边界同时是题库加载、结果保存和最佳分展示的共同边界。 这要求页面区分数据不存在、动作未完成和资源不可用并提供返回选择、补充输入、重新下载或再次答题等对应操作不能把不同原因压缩成同一条模糊的成功或失败提示。最后执行“从答题到结果页的验收”时至少观察一次主题动作、一次目标页回读和一次重进或切换后的回读。三次观察都指向相同模型或服务结果才说明实现链路没有依赖旧页面残留。 最后执行“从答题到结果页的验收”时至少观察一次主题动作、一次目标页回读和一次重进或切换后的回读。三次观察都指向相同模型或服务结果才说明实现链路没有依赖旧页面残留。面向维护的检查可以沿三个问题展开输入对象的稳定标识是什么哪个服务或模型决定结果页面如何把该结果显示出来。题库按模块提供问题与选项结果页只在首次进入时保存一次成绩并回读同模块历史最佳避免重复提交和跨模块串分。 这三个问题分别对应数据合同、调用边界和可见反馈只要其中一层被绕过新增入口就可能产生和原入口不同的事实。对失败路径也要保留同样的追踪能力。先让服务返回清晰的不可用、缺项或未完成状态再由页面提供与原因匹配的操作。这样日志、课程、资源和题目发生变化时回归测试能够看到具体的分歧点而不是只能面对一张没有信息的空白页面。这种组织不会承诺所有后续需求都零成本。新增字段需要同步更新模型、服务与验收但它把变更范围限制在可枚举的位置避免把同一判断复制到首页、详情、个人页和结果页后再分别修补。