Merge branch 'feat-cbx'
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
import 'package:new_learn/core/api/lexicon/model.dart';
|
||||
import 'package:new_learn/core/network/dio_client.dart';
|
||||
|
||||
/// ============================================================
|
||||
/// 【lexicon 模块接口】背单词接口 —— 对标 Vue3 端 api/lexicon/index.ts
|
||||
///
|
||||
/// getChapters → GET GetChapters/{bookId} 章节列表
|
||||
/// getLevels → GET GetLevels/{chapterId} 关卡列表
|
||||
/// levelStart → POST LevelStart/{levelId} 开局取题
|
||||
/// answerQuestion → POST Answer 提交答案
|
||||
/// getLevelIsFinish → GET GetLevelIsFinish/{levelId} 关卡是否完成(轮询)
|
||||
/// getLevelOverInfo → GET GetLevelOverInfo/{userLevelId} 闯关小结
|
||||
///
|
||||
/// 注意:getChapters Vue 端带 hideErrorToast,toast 由页面自行处理;
|
||||
/// 异常统一向上抛,页面 catch 后决定提示。
|
||||
/// ============================================================
|
||||
class LexiconApi {
|
||||
LexiconApi._();
|
||||
|
||||
static final LexiconApi instance = LexiconApi._();
|
||||
|
||||
/// 后端响应壳 {code, message, data} 统一取 data(用于裸值响应)
|
||||
dynamic _unwrap(dynamic data) =>
|
||||
data is Map<String, dynamic> && data['data'] != null
|
||||
? data['data']
|
||||
: data;
|
||||
|
||||
/// 章节列表(model 内部兼容响应壳)
|
||||
Future<ChapterResponse> getChapters(int bookId) async {
|
||||
final data = await DioClient.instance.get(
|
||||
'/api/v1/Lexicon/GetChapters/$bookId',
|
||||
);
|
||||
return ChapterResponse.fromJson(data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
/// 关卡列表
|
||||
Future<LevelResponse> getLevels(int chapterId) async {
|
||||
final data = await DioClient.instance.get(
|
||||
'/api/v1/Lexicon/GetLevels/$chapterId',
|
||||
);
|
||||
return LevelResponse.fromJson(data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
/// 开局取题
|
||||
Future<LevelStartInfo> levelStart(int levelId) async {
|
||||
final data = await DioClient.instance.post(
|
||||
'/api/v1/Lexicon/LevelStart/$levelId',
|
||||
);
|
||||
return LevelStartInfo.fromJson(data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
/// 提交答案(返回值与开局信息同构,但页面只关心成功与否)
|
||||
Future<bool> answerQuestion({
|
||||
required int levelId,
|
||||
required int userLevelId,
|
||||
required int lexiconId,
|
||||
required String answer,
|
||||
}) async {
|
||||
final data = await DioClient.instance.post(
|
||||
'/api/v1/Lexicon/Answer',
|
||||
data: {
|
||||
'levelId': levelId,
|
||||
'userLevelId': userLevelId,
|
||||
'lexiconId': lexiconId,
|
||||
'answer': answer,
|
||||
},
|
||||
);
|
||||
return data != null;
|
||||
}
|
||||
|
||||
/// 关卡是否完成(完关后后端有短暂处理延迟,需轮询)
|
||||
Future<bool> getLevelIsFinish(int levelId) async {
|
||||
final data = _unwrap(
|
||||
await DioClient.instance.get('/api/v1/Lexicon/GetLevelIsFinish/$levelId'),
|
||||
);
|
||||
return data == true;
|
||||
}
|
||||
|
||||
/// 闯关小结
|
||||
Future<LevelOverInfo> getLevelOverInfo(int userLevelId) async {
|
||||
final data = await DioClient.instance.get(
|
||||
'/api/v1/Lexicon/GetLevelOverInfo/$userLevelId',
|
||||
);
|
||||
return LevelOverInfo.fromJson(data as Map<String, dynamic>);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
/// ============================================================
|
||||
/// 【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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'package:new_learn/core/api/lexicon_book/model.dart';
|
||||
import 'package:new_learn/core/network/dio_client.dart';
|
||||
|
||||
/// ============================================================
|
||||
/// 【lexicon_book 模块接口】词书接口 —— 对标 Vue3 端 api/lexiconBook/index.ts
|
||||
///
|
||||
/// getCurrentBook → GET GetCurrentLexiconBooks 当前学习的词书
|
||||
/// getLearnDayCount→ GET GetLearnDayCount 坚持学习天数
|
||||
/// getAllBooks → GET GetAllLexiconBooks 我的书库
|
||||
/// startLearn → POST StartLearn/{bookId} 切换/开始学习某书
|
||||
///
|
||||
/// 说明:Vue 端这 4 个接口都带 hideErrorToast(静默失败),
|
||||
/// 异常不在此层吞,交页面按需要 catch 空处理。
|
||||
/// ============================================================
|
||||
class LexiconBookApi {
|
||||
LexiconBookApi._();
|
||||
|
||||
static final LexiconBookApi instance = LexiconBookApi._();
|
||||
|
||||
/// 后端响应壳 {code, message, data} 统一取 data
|
||||
dynamic _unwrap(dynamic data) =>
|
||||
data is Map<String, dynamic> && data['data'] != null
|
||||
? data['data']
|
||||
: data;
|
||||
|
||||
/// 当前学习的词书(未选择时后端可能返回 null)
|
||||
Future<BookInfo?> getCurrentBook() async {
|
||||
final data = _unwrap(
|
||||
await DioClient.instance.get('/api/v1/LexiconBook/GetCurrentLexiconBooks'),
|
||||
);
|
||||
if (data is Map<String, dynamic>) return BookInfo.fromJson(data);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 坚持学习天数
|
||||
Future<int> getLearnDayCount() async {
|
||||
final data = _unwrap(
|
||||
await DioClient.instance.get('/api/v1/LexiconBook/GetLearnDayCount'),
|
||||
);
|
||||
if (data is num) return data.toInt();
|
||||
if (data is String) return int.tryParse(data) ?? 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// 我的书库全部词书
|
||||
Future<List<BookInfo>> getAllBooks() async {
|
||||
final data = _unwrap(
|
||||
await DioClient.instance.get('/api/v1/LexiconBook/GetAllLexiconBooks'),
|
||||
);
|
||||
if (data is List) {
|
||||
return data
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(BookInfo.fromJson)
|
||||
.toList();
|
||||
}
|
||||
return const [];
|
||||
}
|
||||
|
||||
/// 切换 / 开始学习某书
|
||||
Future<bool> startLearn(int bookId) async {
|
||||
final data = _unwrap(
|
||||
await DioClient.instance.post(
|
||||
'/api/v1/LexiconBook/StartLearn/$bookId',
|
||||
),
|
||||
);
|
||||
return data == true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/// ============================================================
|
||||
/// 【lexicon_book 模块模型】词书实体 —— 对标 Vue3 端 api/lexiconBook/type.ts
|
||||
///
|
||||
/// BookInfo:词书(背诵首页当前书 / 教材列表我的书库)
|
||||
/// ============================================================
|
||||
library;
|
||||
|
||||
/// 兼容 number | String 的 id 安全转换(对标 TS 的 number | string)
|
||||
int _safeId(dynamic v) {
|
||||
if (v is num) return v.toInt();
|
||||
if (v is String) return int.tryParse(v) ?? 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// 词书信息
|
||||
class BookInfo {
|
||||
const BookInfo({
|
||||
this.id = 0,
|
||||
this.title = '',
|
||||
this.cover = '',
|
||||
this.poster = '',
|
||||
this.slogan = '',
|
||||
this.lexiconCount = 0,
|
||||
this.totalCount = 0,
|
||||
this.isStudying = false,
|
||||
});
|
||||
|
||||
final int id;
|
||||
final String title; // 书名
|
||||
final String cover; // 封面图
|
||||
final String poster; // 顶部海报图
|
||||
final String slogan; // 海报标语
|
||||
final int lexiconCount; // 已学词数
|
||||
final int totalCount; // 总词数
|
||||
final bool isStudying; // 是否学习中
|
||||
|
||||
/// 学习进度百分比(0~100,取整;totalCount 为 0 时兜底 0)
|
||||
int get progressPercent =>
|
||||
totalCount <= 0 ? 0 : (lexiconCount * 100 ~/ totalCount);
|
||||
|
||||
factory BookInfo.fromJson(Map<String, dynamic> json) {
|
||||
return BookInfo(
|
||||
id: _safeId(json['id']),
|
||||
title: json['title'] as String? ?? '',
|
||||
cover: json['cover'] as String? ?? '',
|
||||
poster: json['poster'] as String? ?? '',
|
||||
slogan: json['slogan'] as String? ?? '',
|
||||
lexiconCount: (json['lexiconCount'] as num?)?.toInt() ?? 0,
|
||||
totalCount: (json['totalCount'] as num?)?.toInt() ?? 0,
|
||||
isStudying: json['isStudying'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user