本篇开发科举考试模块,实现答题、计时、成绩计算

【完整案例】科举考试模块开发 教程结构图

图:【完整案例】科举考试模块开发 的关键流程与实现要点。

学习目标

  • ✅ 设计题库数据结构
  • ✅ 实现答题流程
  • ✅ 开发计时功能
  • ✅ 计算成绩与功名

预计学习时间

约 150 分钟


实战一:题库设计

interface Question {
  id: number;
  content: string;
  options: string[];
  correctIndex: number;
  explanation: string;
  difficulty: 'easy' | 'medium' | 'hard';
}

interface ExamLevel {
  id: string;
  name: string;
  title: string;        // 通过后获得的功名
  questionCount: number;
  timeLimit: number;    // 秒
  passScore: number;    // 及格分数
}

const EXAM_LEVELS: ExamLevel[] = [
  { id: 'county', name: '县试', title: '童生', questionCount: 5, timeLimit: 300, passScore: 60 },
  { id: 'prefecture', name: '府试', title: '秀才', questionCount: 8, timeLimit: 480, passScore: 70 },
  { id: 'provincial', name: '乡试', title: '举人', questionCount: 10, timeLimit: 600, passScore: 75 },
  { id: 'metropolitan', name: '会试', title: '贡士', questionCount: 12, timeLimit: 720, passScore: 80 },
  { id: 'palace', name: '殿试', title: '进士', questionCount: 15, timeLimit: 900, passScore: 85 }
];

**案例效果**:考试级别选择界面:

┌──────────────────────────────────────┐ │ 科举等级一览 │ ├──────────────────────────────────────┤ │ 级别 功名 题数 时限 及格 │ │ ─────────────────────────────────── │ │ 县试 童生 5 5分 60 │ │ 府试 秀才 8 8分 70 │ │ 乡试 举人 10 10分 75 │ │ 会试 贡士 12 12分 80 │ │ 殿试 进士 15 15分 85 │ └──────────────────────────────────────┘


---

## 实战二:答题页面

@Component export struct ExamTestPage { @Consume('mainNavPathStack') navStack: NavPathStack; @State currentLevel: ExamLevel | null = null; @State questions: Question[] = []; @State currentIndex: number = 0; @State selectedAnswer: number = -1; @State answers: number[] = []; @State timeLeft: number = 0; @State isFinished: boolean = false; private timerId: number = -1;

aboutToAppear() { const params = this.navStack.getParamByName('ExamTestPage'); if (params && params.length > 0) { const param = params[0] as { levelId: string }; this.currentLevel = EXAM_LEVELS.find(l => l.id === param.levelId) || null; if (this.currentLevel) { this.loadQuestions(); this.startTimer(); } } }

aboutToDisappear() { if (this.timerId !== -1) clearInterval(this.timerId); }

loadQuestions() { // 从题库随机抽取题目 this.questions = getRandomQuestions(this.currentLevel!.questionCount); this.answers = new Array(this.questions.length).fill(-1); this.timeLeft = this.currentLevel!.timeLimit; }

startTimer() { this.timerId = setInterval(() => { this.timeLeft--; if (this.timeLeft <= 0) { clearInterval(this.timerId); this.finishExam(); } }, 1000); }

selectAnswer(index: number) { this.selectedAnswer = index; this.answers[this.currentIndex] = index; }

nextQuestion() { if (this.currentIndex < this.questions.length - 1) { this.currentIndex++; this.selectedAnswer = this.answers[this.currentIndex]; } else { this.finishExam(); } }

finishExam() { if (this.timerId !== -1) clearInterval(this.timerId); this.isFinished = true;

// 计算成绩 let correctCount = 0; this.questions.forEach((q, i) => { if (this.answers[i] === q.correctIndex) correctCount++; }); const score = Math.round((correctCount / this.questions.length) * 100); const passed = score >= this.currentLevel!.passScore;

// 跳转结果页 this.navStack.replacePath({ name: 'ExamResultPage', param: { levelId: this.currentLevel!.id, score, passed, correctCount, totalCount: this.questions.length } }); }

formatTime(seconds: number): string { const min = Math.floor(seconds / 60); const sec = seconds % 60; return ${min.toString().padStart(2, '0')}:${sec.toString().padStart(2, '0')}; }

build() { NavDestination() { Column() { // 头部:进度和时间 Row() { Text(${this.currentIndex + 1}/${this.questions.length}) .fontSize(14).fontColor('#64748b') Blank() Text(this.formatTime(this.timeLeft)) .fontSize(16).fontWeight(FontWeight.Medium) .fontColor(this.timeLeft < 60 ? '#ef4444' : '#1e293b') } .width('100%').padding(16).backgroundColor(Color.White)

// 进度条 Progress({ value: this.currentIndex + 1, total: this.questions.length }) .width('100%').height(4).color('#c41e3a')

if (this.questions.length > 0) { Scroll() { Column({ space: 20 }) { // 题目 Text(this.questions[this.currentIndex].content) .fontSize(16).fontColor('#1e293b').lineHeight(24) .width('100%').padding(16).backgroundColor(Color.White).borderRadius(12)

// 选项 ForEach(this.questions[this.currentIndex].options, (option: string, index: number) => { Row() { Text(String.fromCharCode(65 + index)) .fontSize(14).fontColor(this.selectedAnswer === index ? Color.White : '#1e293b') .width(28).height(28).textAlign(TextAlign.Center) .backgroundColor(this.selectedAnswer === index ? '#c41e3a' : '#f0f0f0') .borderRadius(14)

Text(option) .fontSize(14).fontColor('#1e293b').margin({ left: 12 }) .layoutWeight(1) } .width('100%').padding(16) .backgroundColor(this.selectedAnswer === index ? '#fef2f2' : Color.White) .borderRadius(12) .border({ width: this.selectedAnswer === index ? 1 : 0, color: '#c41e3a' }) .onClick(() => this.selectAnswer(index)) }) } .padding(16) } .layoutWeight(1)

// 底部按钮 Row() { Button(this.currentIndex === this.questions.length - 1 ? '提交答卷' : '下一题') .width('100%').height(48).backgroundColor('#c41e3a') .enabled(this.selectedAnswer !== -1) .onClick(() => this.nextQuestion()) } .width('100%').padding(16).backgroundColor(Color.White) } } .width('100%').height('100%').backgroundColor('#f8f6f5') } .hideTitleBar(true) } }

案例效果:答题页面展示如下:

┌──────────────────────────────────────┐
│  3/10                       04:32   │
│  ▓▓▓▓▓▓▓▓▓░░░░░░░░░░░░░░░░░░░░░  │ ← 进度条(红色)
├──────────────────────────────────────┤
│                                      │
│  ┌──────────────────────────────┐   │
│  │ 唐朝的最高行政机构"三省"    │   │
│  │ 不包括以下哪个?            │   │
│  └──────────────────────────────┘   │
│                                      │
│  ┌──────────────────────────────┐   │
│  │ Ⓐ  中书省                   │   │
│  └──────────────────────────────┘   │
│  ┌══════════════════════════════┐   │
│  ║ Ⓑ  门下省        ← 已选中   ║   │  ← 红色边框+浅红背景
│  └══════════════════════════════┘   │
│  ┌──────────────────────────────┐   │
│  │ Ⓒ  尚书省                   │   │
│  └──────────────────────────────┘   │
│  ┌──────────────────────────────┐   │
│  │ Ⓓ  翰林院                   │   │
│  └──────────────────────────────┘   │
│                                      │
│  ┌══════════════════════════════┐   │
│  ║          下一题              ║   │  ← 最后一题显示「提交答卷」
│  └══════════════════════════════┘   │
└──────────────────────────────────────┘

效果说明: - 顶部左侧显示题号进度(3/10),右侧显示倒计时 - 时间不足60秒时,计时器文字变红色(#ef4444)警示 - 进度条实时反映答题进度 - 选中选项高亮:红色边框 + 浅红背景(#fef2f2) - 选项字母圆标:选中变红底白字,未选灰底黑字 - 未选择答案时「下一题」按钮禁用(置灰)


实战三:结果页面

@Component
export struct ExamResultPage {
  @Consume('mainNavPathStack') navStack: NavPathStack;
  @State score: number = 0;
  @State passed: boolean = false;
  @State title: string = '';
  @State levelName: string = '';

  aboutToAppear() {
    const params = this.navStack.getParamByName('ExamResultPage');
    if (params && params.length > 0) {
      const param = params[0] as { levelId: string; score: number; passed: boolean };
      this.score = param.score;
      this.passed = param.passed;
      const level = EXAM_LEVELS.find(l => l.id === param.levelId);
      if (level) {
        this.levelName = level.name;
        this.title = param.passed ? level.title : '';
      }
    }
  }

  build() {
    NavDestination() {
      Column({ space: 24 }) {
        Blank().layoutWeight(1)

        // 结果图标
        Image(this.passed ? $r('app.media.ic_trophy') : $r('app.media.ic_close'))
          .width(80).height(80)
          .fillColor(this.passed ? '#fbbf24' : '#ef4444')

        // 结果文字
        Text(this.passed ? '恭喜通过!' : '未能通过')
          .fontSize(24).fontWeight(FontWeight.Bold)
          .fontColor(this.passed ? '#22c55e' : '#ef4444')

        // 分数
        Text(`${this.score}分`)
          .fontSize(48).fontWeight(FontWeight.Bold).fontColor('#1e293b')

        // 功名
        if (this.passed && this.title) {
          Column({ space: 4 }) {
            Text('获得功名')
              .fontSize(14).fontColor('#64748b')
            Text(this.title)
              .fontSize(28).fontWeight(FontWeight.Bold).fontColor('#c41e3a')
          }
          .padding(20).backgroundColor('#fffbeb').borderRadius(12)
        }

        Blank().layoutWeight(1)

        // 按钮
        Column({ space: 12 }) {
          Button('返回首页')
            .width('100%').height(48).backgroundColor('#c41e3a')
            .onClick(() => {
              this.navStack.clear();
            })

          if (!this.passed) {
            Button('重新挑战')
              .width('100%').height(48).backgroundColor('#f0f0f0').fontColor('#1e293b')
              .onClick(() => this.navStack.pop())
          }
        }
        .width('100%').padding(16)
      }
      .width('100%').height('100%').padding(20).backgroundColor('#f8f6f5')
    }
    .hideTitleBar(true)
  }
}

**案例效果**:考试结果展示如下:

┌──────────────── 通过 ────────────────┐ │ │ │ 🏆 │ ← 金色奖杯(通过) │ 恭喜通过! │ ← 绿色文字 │ │ │ 85分 │ ← 超大字号 │ │ │ ┌──────────────┐ │ │ │ 获得功名 │ │ │ │ 举人 │ │ ← 红色大字,黄色背景框 │ └──────────────┘ │ │ │ │ ┌══════════════════════════════┐ │ │ ║ 返回首页 ║ │ │ └══════════════════════════════┘ │ └──────────────────────────────────────┘

┌──────────────── 未通过 ──────────────┐ │ │ │ ✖ │ ← 红色叉号 │ 未能通过 │ ← 红色文字 │ │ │ 58分 │ │ │ │ ┌══════════════════════════════┐ │ │ ║ 返回首页 ║ │ │ └══════════════════════════════┘ │ │ ┌──────────────────────────────┐ │ │ │ 重新挑战 │ │ ← 仅未通过时显示 │ └──────────────────────────────┘ │ └──────────────────────────────────────┘


> **效果说明**:
> - 通过时显示金色🏆奖杯 + 绿色「恭喜通过!」
> - 未通过时显示红色✖ + 红色「未能通过」
> - 分数使用超大字号(48px)居中展示
> - 通过后显示获得的功名(黄色背景框突出)
> - 未通过时额外显示「重新挑战」按钮

---

## 本课小结

| 功能 | 实现方式 |
|---|---|
| 题库管理 | 分难度、分级别 |
| 答题流程 | 状态管理 + 索引控制 |
| 计时功能 | setInterval + 倒计时 |
| 成绩计算 | 正确率 × 100 |

---

## 下一课预告

第26课开发职业性格测试,包括测试题设计、结果类型映射。

项目开源地址

https://gitcode.com/daleishen/gujinzhijian

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐