This commit is contained in:
sjk
2025-11-17 14:09:17 +08:00
commit 31e46c5bf6
479 changed files with 109324 additions and 0 deletions

View File

@@ -0,0 +1,138 @@
class ReadingArticle {
final String id;
final String title;
final String content;
final String category;
final String difficulty;
final int wordCount;
final int estimatedReadingTime; // in minutes
final List<String> tags;
final String source;
final DateTime publishDate;
final bool isCompleted;
final double? comprehensionScore;
final int? readingTime; // actual reading time in seconds
const ReadingArticle({
required this.id,
required this.title,
required this.content,
required this.category,
required this.difficulty,
required this.wordCount,
required this.estimatedReadingTime,
required this.tags,
required this.source,
required this.publishDate,
this.isCompleted = false,
this.comprehensionScore,
this.readingTime,
});
factory ReadingArticle.fromJson(Map<String, dynamic> json) {
return ReadingArticle(
id: json['id'] as String,
title: json['title'] as String,
content: json['content'] as String,
category: json['category'] as String,
difficulty: json['difficulty'] as String,
wordCount: json['wordCount'] as int,
estimatedReadingTime: json['estimatedReadingTime'] as int,
tags: List<String>.from(json['tags'] as List),
source: json['source'] as String,
publishDate: DateTime.parse(json['publishDate'] as String),
isCompleted: json['isCompleted'] as bool? ?? false,
comprehensionScore: json['comprehensionScore'] as double?,
readingTime: json['readingTime'] as int?,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'title': title,
'content': content,
'category': category,
'difficulty': difficulty,
'wordCount': wordCount,
'estimatedReadingTime': estimatedReadingTime,
'tags': tags,
'source': source,
'publishDate': publishDate.toIso8601String(),
'isCompleted': isCompleted,
'comprehensionScore': comprehensionScore,
'readingTime': readingTime,
};
}
ReadingArticle copyWith({
String? id,
String? title,
String? content,
String? category,
String? difficulty,
int? wordCount,
int? estimatedReadingTime,
List<String>? tags,
String? source,
DateTime? publishDate,
bool? isCompleted,
double? comprehensionScore,
int? readingTime,
}) {
return ReadingArticle(
id: id ?? this.id,
title: title ?? this.title,
content: content ?? this.content,
category: category ?? this.category,
difficulty: difficulty ?? this.difficulty,
wordCount: wordCount ?? this.wordCount,
estimatedReadingTime: estimatedReadingTime ?? this.estimatedReadingTime,
tags: tags ?? this.tags,
source: source ?? this.source,
publishDate: publishDate ?? this.publishDate,
isCompleted: isCompleted ?? this.isCompleted,
comprehensionScore: comprehensionScore ?? this.comprehensionScore,
readingTime: readingTime ?? this.readingTime,
);
}
String get difficultyLabel {
switch (difficulty.toLowerCase()) {
case 'a1':
case 'a2':
return '初级';
case 'b1':
case 'b2':
return '中级';
case 'c1':
case 'c2':
return '高级';
default:
return '未知';
}
}
String get categoryLabel {
switch (category.toLowerCase()) {
case 'cet4':
return '四级阅读';
case 'cet6':
return '六级阅读';
case 'toefl':
return '托福阅读';
case 'ielts':
return '雅思阅读';
case 'daily':
return '日常阅读';
case 'business':
return '商务阅读';
case 'academic':
return '学术阅读';
case 'news':
return '新闻阅读';
default:
return category;
}
}
}

View File

@@ -0,0 +1,125 @@
/// 阅读练习类型枚举
enum ReadingExerciseType {
news, // 新闻
story, // 故事
science, // 科学
business, // 商务
culture, // 文化
technology, // 科技
health, // 健康
travel, // 旅游
}
/// 阅读难度枚举
enum ReadingDifficulty {
elementary, // 初级 A1-A2
intermediate, // 中级 B1
upperIntermediate, // 中高级 B2
advanced, // 高级 C1
proficient, // 精通 C2
}
/// 阅读练习分类
class ReadingCategory {
final String id;
final String name;
final String description;
final String icon;
final int articleCount;
final ReadingExerciseType type;
const ReadingCategory({
required this.id,
required this.name,
required this.description,
required this.icon,
required this.articleCount,
required this.type,
});
}
/// 阅读练习
class ReadingExercise {
final String id;
final String title;
final String content;
final String summary;
final ReadingExerciseType type;
final ReadingDifficulty difficulty;
final int wordCount;
final int estimatedTime; // 预估阅读时间(分钟)
final List<ReadingQuestion> questions;
final List<String> tags;
final String source;
final DateTime publishDate;
final String imageUrl;
const ReadingExercise({
required this.id,
required this.title,
required this.content,
required this.summary,
required this.type,
required this.difficulty,
required this.wordCount,
required this.estimatedTime,
required this.questions,
required this.tags,
required this.source,
required this.publishDate,
required this.imageUrl,
});
}
/// 阅读问题
class ReadingQuestion {
final String id;
final String question;
final List<String> options;
final int correctAnswer; // 正确答案的索引
final String explanation;
final String type; // multiple_choice, true_false, fill_blank
const ReadingQuestion({
required this.id,
required this.question,
required this.options,
required this.correctAnswer,
required this.explanation,
required this.type,
});
}
/// 阅读结果
class ReadingResult {
final String exerciseId;
final int correctAnswers;
final int totalQuestions;
final int readingTime; // 实际阅读时间(秒)
final DateTime completedAt;
final List<UserAnswer> userAnswers;
const ReadingResult({
required this.exerciseId,
required this.correctAnswers,
required this.totalQuestions,
required this.readingTime,
required this.completedAt,
required this.userAnswers,
});
double get accuracy => correctAnswers / totalQuestions;
}
/// 用户答案
class UserAnswer {
final String questionId;
final int selectedAnswer;
final bool isCorrect;
const UserAnswer({
required this.questionId,
required this.selectedAnswer,
required this.isCorrect,
});
}

View File

@@ -0,0 +1,241 @@
enum QuestionType {
multipleChoice,
trueFalse,
fillInBlank,
shortAnswer,
}
class ReadingQuestion {
final String id;
final String articleId;
final QuestionType type;
final String question;
final List<String> options; // For multiple choice questions
final String correctAnswer;
final String explanation;
final int order;
final String? userAnswer;
final bool? isCorrect;
const ReadingQuestion({
required this.id,
required this.articleId,
required this.type,
required this.question,
required this.options,
required this.correctAnswer,
required this.explanation,
required this.order,
this.userAnswer,
this.isCorrect,
});
factory ReadingQuestion.fromJson(Map<String, dynamic> json) {
return ReadingQuestion(
id: json['id'] as String,
articleId: json['articleId'] as String,
type: QuestionType.values.firstWhere(
(e) => e.toString().split('.').last == json['type'],
orElse: () => QuestionType.multipleChoice,
),
question: json['question'] as String,
options: List<String>.from(json['options'] as List? ?? []),
correctAnswer: json['correctAnswer'] as String,
explanation: json['explanation'] as String,
order: json['order'] as int,
userAnswer: json['userAnswer'] as String?,
isCorrect: json['isCorrect'] as bool?,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'articleId': articleId,
'type': type.toString().split('.').last,
'question': question,
'options': options,
'correctAnswer': correctAnswer,
'explanation': explanation,
'order': order,
'userAnswer': userAnswer,
'isCorrect': isCorrect,
};
}
ReadingQuestion copyWith({
String? id,
String? articleId,
QuestionType? type,
String? question,
List<String>? options,
String? correctAnswer,
String? explanation,
int? order,
String? userAnswer,
bool? isCorrect,
}) {
return ReadingQuestion(
id: id ?? this.id,
articleId: articleId ?? this.articleId,
type: type ?? this.type,
question: question ?? this.question,
options: options ?? this.options,
correctAnswer: correctAnswer ?? this.correctAnswer,
explanation: explanation ?? this.explanation,
order: order ?? this.order,
userAnswer: userAnswer ?? this.userAnswer,
isCorrect: isCorrect ?? this.isCorrect,
);
}
String get typeLabel {
switch (type) {
case QuestionType.multipleChoice:
return '选择题';
case QuestionType.trueFalse:
return '判断题';
case QuestionType.fillInBlank:
return '填空题';
case QuestionType.shortAnswer:
return '简答题';
}
}
}
class ReadingExercise {
final String id;
final String articleId;
final List<ReadingQuestion> questions;
final DateTime? startTime;
final DateTime? endTime;
final double? score;
final bool isCompleted;
const ReadingExercise({
required this.id,
required this.articleId,
required this.questions,
this.startTime,
this.endTime,
this.score,
this.isCompleted = false,
});
factory ReadingExercise.fromJson(Map<String, dynamic> json) {
return ReadingExercise(
id: json['id'] as String,
articleId: json['articleId'] as String,
questions: (json['questions'] as List)
.map((q) => ReadingQuestion.fromJson(q as Map<String, dynamic>))
.toList(),
startTime: json['startTime'] != null
? DateTime.parse(json['startTime'] as String)
: null,
endTime: json['endTime'] != null
? DateTime.parse(json['endTime'] as String)
: null,
score: json['score'] as double?,
isCompleted: json['isCompleted'] as bool? ?? false,
);
}
Map<String, dynamic> toJson() {
return {
'id': id,
'articleId': articleId,
'questions': questions.map((q) => q.toJson()).toList(),
'startTime': startTime?.toIso8601String(),
'endTime': endTime?.toIso8601String(),
'score': score,
'isCompleted': isCompleted,
};
}
ReadingExercise copyWith({
String? id,
String? articleId,
List<ReadingQuestion>? questions,
DateTime? startTime,
DateTime? endTime,
double? score,
bool? isCompleted,
}) {
return ReadingExercise(
id: id ?? this.id,
articleId: articleId ?? this.articleId,
questions: questions ?? this.questions,
startTime: startTime ?? this.startTime,
endTime: endTime ?? this.endTime,
score: score ?? this.score,
isCompleted: isCompleted ?? this.isCompleted,
);
}
int get totalQuestions => questions.length;
int get answeredQuestions {
return questions.where((q) => q.userAnswer != null).length;
}
int get correctAnswers {
return questions.where((q) => q.isCorrect == true).length;
}
double get progressPercentage {
if (totalQuestions == 0) return 0.0;
return (answeredQuestions / totalQuestions) * 100;
}
Duration? get duration {
if (startTime != null && endTime != null) {
return endTime!.difference(startTime!);
}
return null;
}
}
/// 阅读练习结果
class ReadingExerciseResult {
final double score;
final int correctCount;
final int totalCount;
final Duration timeSpent; // 用时
final double accuracy;
const ReadingExerciseResult({
required this.score,
required this.correctCount,
required this.totalCount,
required this.timeSpent,
required this.accuracy,
});
/// 错误题数
int get wrongCount => totalCount - correctCount;
/// 总题数(别名)
int get totalQuestions => totalCount;
factory ReadingExerciseResult.fromJson(Map<String, dynamic> json) {
return ReadingExerciseResult(
score: (json['score'] as num).toDouble(),
correctCount: json['correctCount'] as int,
totalCount: json['totalCount'] as int,
timeSpent: Duration(seconds: json['timeSpent'] as int),
accuracy: (json['accuracy'] as num).toDouble(),
);
}
Map<String, dynamic> toJson() {
return {
'score': score,
'correctCount': correctCount,
'totalCount': totalCount,
'timeSpent': timeSpent.inSeconds,
'accuracy': accuracy,
};
}
bool get isPassed => score >= 60.0;
}

View File

@@ -0,0 +1,280 @@
class ReadingStats {
final int totalArticlesRead;
final int practicesDone;
final double averageScore;
final int totalReadingTime; // in minutes
final double averageReadingSpeed; // words per minute
final double comprehensionAccuracy; // percentage
final int vocabularyMastered;
final int consecutiveDays;
final Map<String, int> categoryStats; // category -> articles read
final Map<String, double> difficultyStats; // difficulty -> average score
final List<DailyReadingRecord> dailyRecords;
const ReadingStats({
required this.totalArticlesRead,
required this.practicesDone,
required this.averageScore,
required this.totalReadingTime,
required this.averageReadingSpeed,
required this.comprehensionAccuracy,
required this.vocabularyMastered,
required this.consecutiveDays,
required this.categoryStats,
required this.difficultyStats,
required this.dailyRecords,
});
factory ReadingStats.fromJson(Map<String, dynamic> json) {
final totalArticlesRead = json['totalArticlesRead'];
final practicesDone = json['practicesDone'];
final averageScore = json['averageScore'];
final totalReadingTime = json['totalReadingTime'];
final averageReadingSpeed = json['averageReadingSpeed'];
final comprehensionAccuracy = json['comprehensionAccuracy'];
final vocabularyMastered = json['vocabularyMastered'];
final consecutiveDays = json['consecutiveDays'];
final rawCategoryStats = json['categoryStats'];
final Map<String, int> categoryStats = {};
if (rawCategoryStats is Map) {
rawCategoryStats.forEach((key, value) {
final k = key is String ? key : key.toString();
final v = value is num ? value.toInt() : 0;
categoryStats[k] = v;
});
}
final rawDifficultyStats = json['difficultyStats'];
final Map<String, double> difficultyStats = {};
if (rawDifficultyStats is Map) {
rawDifficultyStats.forEach((key, value) {
final k = key is String ? key : key.toString();
final v = value is num ? value.toDouble() : 0.0;
difficultyStats[k] = v;
});
}
final rawDailyRecords = json['dailyRecords'];
final List<DailyReadingRecord> dailyRecords = [];
if (rawDailyRecords is List) {
for (final record in rawDailyRecords) {
if (record is Map<String, dynamic>) {
dailyRecords.add(DailyReadingRecord.fromJson(record));
}
}
}
return ReadingStats(
totalArticlesRead: totalArticlesRead is num ? totalArticlesRead.toInt() : 0,
practicesDone: practicesDone is num ? practicesDone.toInt() : 0,
averageScore: averageScore is num ? averageScore.toDouble() : 0.0,
totalReadingTime: totalReadingTime is num ? totalReadingTime.toInt() : 0,
averageReadingSpeed: averageReadingSpeed is num ? averageReadingSpeed.toDouble() : 0.0,
comprehensionAccuracy: comprehensionAccuracy is num ? comprehensionAccuracy.toDouble() : 0.0,
vocabularyMastered: vocabularyMastered is num ? vocabularyMastered.toInt() : 0,
consecutiveDays: consecutiveDays is num ? consecutiveDays.toInt() : 0,
categoryStats: categoryStats,
difficultyStats: difficultyStats,
dailyRecords: dailyRecords,
);
}
Map<String, dynamic> toJson() {
return {
'totalArticlesRead': totalArticlesRead,
'practicesDone': practicesDone,
'averageScore': averageScore,
'totalReadingTime': totalReadingTime,
'averageReadingSpeed': averageReadingSpeed,
'comprehensionAccuracy': comprehensionAccuracy,
'vocabularyMastered': vocabularyMastered,
'consecutiveDays': consecutiveDays,
'categoryStats': categoryStats,
'difficultyStats': difficultyStats,
'dailyRecords': dailyRecords.map((record) => record.toJson()).toList(),
};
}
ReadingStats copyWith({
int? totalArticlesRead,
int? practicesDone,
double? averageScore,
int? totalReadingTime,
double? averageReadingSpeed,
double? comprehensionAccuracy,
int? vocabularyMastered,
int? consecutiveDays,
Map<String, int>? categoryStats,
Map<String, double>? difficultyStats,
List<DailyReadingRecord>? dailyRecords,
}) {
return ReadingStats(
totalArticlesRead: totalArticlesRead ?? this.totalArticlesRead,
practicesDone: practicesDone ?? this.practicesDone,
averageScore: averageScore ?? this.averageScore,
totalReadingTime: totalReadingTime ?? this.totalReadingTime,
averageReadingSpeed: averageReadingSpeed ?? this.averageReadingSpeed,
comprehensionAccuracy: comprehensionAccuracy ?? this.comprehensionAccuracy,
vocabularyMastered: vocabularyMastered ?? this.vocabularyMastered,
consecutiveDays: consecutiveDays ?? this.consecutiveDays,
categoryStats: categoryStats ?? this.categoryStats,
difficultyStats: difficultyStats ?? this.difficultyStats,
dailyRecords: dailyRecords ?? this.dailyRecords,
);
}
String get readingTimeFormatted {
final hours = totalReadingTime ~/ 60;
final minutes = totalReadingTime % 60;
if (hours > 0) {
return '${hours}小时${minutes}分钟';
}
return '${minutes}分钟';
}
String get averageScoreFormatted {
return '${averageScore.toStringAsFixed(1)}';
}
String get comprehensionAccuracyFormatted {
return '${comprehensionAccuracy.toStringAsFixed(1)}%';
}
String get averageReadingSpeedFormatted {
return '${averageReadingSpeed.toStringAsFixed(0)}词/分钟';
}
}
class DailyReadingRecord {
final DateTime date;
final int articlesRead;
final int practicesDone;
final int readingTime; // in minutes
final double averageScore;
final int vocabularyLearned;
const DailyReadingRecord({
required this.date,
required this.articlesRead,
required this.practicesDone,
required this.readingTime,
required this.averageScore,
required this.vocabularyLearned,
});
factory DailyReadingRecord.fromJson(Map<String, dynamic> json) {
final dateRaw = json['date'];
final articlesRead = json['articlesRead'];
final practicesDone = json['practicesDone'];
final readingTime = json['readingTime'];
final averageScore = json['averageScore'];
final vocabularyLearned = json['vocabularyLearned'];
return DailyReadingRecord(
date: dateRaw is String ? DateTime.parse(dateRaw) : DateTime.fromMillisecondsSinceEpoch(0),
articlesRead: articlesRead is num ? articlesRead.toInt() : 0,
practicesDone: practicesDone is num ? practicesDone.toInt() : 0,
readingTime: readingTime is num ? readingTime.toInt() : 0,
averageScore: averageScore is num ? averageScore.toDouble() : 0.0,
vocabularyLearned: vocabularyLearned is num ? vocabularyLearned.toInt() : 0,
);
}
Map<String, dynamic> toJson() {
return {
'date': date.toIso8601String(),
'articlesRead': articlesRead,
'practicesDone': practicesDone,
'readingTime': readingTime,
'averageScore': averageScore,
'vocabularyLearned': vocabularyLearned,
};
}
DailyReadingRecord copyWith({
DateTime? date,
int? articlesRead,
int? practicesDone,
int? readingTime,
double? averageScore,
int? vocabularyLearned,
}) {
return DailyReadingRecord(
date: date ?? this.date,
articlesRead: articlesRead ?? this.articlesRead,
practicesDone: practicesDone ?? this.practicesDone,
readingTime: readingTime ?? this.readingTime,
averageScore: averageScore ?? this.averageScore,
vocabularyLearned: vocabularyLearned ?? this.vocabularyLearned,
);
}
}
class ReadingProgress {
final String category;
final int totalArticles;
final int completedArticles;
final double averageScore;
final String difficulty;
const ReadingProgress({
required this.category,
required this.totalArticles,
required this.completedArticles,
required this.averageScore,
required this.difficulty,
});
factory ReadingProgress.fromJson(Map<String, dynamic> json) {
return ReadingProgress(
category: json['category'] as String,
totalArticles: json['totalArticles'] as int,
completedArticles: json['completedArticles'] as int,
averageScore: (json['averageScore'] as num).toDouble(),
difficulty: json['difficulty'] as String,
);
}
Map<String, dynamic> toJson() {
return {
'category': category,
'totalArticles': totalArticles,
'completedArticles': completedArticles,
'averageScore': averageScore,
'difficulty': difficulty,
};
}
double get progressPercentage {
if (totalArticles == 0) return 0.0;
return (completedArticles / totalArticles) * 100;
}
String get progressText {
return '$completedArticles/$totalArticles';
}
String get categoryLabel {
switch (category.toLowerCase()) {
case 'cet4':
return '四级阅读';
case 'cet6':
return '六级阅读';
case 'toefl':
return '托福阅读';
case 'ielts':
return '雅思阅读';
case 'daily':
return '日常阅读';
case 'business':
return '商务阅读';
case 'academic':
return '学术阅读';
case 'news':
return '新闻阅读';
default:
return category;
}
}
}