![封面

科二灯光基础覆盖图标认知、上车检查、夜间场景。源码用 subject2CategoryCategoryChip 控制筛选条件。

本文只看科二分类练习。科二灯光基础被拆成图标认知、上车检查、夜间场景三类,当前分类直接决定抽题范围。

先看这篇要解决的具体问题

科二分类 的核心不是某个 ArkTS 语法点,而是边界划分。一个练习功能至少有四个层面:入口从哪里来,数据在哪里定义,用户动作怎样改变状态,结果是否保存。只看 UI 会漏掉判题,只看题库会漏掉重置,只看存储又解释不了按钮为什么这样设计。

更稳的做法是把本文的功能拆成下面六步:默认图标认知 -> 点击分类 Chip -> 更新分类状态 -> 重启科二练习 -> 按 category 过滤题 -> 答题写记录。后面所有代码片段都服务于这条线,不额外展开无关模块。

流程图

源码边界和文件分工

源码位置 作用
entry/src/main/ets/pages/Index.ets 页面状态、Builder、入口分发和业务处理
entry/src/main/ets/models/QuestionBank.ets 模块入口、题库、按钮动作和命令定义

这些节选不是为了贴完整文件,而是为了把当前链路看清楚。

Index.ets 维护 subject2Category 并用 Chip 切换,QuestionBank.ets 的科二种子提供分类题源,selectQuestions 执行过滤。

源码路径:D:\ProgramData\huawei\lesson\The_kemusan
阅读顺序:入口 -> 数据 -> 状态 -> UI -> 本地记录
写作边界:只解释当前源码能证实的功能,不替代真机运行和发布验证

subject2Category 决定当前题池

源码节选:entry/src/main/ets/pages/Index.ets:370

  @Builder
  SubjectTwoTabs() {
    Row({ space: 8 }) {
      this.CategoryChip('图标认知')
      this.CategoryChip('上车检查')
      this.CategoryChip('夜间场景')
    }
    .width('100%')
    .padding({ left: 20, right: 20 })
    .margin({ bottom: 12 })
  }

  @Builder
  CategoryChip(label: string) {
    Text(label)
      .fontSize(14)
      .fontWeight(FontWeight.Medium)
      .fontColor(this.subject2Category === label ? Color.White : $r('app.color.text_secondary'))
      .textAlign(TextAlign.Center)
      .padding({ left: 12, right: 12, top: 8, bottom: 8 })
      .backgroundColor(this.subject2Category === label ? $r('app.color.accent') : $r('app.color.panel_bg'))
      .borderRadius(16)
      .border({ width: 1, color: $r('app.color.border_subtle') })
      .layoutWeight(1)
      .onClick(() => {
        this.subject2Category = label;
        this.startTheoryPractice(SUBJECT_TWO, '科二灯光基础', label);
      })
  }

源码节选:entry/src/main/ets/models/QuestionBank.ets:408

const SUBJECT_TWO_SEEDS: GeneratedQuestionSeed[] = [
  {
    category: '图标认知',
    scenario: '看到仪表盘绿色近光灯图标',
    correct: '确认近光灯处于开启状态',
    wrongOne: '判断远光灯处于开启状态',
    wrongTwo: '判断双闪处于开启状态',
    wrongThree: '认为灯光系统故障',
    explanation: '科二基础认知需要能区分近光、远光、雾灯和双闪图标。'
  },
  {
    category: '图标认知',
    scenario: '看到两个转向指示箭头同时闪烁',
    correct: '通常表示危险报警闪光灯开启',
    wrongOne: '表示远光灯开启',
    wrongTwo: '表示近光灯关闭',
    wrongThree: '表示制动灯开启',
    explanation: '左右转向指示同时闪烁一般对应危险报警闪光灯。'
  },
  {
    category: '图标认知',
    scenario: '识别雾灯图标时',
    correct: '注意前后雾灯图标方向和颜色差异',
    wrongOne: '只看车速表指针',
    wrongTwo: '把雾灯图标都当作远光灯',
    wrongThree: '不需要识别雾灯图标',
    explanation: '雾灯在恶劣天气中很关键,应能通过仪表图标确认状态。'
  },
  {
    category: '上车检查',
    scenario: '上车准备开始灯光项目',
    correct: '先确认灯光开关和仪表状态',
    wrongOne: '直接开启所有灯光',
    wrongTwo: '忽略灯光状态直接起步',
    wrongThree: '只检查座椅不看仪表',
    explanation: '上车检查要先确认车辆初始状态,避免带灯或误灯操作。'
  },
  {
    category: '上车检查',
    scenario: '发现远光灯处于开启状态',
    correct: '切回近光或关闭不必要灯光',
    wrongOne: '保持远光灯继续练习',
    wrongTwo: '开启左转向灯代替处理',
    wrongThree: '开启车内照明灯即可',
    explanation: '远光误开会影响安全,应及时切换到正确状态。'
  },
  {
    category: '上车检查',
    scenario: '准备模拟夜间灯光考试',
    correct: '听到指令后再按场景操作',
    wrongOne: '提前连续点击多个灯光按钮',
    wrongTwo: '只开启双闪等待',
    wrongThree: '不看指令随意操作',
    explanation: '灯光考试重在听清指令,抢答和乱点容易造成错误。'
  },
  {
    category: '夜间场景',
    scenario: '夜间起步前需要确认车辆被看见',
    correct: '按指令开启前照灯或近光灯',
    wrongOne: '只开启远光灯照射前方',
    wrongTwo: '不开灯直接起步',
    wrongThree: '只开启倒车灯',
    explanation: '夜间起步应保证车辆照明状态正确,不能误用远光。'
  },
  {
    category: '夜间场景',
    scenario: '夜间通过人行横道前',
    correct: '减速观察并按要求远近光交替示意',
    wrongOne: '关闭灯光快速通过',
    wrongTwo: '只开启雾灯',
    wrongThree: '持续使用远光灯不变',
    explanation: '夜间通过人行横道应减速礼让,灯光示意用于提醒。'
  },
  {
    category: '夜间场景',
    scenario: '雨雪天视线变差时',
    correct: '开启近光灯并提高可见性',
    wrongOne: '关闭灯光避免耗电',
    wrongTwo: '只使用远光灯',
    wrongThree: '只开转向灯行驶',
    explanation: '雨雪天气应使用合适灯光提高车辆可见性。'
  }
];

科二题不是简单混合题池。图标认知和夜间场景训练目标不同,如果混在一起,用户很难集中补短板。

subject2Category 是题池输入,不只是 UI 选中状态。点击 Chip 后必须重新调用 startTheoryPractice

分类 Chip 触发重新抽题

源码节选:entry/src/main/ets/pages/Index.ets:1116

  private startTheoryPractice(subject: string, mode: string, category: string): void {
    this.activeSubject = subject;
    this.activeMode = mode;
    this.activeQuestions = this.selectTestQuestions(subject, category);
    this.activeQuestionIndex = 0;
    this.selectedAnswer = '';
    this.answerMessage = '请选择答案';
    this.answerCorrect = true;
  }

源码节选:entry/src/main/ets/pages/Index.ets:1200

  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;
  }

  private selectTestQuestions(subject: string, category: string): QuestionItem[] {
    const source = this.selectQuestions(subject, category);
    const shuffled: QuestionItem[] = [];
    for (let index = 0; index < source.length; index++) {
      shuffled.push(source[index]);
    }
    for (let index = shuffled.length - 1; index > 0; index--) {
      const target = Math.floor(Math.random() * (index + 1));
      const current = shuffled[index];
      shuffled[index] = shuffled[target];
      shuffled[target] = current;
    }
    const result: QuestionItem[] = [];
    const maxCount = Math.min(THEORY_TEST_COUNT, shuffled.length);
    for (let index = 0; index < maxCount; index++) {
      result.push(shuffled[index]);
    }
    return result;
  }

分类 Chip 的职责很清楚:更新分类状态,然后用该分类重启科二练习。它不需要自己过滤题目。

真正的过滤发生在 selectQuestions:科目要等于 subject2,分类为空或等于当前分类才进入结果。

结构图

过滤逻辑必须同时看科目和分类

源码节选:entry/src/main/ets/pages/Index.ets:1266

  private answerTheoryQuestion(answer: string): void {
    if (this.selectedAnswer.length > 0 || this.activeQuestions.length === 0) {
      return;
    }
    const question = this.getCurrentQuestion();
    const correctAnswer = getQuestionAnswer(question);
    const correct = answer === correctAnswer;
    const correctText = this.getOptionText(question, correctAnswer);
    const selectedText = this.getOptionText(question, answer);
    this.selectedAnswer = answer;
    this.answerCorrect = correct;
    this.answerMessage = correct ? '回答正确' : `回答错误,正确选项:${correctAnswer}. ${correctText}`;
    this.addSimpleRecord(question.subject, this.activeMode, correct, correct ? `答对:${question.title}` : `答错:${question.title}`,
      correct ? '' : question.id, correct ? '' : correctText, correct ? '' : selectedText);
  }

分类状态会影响 activeQuestions、当前题索引、已选答案和反馈文案。切换分类时这些状态都要回到新题池的初始状态。

迁移时把分类作为抽题参数,而不是作为显示标签。这样“重新开始”和“下一题”都会留在同一分类里。

迁移到自己的 HarmonyOS 项目

迁移顺序建议保持保守:先抽模型,再接入口,然后接页面,最后接本地记录。不要一上来就把所有 Builder、题库和存储混在一个文件里。当前主题 科二分类 可以按下面的最小切片实现:

1. 定义模型:Subject、QuestionItem、PracticeRecord、action 常量。
2. 定义数据:模块入口、题库、灯光命令或历史记录结构。
3. 定义状态:当前页面、当前题、当前灯光、反馈文案和记录列表。
4. 定义 UI:状态行、题面、按钮区、历史卡片或搜索结果。
5. 定义验证:正确流、错误流、重置流和边界流都要能复现。

分类输入稳定后,后续增加“灯光开关位置”或“仪表提示”也只是新增分类和题源。

验证路线

  1. 进入科二默认图标认知
  2. 切分类后题目随之变化
  3. 下一题不跳到其他分类

除了上面的业务核对,还建议记录一个最小验证表:

主题:科二分类
输入:入口点击、按钮点击、题目选择或关键词输入
状态:currentPage、lightState、timeLeft、selectedAnswer、records
输出:页面提示、状态高亮、历史记录、搜索结果
边界:返回首页、重置、重复点击、空记录、超时

这里的验证是面向源码链路的核对,不等同于真机兼容性、性能或上架流程。如果要证明设备行为,还需要单独构建安装并操作页面。

常见问题与处理方式

现象 常见原因 处理方式
页面能点但结果不对 按钮 action 与题库 action 没有对齐 先看模型常量,再看题库命令和处理函数
返回后状态残留 计时器或闪烁器没有释放 核对 stopTimer、stopAltFlash 和 reset 方法
历史记录不完整 写记录时只保存了展示文案 补齐 subject、mode、correctOption、selectedOption

科二题不变时先查 Chip 点击是否重新抽题,再查题源的 category 文案是否和筛选条件完全一致。

小结

科二分类 这条线把灯光模拟从“能点按钮”推进到“可练习、可判题、可复盘”。The_kemusan 的代码没有把所有能力做成复杂框架,而是用清晰的模型、页面状态和本地记录完成闭环。写 CSDN 文章时,把这条闭环讲清楚,比堆更多 API 名词更有价值。
](https://i-blog.csdnimg.cn/direct/ec4a4f74b93a4a018066e9706ef9fe32.png#pic_center)
在这里插入图片描述
在这里插入图片描述

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐