本次提交实现了背诵模块的完整基础链路: 1. 新增词书、背单词接口层与对应数据模型 2. 完成背诵首页、章节列表页等核心页面开发 3. 实现音频播放、加载弹窗等通用组件 4. 配置路由跳转链路,替换旧版缓存传参方案 5. 对齐Vue端业务逻辑与UI样式
301 lines
8.9 KiB
Dart
301 lines
8.9 KiB
Dart
/// ============================================================
|
||
/// 【lexicon 模块模型】背单词:章节 / 关卡 / 题目 / 小结
|
||
/// —— 对标 Vue3 端 api/lexicon/type.ts
|
||
/// ============================================================
|
||
library;
|
||
|
||
/// 兼容 number | String 的 id 安全转换(对标 TS 的 number | string)
|
||
int safeLexiconId(dynamic v) {
|
||
if (v is num) return v.toInt();
|
||
if (v is String) return int.tryParse(v) ?? 0;
|
||
return 0;
|
||
}
|
||
|
||
double _safeDouble(dynamic v) {
|
||
if (v is num) return v.toDouble();
|
||
if (v is String) return double.tryParse(v) ?? 0;
|
||
return 0;
|
||
}
|
||
|
||
/// 章节(词汇章节列表项)
|
||
class Chapter {
|
||
const Chapter({
|
||
this.id = 0,
|
||
this.title = '',
|
||
this.icon = '',
|
||
this.finishLexiconCount = 0,
|
||
this.levelCount = 0,
|
||
this.lexiconCount = 0,
|
||
this.finishProgress = 0,
|
||
this.cover = '',
|
||
this.index = '',
|
||
});
|
||
|
||
final int id;
|
||
final String title;
|
||
final String icon;
|
||
final int finishLexiconCount;
|
||
final int levelCount;
|
||
final int lexiconCount;
|
||
final double finishProgress; // 0~1
|
||
final String cover;
|
||
final String index; // 封面叠字序号(接口可能下发,Vue 端直接渲染 item.index)
|
||
|
||
factory Chapter.fromJson(Map<String, dynamic> json) {
|
||
return Chapter(
|
||
id: safeLexiconId(json['id']),
|
||
title: json['title'] as String? ?? '',
|
||
icon: json['icon'] as String? ?? '',
|
||
finishLexiconCount: (json['finishLexiconCount'] as num?)?.toInt() ?? 0,
|
||
levelCount: (json['levelCount'] as num?)?.toInt() ?? 0,
|
||
lexiconCount: (json['lexiconCount'] as num?)?.toInt() ?? 0,
|
||
finishProgress: _safeDouble(json['finishProgress']),
|
||
cover: json['cover'] as String? ?? '',
|
||
index: json['index']?.toString() ?? '',
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 章节列表响应
|
||
class ChapterResponse {
|
||
const ChapterResponse({this.chapters = const [], this.totalFinishProgress = 0});
|
||
|
||
final List<Chapter> chapters;
|
||
final double totalFinishProgress; // 0~1
|
||
|
||
factory ChapterResponse.fromJson(Map<String, dynamic> json) {
|
||
final payload = (json['data'] as Map<String, dynamic>?) ?? json;
|
||
return ChapterResponse(
|
||
chapters: (payload['chapters'] as List<dynamic>? ?? const [])
|
||
.whereType<Map<String, dynamic>>()
|
||
.map(Chapter.fromJson)
|
||
.toList(),
|
||
totalFinishProgress: _safeDouble(payload['totalFinishProgress']),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 关卡
|
||
class Level {
|
||
const Level({
|
||
this.id = 0,
|
||
this.title = '',
|
||
this.lexiconCount = 0,
|
||
this.isOpen = false,
|
||
this.finishLexiconCount = 0,
|
||
this.finishProgress = 0,
|
||
});
|
||
|
||
final int id;
|
||
final String title;
|
||
final int lexiconCount;
|
||
final bool isOpen; // 是否解锁
|
||
final int finishLexiconCount;
|
||
final double finishProgress; // 0~1
|
||
|
||
factory Level.fromJson(Map<String, dynamic> json) {
|
||
return Level(
|
||
id: safeLexiconId(json['id']),
|
||
title: json['title'] as String? ?? '',
|
||
lexiconCount: (json['lexiconCount'] as num?)?.toInt() ?? 0,
|
||
isOpen: json['isOpen'] as bool? ?? false,
|
||
finishLexiconCount: (json['finishLexiconCount'] as num?)?.toInt() ?? 0,
|
||
finishProgress: _safeDouble(json['finishProgress']),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 关卡列表响应(含章节头部汇总)
|
||
class LevelResponse {
|
||
const LevelResponse({
|
||
this.id = 0,
|
||
this.title = '',
|
||
this.lexiconCount = 0,
|
||
this.finishLexiconCount = 0,
|
||
this.finishProgress = 0,
|
||
this.levels = const [],
|
||
});
|
||
|
||
final int id;
|
||
final String title;
|
||
final int lexiconCount;
|
||
final int finishLexiconCount;
|
||
final double finishProgress;
|
||
final List<Level> levels;
|
||
|
||
factory LevelResponse.fromJson(Map<String, dynamic> json) {
|
||
final payload = (json['data'] as Map<String, dynamic>?) ?? json;
|
||
return LevelResponse(
|
||
id: safeLexiconId(payload['id']),
|
||
title: payload['title'] as String? ?? '',
|
||
lexiconCount: (payload['lexiconCount'] as num?)?.toInt() ?? 0,
|
||
finishLexiconCount:
|
||
(payload['finishLexiconCount'] as num?)?.toInt() ?? 0,
|
||
finishProgress: _safeDouble(payload['finishProgress']),
|
||
levels: (payload['levels'] as List<dynamic>? ?? const [])
|
||
.whereType<Map<String, dynamic>>()
|
||
.map(Level.fromJson)
|
||
.toList(),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 答题页单词题目
|
||
class Question {
|
||
const Question({
|
||
this.id = 0,
|
||
this.americanEnglish = '',
|
||
this.americanPhonetic = '',
|
||
this.americanVoiceUrl = '',
|
||
this.imageDesc = '',
|
||
this.contextMatch = '',
|
||
this.english = '',
|
||
this.phonetic = '',
|
||
this.voiceUrl = '',
|
||
this.chineseDesc = '',
|
||
this.options = const [],
|
||
});
|
||
|
||
final int id;
|
||
final String americanEnglish;
|
||
final String americanPhonetic;
|
||
final String americanVoiceUrl;
|
||
final String imageDesc; // 配图
|
||
final String contextMatch; // 语境搭配(空则不可翻转)
|
||
final String english;
|
||
final String phonetic;
|
||
final String voiceUrl;
|
||
final String chineseDesc; // 正确答案
|
||
final List<String> options;
|
||
|
||
factory Question.fromJson(Map<String, dynamic> json) {
|
||
return Question(
|
||
id: safeLexiconId(json['id']),
|
||
americanEnglish: json['americanEnglish'] as String? ?? '',
|
||
americanPhonetic: json['americanPhonetic'] as String? ?? '',
|
||
americanVoiceUrl: json['americanVoiceUrl'] as String? ?? '',
|
||
imageDesc: json['imageDesc'] as String? ?? '',
|
||
contextMatch: json['contextMatch'] as String? ?? '',
|
||
english: json['english'] as String? ?? '',
|
||
phonetic: json['phonetic'] as String? ?? '',
|
||
voiceUrl: json['voiceUrl'] as String? ?? '',
|
||
chineseDesc: json['chineseDesc'] as String? ?? '',
|
||
options: (json['options'] as List<dynamic>? ?? const [])
|
||
.map((e) => e.toString())
|
||
.toList(),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 已作答单词(小结回顾用)
|
||
class Lexicon {
|
||
const Lexicon({
|
||
this.id = 0,
|
||
this.english = '',
|
||
this.phonetic = '',
|
||
this.voiceUrl = '',
|
||
this.chineseDesc = '',
|
||
this.isCorrect = false,
|
||
});
|
||
|
||
final int id;
|
||
final String english;
|
||
final String phonetic;
|
||
final String voiceUrl;
|
||
final String chineseDesc;
|
||
final bool isCorrect;
|
||
|
||
factory Lexicon.fromJson(Map<String, dynamic> json) {
|
||
return Lexicon(
|
||
id: safeLexiconId(json['id']),
|
||
english: json['english'] as String? ?? '',
|
||
phonetic: json['phonetic'] as String? ?? '',
|
||
voiceUrl: json['voiceUrl'] as String? ?? '',
|
||
chineseDesc: json['chineseDesc'] as String? ?? '',
|
||
isCorrect: json['isCorrect'] as bool? ?? false,
|
||
);
|
||
}
|
||
}
|
||
|
||
/// LevelStart 开局信息
|
||
class LevelStartInfo {
|
||
const LevelStartInfo({
|
||
this.id = 0,
|
||
this.userLevelId = 0,
|
||
this.finishLexiconCount = 0,
|
||
this.lexiconCount = 0,
|
||
this.questions = const [],
|
||
this.lexicons = const [],
|
||
});
|
||
|
||
final int id;
|
||
final int userLevelId;
|
||
final int finishLexiconCount;
|
||
final int lexiconCount;
|
||
final List<Question> questions;
|
||
final List<Lexicon> lexicons;
|
||
|
||
factory LevelStartInfo.fromJson(Map<String, dynamic> json) {
|
||
final payload = (json['data'] as Map<String, dynamic>?) ?? json;
|
||
return LevelStartInfo(
|
||
id: safeLexiconId(payload['id']),
|
||
userLevelId: (payload['userLevelId'] as num?)?.toInt() ?? 0,
|
||
finishLexiconCount:
|
||
(payload['finishLexiconCount'] as num?)?.toInt() ?? 0,
|
||
lexiconCount: (payload['lexiconCount'] as num?)?.toInt() ?? 0,
|
||
questions: (payload['questions'] as List<dynamic>? ?? const [])
|
||
.whereType<Map<String, dynamic>>()
|
||
.map(Question.fromJson)
|
||
.toList(),
|
||
lexicons: (payload['lexicons'] as List<dynamic>? ?? const [])
|
||
.whereType<Map<String, dynamic>>()
|
||
.map(Lexicon.fromJson)
|
||
.toList(),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 闯关小结信息
|
||
class LevelOverInfo {
|
||
const LevelOverInfo({
|
||
this.id = 0,
|
||
this.userLevelId = 0,
|
||
this.lexiconCount = 0,
|
||
this.correctCount = 0,
|
||
this.wrongCount = 0,
|
||
this.nextLevelId = 0,
|
||
this.nextChapterId = 0,
|
||
this.lexicons = const [],
|
||
});
|
||
|
||
final int id;
|
||
final int userLevelId;
|
||
final int lexiconCount;
|
||
final int correctCount;
|
||
final int wrongCount;
|
||
final int nextLevelId; // 0 表示没有下一关
|
||
final int nextChapterId; // 0 表示没有下一章
|
||
final List<Lexicon> lexicons;
|
||
|
||
/// 正确率(0~100,四舍五入;无单词兜底 0)
|
||
int get rate =>
|
||
lexiconCount <= 0 ? 0 : (correctCount * 100 / lexiconCount).round();
|
||
|
||
factory LevelOverInfo.fromJson(Map<String, dynamic> json) {
|
||
final payload = (json['data'] as Map<String, dynamic>?) ?? json;
|
||
return LevelOverInfo(
|
||
id: safeLexiconId(payload['id']),
|
||
userLevelId: (payload['userLevelId'] as num?)?.toInt() ?? 0,
|
||
lexiconCount: (payload['lexiconCount'] as num?)?.toInt() ?? 0,
|
||
correctCount: (payload['correctCount'] as num?)?.toInt() ?? 0,
|
||
wrongCount: (payload['wrongCount'] as num?)?.toInt() ?? 0,
|
||
nextLevelId: safeLexiconId(payload['nextLevelId']),
|
||
nextChapterId: safeLexiconId(payload['nextChapterId']),
|
||
lexicons: (payload['lexicons'] as List<dynamic>? ?? const [])
|
||
.whereType<Map<String, dynamic>>()
|
||
.map(Lexicon.fromJson)
|
||
.toList(),
|
||
);
|
||
}
|
||
}
|