灯光模拟HarmonyOS应用实战-57-科二分类从两个入口传成两套:用PracticeQuery统一练习上下文
灯光模拟HarmonyOS应用实战-57-科二分类从两个入口传成两套:用PracticeQuery统一练习上下文
科二灯光练习同时存在两个入口:主页模块进入后,页面会默认选择“图标认知”,顶部还能切换“上车检查”和“夜间场景”;题库页则先选择科目,再点击“开始练习”。两个入口最后都进入同一个 startTheoryPractice(),但它们携带分类的方式并不相同。前者把当前分类继续向下传递,后者传入空字符串。只看最终页面,两条路径都能出现题目,很容易忽略它们建立的其实不是同一种练习上下文。
这类问题的难点不在参数数量,而在参数之间存在约束:科目决定允许哪些分类,模式决定页面标题和历史口径,入口又决定“没有指定分类”究竟表示全部分类,还是调用方忘记传值。如果继续让多个字符串在页面方法之间裸传,新增入口时就只能靠开发者记住隐含规则。更稳的做法是把一次练习所需的信息收进 PracticeQuery,让构造、校验、选题和会话保存都围绕同一个不可拆散的快照工作。

一、两条入口已经呈现出不同的分类语义
Index.ets 第338—397行只在科二页面展示分类标签。用户点击标签后,代码先修改 subject2Category,再把科目、模式和标签文本传给开始方法。第477—513行的题库开始卡片则根据当前科目取得标题和模式,最后一个参数固定传空字符串。第1116—1129行负责把这些值写入活动状态并选题。
// 科二页内入口
this.subject2Category = label;
this.startTheoryPractice(SUBJECT_TWO, '科二灯光基础', label);
// 题库页入口
this.openQuestionBankTest(
this.questionBankSubject,
this.getQuestionBankSubjectTitle(),
this.getQuestionBankSubjectMode(),
''
);
private startTheoryPractice(subject: string, mode: string, category: string): void {
this.activeSubject = subject;
this.activeMode = mode;
this.activeQuestions = this.selectTestQuestions(subject, category);
}
这段控制流说明,分类既是页面选中态,也是选题条件,却没有成为一个有名称的业务概念。空字符串在 selectQuestions() 中会绕过分类比较,因此当前效果接近“该科目全部分类”。这只是从源码条件表达式得到的静态结论;它不能说明产品一定希望题库入口混合全部科二分类,更不能说明用户已经遇到错误题目。
| 入口 | 当前分类来源 | 向下传递值 | 可读出的语义 |
|---|---|---|---|
| 科二主页默认进入 | subject2Category = '图标认知' | 图标认知 | 单一分类练习 |
| 科二顶部标签 | 用户点击的标签 | 标签文本 | 单一分类练习 |
| 题库开始卡片 | 没有分类控件 | '' | 当前实现中相当于全部分类 |
| 将来错题复练 | 可能来自历史记录 | 尚无统一入口契约 | 需要明确继承或覆盖规则 |
二、三个字符串无法表达组合约束
单看函数签名,subject、mode、category 都是合法字符串,编译器无法阻止下面这些组合:科一科目配“夜间场景”、科二模式配科四分类、空科目配非空分类。调用方还可能把页面标题误传到模式字段,因为二者类型完全相同。
真正需要维护的是组合不变量,而不是单字段非空:
subject必须来自受支持科目集合。mode必须与科目匹配,不能由任意页面文案代替。- 科二允许三个已登记分类;科一和科四是否支持分类筛选要单独定义。
- “全部分类”应该是明确枚举,不能借用空字符串。
- 一旦会话启动,查询快照不能再随页面标签变化。
如果这些规则散落在 CategoryChip()、题库卡片、openQuestionBankTest() 和选题函数中,任何一处新增分支都可能产生另一种解释。PracticeQuery 的价值就在于给组合一个单独的校验入口。
三、用判别联合写出分类范围
先不要急着把三个字符串换成一个对象。对象仍然可以装入任意字符串,只有类型把可选项和适用范围写清,才能真正收紧入口。
type PracticeSubject = 'subject-one' | 'subject-two' | 'subject-four';
type SubjectTwoCategory = '图标认知' | '上车检查' | '夜间场景';
type CategoryScope =
| { kind: 'all' }
| { kind: 'subject-two'; value: SubjectTwoCategory };
type PracticeEntry =
| 'home-module'
| 'category-chip'
| 'question-bank'
| 'wrong-review';
interface PracticeQuery {
subject: PracticeSubject;
mode: string;
category: CategoryScope;
entry: PracticeEntry;
}
CategoryScope 把“全部”与“科二具体分类”分成两个分支。后续代码必须先判断 kind 才能读取 value,不再靠 category.length === 0 猜调用意图。entry 不参与选题,却能帮助历史与排障记录说明这次会话从哪里建立;它应记录有限枚举,不能保存页面对象、路由实例或用户输入全文。
这里保留 mode: string 是迁移阶段的折中。如果工程已经有稳定模式常量,可以进一步收窄为联合类型;若模式文本还承担展示职责,应先分出稳定的 modeId 和本地化标题,避免一次改造同时改变过多层。
四、查询工厂集中处理科目、模式与分类
页面不应直接拼装任意 PracticeQuery。建立一个小型工厂,让各入口只能提交自己真实拥有的信息,再由工厂补齐模式并验证分类归属。
interface PracticeQueryResult {
ok: boolean;
query?: PracticeQuery;
reason?: 'UNSUPPORTED_SUBJECT' | 'CATEGORY_NOT_ALLOWED';
}
const SUBJECT_TWO_CATEGORIES: SubjectTwoCategory[] = [
'图标认知',
'上车检查',
'夜间场景'
];
function isSubjectTwoCategory(value: string): value is SubjectTwoCategory {
return SUBJECT_TWO_CATEGORIES.indexOf(value as SubjectTwoCategory) >= 0;
}
function createSubjectTwoQuery(category: string, entry: PracticeEntry): PracticeQueryResult {
if (!isSubjectTwoCategory(category)) {
return { ok: false, reason: 'CATEGORY_NOT_ALLOWED' };
}
return {
ok: true,
query: {
subject: 'subject-two',
mode: '科二灯光基础',
category: { kind: 'subject-two', value: category },
entry
}
};
}
工厂拥有科二分类白名单,因此页面标签与题库种子使用的分类名称发生漂移时,会在会话开始前返回明确原因。它不擅自把未知分类改成“全部”,因为静默降级会掩盖配置错误。实际接入时还应让标签数据也从同一分类目录生成,避免类型数组和 UI 文案再复制一份。
PracticeQueryResult 使用可选字段是为了展示业务结果形态。正式实现可改成更严格的判别联合,让 ok: true 分支必有 query,ok: false 分支必有 reason;选择哪种写法要以工程当前 ArkTS 规则和目标 SDK 编译结果为准。
五、把每个页面入口改造成薄适配器
工厂建立后,页面只负责收集交互值和处理失败,不再决定选题规则。科二分类标签可以传精确分类,题库入口则要由产品语义决定是默认某一类,还是明确选择全部。
private startFromSubjectTwoChip(label: string): void {
const result = createSubjectTwoQuery(label, 'category-chip');
if (!result.ok || result.query === undefined) {
this.answerMessage = '该分类暂不可用';
return;
}
this.beginPractice(result.query);
}
private startSubjectTwoFromBank(): void {
const query: PracticeQuery = {
subject: 'subject-two',
mode: '科二灯光基础',
category: { kind: 'all' },
entry: 'question-bank'
};
this.beginPractice(query);
}
第二个适配器显式写出 kind: 'all',这不代表产品必须保留当前混合行为,而是让选择可见、可审阅。如果产品希望题库入口沿用上次科二分类,就应该从稳定的偏好设置读取合法枚举,读取失败时给出明确默认值;不能继续把空字符串同时当作“全部”“没有选择”和“读取失败”。

六、题目选择器只消费查询,不读取页面字段
目前 startTheoryPractice() 先写多个响应式字段,再调用 selectTestQuestions(subject, category)。改造后,选择器应只接收已经通过入口校验的查询快照。这样即使用户快速切换标签,正在建立的会话也不会在一半流程里读到新分类。
function selectByPracticeQuery(
source: QuestionItem[],
query: PracticeQuery
): QuestionItem[] {
const result: QuestionItem[] = [];
for (let index = 0; index < source.length; index++) {
const question = source[index];
if (question.subject !== query.subject) {
continue;
}
if (query.category.kind === 'subject-two' &&
question.category !== query.category.value) {
continue;
}
result.push(question);
}
return result;
}
选择器不再知道 subject2Category、questionBankSubject 或当前页面。它只比较题目字段与查询字段,因此可以在不创建 ArkUI 页面的情况下验证。还要注意,示例中的 PracticeSubject 值需要与工程真实 SUBJECT_TWO 常量做适配,不能把文章里的字符串直接替换进现有数据后假定兼容。
若返回空数组,调用方必须区分“这个合法分类目前没有题”和“查询不合法”。前者属于内容缺口,后者属于入口错误;两种情况都不应自动回退到另一分类,更不能补入内置第一题。
七、会话一次性提交状态,避免半更新
查询完成并取得题目后,再生成会话启动结果。页面只提交一次完整快照,而不是先改 activeSubject、再改 activeMode、最后等待题目数组出现。
interface PracticeStartSnapshot {
query: PracticeQuery;
questions: QuestionItem[];
firstIndex: number;
initialMessage: string;
}
type PracticeStartResult =
| { kind: 'ready'; snapshot: PracticeStartSnapshot }
| { kind: 'empty'; query: PracticeQuery }
| { kind: 'rejected'; reason: string };
function preparePractice(query: PracticeQuery, catalog: QuestionItem[]): PracticeStartResult {
const questions = selectByPracticeQuery(catalog, query);
if (questions.length === 0) {
return { kind: 'empty', query };
}
return {
kind: 'ready',
snapshot: {
query,
questions,
firstIndex: 0,
initialMessage: '请选择答案'
}
};
}
这个结果把“准备”与“提交”分开。preparePractice() 是纯计算,可以核对输入输出;页面拿到 ready 后再一次性更新活动查询、题目、索引、已选答案和提示文案。empty 分支保留原查询,UI 才能告诉用户究竟哪个分类没有内容,而不是只显示一个没有上下文的空页。

八、历史与重进保存稳定查询快照
会话开始后,应把 PracticeQuery 视为只读事实。历史记录可以保存科目、分类范围和入口,但不应在展示时重新读取当前标签。重进练习时还要重新校验:旧分类可能在新版本中被移除,旧模式名称也可能已经迁移。
interface StoredPracticeContextV1 {
schemaVersion: 1;
subject: PracticeSubject;
categoryKind: 'all' | 'subject-two';
categoryValue?: SubjectTwoCategory;
entry: PracticeEntry;
}
function toStoredContext(query: PracticeQuery): StoredPracticeContextV1 {
return {
schemaVersion: 1,
subject: query.subject,
categoryKind: query.category.kind,
categoryValue: query.category.kind === 'subject-two'
? query.category.value
: undefined,
entry: query.entry
};
}
存储对象只包含稳定枚举,不保存页面标题,也不保存 QuestionItem[] 的整份副本。恢复时根据 schemaVersion 迁移,再交给查询工厂核对。若旧数据缺少分类,迁移规则必须明确选择 all 或某个产品默认值,并记录这是兼容决定,不能假装旧记录原本就携带了分类。
九、用入口矩阵验证上下文不会漂移
这次改造不能只核对“能进入页面”。需要把每个入口、分类切换、空题组和快速操作都放进矩阵,观察最终快照和题目集合是否一致。
| 编号 | 操作 | 应形成的查询 | 应观察到的结果 |
|---|---|---|---|
| P57-01 | 从科二模块首次进入 | 科二、图标认知、主页入口 | 只出现图标认知题 |
| P57-02 | 点击“上车检查” | 科二、上车检查、标签入口 | 新会话快照完整替换旧快照 |
| P57-03 | 从题库选择科二并开始 | 科二、明确的 all 或产品默认分类 | 行为与产品规则一致,不依赖空串 |
| P57-04 | 传入未知分类 | 工厂拒绝 | 不启动、不回退、不写历史 |
| P57-05 | 合法分类没有题 | empty | 页面展示科目与分类,并允许返回 |
| P57-06 | 启动过程中快速切换标签 | 原查询被冻结 | 已开始会话不被新标签污染 |
| P57-07 | 从历史重进旧分类 | 迁移后查询 | 合法时恢复,失效时给出恢复入口 |
| P57-08 | 科一入口误传科二分类 | 组合校验拒绝 | 不出现跨科目题目 |
除结果数组外,还应核对两个不变量:所有题目的 subject 必须等于查询科目;当分类分支是 subject-two 时,所有题目的 category 必须等于查询分类。若选择器后面还会洗牌或截取,这两个不变量应在最终会话数组上再核对一次。
十、验证清单、排障表与事实边界
- 所有练习入口都通过查询工厂或明确适配器建立
PracticeQuery。 - “全部分类”使用显式分支,不再借用空字符串。
- 科二分类名称来自单一目录,UI 与题库不各存一份。
- 非法科目与分类组合在选题前被拒绝。
- 选择器只消费查询和题目目录,不读取页面响应式字段。
- 会话状态由一个完整快照提交,不产生半更新画面。
- 空题组保留原查询并显示准确恢复入口。
- 历史只保存稳定枚举和版本,不保存页面对象或完整题库。
- 快速切换、返回重进和旧数据迁移都有独立用例。
- 目标 SDK 下的联合类型、集合写法和 ArkUI 状态提交方式经过文档与编译复核。
| 症状 | 优先检查 | 常见原因 | 处理方向 |
|---|---|---|---|
| 同为科二入口却出现不同题组 | category.kind | 一个入口传具体分类,另一个仍传空串 | 统一由适配器构造查询 |
| 点击未知分类后出现全部题 | 工厂失败分支 | 非法值被降级为 all | 明确拒绝并保留当前会话 |
| 切换标签后旧会话题目变化 | 查询是否冻结 | 选择器读取了页面当前字段 | 会话保存不可变查询快照 |
| 历史卡片分类与实际题目不符 | 历史字段来源 | 展示时读取当前标签 | 从已提交查询映射历史 |
| 空分类恢复成内置第一题 | 空结果处理 | 仍存在隐式兜底 | 返回 empty 并停止提交 |
| 新增第四个科二分类后入口不一致 | 分类目录 | UI、工厂和题库分别维护 | 用同一目录生成选项并做差异审阅 |
| 旧记录无法重进 | schemaVersion | 旧数据没有分类范围 | 写清迁移规则并提供安全退出 |
当前源码可以确认:科二页内分类标签会把具体标签传入 startTheoryPractice();题库开始卡片会把分类传为空字符串;开始方法再用科目与分类选择题目。这些是指定源码行的静态事实,不等于已经在设备上观察到跨分类故障。本文提出的 PracticeQuery、查询工厂、入口适配器、准备结果和存储快照均为建议方案,尚未合入或写入 The_kemusan。本次未构建项目,没有生成新的 HAP,也没有在模拟器或真机上验证科二入口、快速切换、历史重进和空题组行为。真正接入后仍需依据目标 SDK 文档、编译结果和设备操作补齐证据。
更多推荐



所有评论(0)