559 lines
16 KiB
Dart
559 lines
16 KiB
Dart
/// 测试类型枚举
|
|
enum TestType {
|
|
quick, // 快速测试
|
|
standard, // 标准测试
|
|
full, // 完整测试
|
|
mock, // 模拟考试
|
|
vocabulary, // 词汇专项
|
|
grammar, // 语法专项
|
|
reading, // 阅读专项
|
|
listening, // 听力专项
|
|
speaking, // 口语专项
|
|
writing, // 写作专项
|
|
}
|
|
|
|
/// 测试状态枚举
|
|
enum TestStatus {
|
|
notStarted, // 未开始
|
|
inProgress, // 进行中
|
|
paused, // 暂停
|
|
completed, // 已完成
|
|
expired, // 已过期
|
|
}
|
|
|
|
/// 题目类型枚举
|
|
enum QuestionType {
|
|
multipleChoice, // 单选题
|
|
multipleSelect, // 多选题
|
|
fillInBlank, // 填空题
|
|
reading, // 阅读理解
|
|
listening, // 听力理解
|
|
speaking, // 口语题
|
|
writing, // 写作题
|
|
}
|
|
|
|
/// 难度等级枚举
|
|
enum DifficultyLevel {
|
|
beginner, // 初级
|
|
elementary, // 基础
|
|
intermediate, // 中级
|
|
upperIntermediate, // 中高级
|
|
advanced, // 高级
|
|
expert, // 专家级
|
|
}
|
|
|
|
/// 技能类型枚举
|
|
enum SkillType {
|
|
vocabulary, // 词汇
|
|
grammar, // 语法
|
|
reading, // 阅读
|
|
listening, // 听力
|
|
speaking, // 口语
|
|
writing, // 写作
|
|
}
|
|
|
|
/// 测试题目模型
|
|
class TestQuestion {
|
|
final String id;
|
|
final QuestionType type;
|
|
final SkillType skillType;
|
|
final DifficultyLevel difficulty;
|
|
final String content;
|
|
final List<String> options;
|
|
final List<String> correctAnswers;
|
|
final String? audioUrl;
|
|
final String? imageUrl;
|
|
final String? explanation;
|
|
final int points;
|
|
final int timeLimit; // 秒
|
|
final Map<String, dynamic>? metadata;
|
|
|
|
const TestQuestion({
|
|
required this.id,
|
|
required this.type,
|
|
required this.skillType,
|
|
required this.difficulty,
|
|
required this.content,
|
|
this.options = const [],
|
|
this.correctAnswers = const [],
|
|
this.audioUrl,
|
|
this.imageUrl,
|
|
this.explanation,
|
|
this.points = 1,
|
|
this.timeLimit = 60,
|
|
this.metadata,
|
|
});
|
|
|
|
factory TestQuestion.fromJson(Map<String, dynamic> json) {
|
|
return TestQuestion(
|
|
id: json['id'] as String,
|
|
type: QuestionType.values.firstWhere(
|
|
(e) => e.name == json['type'],
|
|
orElse: () => QuestionType.multipleChoice,
|
|
),
|
|
skillType: SkillType.values.firstWhere(
|
|
(e) => e.name == json['skillType'],
|
|
orElse: () => SkillType.vocabulary,
|
|
),
|
|
difficulty: DifficultyLevel.values.firstWhere(
|
|
(e) => e.name == json['difficulty'],
|
|
orElse: () => DifficultyLevel.intermediate,
|
|
),
|
|
content: json['content'] as String,
|
|
options: List<String>.from(json['options'] ?? []),
|
|
correctAnswers: List<String>.from(json['correctAnswers'] ?? []),
|
|
audioUrl: json['audioUrl'] as String?,
|
|
imageUrl: json['imageUrl'] as String?,
|
|
explanation: json['explanation'] as String?,
|
|
points: json['points'] as int? ?? 1,
|
|
timeLimit: json['timeLimit'] as int? ?? 60,
|
|
metadata: json['metadata'] as Map<String, dynamic>?,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'id': id,
|
|
'type': type.name,
|
|
'skillType': skillType.name,
|
|
'difficulty': difficulty.name,
|
|
'content': content,
|
|
'options': options,
|
|
'correctAnswers': correctAnswers,
|
|
'audioUrl': audioUrl,
|
|
'imageUrl': imageUrl,
|
|
'explanation': explanation,
|
|
'points': points,
|
|
'timeLimit': timeLimit,
|
|
'metadata': metadata,
|
|
};
|
|
}
|
|
}
|
|
|
|
/// 语言技能枚举 (用于UI显示)
|
|
enum LanguageSkill {
|
|
listening, // 听力
|
|
reading, // 阅读
|
|
speaking, // 口语
|
|
writing, // 写作
|
|
vocabulary, // 词汇
|
|
grammar, // 语法
|
|
pronunciation, // 发音
|
|
comprehension, // 理解
|
|
}
|
|
|
|
/// 用户技能统计模型
|
|
class UserSkillStatistics {
|
|
final SkillType skillType;
|
|
final double averageScore;
|
|
final int totalTests;
|
|
final int correctAnswers;
|
|
final int totalQuestions;
|
|
final DifficultyLevel strongestLevel;
|
|
final DifficultyLevel weakestLevel;
|
|
final DateTime lastTestDate;
|
|
|
|
const UserSkillStatistics({
|
|
required this.skillType,
|
|
required this.averageScore,
|
|
required this.totalTests,
|
|
required this.correctAnswers,
|
|
required this.totalQuestions,
|
|
required this.strongestLevel,
|
|
required this.weakestLevel,
|
|
required this.lastTestDate,
|
|
});
|
|
|
|
factory UserSkillStatistics.fromJson(Map<String, dynamic> json) {
|
|
return UserSkillStatistics(
|
|
skillType: SkillType.values.firstWhere(
|
|
(e) => e.toString().split('.').last == json['skillType'],
|
|
),
|
|
averageScore: (json['averageScore'] as num).toDouble(),
|
|
totalTests: json['totalTests'] as int,
|
|
correctAnswers: json['correctAnswers'] as int,
|
|
totalQuestions: json['totalQuestions'] as int,
|
|
strongestLevel: DifficultyLevel.values.firstWhere(
|
|
(e) => e.toString().split('.').last == json['strongestLevel'],
|
|
),
|
|
weakestLevel: DifficultyLevel.values.firstWhere(
|
|
(e) => e.toString().split('.').last == json['weakestLevel'],
|
|
),
|
|
lastTestDate: DateTime.parse(json['lastTestDate']),
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'skillType': skillType.toString().split('.').last,
|
|
'averageScore': averageScore,
|
|
'totalTests': totalTests,
|
|
'correctAnswers': correctAnswers,
|
|
'totalQuestions': totalQuestions,
|
|
'strongestLevel': strongestLevel.toString().split('.').last,
|
|
'weakestLevel': weakestLevel.toString().split('.').last,
|
|
'lastTestDate': lastTestDate.toIso8601String(),
|
|
};
|
|
}
|
|
|
|
double get accuracy => totalQuestions > 0 ? correctAnswers / totalQuestions : 0.0;
|
|
}
|
|
|
|
/// 用户答案模型
|
|
class UserAnswer {
|
|
final String questionId;
|
|
final List<String> selectedAnswers;
|
|
final String? textAnswer;
|
|
final String? audioUrl;
|
|
final DateTime answeredAt;
|
|
final int timeSpent; // 秒
|
|
|
|
const UserAnswer({
|
|
required this.questionId,
|
|
this.selectedAnswers = const [],
|
|
this.textAnswer,
|
|
this.audioUrl,
|
|
required this.answeredAt,
|
|
required this.timeSpent,
|
|
});
|
|
|
|
factory UserAnswer.fromJson(Map<String, dynamic> json) {
|
|
return UserAnswer(
|
|
questionId: json['questionId'] as String,
|
|
selectedAnswers: List<String>.from(json['selectedAnswers'] ?? []),
|
|
textAnswer: json['textAnswer'] as String?,
|
|
audioUrl: json['audioUrl'] as String?,
|
|
answeredAt: DateTime.parse(json['answeredAt'] as String),
|
|
timeSpent: json['timeSpent'] as int,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'questionId': questionId,
|
|
'selectedAnswers': selectedAnswers,
|
|
'textAnswer': textAnswer,
|
|
'audioUrl': audioUrl,
|
|
'answeredAt': answeredAt.toIso8601String(),
|
|
'timeSpent': timeSpent,
|
|
};
|
|
}
|
|
}
|
|
|
|
/// 技能得分模型
|
|
class SkillScore {
|
|
final SkillType skillType;
|
|
final int score;
|
|
final int maxScore;
|
|
final double percentage;
|
|
final DifficultyLevel level;
|
|
final String feedback;
|
|
|
|
const SkillScore({
|
|
required this.skillType,
|
|
required this.score,
|
|
required this.maxScore,
|
|
required this.percentage,
|
|
required this.level,
|
|
required this.feedback,
|
|
});
|
|
|
|
factory SkillScore.fromJson(Map<String, dynamic> json) {
|
|
return SkillScore(
|
|
skillType: SkillType.values.firstWhere(
|
|
(e) => e.name == json['skillType'],
|
|
orElse: () => SkillType.vocabulary,
|
|
),
|
|
score: json['score'] as int,
|
|
maxScore: json['maxScore'] as int,
|
|
percentage: (json['percentage'] as num).toDouble(),
|
|
level: DifficultyLevel.values.firstWhere(
|
|
(e) => e.name == json['level'],
|
|
orElse: () => DifficultyLevel.intermediate,
|
|
),
|
|
feedback: json['feedback'] as String,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'skillType': skillType.name,
|
|
'score': score,
|
|
'maxScore': maxScore,
|
|
'percentage': percentage,
|
|
'level': level.name,
|
|
'feedback': feedback,
|
|
};
|
|
}
|
|
}
|
|
|
|
/// 测试结果模型
|
|
class TestResult {
|
|
final String id;
|
|
final String testId;
|
|
final String userId;
|
|
final TestType testType;
|
|
final int totalScore;
|
|
final int maxScore;
|
|
final double percentage;
|
|
final DifficultyLevel overallLevel;
|
|
final List<SkillScore> skillScores;
|
|
final List<UserAnswer> answers;
|
|
final DateTime startTime;
|
|
final DateTime endTime;
|
|
final int duration; // 秒
|
|
final String feedback;
|
|
final Map<String, dynamic>? recommendations;
|
|
|
|
const TestResult({
|
|
required this.id,
|
|
required this.testId,
|
|
required this.userId,
|
|
required this.testType,
|
|
required this.totalScore,
|
|
required this.maxScore,
|
|
required this.percentage,
|
|
required this.overallLevel,
|
|
required this.skillScores,
|
|
required this.answers,
|
|
required this.startTime,
|
|
required this.endTime,
|
|
required this.duration,
|
|
required this.feedback,
|
|
this.recommendations,
|
|
});
|
|
|
|
factory TestResult.fromJson(Map<String, dynamic> json) {
|
|
return TestResult(
|
|
id: json['id'] as String,
|
|
testId: json['testId'] as String,
|
|
userId: json['userId'] as String,
|
|
testType: TestType.values.firstWhere(
|
|
(e) => e.name == json['testType'],
|
|
orElse: () => TestType.standard,
|
|
),
|
|
totalScore: json['totalScore'] as int,
|
|
maxScore: json['maxScore'] as int,
|
|
percentage: (json['percentage'] as num).toDouble(),
|
|
overallLevel: DifficultyLevel.values.firstWhere(
|
|
(e) => e.name == json['overallLevel'],
|
|
orElse: () => DifficultyLevel.intermediate,
|
|
),
|
|
skillScores: (json['skillScores'] as List<dynamic>)
|
|
.map((e) => SkillScore.fromJson(e as Map<String, dynamic>))
|
|
.toList(),
|
|
answers: (json['answers'] as List<dynamic>)
|
|
.map((e) => UserAnswer.fromJson(e as Map<String, dynamic>))
|
|
.toList(),
|
|
startTime: DateTime.parse(json['startTime'] as String),
|
|
endTime: DateTime.parse(json['endTime'] as String),
|
|
duration: json['duration'] as int,
|
|
feedback: json['feedback'] as String,
|
|
recommendations: json['recommendations'] as Map<String, dynamic>?,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'id': id,
|
|
'testId': testId,
|
|
'userId': userId,
|
|
'testType': testType.name,
|
|
'totalScore': totalScore,
|
|
'maxScore': maxScore,
|
|
'percentage': percentage,
|
|
'overallLevel': overallLevel.name,
|
|
'skillScores': skillScores.map((e) => e.toJson()).toList(),
|
|
'answers': answers.map((e) => e.toJson()).toList(),
|
|
'startTime': startTime.toIso8601String(),
|
|
'endTime': endTime.toIso8601String(),
|
|
'duration': duration,
|
|
'feedback': feedback,
|
|
'recommendations': recommendations,
|
|
};
|
|
}
|
|
}
|
|
|
|
/// 测试模板模型
|
|
class TestTemplate {
|
|
final String id;
|
|
final String name;
|
|
final String description;
|
|
final TestType type;
|
|
final int duration; // 分钟
|
|
final int totalQuestions;
|
|
final Map<SkillType, int> skillDistribution;
|
|
final Map<DifficultyLevel, int> difficultyDistribution;
|
|
final List<String> questionIds;
|
|
final bool isActive;
|
|
final DateTime createdAt;
|
|
final DateTime updatedAt;
|
|
|
|
const TestTemplate({
|
|
required this.id,
|
|
required this.name,
|
|
required this.description,
|
|
required this.type,
|
|
required this.duration,
|
|
required this.totalQuestions,
|
|
required this.skillDistribution,
|
|
required this.difficultyDistribution,
|
|
required this.questionIds,
|
|
this.isActive = true,
|
|
required this.createdAt,
|
|
required this.updatedAt,
|
|
});
|
|
|
|
factory TestTemplate.fromJson(Map<String, dynamic> json) {
|
|
return TestTemplate(
|
|
id: json['id'] as String,
|
|
name: json['name'] as String,
|
|
description: json['description'] as String,
|
|
type: TestType.values.firstWhere(
|
|
(e) => e.name == json['type'],
|
|
orElse: () => TestType.standard,
|
|
),
|
|
duration: json['duration'] as int,
|
|
totalQuestions: json['totalQuestions'] as int,
|
|
skillDistribution: Map<SkillType, int>.fromEntries(
|
|
(json['skillDistribution'] as Map<String, dynamic>).entries.map(
|
|
(e) => MapEntry(
|
|
SkillType.values.firstWhere((skill) => skill.name == e.key),
|
|
e.value as int,
|
|
),
|
|
),
|
|
),
|
|
difficultyDistribution: Map<DifficultyLevel, int>.fromEntries(
|
|
(json['difficultyDistribution'] as Map<String, dynamic>).entries.map(
|
|
(e) => MapEntry(
|
|
DifficultyLevel.values.firstWhere((level) => level.name == e.key),
|
|
e.value as int,
|
|
),
|
|
),
|
|
),
|
|
questionIds: List<String>.from(json['questionIds']),
|
|
isActive: json['isActive'] as bool? ?? true,
|
|
createdAt: DateTime.parse(json['createdAt'] as String),
|
|
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'id': id,
|
|
'name': name,
|
|
'description': description,
|
|
'type': type.name,
|
|
'duration': duration,
|
|
'totalQuestions': totalQuestions,
|
|
'skillDistribution': skillDistribution.map(
|
|
(key, value) => MapEntry(key.name, value),
|
|
),
|
|
'difficultyDistribution': difficultyDistribution.map(
|
|
(key, value) => MapEntry(key.name, value),
|
|
),
|
|
'questionIds': questionIds,
|
|
'isActive': isActive,
|
|
'createdAt': createdAt.toIso8601String(),
|
|
'updatedAt': updatedAt.toIso8601String(),
|
|
};
|
|
}
|
|
}
|
|
|
|
/// 测试会话模型
|
|
class TestSession {
|
|
final String id;
|
|
final String templateId;
|
|
final String userId;
|
|
final TestStatus status;
|
|
final List<TestQuestion> questions;
|
|
final List<UserAnswer> answers;
|
|
final int currentQuestionIndex;
|
|
final DateTime startTime;
|
|
final DateTime? endTime;
|
|
final int timeRemaining; // 秒
|
|
final Map<String, dynamic>? metadata;
|
|
|
|
const TestSession({
|
|
required this.id,
|
|
required this.templateId,
|
|
required this.userId,
|
|
required this.status,
|
|
required this.questions,
|
|
this.answers = const [],
|
|
this.currentQuestionIndex = 0,
|
|
required this.startTime,
|
|
this.endTime,
|
|
required this.timeRemaining,
|
|
this.metadata,
|
|
});
|
|
|
|
factory TestSession.fromJson(Map<String, dynamic> json) {
|
|
return TestSession(
|
|
id: json['id'] as String,
|
|
templateId: json['templateId'] as String,
|
|
userId: json['userId'] as String,
|
|
status: TestStatus.values.firstWhere(
|
|
(e) => e.name == json['status'],
|
|
orElse: () => TestStatus.notStarted,
|
|
),
|
|
questions: (json['questions'] as List<dynamic>)
|
|
.map((e) => TestQuestion.fromJson(e as Map<String, dynamic>))
|
|
.toList(),
|
|
answers: (json['answers'] as List<dynamic>?)
|
|
?.map((e) => UserAnswer.fromJson(e as Map<String, dynamic>))
|
|
.toList() ?? [],
|
|
currentQuestionIndex: json['currentQuestionIndex'] as int? ?? 0,
|
|
startTime: DateTime.parse(json['startTime'] as String),
|
|
endTime: json['endTime'] != null
|
|
? DateTime.parse(json['endTime'] as String)
|
|
: null,
|
|
timeRemaining: json['timeRemaining'] as int,
|
|
metadata: json['metadata'] as Map<String, dynamic>?,
|
|
);
|
|
}
|
|
|
|
Map<String, dynamic> toJson() {
|
|
return {
|
|
'id': id,
|
|
'templateId': templateId,
|
|
'userId': userId,
|
|
'status': status.name,
|
|
'questions': questions.map((e) => e.toJson()).toList(),
|
|
'answers': answers.map((e) => e.toJson()).toList(),
|
|
'currentQuestionIndex': currentQuestionIndex,
|
|
'startTime': startTime.toIso8601String(),
|
|
'endTime': endTime?.toIso8601String(),
|
|
'timeRemaining': timeRemaining,
|
|
'metadata': metadata,
|
|
};
|
|
}
|
|
|
|
TestSession copyWith({
|
|
String? id,
|
|
String? templateId,
|
|
String? userId,
|
|
TestStatus? status,
|
|
List<TestQuestion>? questions,
|
|
List<UserAnswer>? answers,
|
|
int? currentQuestionIndex,
|
|
DateTime? startTime,
|
|
DateTime? endTime,
|
|
int? timeRemaining,
|
|
Map<String, dynamic>? metadata,
|
|
}) {
|
|
return TestSession(
|
|
id: id ?? this.id,
|
|
templateId: templateId ?? this.templateId,
|
|
userId: userId ?? this.userId,
|
|
status: status ?? this.status,
|
|
questions: questions ?? this.questions,
|
|
answers: answers ?? this.answers,
|
|
currentQuestionIndex: currentQuestionIndex ?? this.currentQuestionIndex,
|
|
startTime: startTime ?? this.startTime,
|
|
endTime: endTime ?? this.endTime,
|
|
timeRemaining: timeRemaining ?? this.timeRemaining,
|
|
metadata: metadata ?? this.metadata,
|
|
);
|
|
}
|
|
} |