灯光模拟HarmonyOS应用实战-91-考试已经合格,再点灯光为何会改成演示提示:用ExamResultSeal冻结终局视图
2026/9/11 19:44:20 网站建设 项目流程

灯光模拟HarmonyOS应用实战-91-考试已经合格,再点灯光为何会改成演示提示:用ExamResultSeal冻结终局视图

科三灯光考试结束后,页面已经显示“考试合格!”或“考试未合格”,历史写入也已经发起。此时用户如果顺手再点一次近光灯、远光灯或雾灯,结果应该保持不变,还是立即切回自由演示?The_kemusan当前选择了一个隐式答案:结束态也进入演示分支,继续改变灯光状态,并把结果说明覆盖成“当前为灯光演示,不计入考试”。

这不代表历史记录一定被改写。当前赛后点击分支没有再次调用addSimpleRecord(),已发起的结束记录与页面提示属于两条不同链路。真正需要治理的是终局视图的所有权:一份已经形成的考试结论不应继续由通用演示提示字段覆盖;如果产品允许赛后试灯,也应进入独立演示会话,而不是让同一组字段同时扮演成绩卡和演示台。

本文只做源码审阅与建议设计。本次没有在模拟器或真机上完成一场考试后连续点击灯光,因此不会把可见闪烁、历史变化或特定交互时序写成已复现事实。文中的ExamResultSealCompletionSnapshotControlRouter尚未接入当前工程。

一、结束函数已经写出一组明确的终局字段

普通灯光路径在Index.ets:1522-1532进入finishLightExam()。函数锁住动作、结束考试、停止倒计时,并根据结果写入标题和说明:

private finishLightExam(passed: boolean, reason: string, wrongQuestionId: string, correctOption: string, selectedOption: string): void { this.actionLocked = true; this.examActive = false; this.examFinished = true; this.stopTimer(); this.currentPrompt = passed ? '考试合格!' : '考试未合格'; this.message = passed ? reason : `${reason}${correctOption.length > 0 ? ',正确选项:' + correctOption : ''}`; this.isSuccessMessage = passed; this.addSimpleRecord(SUBJECT_THREE, this.getLightExamMode(), passed, reason, wrongQuestionId, correctOption, selectedOption, this.correctCount, this.totalQuestions, Math.max(1, Math.floor((Date.now() - this.startAt) / 1000))); }

finishPracticalExam()1534-1544采用同样结构,只把标题改为“实操考试合格!”或“实操考试未合格”。从静态赋值顺序可以确认,结束时页面拥有一组一致的终局状态:examActive=falseexamFinished=truecurrentPrompt为结论、message为原因、isSuccessMessage为结果语义。

问题不是没有结束标志,而是后续操作没有把这个标志视为不可覆盖边界。

二、赛后点击与赛前演示共用了同一个分支

普通控制按钮最终进入handleLightAction()Index.ets:1426-1432在“考试未开始”或“考试已结束”时执行同一段代码:

private handleLightAction(action: string): void { if (!this.examActive || this.examFinished) { this.applyActionToState(action); this.message = '当前为灯光演示,不计入考试'; this.isSuccessMessage = true; return; } // 进行中考试的判题分支 }

实操按钮也一样:

private handlePracticalAction(action: string): void { if (!this.examActive || this.examFinished) { this.applyPracticalActionToState(action); this.message = '当前为实操按钮演示,不计入考试'; this.isSuccessMessage = true; return; } // 进行中实操的判题分支 }

由于结束函数已经把examActive设为false,赛后点击必然满足第一项条件;examFinished的判断在这里甚至不是必需条件。控制器先改变lightState,再覆盖message和颜色语义。currentPrompt仍可能保持“考试合格!”,于是同一屏幕可能同时携带“终局标题”和“演示说明”两种来源。

三、页面说明会变,不等于持久化结果也会变

把风险边界说清楚,才能避免修错位置。当前结束函数只在完成时调用一次addSimpleRecord();赛后演示分支没有再次保存记录,也没有修改correctCounttotalQuestions。因此静态代码支持以下结论:

对象结束时发生的动作赛后点击当前会做什么本次能否确认
currentPrompt写入合格/未合格标题当前分支不改源码可确认
message写入结束原因改成演示提示源码可确认
isSuccessMessage按结论赋值强制改为true源码可确认
lightState保留结束时灯态按按钮继续变化源码可确认
PracticeRecord发起一次异步写入没有新增写入调用源码可确认
用户实际看到的帧序列依赖渲染与操作时机本次未操作设备尚不能确认

这篇文章处理终局展示与赛后交互,不重复讨论写入失败、并发覆盖或历史迁移。即使记录最终可靠落盘,结果页被演示提示覆盖仍会让用户难以判断“刚才到底是否通过”。

四、先选产品策略:锁定、显式进入演示,或离开结果页

终局不是只能“全部禁用”。产品可以在三个策略中选择一个,但选择必须成为可见动作:

策略结束后控制按钮结果卡适合场景
完全锁定禁用并说明“重新开始后可操作”一直保持强考试感、避免误触
显式进入演示点击“进入灯光演示”后启用保存为可返回的终局兼顾复盘与自由试灯
离开结果页控制按钮只在独立演示页出现结果页不承载灯光控制页面职责最清楚

当前单页架构改动最小的是第二种:结束后保持成绩卡和控制区的禁用态,提供一个明确按钮创建DemoSession;进入演示后,页面不再声称自己仍是本场结果页。若产品不需要赛后演示,第一种更简单,不必为“以后可能用”提前建复杂路由。

五、CompletionSnapshot把终局从散字段变成一个对象

当前结果分散在currentPromptmessageisSuccessMessagecorrectCounttotalQuestions和灯态中,任一通用方法都可能覆盖其中一部分。建议在结束瞬间构造不可变语义快照:

interface CompletionSnapshot { sessionId: string; mode: string; passed: boolean; headline: string; reason: string; correctCount: number; plannedCount: number; finalLampState: string; finishedAt: number; } function createCompletionSnapshot(sessionId: string, mode: string, passed: boolean, reason: string, correctCount: number, plannedCount: number, finalLampState: string, finishedAt: number): CompletionSnapshot { return { sessionId, mode, passed, headline: passed ? '考试合格!' : '考试未合格', reason, correctCount, plannedCount, finalLampState, finishedAt }; }

函数一次性接收结束事件已经拥有的值,不在结果页重新推导。finalLampState表示结束瞬间的事实快照,后续演示灯态即使变化,也不能回写它。finishedAt应由可替换时钟提供,便于固定测试输入;示例直接传参,避免在纯函数里隐藏Date.now()

六、ExamResultSeal拒绝终局后的通用字段写入

“冻结”不是把对象序列化后锁进文件,而是让所有后续事件通过同一状态迁移入口。可以先把页面阶段写成枚举:

enum ScreenPhase { PREVIEW = 'PREVIEW', EXAM = 'EXAM', RESULT = 'RESULT', DEMO = 'DEMO' } enum ControlChannel { JUDGE_EXAM = 'JUDGE_EXAM', APPLY_DEMO = 'APPLY_DEMO', REJECT_ON_RESULT = 'REJECT_ON_RESULT' } function routeControl(phase: ScreenPhase): ControlChannel { if (phase === ScreenPhase.EXAM) { return ControlChannel.JUDGE_EXAM; } if (phase === ScreenPhase.DEMO || phase === ScreenPhase.PREVIEW) { return ControlChannel.APPLY_DEMO; } return ControlChannel.REJECT_ON_RESULT; }

RESULT不再落入!examActive的宽泛演示分支。控制点击先得到通道,再由对应所有者处理。结果页可以显示“考试已结束,选择重新考试或进入演示”,但不能调用applyActionToState()后再补一条提示,因为状态改变已经发生。

七、演示使用独立LampPreview,不覆盖CompletionSnapshot

如果用户明确进入演示,应创建只负责灯态的对象。它不持有分数、原因或passed

interface DemoSession { id: string; lampState: string; message: string; } function applyDemoAction(session: DemoSession, action: string): DemoSession { return { id: session.id, lampState: reduceLampState(session.lampState, action), message: '当前为灯光演示,不计入考试' }; }

这里的reduceLampState()代表从当前applyActionToState()提取出的纯状态映射;交替闪烁的定时副作用仍由LampPreview适配层拥有。DemoSession的消息可以变化,CompletionSnapshot始终保持结束时值。

页面可以在两个状态之间显式切换:

private handleControlIntent(action: string): void { const channel = routeControl(this.screenPhase); if (channel === ControlChannel.JUDGE_EXAM) { this.judgeExamAction(action); return; } if (channel === ControlChannel.APPLY_DEMO) { this.demoSession = applyDemoAction(this.demoSession, action); return; } this.resultNotice = '考试已经结束,请先选择重新考试或进入演示'; }

最后一个分支允许给出短暂辅助提示,但提示应放在resultNotice,不能覆盖快照的reason。如果不希望结果页出现任何动态提示,可以直接禁用按钮并让无障碍说明表达原因。

八、历史写入、结果展示和赛后演示要分别确认成功

当前addSimpleRecord()异步调用store.addRecord(...).then(...),没有返回给结束函数一份明确的持久化回执。因此即使页面封住了终局,也只能说明“结果快照已形成”,不能同时声称“历史已经耐久保存”。建议把证据分成三层:

  1. CompletionSnapshot创建成功:本场结论、分数、原因和结束灯态已冻结。
  2. SaveReceipt成功:仓储回读确认对应sessionId已保存。
  3. DemoSession启动:用户明确选择进入演示,后续灯态只属于演示。

三个步骤不能合并成一个isSuccessMessage=true。如果持久化失败,结果卡仍可显示本场结论,同时单独提示“历史未保存,可重试”;它不应把考试结论改成失败,也不能假装演示操作能补写历史。

建议保存接口返回清楚的结果:

interface SaveReceipt { sessionId: string; saved: boolean; issueCode: string; } interface CompletionRepository { save(snapshot: CompletionSnapshot): Promise<SaveReceipt>; }

issueCode使用有限白名单,不携带 Preferences 原文或用户答题内容。页面根据saved显示历史状态,而终局标题始终读取snapshot.passed

九、用正反用例证明“点击发生了,结果没有变”

只有一张结束页截图无法证明后续操作不会覆盖。实现后应保存快照,执行赛后点击,再比较所有终局字段。纯逻辑用例如下:

describe('ExamResultSeal', () => { it('rejects_control_on_result_phase', 0, () => { const channel = routeControl(ScreenPhase.RESULT); expect(channel).assertEqual(ControlChannel.REJECT_ON_RESULT); }); it('keeps_completion_when_demo_changes', 0, () => { const completion = createCompletionSnapshot( 'exam-91', '科三灯光模拟', true, '全部题目操作正确', 8, 8, ACTION_CLOSE, 1000); const demoBefore: DemoSession = { id: 'demo-91', lampState: ACTION_CLOSE, message: '' }; const demoAfter = applyDemoAction(demoBefore, ACTION_HIGH); expect(completion.reason).assertEqual('全部题目操作正确'); expect(completion.finalLampState).assertEqual(ACTION_CLOSE); expect(demoAfter.lampState).assertEqual(ACTION_HIGH); }); });

还需要覆盖以下交互:

  • 合格后连续点击全部普通灯光按钮,结果标题、原因和分数不变。
  • 未合格后点击控制按钮,失败原因和正确选项不被覆盖。
  • 实操模式与普通灯光模式采用同一终局门禁。
  • 明确进入演示后,只改变DemoSession.lampState
  • 从演示返回结果时,仍能读到原CompletionSnapshot
  • 点击“重新考试”创建新sessionId,不会改写旧快照。
  • 交替灯动画在结果阶段不能被误启动。
  • 历史保存失败与考试结论分别呈现。
  • 进程重建后若不支持恢复结果,应明确回首页,不能拼接半份旧状态。

十、排障表与当前事实边界

现象优先查看常见原因处理方向
合格标题仍在,说明变成演示message所有者结果与演示共用字段结果读快照,演示用独立消息
赛后点灯改变结束灯态lightState所有者终局事实与实时预览共用变量保存finalLampState,另建demoState
失败结果突然显示成功色isSuccessMessage演示分支强制写true结果色从snapshot.passed派生
结果页误启动交替动画控制路由RESULT 落入演示通道在副作用前拒绝,不在之后补提示
演示后无法回看成绩页面阶段进入演示时删除了快照由 ResultStore 保留,页面只切换阶段
成绩可见但历史没有保存回执页面结果与持久化混为成功单独展示SaveReceipt
重开后旧结果被改写会话身份复用同一对象或 sessionId新建会话,旧快照只读

当前源码可以确认:两个结束函数会写入终局标题、原因和结果语义;两个控制处理函数会在examActive=falseexamFinished=true时继续应用灯光动作,并把message覆盖为演示提示;赛后演示分支没有再次调用历史写入。由此能确定字段所有权存在冲突,但不能把未执行的设备画面或存储结果写成已发生。

ExamResultSeal的核心不是增加一个finished布尔值,因为当前已经有examFinished。真正的改动是让结果、保存与演示分别拥有自己的对象和通道,所有赛后点击先经过阶段决策,终局字段不再被通用演示逻辑修改。本文没有修改项目源码,没有执行构建,没有生成 HAP,也没有在模拟器或真机完成赛后点击验证;实现后仍需按上述矩阵确认渲染、动画、存储与重开行为。

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

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

立即咨询