HarmonyOS APP<<古今职鉴定>>开源教程第25篇:【完整案例】科举考试模块开发
本篇开发科举考试模块,实现答题、计时、成绩计算

图:【完整案例】科举考试模块开发 的关键流程与实现要点。
学习目标
- ✅ 设计题库数据结构
- ✅ 实现答题流程
- ✅ 开发计时功能
- ✅ 计算成绩与功名
预计学习时间
约 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课开发职业性格测试,包括测试题设计、结果类型映射。
项目开源地址
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)