519 lines
12 KiB
Dart
519 lines
12 KiB
Dart
|
|
import 'package:json_annotation/json_annotation.dart';
|
|||
|
|
|
|||
|
|
part 'word_model.g.dart';
|
|||
|
|
|
|||
|
|
/// 单词难度等级
|
|||
|
|
enum WordDifficulty {
|
|||
|
|
@JsonValue('beginner')
|
|||
|
|
beginner,
|
|||
|
|
@JsonValue('elementary')
|
|||
|
|
elementary,
|
|||
|
|
@JsonValue('intermediate')
|
|||
|
|
intermediate,
|
|||
|
|
@JsonValue('advanced')
|
|||
|
|
advanced,
|
|||
|
|
@JsonValue('expert')
|
|||
|
|
expert,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 单词类型
|
|||
|
|
enum WordType {
|
|||
|
|
@JsonValue('noun')
|
|||
|
|
noun,
|
|||
|
|
@JsonValue('verb')
|
|||
|
|
verb,
|
|||
|
|
@JsonValue('adjective')
|
|||
|
|
adjective,
|
|||
|
|
@JsonValue('adverb')
|
|||
|
|
adverb,
|
|||
|
|
@JsonValue('preposition')
|
|||
|
|
preposition,
|
|||
|
|
@JsonValue('conjunction')
|
|||
|
|
conjunction,
|
|||
|
|
@JsonValue('interjection')
|
|||
|
|
interjection,
|
|||
|
|
@JsonValue('pronoun')
|
|||
|
|
pronoun,
|
|||
|
|
@JsonValue('article')
|
|||
|
|
article,
|
|||
|
|
@JsonValue('phrase')
|
|||
|
|
phrase,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 学习状态
|
|||
|
|
enum LearningStatus {
|
|||
|
|
@JsonValue('new')
|
|||
|
|
newWord,
|
|||
|
|
@JsonValue('learning')
|
|||
|
|
learning,
|
|||
|
|
@JsonValue('reviewing')
|
|||
|
|
reviewing,
|
|||
|
|
@JsonValue('mastered')
|
|||
|
|
mastered,
|
|||
|
|
@JsonValue('forgotten')
|
|||
|
|
forgotten,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 单词模型
|
|||
|
|
@JsonSerializable()
|
|||
|
|
class Word {
|
|||
|
|
/// 单词ID
|
|||
|
|
@JsonKey(name: 'id', fromJson: _idFromJson)
|
|||
|
|
final String id;
|
|||
|
|
|
|||
|
|
/// 处理id字段的类型转换(后端返回int64)
|
|||
|
|
static String _idFromJson(dynamic value) {
|
|||
|
|
if (value is int) {
|
|||
|
|
return value.toString();
|
|||
|
|
}
|
|||
|
|
return value.toString();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 单词
|
|||
|
|
final String word;
|
|||
|
|
|
|||
|
|
/// 音标
|
|||
|
|
final String? phonetic;
|
|||
|
|
|
|||
|
|
/// 音频URL
|
|||
|
|
@JsonKey(name: 'audio_url')
|
|||
|
|
final String? audioUrl;
|
|||
|
|
|
|||
|
|
/// 词性和释义列表
|
|||
|
|
@JsonKey(defaultValue: [])
|
|||
|
|
final List<WordDefinition> definitions;
|
|||
|
|
|
|||
|
|
/// 例句列表
|
|||
|
|
@JsonKey(defaultValue: [])
|
|||
|
|
final List<WordExample> examples;
|
|||
|
|
|
|||
|
|
/// 同义词
|
|||
|
|
@JsonKey(defaultValue: [])
|
|||
|
|
final List<String> synonyms;
|
|||
|
|
|
|||
|
|
/// 反义词
|
|||
|
|
@JsonKey(defaultValue: [])
|
|||
|
|
final List<String> antonyms;
|
|||
|
|
|
|||
|
|
/// 词根词缀
|
|||
|
|
final WordEtymology? etymology;
|
|||
|
|
|
|||
|
|
/// 难度等级
|
|||
|
|
@JsonKey(name: 'difficulty', fromJson: _difficultyFromJson)
|
|||
|
|
final WordDifficulty difficulty;
|
|||
|
|
|
|||
|
|
/// 频率等级 (1-5)
|
|||
|
|
@JsonKey(defaultValue: 0)
|
|||
|
|
final int frequency;
|
|||
|
|
|
|||
|
|
/// 图片URL
|
|||
|
|
@JsonKey(name: 'image_url')
|
|||
|
|
final String? imageUrl;
|
|||
|
|
|
|||
|
|
/// 记忆技巧
|
|||
|
|
@JsonKey(name: 'memory_tip')
|
|||
|
|
final String? memoryTip;
|
|||
|
|
|
|||
|
|
/// 创建时间
|
|||
|
|
@JsonKey(name: 'created_at')
|
|||
|
|
final DateTime createdAt;
|
|||
|
|
|
|||
|
|
/// 更新时间
|
|||
|
|
@JsonKey(name: 'updated_at')
|
|||
|
|
final DateTime updatedAt;
|
|||
|
|
|
|||
|
|
const Word({
|
|||
|
|
required this.id,
|
|||
|
|
required this.word,
|
|||
|
|
this.phonetic,
|
|||
|
|
this.audioUrl,
|
|||
|
|
required this.definitions,
|
|||
|
|
this.examples = const [],
|
|||
|
|
this.synonyms = const [],
|
|||
|
|
this.antonyms = const [],
|
|||
|
|
this.etymology,
|
|||
|
|
required this.difficulty,
|
|||
|
|
required this.frequency,
|
|||
|
|
this.imageUrl,
|
|||
|
|
this.memoryTip,
|
|||
|
|
required this.createdAt,
|
|||
|
|
required this.updatedAt,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
factory Word.fromJson(Map<String, dynamic> json) => _$WordFromJson(json);
|
|||
|
|
Map<String, dynamic> toJson() => _$WordToJson(this);
|
|||
|
|
|
|||
|
|
/// 处理difficulty字段(后端可能为null、空字符串或其他值)
|
|||
|
|
static WordDifficulty _difficultyFromJson(dynamic value) {
|
|||
|
|
if (value == null || value == '') return WordDifficulty.beginner;
|
|||
|
|
|
|||
|
|
final stringValue = value.toString().toLowerCase();
|
|||
|
|
switch (stringValue) {
|
|||
|
|
case 'beginner':
|
|||
|
|
case '1':
|
|||
|
|
return WordDifficulty.beginner;
|
|||
|
|
case 'elementary':
|
|||
|
|
case '2':
|
|||
|
|
return WordDifficulty.elementary;
|
|||
|
|
case 'intermediate':
|
|||
|
|
case '3':
|
|||
|
|
return WordDifficulty.intermediate;
|
|||
|
|
case 'advanced':
|
|||
|
|
case '4':
|
|||
|
|
return WordDifficulty.advanced;
|
|||
|
|
case 'expert':
|
|||
|
|
case '5':
|
|||
|
|
return WordDifficulty.expert;
|
|||
|
|
default:
|
|||
|
|
return WordDifficulty.beginner; // 默认为初级
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
Word copyWith({
|
|||
|
|
String? id,
|
|||
|
|
String? word,
|
|||
|
|
String? phonetic,
|
|||
|
|
String? audioUrl,
|
|||
|
|
List<WordDefinition>? definitions,
|
|||
|
|
List<WordExample>? examples,
|
|||
|
|
List<String>? synonyms,
|
|||
|
|
List<String>? antonyms,
|
|||
|
|
WordEtymology? etymology,
|
|||
|
|
WordDifficulty? difficulty,
|
|||
|
|
int? frequency,
|
|||
|
|
String? imageUrl,
|
|||
|
|
String? memoryTip,
|
|||
|
|
DateTime? createdAt,
|
|||
|
|
DateTime? updatedAt,
|
|||
|
|
}) {
|
|||
|
|
return Word(
|
|||
|
|
id: id ?? this.id,
|
|||
|
|
word: word ?? this.word,
|
|||
|
|
phonetic: phonetic ?? this.phonetic,
|
|||
|
|
audioUrl: audioUrl ?? this.audioUrl,
|
|||
|
|
definitions: definitions ?? this.definitions,
|
|||
|
|
examples: examples ?? this.examples,
|
|||
|
|
synonyms: synonyms ?? this.synonyms,
|
|||
|
|
antonyms: antonyms ?? this.antonyms,
|
|||
|
|
etymology: etymology ?? this.etymology,
|
|||
|
|
difficulty: difficulty ?? this.difficulty,
|
|||
|
|
frequency: frequency ?? this.frequency,
|
|||
|
|
imageUrl: imageUrl ?? this.imageUrl,
|
|||
|
|
memoryTip: memoryTip ?? this.memoryTip,
|
|||
|
|
createdAt: createdAt ?? this.createdAt,
|
|||
|
|
updatedAt: updatedAt ?? this.updatedAt,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 单词释义
|
|||
|
|
@JsonSerializable()
|
|||
|
|
class WordDefinition {
|
|||
|
|
/// 词性
|
|||
|
|
@JsonKey(name: 'type', fromJson: _typeFromJson)
|
|||
|
|
final WordType type;
|
|||
|
|
|
|||
|
|
/// 释义
|
|||
|
|
final String definition;
|
|||
|
|
|
|||
|
|
/// 中文翻译
|
|||
|
|
@JsonKey(name: 'translation', fromJson: _translationFromJson)
|
|||
|
|
final String translation;
|
|||
|
|
|
|||
|
|
/// 使用频率 (1-5)
|
|||
|
|
@JsonKey(defaultValue: 3)
|
|||
|
|
final int frequency;
|
|||
|
|
|
|||
|
|
const WordDefinition({
|
|||
|
|
required this.type,
|
|||
|
|
required this.definition,
|
|||
|
|
required this.translation,
|
|||
|
|
this.frequency = 3,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/// 处理type字段(后端可能为null或空字符串)
|
|||
|
|
static WordType _typeFromJson(dynamic value) {
|
|||
|
|
if (value == null || value == '') return WordType.noun;
|
|||
|
|
|
|||
|
|
final stringValue = value.toString().toLowerCase();
|
|||
|
|
switch (stringValue) {
|
|||
|
|
case 'noun':
|
|||
|
|
case 'n':
|
|||
|
|
return WordType.noun;
|
|||
|
|
case 'verb':
|
|||
|
|
case 'v':
|
|||
|
|
return WordType.verb;
|
|||
|
|
case 'adjective':
|
|||
|
|
case 'adj':
|
|||
|
|
return WordType.adjective;
|
|||
|
|
case 'adverb':
|
|||
|
|
case 'adv':
|
|||
|
|
return WordType.adverb;
|
|||
|
|
case 'preposition':
|
|||
|
|
case 'prep':
|
|||
|
|
return WordType.preposition;
|
|||
|
|
case 'conjunction':
|
|||
|
|
case 'conj':
|
|||
|
|
return WordType.conjunction;
|
|||
|
|
case 'interjection':
|
|||
|
|
case 'interj':
|
|||
|
|
return WordType.interjection;
|
|||
|
|
case 'pronoun':
|
|||
|
|
case 'pron':
|
|||
|
|
return WordType.pronoun;
|
|||
|
|
case 'article':
|
|||
|
|
case 'art':
|
|||
|
|
return WordType.article;
|
|||
|
|
case 'phrase':
|
|||
|
|
return WordType.phrase;
|
|||
|
|
default:
|
|||
|
|
return WordType.noun; // 默认为名词
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 处理translation字段(后端可能为null)
|
|||
|
|
static String _translationFromJson(dynamic value) {
|
|||
|
|
if (value == null) return '';
|
|||
|
|
return value.toString();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
factory WordDefinition.fromJson(Map<String, dynamic> json) => _$WordDefinitionFromJson(json);
|
|||
|
|
Map<String, dynamic> toJson() => _$WordDefinitionToJson(this);
|
|||
|
|
|
|||
|
|
WordDefinition copyWith({
|
|||
|
|
WordType? type,
|
|||
|
|
String? definition,
|
|||
|
|
String? translation,
|
|||
|
|
int? frequency,
|
|||
|
|
}) {
|
|||
|
|
return WordDefinition(
|
|||
|
|
type: type ?? this.type,
|
|||
|
|
definition: definition ?? this.definition,
|
|||
|
|
translation: translation ?? this.translation,
|
|||
|
|
frequency: frequency ?? this.frequency,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 单词例句
|
|||
|
|
@JsonSerializable()
|
|||
|
|
class WordExample {
|
|||
|
|
/// 例句
|
|||
|
|
@JsonKey(name: 'example', fromJson: _sentenceFromJson)
|
|||
|
|
final String sentence;
|
|||
|
|
|
|||
|
|
/// 中文翻译
|
|||
|
|
@JsonKey(name: 'translation', fromJson: _exampleTranslationFromJson)
|
|||
|
|
final String translation;
|
|||
|
|
|
|||
|
|
/// 音频URL
|
|||
|
|
@JsonKey(name: 'audio_url')
|
|||
|
|
final String? audioUrl;
|
|||
|
|
|
|||
|
|
/// 来源
|
|||
|
|
final String? source;
|
|||
|
|
|
|||
|
|
const WordExample({
|
|||
|
|
required this.sentence,
|
|||
|
|
required this.translation,
|
|||
|
|
this.audioUrl,
|
|||
|
|
this.source,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
/// 处理example字段(映射到sentence)
|
|||
|
|
static String _sentenceFromJson(dynamic value) {
|
|||
|
|
if (value == null) return '';
|
|||
|
|
return value.toString();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 处理translation字段(后端可能为null)
|
|||
|
|
static String _exampleTranslationFromJson(dynamic value) {
|
|||
|
|
if (value == null) return '';
|
|||
|
|
return value.toString();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
factory WordExample.fromJson(Map<String, dynamic> json) => _$WordExampleFromJson(json);
|
|||
|
|
Map<String, dynamic> toJson() => _$WordExampleToJson(this);
|
|||
|
|
|
|||
|
|
WordExample copyWith({
|
|||
|
|
String? sentence,
|
|||
|
|
String? translation,
|
|||
|
|
String? audioUrl,
|
|||
|
|
String? source,
|
|||
|
|
}) {
|
|||
|
|
return WordExample(
|
|||
|
|
sentence: sentence ?? this.sentence,
|
|||
|
|
translation: translation ?? this.translation,
|
|||
|
|
audioUrl: audioUrl ?? this.audioUrl,
|
|||
|
|
source: source ?? this.source,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 词根词缀
|
|||
|
|
@JsonSerializable()
|
|||
|
|
class WordEtymology {
|
|||
|
|
/// 词根
|
|||
|
|
final List<String> roots;
|
|||
|
|
|
|||
|
|
/// 前缀
|
|||
|
|
final List<String> prefixes;
|
|||
|
|
|
|||
|
|
/// 后缀
|
|||
|
|
final List<String> suffixes;
|
|||
|
|
|
|||
|
|
/// 词源说明
|
|||
|
|
final String? origin;
|
|||
|
|
|
|||
|
|
const WordEtymology({
|
|||
|
|
this.roots = const [],
|
|||
|
|
this.prefixes = const [],
|
|||
|
|
this.suffixes = const [],
|
|||
|
|
this.origin,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
factory WordEtymology.fromJson(Map<String, dynamic> json) => _$WordEtymologyFromJson(json);
|
|||
|
|
Map<String, dynamic> toJson() => _$WordEtymologyToJson(this);
|
|||
|
|
|
|||
|
|
WordEtymology copyWith({
|
|||
|
|
List<String>? roots,
|
|||
|
|
List<String>? prefixes,
|
|||
|
|
List<String>? suffixes,
|
|||
|
|
String? origin,
|
|||
|
|
}) {
|
|||
|
|
return WordEtymology(
|
|||
|
|
roots: roots ?? this.roots,
|
|||
|
|
prefixes: prefixes ?? this.prefixes,
|
|||
|
|
suffixes: suffixes ?? this.suffixes,
|
|||
|
|
origin: origin ?? this.origin,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 用户单词学习记录
|
|||
|
|
@JsonSerializable()
|
|||
|
|
class UserWordProgress {
|
|||
|
|
/// 记录ID
|
|||
|
|
@JsonKey(fromJson: _idFromJson)
|
|||
|
|
final String id;
|
|||
|
|
|
|||
|
|
/// 用户ID
|
|||
|
|
@JsonKey(name: 'user_id', fromJson: _userIdFromJson)
|
|||
|
|
final String userId;
|
|||
|
|
|
|||
|
|
/// 单词ID
|
|||
|
|
@JsonKey(name: 'vocabulary_id', fromJson: _wordIdFromJson)
|
|||
|
|
final String wordId;
|
|||
|
|
|
|||
|
|
/// 学习状态
|
|||
|
|
final LearningStatus status;
|
|||
|
|
|
|||
|
|
/// 学习次数
|
|||
|
|
@JsonKey(name: 'study_count')
|
|||
|
|
final int studyCount;
|
|||
|
|
|
|||
|
|
/// 正确次数
|
|||
|
|
@JsonKey(name: 'correct_count')
|
|||
|
|
final int correctCount;
|
|||
|
|
|
|||
|
|
/// 错误次数
|
|||
|
|
@JsonKey(name: 'wrong_count')
|
|||
|
|
final int wrongCount;
|
|||
|
|
|
|||
|
|
/// 熟练度 (0-100)
|
|||
|
|
final int proficiency;
|
|||
|
|
|
|||
|
|
/// 下次复习时间
|
|||
|
|
@JsonKey(name: 'next_review_at')
|
|||
|
|
final DateTime? nextReviewAt;
|
|||
|
|
|
|||
|
|
/// 复习间隔 (天)
|
|||
|
|
@JsonKey(name: 'review_interval')
|
|||
|
|
final int reviewInterval;
|
|||
|
|
|
|||
|
|
/// 首次学习时间
|
|||
|
|
@JsonKey(name: 'first_studied_at')
|
|||
|
|
final DateTime firstStudiedAt;
|
|||
|
|
|
|||
|
|
/// 最后学习时间
|
|||
|
|
@JsonKey(name: 'last_studied_at')
|
|||
|
|
final DateTime lastStudiedAt;
|
|||
|
|
|
|||
|
|
/// 掌握时间
|
|||
|
|
@JsonKey(name: 'mastered_at')
|
|||
|
|
final DateTime? masteredAt;
|
|||
|
|
|
|||
|
|
const UserWordProgress({
|
|||
|
|
required this.id,
|
|||
|
|
required this.userId,
|
|||
|
|
required this.wordId,
|
|||
|
|
required this.status,
|
|||
|
|
this.studyCount = 0,
|
|||
|
|
this.correctCount = 0,
|
|||
|
|
this.wrongCount = 0,
|
|||
|
|
this.proficiency = 0,
|
|||
|
|
this.nextReviewAt,
|
|||
|
|
this.reviewInterval = 1,
|
|||
|
|
required this.firstStudiedAt,
|
|||
|
|
required this.lastStudiedAt,
|
|||
|
|
this.masteredAt,
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
factory UserWordProgress.fromJson(Map<String, dynamic> json) => _$UserWordProgressFromJson(json);
|
|||
|
|
Map<String, dynamic> toJson() => _$UserWordProgressToJson(this);
|
|||
|
|
|
|||
|
|
UserWordProgress copyWith({
|
|||
|
|
String? id,
|
|||
|
|
String? userId,
|
|||
|
|
String? wordId,
|
|||
|
|
LearningStatus? status,
|
|||
|
|
int? studyCount,
|
|||
|
|
int? correctCount,
|
|||
|
|
int? wrongCount,
|
|||
|
|
int? proficiency,
|
|||
|
|
DateTime? nextReviewAt,
|
|||
|
|
int? reviewInterval,
|
|||
|
|
DateTime? firstStudiedAt,
|
|||
|
|
DateTime? lastStudiedAt,
|
|||
|
|
DateTime? masteredAt,
|
|||
|
|
}) {
|
|||
|
|
return UserWordProgress(
|
|||
|
|
id: id ?? this.id,
|
|||
|
|
userId: userId ?? this.userId,
|
|||
|
|
wordId: wordId ?? this.wordId,
|
|||
|
|
status: status ?? this.status,
|
|||
|
|
studyCount: studyCount ?? this.studyCount,
|
|||
|
|
correctCount: correctCount ?? this.correctCount,
|
|||
|
|
wrongCount: wrongCount ?? this.wrongCount,
|
|||
|
|
proficiency: proficiency ?? this.proficiency,
|
|||
|
|
nextReviewAt: nextReviewAt ?? this.nextReviewAt,
|
|||
|
|
reviewInterval: reviewInterval ?? this.reviewInterval,
|
|||
|
|
firstStudiedAt: firstStudiedAt ?? this.firstStudiedAt,
|
|||
|
|
lastStudiedAt: lastStudiedAt ?? this.lastStudiedAt,
|
|||
|
|
masteredAt: masteredAt ?? this.masteredAt,
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 计算学习准确率
|
|||
|
|
double get accuracy {
|
|||
|
|
if (studyCount == 0) return 0.0;
|
|||
|
|
return correctCount / studyCount;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 是否需要复习
|
|||
|
|
bool get needsReview {
|
|||
|
|
if (nextReviewAt == null) return false;
|
|||
|
|
return DateTime.now().isAfter(nextReviewAt!);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 是否为新单词
|
|||
|
|
bool get isNew => status == LearningStatus.newWord;
|
|||
|
|
|
|||
|
|
/// 是否已掌握
|
|||
|
|
bool get isMastered => status == LearningStatus.mastered;
|
|||
|
|
|
|||
|
|
/// 类型转换方法
|
|||
|
|
static String _idFromJson(dynamic value) => value.toString();
|
|||
|
|
static String _userIdFromJson(dynamic value) => value.toString();
|
|||
|
|
static String _wordIdFromJson(dynamic value) => value.toString();
|
|||
|
|
}
|