Merge branch 'master' into chenshuainan

This commit is contained in:
chenshuainan
2026-09-11 09:38:12 +08:00
31 changed files with 4526 additions and 170 deletions
+20
View File
@@ -137,6 +137,26 @@ class UserApi {
}
}
// ==================== 报告状态更新 ====================
/// 更新入门测试报告显示状态(看完后置回 false)
/// → POST /api/v1/User/ChangeShowReport
///
/// 对齐 Vue api/user/index.ts 的 UpdateReportStatus(),
/// 在 reportComputation finishedPopup 点"立即开启学习蝶变"后调用。
static Future<void> changeShowReport() async {
await DioClient.instance.post('/api/v1/User/ChangeShowReport');
}
/// 更新出门测试报告显示状态(看完后置回 false)
/// → POST /api/v1/User/ChangeShowAfterReport
///
/// 对齐 H5 壳子 api/mock/index.ts 的 updateAfterReportStatus(),
/// 在出门测 H5 壳子 navigateReport() 里由 Flutter JS Bridge 拦截后调用。
static Future<void> changeShowAfterReport() async {
await DioClient.instance.post('/api/v1/User/ChangeShowAfterReport');
}
/// MD5 加密(UniApp stringToMd5 的等价实现)
static String _md5(String text) {
return md5.convert(utf8.encode(text)).toString();
+5
View File
@@ -55,6 +55,7 @@ class UserInfo {
this.avatar = '',
this.phone = '',
this.isShowReport = false,
this.isShowAfterReport = false,
});
final int userId;
@@ -64,6 +65,9 @@ class UserInfo {
final String phone;
final bool isShowReport;
/// 登录响应里的出门测试报告标记(对齐 Vue api/login/type.ts 的 UserInfo)
final bool isShowAfterReport;
factory UserInfo.fromJson(Map<String, dynamic> json) {
return UserInfo(
// userId 可能是字符串(后端统一风格),不能直接 as num
@@ -73,6 +77,7 @@ class UserInfo {
avatar: json['avatar'] as String? ?? '',
phone: json['phone'] as String? ?? '',
isShowReport: json['isShowReport'] as bool? ?? false,
isShowAfterReport: json['isShowAfterReport'] as bool? ?? false,
);
}
}
+51
View File
@@ -0,0 +1,51 @@
import 'package:new_learn/core/api/exam/model.dart';
import 'package:new_learn/core/network/dio_client.dart';
/// ============================================================
/// 【exam 模块接口】我的考试 —— 对标 Vue3 端 api/exam/index.ts
///
/// getSubjects —— 获取考试学科筛选 Tab 列表
/// search —— 我的考试分页列表
///
/// 注意:考试报告详情(getReport)等考试做题相关接口,
/// 后续扩展时再按模块拆分补进来。
/// ============================================================
class ExamApi {
ExamApi._();
static final ExamApi instance = ExamApi._();
/// 考试学科筛选 Tab 列表
/// GET /api/v1/Evaluation/GetSubjects
Future<List<ExamSubjectTab>> getSubjects() async {
final data = await DioClient.instance
.get('/api/v1/Evaluation/GetSubjects');
final list = data is List
? data.whereType<Map<String, dynamic>>().toList()
: (data is Map<String, dynamic>
? (data['data'] as List<dynamic>? ?? const [])
: const [])
.whereType<Map<String, dynamic>>()
.toList();
return list.map(ExamSubjectTab.fromJson).toList();
}
/// 我的考试分页列表
/// GET /api/v1/Evaluation/Search
/// 参数:baseSubjectId(可 null=全部)/ pageIndex / pageSize
Future<ExamResponse> search({
int? baseSubjectId,
required int pageIndex,
int pageSize = 10,
}) async {
final data = await DioClient.instance.get(
'/api/v1/Evaluation/Search',
query: {
if (baseSubjectId != null) 'baseSubjectId': baseSubjectId,
'pageIndex': pageIndex,
'pageSize': pageSize,
},
);
return ExamResponse.fromJson(data as Map<String, dynamic>);
}
}
+109
View File
@@ -0,0 +1,109 @@
/// ============================================================
/// 【exam 模块模型】我的考试 —— 对标 Vue3 端 api/exam/type.ts
///
/// ExamDto —— 单条考试记录
/// ExamSubjectTab —— 考试科目筛选 Tab
/// ExamResponse —— 考试列表分页响应
///
/// 序列化:手动 fromJson,与项目其他模块风格一致,不依赖 build_runner。
///
/// ⚠ 注意:后端返回的 id 字段类型不统一(数字 or 字符串),
/// 所有数值字段统一用 _asInt() 做兼容解析,避免类型转换异常。
/// ============================================================
library;
int? _optInt(dynamic v) {
if (v == null) return null;
if (v is int) return v;
if (v is double) return v.toInt();
final s = v.toString();
final n = int.tryParse(s);
if (n != null) return n;
return double.tryParse(s)?.toInt();
}
int _asInt(dynamic v, [int fallback = 0]) => _optInt(v) ?? fallback;
/// 单条考试记录
class ExamDto {
const ExamDto({
this.id = 0,
this.taskId = 0,
this.taskName = '',
this.baseSubjectId = 0,
this.baseSubjectName = '',
this.startTime = '',
this.endTime = '',
this.totalScore = 0,
this.userScore = 0,
});
final int id;
final int taskId;
final String taskName;
final int baseSubjectId;
final String baseSubjectName;
final String startTime;
final String endTime;
final int totalScore;
final int userScore;
factory ExamDto.fromJson(Map<String, dynamic> json) {
return ExamDto(
id: _asInt(json['id']),
taskId: _asInt(json['taskId']),
taskName: (json['taskName'] as String?) ?? '',
baseSubjectId: _asInt(json['baseSubjectId']),
baseSubjectName: (json['baseSubjectName'] as String?) ?? '',
startTime: (json['startTime'] as String?) ?? '',
endTime: (json['endTime'] as String?) ?? '',
totalScore: _asInt(json['totalScore']),
userScore: _asInt(json['userScore']),
);
}
}
/// 考试科目筛选 Tab
class ExamSubjectTab {
const ExamSubjectTab({this.id, this.name = ''});
/// 为 null 表示"全部"
final int? id;
final String name;
factory ExamSubjectTab.fromJson(Map<String, dynamic> json) {
return ExamSubjectTab(
id: _optInt(json['id']),
name: (json['name'] as String?) ?? '',
);
}
}
/// 考试列表分页响应
class ExamResponse {
const ExamResponse({
this.data = const [],
this.total = 0,
this.pageIndex = 1,
this.pageSize = 10,
});
final List<ExamDto> data;
final int total;
final int pageIndex;
final int pageSize;
factory ExamResponse.fromJson(Map<String, dynamic> json) {
final payload = (json['data'] as Map<String, dynamic>?) ?? json;
final list = (payload['data'] as List<dynamic>? ?? const [])
.whereType<Map<String, dynamic>>()
.map(ExamDto.fromJson)
.toList();
return ExamResponse(
data: list,
total: (payload['total'] as num?)?.toInt() ?? list.length,
pageIndex: (payload['pageIndex'] as num?)?.toInt() ?? 1,
pageSize: (payload['pageSize'] as num?)?.toInt() ?? 10,
);
}
}
+46
View File
@@ -0,0 +1,46 @@
import 'package:new_learn/core/api/report/model.dart';
import 'package:new_learn/core/network/dio_client.dart';
/// ============================================================
/// 【report 模块接口】入门测/出门测诊断报告 —— 对标 Vue3 端 api/report/index.ts
///
/// 注意:这个模块是"诊断报告包"那条链路的接口(studentInfo / reportComputation 页用),
/// 与 core/api/my_report(历史报告列表)不是同一个业务模块。
///
/// getCurrentReport() → 入门测报告(placementTest)
/// getCurrentAfterReport() → 出门测报告(exitTest)
/// getCurrentReportKnow() → 当前用户未掌握知识点(reportComputation AI 动画页用)
/// ============================================================
class ReportApi {
ReportApi._();
static final ReportApi instance = ReportApi._();
/// 获取当前用户入门测试报告 → GET /api/v1/UserReport/GetCurrentReport
Future<UserReportType> getCurrentReport() async {
final data = await DioClient.instance.get(
'/api/v1/UserReport/GetCurrentReport',
);
return UserReportType.fromJson(data as Map<String, dynamic>);
}
/// 获取当前用户出门测试报告 → GET /api/v1/UserReport/GetCurrentAfterReport
Future<UserReportType> getCurrentAfterReport() async {
final data = await DioClient.instance.get(
'/api/v1/UserReport/GetCurrentAfterReport',
);
return UserReportType.fromJson(data as Map<String, dynamic>);
}
/// 获取当前用户未掌握的知识点 → GET /api/v1/UserReport/GetCurrentReportKnow
Future<List<UserReportKnowType>> getCurrentReportKnow() async {
final data = await DioClient.instance.get(
'/api/v1/UserReport/GetCurrentReportKnow',
);
final list = data is List ? data : (data['data'] as List<dynamic>? ?? const []);
return list
.whereType<Map<String, dynamic>>()
.map(UserReportKnowType.fromJson)
.toList();
}
}
+80
View File
@@ -0,0 +1,80 @@
/// ============================================================
/// 【report 模块模型】入门测/出门测报告 —— 对标 Vue3 端 api/report/type.ts
///
/// 注意:这个模块是"诊断报告包"(报告状态流转那条链路用的),
/// 与 core/api/my_report(历史报告列表)不是同一个业务模块。
///
/// SubjectReportType → 单科目报告(科目名/图标/知识点数/报告URL)
/// UserReportType → 当前用户诊断报告整体(用户名/能力等级/科目列表)
/// UserReportKnowType → 当前用户未掌握的知识点(reportComputation 页用)
///
/// 序列化:手动 fromJson,与项目其他模块风格一致,不依赖 build_runner。
/// ============================================================
library;
/// 单科目报告信息
class SubjectReportType {
const SubjectReportType({
this.subjectName = '',
this.url = '',
this.icon = '',
this.knowledgePoint = 0,
});
final String subjectName;
final String url;
final String icon;
final int knowledgePoint;
factory SubjectReportType.fromJson(Map<String, dynamic> json) {
return SubjectReportType(
subjectName: (json['subjectName'] as String?) ?? '',
url: (json['url'] as String?) ?? '',
icon: (json['icon'] as String?) ?? '',
knowledgePoint: (json['knowledgePoint'] as num?)?.toInt() ?? 0,
);
}
}
/// 当前用户诊断报告整体
class UserReportType {
const UserReportType({
this.userName = '',
this.userLevel = '',
this.subjectReports = const [],
});
final String userName;
final String userLevel;
final List<SubjectReportType> subjectReports;
factory UserReportType.fromJson(Map<String, dynamic> json) {
final payload = (json['data'] as Map<String, dynamic>?) ?? json;
return UserReportType(
userName: (payload['userName'] as String?) ?? '',
userLevel: (payload['userLevel'] as String?) ?? '',
subjectReports: (payload['subjectReports'] as List<dynamic>? ?? const [])
.whereType<Map<String, dynamic>>()
.map(SubjectReportType.fromJson)
.toList(),
);
}
}
/// 当前用户未掌握的知识点(reportComputation AI加载动画页用)
class UserReportKnowType {
const UserReportKnowType({
this.subjectName = '',
this.knowCount = 0,
});
final String subjectName;
final int knowCount;
factory UserReportKnowType.fromJson(Map<String, dynamic> json) {
return UserReportKnowType(
subjectName: (json['subjectName'] as String?) ?? '',
knowCount: (json['knowCount'] as num?)?.toInt() ?? 0,
);
}
}
+66
View File
@@ -0,0 +1,66 @@
import 'package:new_learn/core/api/user_collect/model.dart';
import 'package:new_learn/core/network/dio_client.dart';
/// ============================================================
/// 【user_collect 模块接口】我的收藏 + 错题本 —— 对标 Vue3 端 api/userCollect/index.ts
///
/// getSubjects —— 学科筛选项(顶部 Tab)
/// GET /api/v1/UserCollect/GetUserCollectSubject/{type}
///
/// getCollectList —— 收藏/错题分页列表(游标分页)
/// GET /api/v1/UserCollect/GetUserCollect
/// 参数:type(1收藏/2错题) / subjectId(可选) / pageIndex / pageSize / prevPageId
///
/// 游标分页说明(对齐 Vue useList):
/// 第一页 prevPageId=0,之后每页传上一页最后一条的 id;
/// 切学科/下拉刷新时 prevPageId 重置为 0。
/// ============================================================
class UserCollectApi {
UserCollectApi._();
static final UserCollectApi instance = UserCollectApi._();
/// 学科筛选项([type] 1=我的收藏 2=错题本)
Future<List<CollectTab>> getSubjects(int type) async {
final data = await DioClient.instance.get(
'/api/v1/UserCollect/GetUserCollectSubject/$type',
);
// 兼容裸数组 / {data: [...]} 响应壳
final List<dynamic> list = data is List
? data
: (data is Map<String, dynamic>
? (data['data'] as List<dynamic>? ?? const [])
: const []);
return list
.whereType<Map<String, dynamic>>()
.map(CollectTab.fromJson)
.toList();
}
/// 收藏/错题分页列表
///
/// [type] 1=我的收藏 2=错题本;[subjectId] 不传 = 全部学科;
/// [prevPageId] 游标:首页传 0,翻页传上一页最后一条 id。
Future<UserCollectPage> getCollectList({
required int type,
int? subjectId,
required int pageIndex,
int pageSize = 10,
int prevPageId = 0,
}) async {
final query = <String, dynamic>{
'type': type,
'pageIndex': pageIndex,
'pageSize': pageSize,
'prevPageId': prevPageId,
};
// 对齐 Vue useList:subjectId 为 null 时不带该参数
if (subjectId != null) query['subjectId'] = subjectId;
final data = await DioClient.instance.get(
'/api/v1/UserCollect/GetUserCollect',
query: query,
);
return UserCollectPage.fromJson(data as Map<String, dynamic>);
}
}
+124
View File
@@ -0,0 +1,124 @@
/// ============================================================
/// 【user_collect 模块模型】我的收藏 + 错题本 —— 对标 Vue3 端 api/userCollect/type.ts
///
/// CollectTab 学科筛选 Tab(GetUserCollectSubject 返回项)
/// UserCollectItem 列表项(收藏/错题通用,靠 type 区分)
/// UserCollectPage 分页响应(游标分页:pageIndex + prevPageId)
///
/// 说明:错题本页(myWrong)与收藏页(myCollect)共用同一套接口,
/// type=1 我的收藏 / type=2 错题本。
/// 序列化:手动 fromJson(兼容 {code, message, data: {...}} 响应壳),
/// 不依赖 build_runner。
/// ============================================================
library;
/// 兼容后端 id 字段:雪花 id 后端可能以 String 返回(防 JS 精度丢失),
/// 也可能直接是 num;统一转 int,无法解析返回 null。
int? _toInt(dynamic v) {
if (v == null) return null;
if (v is num) return v.toInt();
if (v is String && v.isNotEmpty) return int.tryParse(v);
return null;
}
/// 学科筛选 Tab
class CollectTab {
const CollectTab({
this.id,
required this.name,
this.remark = '',
this.status = 0,
});
/// 学科 id;null = 客户端插入的"全部"
final int? id;
final String name;
final String remark;
final int status;
factory CollectTab.fromJson(Map<String, dynamic> json) {
return CollectTab(
id: _toInt(json['id']),
name: json['name'] as String? ?? '',
remark: json['remark'] as String? ?? '',
status: _toInt(json['status']) ?? 0,
);
}
}
/// 收藏/错题列表项
class UserCollectItem {
const UserCollectItem({
this.id = 0,
this.questionId = 0,
this.recordQuestionId = 0,
this.questionStem = '',
this.subjectName = '',
this.packName = '',
this.createTime = '',
});
/// 列表明细 id(游标分页 prevPageId 取本页最后一条的 id)
final int id;
/// 题目 id(详情页参数 questionId)
final int questionId;
/// 答题记录 id(详情页参数 recordQuestionId)
final int recordQuestionId;
/// 题干 HTML(RichTextContent 渲染)
final String questionStem;
/// 学科名(SubjectTag 配色用)
final String subjectName;
/// 来源试卷/礼包名
final String packName;
/// 收藏/错题时间字符串
final String createTime;
factory UserCollectItem.fromJson(Map<String, dynamic> json) {
return UserCollectItem(
id: _toInt(json['id']) ?? 0,
questionId: _toInt(json['questionId']) ?? 0,
recordQuestionId: _toInt(json['recordQuestionId']) ?? 0,
questionStem: json['questionStem'] as String? ?? '',
subjectName: json['subjectName'] as String? ?? '',
packName: json['packName'] as String? ?? '',
createTime: json['createTime'] as String? ?? '',
);
}
}
/// 收藏/错题分页响应
class UserCollectPage {
const UserCollectPage({
this.data = const [],
this.total = 0,
this.pageIndex = 1,
this.pageSize = 10,
});
final List<UserCollectItem> data; // 当前页数据
final int total; // 总条数(顶部"X 道错题/收藏题目")
final int pageIndex;
final int pageSize;
/// 兼容业务响应壳 {code, message, data: {...}}:data 存在就取 data
factory UserCollectPage.fromJson(Map<String, dynamic> json) {
final payload = (json['data'] is Map<String, dynamic>)
? json['data'] as Map<String, dynamic>
: json;
return UserCollectPage(
data: (payload['data'] as List<dynamic>? ?? const [])
.whereType<Map<String, dynamic>>()
.map(UserCollectItem.fromJson)
.toList(),
total: _toInt(payload['total']) ?? 0,
pageIndex: _toInt(payload['pageIndex']) ?? 1,
pageSize: _toInt(payload['pageSize']) ?? 10,
);
}
}
+67
View File
@@ -0,0 +1,67 @@
import 'package:new_learn/core/api/user_collect/model.dart';
import 'package:new_learn/core/api/user_note/model.dart';
import 'package:new_learn/core/network/dio_client.dart';
/// ============================================================
/// 【user_note 模块接口】我的笔记 —— 对标 Vue3 端 api/userNote/index.ts
///
/// getSubjects —— 学科筛选项(顶部 Tab,无参)
/// GET /api/v1/UserNote/GetUserCollectSubject
///
/// getNoteList —— 笔记分页列表(游标分页)
/// GET /api/v1/UserNote/GetUserNotes
/// 参数:subjectId(可选) / pageIndex / pageSize / prevPageId
///
/// 列表不按笔记类型过滤;type(1题目/2讲义) 由每条数据自带,
/// 前端用卡片角标图标区分。
///
/// 注意:Vue 同文件里的 OSS 凭证/上传地址接口归属 OSS 模块
/// (Flutter 端见 core/network/oss_api.dart),不在此重复封装。
/// ============================================================
class UserNoteApi {
UserNoteApi._();
static final UserNoteApi instance = UserNoteApi._();
/// 学科筛选项
Future<List<CollectTab>> getSubjects() async {
final data = await DioClient.instance.get(
'/api/v1/UserNote/GetUserCollectSubject',
);
// 兼容裸数组 / {data: [...]} 响应壳
final List<dynamic> list = data is List
? data
: (data is Map<String, dynamic>
? (data['data'] as List<dynamic>? ?? const [])
: const []);
return list
.whereType<Map<String, dynamic>>()
.map(CollectTab.fromJson)
.toList();
}
/// 笔记分页列表
///
/// [subjectId] 不传 = 全部学科;[prevPageId] 游标:首页传 0,
/// 翻页传上一页最后一条 id。
Future<UserNotePage> getNoteList({
int? subjectId,
required int pageIndex,
int pageSize = 10,
int prevPageId = 0,
}) async {
final query = <String, dynamic>{
'pageIndex': pageIndex,
'pageSize': pageSize,
'prevPageId': prevPageId,
};
// 对齐 Vue useList:subjectId 为 null 时不带该参数
if (subjectId != null) query['subjectId'] = subjectId;
final data = await DioClient.instance.get(
'/api/v1/UserNote/GetUserNotes',
query: query,
);
return UserNotePage.fromJson(data as Map<String, dynamic>);
}
}
+90
View File
@@ -0,0 +1,90 @@
/// ============================================================
/// 【user_note 模块模型】我的笔记 —— 对标 Vue3 端 api/userNote/type.ts
///
/// UserNoteItem 笔记列表项(type 1=题目笔记 2=讲义笔记)
/// UserNotePage 分页响应(游标分页:pageIndex + prevPageId)
///
/// 学科筛选 Tab 复用收藏模块的 CollectTab(见 api.dart 的 import,
/// 与 Vue 端 api/userNote/index.ts 从 ../userCollect/type 引入一致)。
/// ============================================================
library;
/// 兼容后端 id 字段:雪花 id 后端可能以 String 返回(防 JS 精度丢失)
int? _noteToInt(dynamic v) {
if (v == null) return null;
if (v is num) return v.toInt();
if (v is String && v.isNotEmpty) return int.tryParse(v);
return null;
}
/// 我的笔记列表项
class UserNoteItem {
const UserNoteItem({
this.id = 0,
this.subjectName = '',
this.type = 0,
this.title = '',
this.content = '',
this.createTime = '',
});
/// 笔记 id(游标分页 prevPageId 取本页最后一条的 id;详情页参数 id)
final int id;
/// 学科名(SubjectTag 配色用)
final String subjectName;
/// 笔记类型:1=题目笔记 2=讲义笔记(卡片角标/详情页区分用)
final int type;
/// 笔记标题
final String title;
/// 笔记内容 HTML(RichTextContent 渲染)
final String content;
/// 记笔记时间字符串
final String createTime;
factory UserNoteItem.fromJson(Map<String, dynamic> json) {
return UserNoteItem(
id: _noteToInt(json['id']) ?? 0,
subjectName: json['subjectName'] as String? ?? '',
type: _noteToInt(json['type']) ?? 0,
title: json['title'] as String? ?? '',
content: json['content'] as String? ?? '',
createTime: json['createTime'] as String? ?? '',
);
}
}
/// 我的笔记分页响应
class UserNotePage {
const UserNotePage({
this.data = const [],
this.total = 0,
this.pageIndex = 1,
this.pageSize = 10,
});
final List<UserNoteItem> data;
final int total;
final int pageIndex;
final int pageSize;
/// 兼容业务响应壳 {code, message, data: {...}}:data 存在就取 data
factory UserNotePage.fromJson(Map<String, dynamic> json) {
final payload = (json['data'] is Map<String, dynamic>)
? json['data'] as Map<String, dynamic>
: json;
return UserNotePage(
data: (payload['data'] as List<dynamic>? ?? const [])
.whereType<Map<String, dynamic>>()
.map(UserNoteItem.fromJson)
.toList(),
total: _noteToInt(payload['total']) ?? 0,
pageIndex: _noteToInt(payload['pageIndex']) ?? 1,
pageSize: _noteToInt(payload['pageSize']) ?? 10,
);
}
}