灯光模拟HarmonyOS应用实战-74-科一点击“重新开始”为什么可能变空:别把subject2Category传给所有科目
灯光模拟HarmonyOS应用实战-74-科一点击“重新开始”为什么可能变空:别把subject2Category传给所有科目
科一第一次从主页进入时,代码明确把分类参数传成空字符串,表示按科目取题;可是理论页底部的“重新开始”不区分科目,统一把 subject2Category 传回选题函数。这个字段默认值是“图标认知”,本来只属于科二。于是科一首轮可以正常取题,重新开始时却可能带着科二分类再次筛选,得到空数组。
真正的问题不是按钮少写了一个 if,而是重开动作没有保存“本次会话如何建立”的上下文。页面从另一个功能域读取一个仍然有值的状态字段,碰巧把它当作所有理论科目的分类。修复应让重开复用不可变的 PracticeQuerySnapshot,同时用类型保证只有科二能携带科二分类。

本文围绕当前控制流、状态串味、强类型查询、原子重开、空题处理和回归矩阵展开。所有实现代码都是建议方案;当前源码没有被本文修改。
一、科一首次进入和重新开始使用了不同分类
当前 openModule() 针对科一调用 startTheoryPractice() 时,第三个参数是空字符串;科二分支才会设置并传入 subject2Category。
if (id === PAGE_SUBJECT_ONE) {
this.startTheoryPractice(
SUBJECT_ONE,
'科一灯光题',
''
);
} else if (id === PAGE_SUBJECT_TWO) {
this.subject2Category = '图标认知';
this.startTheoryPractice(
SUBJECT_TWO,
'科二灯光基础',
this.subject2Category
);
}
但共用的理论页脚没有读取首次调用的分类,而是直接读取页面字段:
this.RowActionButton('重新开始', () => {
this.startTheoryPractice(
this.activeSubject,
this.activeMode,
this.subject2Category
);
});
subject2Category 的初始值是“图标认知”。所以即使用户从未进入科二,它也不是空值。继续核对当前 SubjectOneLightBank.ets,42 道手工科一题使用的分类包括“使用规则”“近光灯”“远光灯”“会车”“远近交替”“恶劣天气”“危险报警闪光灯”“转向灯”和“超车灯光”,没有“图标认知”。buildBuiltinQuestions() 当前只追加这组手工科一题,没有启用科一自动生成目录。因此,在这份源码快照中,默认状态下的“科一 + 图标认知”会得到空题组。这里仍只是在说明代码路径,不能替代设备上的一次点击证据。
二、选题条件会同时比较科目与分类
selectQuestions() 对每道题先比较科目,然后在分类非空时继续比较 question.category。因此“科一 + 图标认知”不是忽略多余字段,而是一个真实的联合过滤条件。
private selectQuestions(
subject: string,
category: string
): QuestionItem[] {
const source = this.getAllQuestions();
const result: QuestionItem[] = [];
for (let index = 0; index < source.length; index++) {
const question = source[index];
if (question.subject === subject &&
(category.length === 0 ||
question.category === category)) {
result.push(question);
}
}
return result;
}
首轮科一的查询是 subject1 + '',分类条件被绕过;重开查询可能变成 subject1 + 图标认知,只有同时满足两个字段的题才会保留。两次点击看似执行同一个“开始方法”,输入契约已经变化。
| 路径 | 科目 | 分类 | 当前过滤语义 |
|---|---|---|---|
| 主页进入科一 | subject1 | '' | 科一全部分类 |
| 科一页重新开始 | activeSubject 为 subject1 | 图标认知 或遗留科二值 | 科一中的指定科二分类文本 |
| 主页进入科二 | subject2 | 默认“图标认知” | 科二单分类 |
| 科二切换标签后重开 | subject2 | 当前标签 | 科二当前分类 |
| 主页进入科四后重开 | subject4 | 页面遗留字段 | 同样存在跨科目串味风险 |

三、页面有值不代表这个值属于当前会话
subject2Category 是科二标签的页面状态。它决定哪个标签高亮,也在点击标签时建立科二练习。这个字段可以长期保留,是合理的 UI 行为;错误发生在通用重开按钮把它解释为当前会话分类。
建议把状态分成三组:
subject2Category:科二页面下次显示哪个标签,只属于 UI 偏好。activePracticeQuery:当前理论会话真正使用的科目、模式和分类。activeQuestions:由查询生成的题目快照。
这三组状态的生命周期不同。重开应该复制第二组并重新抽题,不能临时从第一组重建第二组。否则用户在不同模块之间切换、恢复页面或新增题库入口后,仍会遇到类似串味。
四、用判别联合阻止科一携带科二分类
如果查询仍然是三个 string,任何调用方都能构造“科一 + 图标认知”。类型层可以把合法组合写成判别联合。
type TheoryPracticeQuery =
| {
kind: 'subject-one';
subject: 'subject1';
mode: '科一灯光题';
scope: 'all';
}
| {
kind: 'subject-two';
subject: 'subject2';
mode: '科二灯光基础';
scope: SubjectTwoScope;
}
| {
kind: 'subject-four';
subject: 'subject4';
mode: '科四灯光题';
scope: 'all';
};
type SubjectTwoScope =
| '图标认知'
| '上车检查'
| '夜间场景';
科一和科四分支没有 subject2Category 字段,调用方无法给它们附加科二分类。正式接入时可以继续使用工程现有 SUBJECT_ONE 常量,通过工厂返回对应联合分支;示例中的字面量用于说明类型关系,不代表必须改动现有常量值。
五、首次进入时保存会话快照
页面入口不再把三个参数直接写入多个状态,而是先构造合法查询、准备题组,成功后一次提交。
interface PracticeQuerySnapshot {
query: TheoryPracticeQuery;
startedFrom: string;
generation: number;
}
@State activePracticeQuery:
TheoryPracticeQuery | undefined = undefined;
private openSubjectOnePractice(): void {
const query: TheoryPracticeQuery = {
kind: 'subject-one',
subject: 'subject1',
mode: '科一灯光题',
scope: 'all'
};
this.prepareAndCommitPractice(query, 'home-module');
}
startedFrom 可以帮助排查入口差异,但重开时不应假装再次来自主页。generation 表示同一查询第几轮重抽,适合隔离延迟回调;它不代替题目随机种子。若项目需要可复现顺序,还应保存独立会话种子。
页面只在准备成功后更新 activePracticeQuery、activeQuestions、题号和提示。先写科目、后发现题组为空,会让标题和内容处于半更新状态。
六、选择器按联合分支展开分类规则

选择器接收查询对象后,先比较科目。只有 subject-two 分支才继续比较分类。
function selectTheoryQuestions(
source: QuestionItem[],
query: TheoryPracticeQuery
): QuestionItem[] {
const result: QuestionItem[] = [];
for (let index = 0; index < source.length; index++) {
const question = source[index];
if (question.subject !== query.subject) {
continue;
}
if (query.kind === 'subject-two' &&
question.category !== query.scope) {
continue;
}
result.push(question);
}
return result;
}
这种写法把“分类仅适用于科二”放在一个可测试的分支。将来若科一也需要章节筛选,应为科一定义自己的 SubjectOneScope,不能复用 SubjectTwoScope,即使两个分类文案偶然相同。
选择器不读取 this.subject2Category,因此 UI 标签在选择完成后发生变化,也不会悄悄改写当前题组。新的标签点击应创建下一份查询快照,而不是修改正在使用的对象。
七、重新开始只复制当前查询,不重新拼参数
重开时首先要求存在活动查询,然后以同一查询准备新的题序。按钮不再知道科目或分类规则。
private restartTheoryPractice(): void {
const query = this.activePracticeQuery;
if (query === undefined) {
this.showPracticeError('MISSING_ACTIVE_QUERY');
return;
}
this.prepareAndCommitPractice(
query,
'restart-button'
);
}
// 页脚只绑定动作
this.RowActionButton('重新开始', () => {
this.restartTheoryPractice();
});
“同一查询”不等于“同一题序”。如果当前产品语义是重新抽取 20 题,准备函数可以在相同查询范围内重新洗牌;如果语义是回到本轮第一题,就应复用原 activeQuestions 并重置游标。两个动作最好分别命名为“重新抽题”和“回到第一题”,避免一个按钮同时承担两种解释。
题库搜索进入的练习还可能需要保存搜索范围。重开究竟复用搜索命中快照还是回到全科,也应由查询种类明确表达,不能从 questionBankKeyword 或其他页面字段临时推导。
八、空题结果必须阻止幽灵题进入页面
当前 getCurrentQuestion() 在空数组时返回 BUILTIN_QUESTIONS[0]。如果重开因错误分类得到空题组,页面可能展示与本次查询无关的第一题。建议让准备函数先返回结果类型。
type PracticePreparation =
| { kind: 'ready';
query: TheoryPracticeQuery;
questions: QuestionItem[] }
| { kind: 'empty';
query: TheoryPracticeQuery }
| { kind: 'invalid';
reasonCode: string };
function preparePractice(
source: QuestionItem[],
query: TheoryPracticeQuery
): PracticePreparation {
const questions = selectTheoryQuestions(source, query);
if (questions.length === 0) {
return { kind: 'empty', query };
}
return { kind: 'ready', query, questions };
}
ready 才提交练习页;empty 保留原会话并显示“当前范围没有可用题目”;invalid 显示配置错误。不能在 empty 时自动删掉分类或切到科一,因为那会把内容缺口变成另一场练习。
若页面渲染必须获得一个题目对象,可以把题目卡放在 activeQuestions.length > 0 分支,而不是让 getter 返回真实题库中的占位题。
九、回归矩阵要覆盖跨模块遗留状态
最有价值的用例不是只打开一次科一,而是先制造可能污染通用按钮的页面状态,再重开当前会话。
describe('理论练习重新开始', () => {
it('科一重开仍使用 all 范围', () => {
const query = createSubjectOneQuery();
const restarted = restartFromSnapshot(query);
expect(restarted.query.kind)
.assertEqual('subject-one');
expect(restarted.query.scope)
.assertEqual('all');
});
it('科二切到夜间场景后重开保留该分类', () => {
const query = createSubjectTwoQuery('夜间场景');
const restarted = restartFromSnapshot(query);
expect(restarted.query.scope)
.assertEqual('夜间场景');
});
});
页面层还要覆盖这些路径:科二切换到“夜间场景”后返回主页,再进入科一并重开;恢复进程时 subject2Category 有旧值但活动查询是科四;从题库进入科一后重开;空题返回时原题组是否保持;连续快速点击重开是否只有最后一轮提交。
对于随机抽题,断言每道题都属于查询范围,不要断言固定顺序。若引入会话种子,再单独验证同种子可复放。
十、排障表、验证清单与事实边界
| 现象 | 优先核对 | 常见原因 | 处理方向 |
|---|---|---|---|
| 科一首次正常,重开为空 | 重开传入的分类 | 共用 subject2Category | 从活动查询快照重开 |
| 科四重开出现同类问题 | 页面遗留字段 | 修复只特判科一 | 用联合类型覆盖所有理论科目 |
| 科二重开丢失当前标签 | 快照创建时机 | 只保存科目,没保存分类 | 标签点击时建立完整查询 |
| 空数组仍显示一题 | 当前题 getter | 借用内置第一题兜底 | 用空态分支阻止题卡渲染 |
| 重开后标题先变、题组稍后失败 | 状态提交顺序 | 多字段分步写入 | 准备成功后原子提交 |
| 快速连点后旧题组覆盖新题组 | 会话代次 | 异步结果没有代次隔离 | 只接受当前 generation |
| “重新开始”行为理解不一致 | 按钮语义 | 重抽与回第一题混用 | 拆成明确动作和结果 |
- 科一和科四查询类型没有科二分类字段。
- 科二分类来自受限枚举,不接受任意字符串。
- 首次进入成功后保存完整活动查询。
- 重开按钮只调用
restartTheoryPractice()。 - 选题器只消费查询,不读取页面标签状态。
- 科二 UI 偏好与活动会话快照分开保存。
- 空题时不借用其他科目的内置第一题。
- 重抽与回到第一题的产品语义已经明确。
- 跨模块遗留状态、恢复和快速连点均有回归用例。
- 页面标题、题组、游标和提示只在准备成功后一起更新。
当前源码能够确认:subject2Category 默认是“图标认知”;科一首次进入传空分类;通用“重新开始”传入 subject2Category;选题器在分类非空时进行精确比较;当前科一手工题没有“图标认知”分类;空题组 getter 会返回内置第一题。因此当前源码默认路径会在科一重开时形成空活动题组,并可能由页面继续展示内置第一题。实际设备是否呈现为“空白”、幽灵题或其他视觉结果,仍取决于页面渲染和运行状态,本文没有把静态路径写成已操作结论。
TheoryPracticeQuery、PracticeQuerySnapshot、准备结果和会话代次属于建议方案,尚未写入 The_kemusan。本文没有执行编译,没有生成 HAP,也没有在模拟器或真机验证按钮行为;实现完成后仍需结合真实题库和页面生命周期做运行回归。
更多推荐



所有评论(0)