feat(recite): 完成背诵模块基础框架与核心页面开发
本次提交实现了背诵模块的完整基础链路: 1. 新增词书、背单词接口层与对应数据模型 2. 完成背诵首页、章节列表页等核心页面开发 3. 实现音频播放、加载弹窗等通用组件 4. 配置路由跳转链路,替换旧版缓存传参方案 5. 对齐Vue端业务逻辑与UI样式
This commit is contained in:
@@ -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