一、引言

问答(Quiz)是人类最古老的知识测试形式——从科举考试到电视智力竞赛,从驾照科目一到社交网络上的性格测试,问答的核心公式从未改变:给出一个问题,提供若干选项,选出正确答案,最后看分数。

从技术角度看,趣味问答是一个三阶段状态机:开始界面 → 答题循环 → 结果展示。与井字棋(回合制博弈)和记忆翻牌(实时翻牌+延时)不同,问答的交互是线性推进的——每道题的选择→反馈→下一题,10 道题构成一条不可逆的直线。这种线性结构简化了状态管理,但带来了新的挑战:即时反馈的视觉呈现(绿色正确/红色错误)、自动推进的节奏控制(1.5s 等待)、以及最终评级的设计(A-D 四个等级)。

本文用 ArkUI 从零构建一个趣味问答应用,包含 10 道涵盖编程与常识的选择题、即时正误反馈、自动推进下一题、计时计分和 A-D 评级。整个应用是一个三页面的条件渲染——开始页、答题页和结果页共享同一个 build() 方法,通过 quizStartedquizFinished 两个布尔变量切换。

阅读完本文,你将能够:

  • 用条件渲染实现三阶段页面切换(开始 / 答题中 / 结果)
  • 管理问答状态机(选题 → 反馈 → 自动推进)
  • 实现即时正误反馈(绿色/红色高亮,1.5s 后自动下一题)
  • 用进度条 + ABCD 评级展示答题成绩
  • 用 Fisher-Yates 洗牌随机化题目顺序

二、应用设计

2.1 三阶段状态机

应用有三个互斥的界面阶段:

开始界面                答题界面                结果界面
┌──────────┐         ┌──────────┐         ┌──────────┐
│  📝 图标  │  点击   │ 第 3/10  │  答完   │  🎉 图标  │
│ 趣味问答  │ ─────→ │ 问题...  │ ─────→ │ 答对 8/10 │
│ 开始答题  │         │ A. 选项  │         │  等级 B   │
│          │         │ B. 选项  │         │ 用时 1:23 │
│          │         │ C. 选项  │         │ 再来一次  │
│          │         │ D. 选项  │         │          │
└──────────┘         └──────────┘         └──────────┘

用两个布尔 @State 变量控制:

@State quizStarted: boolean = false;   // 是否已开始
@State quizFinished: boolean = false;  // 是否已结束

三种界面用 if / else if / else 条件渲染:

if (!this.quizStarted && !this.quizFinished) {
  // 开始界面
} else if (this.quizFinished) {
  // 结果界面
} else {
  // 答题界面
}

两个布尔变量的组合产生三种有效状态。第四种组合(quizStarted=truequizFinished=true)不应该出现,但即使出现也会走到结果界面——这是一种防御性设计。

2.2 交互流程

一局完整的问答包含 4 个交互点:

  1. 开始答题:点击"开始答题"按钮 → 初始化题库、打乱顺序、启动计时
  2. 选择答案:点击 A/B/C/D 其中一个选项 → 即时显示正误反馈
  3. 自动推进:1.5s 后自动跳到下一题(如果是最后一题则进入结果页)
  4. 再来一次:查看成绩和评级后,点击"再来一次"重新开始

注意"自动推进"不是用户主动触发的——它是 setInterval 驱动的被动行为。这意味着在选择答案后,用户有 1.5s 的时间观察反馈(哪道对了、哪道错了、正确答案是什么),然后无操作地进入下一题。这种设计给了用户"喘一口气"的时间,同时保持了答题的流畅节奏。
在这里插入图片描述

三、题库设计

3.1 Question 接口

每道题的数据结构:

interface Question {
  question: string;     // 题目文字
  options: string[];    // 四个选项
  answer: number;       // 正确答案的索引(0-3)
}

answer 是正确选项在 options 数组中的索引。这个设计比"存储正确选项的文字"更健壮——因为索引不会随着选项顺序的调整而变化,且比较时只需要一次整数相等判断(optIdx === q.answer)。

3.2 题库与答案分布

题库包含 10 道题,涵盖编程、科学、常识三个领域:

const Q_BANK: Question[] = [
  { question: '鸿蒙系统的开发语言是?',
    options: ['ArkTS', 'Java', 'Swift', 'Kotlin'], answer: 0 },
  { question: 'typeof null 在JavaScript中的结果是?',
    options: ['"null"', '"object"', '"undefined"', '"boolean"'], answer: 1 },
  { question: 'HTTP状态码 404 表示什么?',
    options: ['服务器错误', '重定向', '资源未找到', '请求成功'], answer: 2 },
  // ... 共 10 题
];

一个细微但重要的设计点:正确答案的位置不是全部放在 A(索引 0),而是分散在 A/B/C/D 四个位置。题库中 answer 的分布为 0、0、1、2、0、1、0、1、0、1——不均匀但分散,确保用户不会形成"总是选 A"的肌肉记忆。

3.3 题目随机化

每次点击"开始答题"时,使用 Fisher-Yates 洗牌算法打乱题目顺序:

startQuiz(): void {
  const pool = [...Q_BANK];
  this.shuffle(pool);
  this.questions = pool;
  // ...
}

shuffle(arr: Question[]): void {
  for (let i = arr.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    const tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
  }
}

洗牌只打乱题目的出现顺序,不打乱每道题内的选项顺序。这是因为选项顺序在题库设计中已经做好了分散(A/B/C/D 四个位置的答案分布不同),再打乱选项会导致用户困惑——例如看到"A. 太平洋"和"B. 大西洋"时,"太平洋"是正确答案,但换了顺序后"A. 大西洋"和"B. 太平洋"会让熟悉题目顺序的用户选错(尽管答案仍然是"太平洋"对应的新位置)。因此选项顺序保持题库中的定义,题目顺序随机化以增加重复玩耍的趣味性。
在这里插入图片描述

四、答题交互

4.1 selectOption 方法

用户点击某个选项后,执行以下逻辑:

selectOption(optIdx: number): void {
  if (this.selectedOption !== -1) return;  // 已选择,忽略重复点击

  const q = this.questions[this.currentIdx];
  if (optIdx === q.answer) this.score++;   // 正确则加分
  this.selectedOption = optIdx;             // 记录选择,触发反馈 UI

  this.feedbackTimerId = setInterval(() => {
    clearInterval(this.feedbackTimerId);
    this.feedbackTimerId = -1;
    this.selectedOption = -1;               // 清除反馈状态

    if (this.currentIdx < this.questions.length - 1) {
      this.currentIdx++;                    // 下一题
    } else {
      clearInterval(this.timerId);         // 最后一题 → 结束
      this.timerId = -1;
      this.quizFinished = true;
    }
  }, 1500);
}

整个方法的逻辑可以分为四个阶段:

阶段一(守卫)selectedOption !== -1 意味着用户已经做出了选择,正在进行 1.5s 的反馈展示。在此期间点击任何选项都被忽略,防止"改答案"或"快速连点跳过"。

阶段二(计分):比较 optIdx === q.answer,如果选中了正确选项则 score++。这个比较只在用户首次点击时执行一次。

阶段三(反馈):设置 selectedOption = optIdx,触发 UI 的颜色变化。此时所有四个选项变为:正确选项绿底、错误选择红底(如果选错)、其余灰色。

阶段四(推进)setInterval 在 1.5s 后清除反馈状态并推进到下一题。使用 setInterval 而非 setTimeout 是基于记忆翻牌的经验教训——setInterval 在 ArkTS 中经过验证可靠。

4.2 即时反馈

反馈期间的选项颜色由 optionBg() 方法动态计算:

optionBg(optIdx: number): string {
  if (this.selectedOption === -1) return '#F5F5FA';           // 未选择:浅灰
  const correct = this.questions[this.currentIdx].answer;
  if (optIdx === correct) return '#C8E6C9';                    // 正确:浅绿
  if (optIdx === this.selectedOption && optIdx !== correct)
    return '#FFCDD2';                                           // 选错:浅红
  return '#E8E8EE';                                            // 其余:更浅灰
}

三种反馈颜色传递的信息:

选项状态 背景色 文字色 边框 含义
正确答案 #C8E6C9 浅绿 #2E7D32 深绿 绿色边框 “这是对的”
选错答案 #FFCDD2 浅红 #C62828 深红 红色边框 “你选了这个,但它不对”
未选错误 #E8E8EE 浅灰 #BBBBBB “无关选项”

这种三色反馈同时达到了两个目的:(1) 让用户知道自己是否答对,(2) 如果答错,让用户知道正确答案是什么。1.5s 的展示时间足够用户消化这个信息。

4.3 选项标签

每个选项前有一个圆形标签(A/B/C/D),在反馈阶段也跟随变色:

Text(label)
  .width(24)
  .height(24)
  .borderRadius(12)
  .backgroundColor(
    this.selectedOption === -1 ? '#E8E8EE' :        // 未选:灰圆
    (oi === correct ? '#C8E6C9' :                    // 正确:绿圆
    (oi === this.selectedOption ? '#FFCDD2' : '#E8E8EE')))  // 选错:红圆
  .textAlign(TextAlign.Center)

圆标签的引入不仅仅是为了美观——它将四个选项从"无差别文字块"变成"A/B/C/D 四个可区分的实体",降低了用户的视觉搜索成本。用户可以在扫一眼之后直接记住"答案是 C",而不需要重读选项文字来确认位置。

五、计时与计分

5.1 计时器

计时从"开始答题"按钮被点击时启动,在最后一题的反馈结束后停止:

// startQuiz() 中:
this.timerId = setInterval(() => { this.elapsedSec++; }, 1000);

// selectOption() 中,最后一题反馈后:
clearInterval(this.timerId);

计时器只在答题阶段运行,不在开始界面和结果界面运转。这样用户可以在开始界面从容阅读说明,在结果界面仔细查看成绩,而不必担心计时器空转。

5.2 进度条

页面顶部有一个细长的进度条,实时反映答题进度:

Row() {
  Row() {
  }
  .width(`${(this.currentIdx / this.questions.length) * 100}%`)
  .height(4)
  .backgroundColor('#667eea')
  .borderRadius(2)
}
.width('100%')
.height(4)
.backgroundColor('#E8E8EE')
.borderRadius(2)

外层的灰色 Row 是进度条背景,内层的蓝色 Row 是填充部分。填充宽度使用百分比字符串动态计算——currentIdx / questions.length * 100。例如第 5 题时,进度为 5/10 = 50%。

进度条的上方还有三个信息项:当前题号(第 X/10 题)、用时(MM:SS)和当前得分(✓ X),形成一个紧凑的信息行。

5.3 评级系统

结果页面使用 A-D 四个等级评价成绩:

grade(): string {
  const pct = this.score / this.questions.length;
  if (pct >= 0.9) return 'A';
  if (pct >= 0.7) return 'B';
  if (pct >= 0.5) return 'C';
  return 'D';
}

gradeColor(): string {
  const pct = this.score / this.questions.length;
  if (pct >= 0.9) return '#52C41A';  // A → 绿色
  if (pct >= 0.7) return '#1677FF';  // B → 蓝色
  if (pct >= 0.5) return '#FAAD14';  // C → 黄色(背景用,非白字叠底)
  return '#FF4D4F';                   // D → 红色
}

等级跨度:

  • A(90%+):9-10 题正确,卓越
  • B(70-89%):7-8 题正确,良好
  • C(50-69%):5-6 题正确,及格
  • D(<50%):0-4 题正确,继续努力

等级用 48sp 大号粗体字展示,颜色随等级变化。下方有一个水平进度条,填充宽度等于正确率百分比,颜色与等级颜色一致——在分数和视觉上双重呈现成绩。

注意 C 级使用 #FAAD14(黄色),但仅用于评级文字和进度条,不作为白色文字的叠加背景——遵循项目中"黄色不给白字做背景"的颜色规则。

六、UI 设计

6.1 开始界面

开始界面是一个垂直居中的介绍页,包含:

  • 64sp 📝 图标(大号 emoji,营造轻松氛围)
  • "趣味问答"标题
  • 两句说明文字(“10 道题,测试你的知识储备” + “涵盖编程、科学、常识等多个领域”)
  • "开始答题"按钮(紫色 #667eea

界面极简、无干扰——用户只有一个操作:点击"开始答题"。这是一个经典的"单按钮启动页"设计,减少初次用户的选择成本。

6.2 答题界面

答题界面从上到下依次为:

第 3/10 题              01:23           ✓ 2
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━  ← 进度条

光年是什么的单位?

  A  距离                                ← 四个选项
  B  时间
  C  速度
  D  质量

问题文字使用 18sp 粗体,独占一行,与选项之间有 24vp 的间距——确保问题和选项在视觉上明确分开。选项使用全宽卡片布局,点击区域覆盖整个卡片(而非仅文字),减少误触。

6.3 结果界面

结果界面以垂直居中方式展示:

  • 表情图标(🎉 8+ 分 / 👍 5-7 分 / 💪 0-4 分)
  • "答题完成"标题
  • 答对数字(“答对 X / 10 题”)
  • 48sp 彩色评级字母
  • 用时统计
  • 百分比进度条
  • "再来一次"按钮

表情图标的选择是一个细节:高分用 🎉(庆祝)、中等用 👍(鼓励)、低分用 💪(加油),不使用任何带有贬低或负面含义的图标。这种"总是鼓励"的反馈风格让用户即使得分不高也愿意再试一次。

七、完整代码结构

QuizPage (~270 行)
├── 数据定义
│   ├── Question — 题目接口(question / options / answer)
│   ├── Q_BANK[10] — 题库
│   └── LABELS[4] — 选项标签 ['A','B','C','D']
├── 状态变量
│   ├── @State questions — 当前局的题目(已洗牌)
│   ├── @State currentIdx / score / elapsedSec — 答题进度
│   ├── @State quizStarted / quizFinished — 阶段控制
│   └── @State selectedOption — 反馈状态(-1=未选)
├── 游戏逻辑
│   ├── startQuiz() — 洗牌 + 初始化 + 启动计时
│   ├── selectOption() — 选题 → 计分 → 反馈 → 推进
│   └── shuffle() — Fisher-Yates 洗牌
├── 视觉辅助
│   ├── optionBg() — 选项背景色(灰/绿/红)
│   ├── optionTextColor() — 选项文字色
│   ├── grade() — A-D 等级
│   └── gradeColor() — 等级颜色
├── 视图(三阶段条件渲染)
│   ├── 开始界面 — 图标 + 说明 + 开始按钮
│   ├── 答题界面 — 进度条 + 问题 + 四选项(ForEach)
│   └── 结果界面 — 等级 + 分数 + 进度条 + 再来一次
└── 生命周期
    └── aboutToDisappear() — 清理计时器和反馈定时器

八、总结

本文从零构建了一个趣味问答应用。与前两篇游戏类文章(记忆翻牌、井字棋)不同,趣味问答不是一款游戏——它是教育测试工具,每道题的反馈都有"学习"的目的。从技术角度看,它也是三阶段条件渲染的典型示例——两个布尔变量切换三个完整界面,每个界面的布局、交互和信息密度都截然不同。

核心要点回顾:

  1. 三阶段状态机:用 quizStartedquizFinished 两个 @State 布尔变量控制"开始 → 答题 → 结果"三阶段的 if/else if/else 条件渲染。两个变量产生 2²=4 种组合,其中 3 种有效(第四种防御性兜底)。

  2. 即时三色反馈:选中答案后,正确选项绿底绿字、错误选择红底红字、其余灰色——三色系统同时传达"正确答案是什么"和"你选了什么"两个信息。1.5s 反馈时长给用户充足的学习时间,又不打断答题节奏。

  3. 自动推进setInterval(fn, 1500) 在反馈期结束后自动调用下一题或结束。selectedOption 从 -1 变为选择索引再变回 -1,驱动 UI 在"可交互"和"反馈展示"之间切换。重复点击守卫(if (this.selectedOption !== -1) return)防止反馈期间改答案。

  4. Fisher-Yates 洗题:每次开始前打乱题目顺序,但保持选项内部顺序不变——洗题目增加重玩性,不洗选项避免混淆。这是与密码生成器和记忆翻牌中同一算法的第三次应用。

  5. A-D 评级 + 进度条:4 级评级(90%→A, 70%→B, 50%→C, 其余→D),四种等级颜色(绿/蓝/黄/红)。水平进度条用百分比宽度可视化成绩。C 级的黄色仅用于文字和进度条,不给白字做背景。

  6. 总是鼓励的结果设计:高分 🎉、中等 👍、低分 💪——三个表情符号覆盖全部等级,没有一个暗示"失败"。这种"正向引导"的结果展示降低了用户放弃的可能性,增加了"再来一次"的点击率。

趣味问答是三道选择题和一段说明文字构成的最小知识测试单元,但在这个 270 行的 ArkUI 实现中,它包含了题库管理、随机化、即时反馈、自动推进、计时计分和评级展示的完整流水线。它是教育类应用的微型原型,也是三阶段条件渲染的集中示范。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐