第27篇:考试系统 - 成绩分析与错题

📚 本篇导读
在上一篇中,我们实现了考试系统的题库管理和答题流程。本篇将完善考试系统的后续功能,包括成绩详情展示、错题分析、历史记录管理等,帮助用户更好地了解学习效果和薄弱环节。
本篇将实现:
- 📊 成绩详情展示(得分、正确率、用时统计)
- 📈 答题统计分析(知识点分析、薄弱环节)
- ❌ 错题本功能(错题收集、解析查看)
- 📝 答案解析展示(详细解释、知识点说明)
- 📋 历史记录管理(考试记录、成绩趋势)
🎯 学习目标
完成本篇教程后,你将掌握:
- 如何设计成绩展示页面
- 如何实现错题分析功能
- 如何展示答案解析
- 如何管理历史记录
- 数据统计和可视化展示
一、成绩结果页面
1.1 页面结构设计
成绩结果页
├── 成绩概览
│ ├── 通过状态(通过/未通过)
│ ├── 总分显示
│ ├── 答题统计(总题数、正确数、错误数)
│ └── 用时统计
│
├── 操作按钮
│ ├── 查看错题解析
│ ├── 重新考试
│ └── 返回考试中心
│
└── 详细分析
├── 错题列表
├── 答案解析
├── 知识点分析
└── 薄弱环节提示
1.2 成绩展示页面
文件位置 :entry/src/main/ets/pages/Exam/ExamResultPage.ets
typescript
import { router } from '@kit.ArkUI';
import { ExamRecord, ExamQuestion, UserAnswer } from '../../models/ExamModels';
@Entry
@Component
export struct ExamResultPage {
@State record: ExamRecord | null = null;
@State questions: ExamQuestion[] = [];
@State showAnalysis: boolean = false;
aboutToAppear(): void {
const params = router.getParams() as Record<string, Object>;
this.record = params['record'] as ExamRecord;
this.questions = params['questions'] as ExamQuestion[];
}
build() {
Column() {
if (this.record) {
if (!this.showAnalysis) {
this.buildResultSummary()
} else {
this.buildAnalysisView()
}
}
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.background'))
}
}
1.3 成绩概览卡片
typescript
@Builder
buildResultSummary() {
Column() {
// 顶部结果卡片
Column() {
// 通过/未通过图标
Text(this.record!.passed ? '🎉' : '😔')
.fontSize(64)
.margin({ bottom: 16 })
Text(this.record!.passed ? '恭喜通过!' : '继续加油!')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
.margin({ bottom: 8 })
Text(`得分:${this.record!.score}分`)
.fontSize(48)
.fontWeight(FontWeight.Bold)
.fontColor(this.record!.passed ? '#4CAF50' : '#F44336')
.margin({ bottom: 24 })
// 统计信息
Row({ space: 32 }) {
this.buildStatItem('总题数', this.record!.totalQuestions.toString())
this.buildStatItem('正确', this.record!.correctCount.toString(), '#4CAF50')
this.buildStatItem('错误',
(this.record!.totalQuestions - this.record!.correctCount).toString(), '#F44336')
}
.margin({ bottom: 24 })
// 用时
Text(`用时:${this.formatDuration(this.record!.endTime - this.record!.startTime)}`)
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
}
.width('100%')
.padding(32)
.backgroundColor($r('app.color.card_background'))
.borderRadius(16)
.margin(16)
// 操作按钮
this.buildActionButtons()
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
@Builder
buildStatItem(label: string, value: string, color?: string) {
Column({ space: 4 }) {
Text(value)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor(color || $r('app.color.text_primary'))
Text(label)
.fontSize(12)
.fontColor($r('app.color.text_tertiary'))
}
}
@Builder
buildActionButtons() {
Column({ space: 12 }) {
Button('查看错题解析')
.width('80%')
.height(48)
.fontSize(16)
.backgroundColor($r('app.color.primary_professional'))
.onClick(() => {
this.showAnalysis = true;
})
Button('重新考试')
.width('80%')
.height(48)
.fontSize(16)
.backgroundColor($r('app.color.card_background'))
.fontColor($r('app.color.text_primary'))
.onClick(() => {
this.retakeExam();
})
Button('返回考试中心')
.width('80%')
.height(48)
.fontSize(16)
.backgroundColor($r('app.color.card_background'))
.fontColor($r('app.color.text_primary'))
.onClick(() => {
router.back();
})
}
.width('100%')
.padding(16)
}
/**
* 格式化时长
*/
private formatDuration(milliseconds: number): string {
const minutes = Math.floor(milliseconds / 60000);
const seconds = Math.floor((milliseconds % 60000) / 1000);
return `${minutes}分${seconds}秒`;
}
/**
* 重新考试
*/
private retakeExam(): void {
router.replaceUrl({
url: 'pages/Exam/ExamQuestionPage',
params: { level: this.record!.level }
});
}
二、错题分析功能
2.1 错题分析视图
typescript
@Builder
buildAnalysisView() {
Column() {
// 顶部导航
Row() {
Text('< 返回成绩')
.fontSize(16)
.fontColor($r('app.color.primary_professional'))
.onClick(() => {
this.showAnalysis = false;
})
Blank()
Text('错题解析')
.fontSize(18)
.fontWeight(FontWeight.Bold)
Blank()
Text(' ')
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.backgroundColor($r('app.color.card_background'))
// 错题统计
this.buildErrorStats()
// 错题列表
Scroll() {
Column({ space: 16 }) {
ForEach(this.getWrongAnswers(), (answer: UserAnswer, index: number) => {
this.buildErrorQuestionCard(answer, index)
})
}
.padding(16)
}
.layoutWeight(1)
}
.width('100%')
.height('100%')
}
@Builder
buildErrorStats() {
const wrongCount = this.getWrongAnswers().length;
const totalCount = this.record!.totalQuestions;
const correctRate = Math.round((this.record!.correctCount / totalCount) * 100);
Row({ space: 24 }) {
Column({ space: 4 }) {
Text(`${wrongCount}`)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#F44336')
Text('错题数')
.fontSize(12)
.fontColor($r('app.color.text_tertiary'))
}
Column({ space: 4 }) {
Text(`${correctRate}%`)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor('#4CAF50')
Text('正确率')
.fontSize(12)
.fontColor($r('app.color.text_tertiary'))
}
Column({ space: 4 }) {
Text(this.getMostWeakCategory())
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#FF9800')
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text('薄弱环节')
.fontSize(12)
.fontColor($r('app.color.text_tertiary'))
}
.layoutWeight(1)
}
.width('100%')
.padding(16)
.backgroundColor($r('app.color.card_background'))
.margin({ left: 16, right: 16, top: 16 })
.borderRadius(12)
}
/**
* 获取错题列表
*/
private getWrongAnswers(): UserAnswer[] {
if (!this.record) return [];
return this.record.answers.filter(answer => !answer.isCorrect);
}
/**
* 获取最薄弱的知识点
*/
private getMostWeakCategory(): string {
const wrongAnswers = this.getWrongAnswers();
if (wrongAnswers.length === 0) return '无';
const categoryCount: Record<string, number> = {};
wrongAnswers.forEach(answer => {
const question = this.questions.find(q => q.id === answer.questionId);
if (question) {
categoryCount[question.category] = (categoryCount[question.category] || 0) + 1;
}
});
let maxCount = 0;
let weakestCategory = '无';
Object.entries(categoryCount).forEach(([category, count]) => {
if (count > maxCount) {
maxCount = count;
weakestCategory = category;
}
});
return weakestCategory;
}
2.2 错题卡片展示
typescript
@Builder
buildErrorQuestionCard(answer: UserAnswer, index: number) {
const question = this.questions.find(q => q.id === answer.questionId);
if (!question) return;
Column({ space: 12 }) {
// 题目标题
Row() {
Text(`错题 ${index + 1}`)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor('#F44336')
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.backgroundColor('#FFEBEE')
.borderRadius(12)
Blank()
Text(question.category)
.fontSize(12)
.fontColor($r('app.color.text_tertiary'))
.padding({ left: 8, right: 8, top: 4, bottom: 4 })
.backgroundColor($r('app.color.background'))
.borderRadius(10)
}
.width('100%')
// 题目内容
Text(question.question)
.fontSize(15)
.fontColor($r('app.color.text_primary'))
.lineHeight(24)
.width('100%')
// 选项展示
ForEach(question.options, (option: QuestionOption) => {
this.buildAnalysisOption(option, question.correctAnswer, answer.selectedAnswer)
})
// 答案解析
Column({ space: 8 }) {
Text('📝 答案解析')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
.width('100%')
Text(question.explanation)
.fontSize(14)
.fontColor($r('app.color.text_secondary'))
.lineHeight(22)
.width('100%')
Row({ space: 8 }) {
Text('💡 知识点:')
.fontSize(12)
.fontColor($r('app.color.text_tertiary'))
Text(question.knowledgePoint)
.fontSize(12)
.fontColor($r('app.color.primary_professional'))
}
.width('100%')
}
.width('100%')
.padding(12)
.backgroundColor('#F8F9FA')
.borderRadius(8)
}
.width('100%')
.padding(16)
.backgroundColor($r('app.color.card_background'))
.borderRadius(12)
.shadow({ radius: 4, color: $r('app.color.shadow_light'), offsetY: 2 })
}
@Builder
buildAnalysisOption(option: QuestionOption, correctAnswer: string, selectedAnswer: string) {
const isCorrect = option.id === correctAnswer;
const isSelected = option.id === selectedAnswer;
const isWrong = isSelected && !isCorrect;
Row({ space: 12 }) {
// 选项标识
Text(option.id)
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(isCorrect ? Color.White :
(isWrong ? Color.White : $r('app.color.text_primary')))
.width(28)
.height(28)
.textAlign(TextAlign.Center)
.borderRadius(14)
.backgroundColor(isCorrect ? '#4CAF50' :
(isWrong ? '#F44336' : $r('app.color.background')))
// 选项内容
Text(option.content)
.fontSize(14)
.fontColor($r('app.color.text_primary'))
.layoutWeight(1)
// 状态标识
if (isCorrect) {
Text('✓ 正确答案')
.fontSize(12)
.fontColor('#4CAF50')
} else if (isWrong) {
Text('✗ 你的选择')
.fontSize(12)
.fontColor('#F44336')
}
}
.width('100%')
.padding(8)
.backgroundColor(isCorrect ? '#E8F5E9' :
(isWrong ? '#FFEBEE' : Color.Transparent))
.borderRadius(6)
}
三、历史记录管理
3.1 历史记录页面
文件位置 :entry/src/main/ets/pages/Exam/ExamHistoryPage.ets
typescript
import { router } from '@kit.ArkUI';
import { ExamLevel, ExamRecord } from '../../models/ExamModels';
import { ExamService } from '../../services/ExamService';
@Entry
@Component
export struct ExamHistoryPage {
@State records: ExamRecord[] = [];
@State level: ExamLevel = ExamLevel.BEGINNER;
@State isLoading: boolean = true;
private examService = ExamService.getInstance();
async aboutToAppear(): Promise<void> {
const params = router.getParams() as Record<string, Object>;
this.level = params['level'] as ExamLevel;
await this.loadRecords();
}
async loadRecords(): Promise<void> {
this.isLoading = true;
try {
this.records = await this.examService.getExamRecordsByLevel(this.level);
// 按时间倒序排列
this.records.sort((a, b) => b.startTime - a.startTime);
} catch (error) {
console.error('[ExamHistoryPage] Failed to load records:', error);
} finally {
this.isLoading = false;
}
}
build() {
Column() {
this.buildHeader()
if (this.isLoading) {
this.buildLoadingView()
} else if (this.records.length === 0) {
this.buildEmptyView()
} else {
this.buildRecordList()
}
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.background'))
}
}
3.2 记录列表展示
typescript
@Builder
buildHeader() {
Row() {
Text('< 返回')
.fontSize(16)
.fontColor($r('app.color.primary_professional'))
.onClick(() => {
router.back();
})
Blank()
Text(this.getLevelName() + '历史记录')
.fontSize(18)
.fontWeight(FontWeight.Bold)
Blank()
Text(' ')
}
.width('100%')
.height(56)
.padding({ left: 16, right: 16 })
.backgroundColor($r('app.color.card_background'))
}
@Builder
buildRecordList() {
Scroll() {
Column({ space: 12 }) {
ForEach(this.records, (record: ExamRecord, index: number) => {
this.buildRecordCard(record, index + 1)
})
}
.padding(16)
}
.layoutWeight(1)
}
@Builder
buildRecordCard(record: ExamRecord, index: number) {
Column({ space: 12 }) {
// 头部信息
Row() {
Column({ space: 4 }) {
Text(`第${index}次考试`)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
Text(this.formatDate(record.startTime))
.fontSize(12)
.fontColor($r('app.color.text_tertiary'))
}
.alignItems(HorizontalAlign.Start)
Blank()
// 通过状态
Text(record.passed ? '✓ 通过' : '✗ 未通过')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor(record.passed ? '#4CAF50' : '#F44336')
.padding({ left: 12, right: 12, top: 6, bottom: 6 })
.backgroundColor(record.passed ? '#E8F5E9' : '#FFEBEE')
.borderRadius(12)
}
.width('100%')
// 成绩信息
Row({ space: 24 }) {
Column({ space: 4 }) {
Text(`${record.score}`)
.fontSize(24)
.fontWeight(FontWeight.Bold)
.fontColor(record.passed ? '#4CAF50' : '#F44336')
Text('得分')
.fontSize(12)
.fontColor($r('app.color.text_tertiary'))
}
Column({ space: 4 }) {
Text(`${record.correctCount}/${record.totalQuestions}`)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
Text('正确率')
.fontSize(12)
.fontColor($r('app.color.text_tertiary'))
}
Column({ space: 4 }) {
Text(this.formatDuration(record.endTime - record.startTime))
.fontSize(16)
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
Text('用时')
.fontSize(12)
.fontColor($r('app.color.text_tertiary'))
}
}
.width('100%')
// 操作按钮
Row({ space: 8 }) {
Button('查看详情')
.layoutWeight(1)
.height(36)
.fontSize(14)
.backgroundColor($r('app.color.card_background'))
.fontColor($r('app.color.text_primary'))
.onClick(() => {
this.viewRecordDetail(record);
})
if (!record.passed) {
Button('重新考试')
.layoutWeight(1)
.height(36)
.fontSize(14)
.backgroundColor($r('app.color.primary_professional'))
.onClick(() => {
this.retakeExam();
})
}
}
.width('100%')
}
.width('100%')
.padding(16)
.backgroundColor($r('app.color.card_background'))
.borderRadius(12)
.shadow({ radius: 4, color: $r('app.color.shadow_light'), offsetY: 2 })
}
/**
* 获取级别名称
*/
private getLevelName(): string {
switch (this.level) {
case ExamLevel.BEGINNER:
return '初级';
case ExamLevel.INTERMEDIATE:
return '中级';
case ExamLevel.ADVANCED:
return '高级';
default:
return '未知';
}
}
/**
* 格式化日期
*/
private formatDate(timestamp: number): string {
const date = new Date(timestamp);
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
}
/**
* 查看记录详情
*/
private viewRecordDetail(record: ExamRecord): void {
// TODO: 跳转到详情页面或显示详情对话框
router.pushUrl({
url: 'pages/Exam/ExamResultPage',
params: {
record: record,
questions: [] // 需要重新获取题目数据
}
});
}
/**
* 重新考试
*/
private retakeExam(): void {
router.pushUrl({
url: 'pages/Exam/ExamQuestionPage',
params: { level: this.level }
});
}
四、统计分析功能
4.1 ExamService 统计方法扩展
在 ExamService 中添加更多统计分析方法:
typescript
/**
* 获取考试统计数据
*/
public async getExamStats(level?: ExamLevel): Promise<ExamStats> {
const records = level ?
await this.getExamRecordsByLevel(level) :
await this.getExamRecords();
if (records.length === 0) {
return {
totalExams: 0,
passedExams: 0,
averageScore: 0,
bestScore: 0,
weakCategories: []
};
}
const totalExams = records.length;
const passedExams = records.filter(r => r.passed).length;
const averageScore = Math.round(records.reduce((sum, r) => sum + r.score, 0) / totalExams);
const bestScore = Math.max(...records.map(r => r.score));
// 统计薄弱知识点
const categoryErrors: Record<string, number> = {};
records.forEach(record => {
record.answers.forEach(answer => {
if (!answer.isCorrect) {
const question = this.getQuestionById(answer.questionId);
if (question) {
categoryErrors[question.category] = (categoryErrors[question.category] || 0) + 1;
}
}
});
});
// 获取错误最多的3个知识点
const weakCategories = Object.entries(categoryErrors)
.sort(([,a], [,b]) => b - a)
.slice(0, 3)
.map(([category]) => category);
return {
totalExams,
passedExams,
averageScore,
bestScore,
weakCategories
};
}
/**
* 获取成绩趋势数据
*/
public async getScoreTrend(level: ExamLevel): Promise<number[]> {
const records = await this.getExamRecordsByLevel(level);
return records
.sort((a, b) => a.startTime - b.startTime)
.map(r => r.score);
}
/**
* 获取错题统计
*/
public async getWrongQuestionStats(level: ExamLevel): Promise<Record<string, number>> {
const records = await this.getExamRecordsByLevel(level);
const categoryErrors: Record<string, number> = {};
records.forEach(record => {
record.answers.forEach(answer => {
if (!answer.isCorrect) {
const question = this.getQuestionById(answer.questionId);
if (question) {
categoryErrors[question.category] = (categoryErrors[question.category] || 0) + 1;
}
}
});
});
return categoryErrors;
}
/**
* 根据ID获取题目
*/
private getQuestionById(questionId: string): ExamQuestion | null {
const allQuestions = this.getAllQuestions();
return allQuestions.find(q => q.id === questionId) || null;
}
五、实操练习
练习1:添加成绩趋势图
任务:在历史记录页面添加成绩趋势图表
提示:
- 使用 Canvas 或第三方图表库
- 显示最近10次考试的成绩变化
- 标注通过线和平均分
参考代码:
typescript
@Builder
buildScoreTrendChart() {
Column({ space: 8 }) {
Text('成绩趋势')
.fontSize(16)
.fontWeight(FontWeight.Bold)
.width('100%')
// 简化的趋势图(使用进度条模拟)
Column({ space: 4 }) {
ForEach(this.getRecentScores(), (score: number, index: number) => {
Row() {
Text(`第${index + 1}次`)
.fontSize(12)
.width(60)
Progress({ value: score, total: 100, type: ProgressType.Linear })
.width(200)
.height(8)
.color(score >= 60 ? '#4CAF50' : '#F44336')
Text(`${score}分`)
.fontSize(12)
.width(50)
}
})
}
}
.width('100%')
.padding(16)
.backgroundColor($r('app.color.card_background'))
.borderRadius(12)
}
private getRecentScores(): number[] {
return this.records.slice(0, 10).reverse().map(r => r.score);
}
练习2:添加错题本功能
任务:创建独立的错题本页面,收集所有错题
提示:
- 从所有考试记录中提取错题
- 按知识点分类显示
- 支持错题重做功能
练习3:添加学习建议
任务:根据考试结果生成个性化学习建议
提示:
- 分析薄弱知识点
- 推荐相关课程
- 制定学习计划
六、本篇总结
6.1 核心知识点
本篇教程完善了考试系统的分析功能,涵盖以下内容:
-
成绩展示
- 成绩概览设计
- 统计数据展示
- 通过状态判断
-
错题分析
- 错题列表展示
- 答案解析功能
- 薄弱环节分析
-
历史记录
- 记录列表管理
- 成绩趋势分析
- 重考功能
-
数据统计
- 考试统计计算
- 知识点分析
- 错题统计
6.2 技术要点
- ✅ 条件渲染:根据状态切换不同视图
- ✅ 数据分析:统计计算和趋势分析
- ✅ 列表展示:ForEach 渲染记录列表
- ✅ 状态标识:不同颜色表示不同状态
- ✅ 交互设计:查看详情、重新考试等操作
6.3 实际应用价值
考试分析系统解决了以下问题:
- 学习效果评估:通过成绩了解学习效果
- 薄弱环节识别:找出需要加强的知识点
- 学习进度跟踪:记录学习历程和进步
- 针对性学习:根据错题进行针对性复习
- 学习动机激励:成绩展示和进步可视化
6.4 下一篇预告
第28篇:用户中心与个人资料
下一篇将实现用户中心功能,包括:
- 👤 个人资料管理
- ⚙️ 账号设置功能
- 📊 学习统计展示
- 🏆 成就系统
- 📱 应用设置
附录:完整代码文件清单
页面层
entry/src/main/ets/pages/Exam/ExamResultPage.ets- 成绩结果页面entry/src/main/ets/pages/Exam/ExamHistoryPage.ets- 历史记录页面
服务层
entry/src/main/ets/services/ExamService.ets- 考试服务(新增统计方法)