feat(my): add 我的考试 page and related modules
1. 新增我的考试页面,包含学科筛选Tab和分页考试列表 2. 新增学科图标工具类,替换我的报告页重复的学科图标逻辑 3. 在我的页面菜单中添加我的考试入口 4. 新增exam模块的API接口和数据模型 5. 优化我的报告页面卡片样式,统一与考试卡片的布局
This commit is contained in:
@@ -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>);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import 'package:new_learn/features/conquerPackage/captureList/index.dart';
|
||||
import 'package:new_learn/features/my/my_point/my_point_page.dart';
|
||||
import 'package:new_learn/features/my/my_report/my_report_page.dart';
|
||||
import 'package:new_learn/features/my/my_report/report_detail_page.dart';
|
||||
import 'package:new_learn/features/my/exam/exam_page.dart';
|
||||
import 'package:new_learn/features/my/settings/settings_page.dart';
|
||||
import 'package:new_learn/features/my/settings/about_app_page.dart';
|
||||
import 'package:new_learn/features/recite/recite_page.dart';
|
||||
@@ -138,6 +139,7 @@ final GoRouter appRouter = GoRouter(
|
||||
),
|
||||
GoRoute(path: '/myPoint', builder: (_, _) => const MyPointPage()),
|
||||
GoRoute(path: '/myReport', builder: (_, _) => const MyReportPage()),
|
||||
GoRoute(path: '/myExam', builder: (_, _) => const ExamPage()),
|
||||
GoRoute(
|
||||
path: '/report-detail',
|
||||
builder: (_, state) {
|
||||
@@ -185,8 +187,7 @@ final GoRouter appRouter = GoRouter(
|
||||
// ---------- 攻克包 - 截图名单 ----------
|
||||
GoRoute(
|
||||
path: '/conquerPackage/captureList',
|
||||
builder: (_, state) =>
|
||||
CaptureListPage(courseId: state.extra as String),
|
||||
builder: (_, state) => CaptureListPage(courseId: state.extra as String),
|
||||
),
|
||||
GoRoute(path: '/register', builder: (_, _) => const RegisterPage()),
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import 'package:new_learn/core/utils/img_url.dart';
|
||||
|
||||
/// ============================================================
|
||||
/// 【学科图标常量】
|
||||
///
|
||||
/// 对标 Vue utils/constant.js 的 SUBJECT_ICON_MAP。
|
||||
/// 我的报告(my_report)、我的考试(exam)等模块共用,
|
||||
/// 通过学科名(如「数学」「英语」)动态取对应图标。
|
||||
/// ============================================================
|
||||
|
||||
/// 学科名 → 图标资源路径
|
||||
const Map<String, String> kSubjectIconMap = {
|
||||
'语文': '/StaticResource/2026-06-11/shuxue1781163770612.png',
|
||||
'化学': '/StaticResource/2026-06-11/huaxue1781163828443.png',
|
||||
'英语': '/StaticResource/2026-06-11/yingyu1781163725441.png',
|
||||
'物理': '/StaticResource/2026-06-11/wuli1781148438346.png',
|
||||
'数学': '/StaticResource/2026-06-11/shuxue1781163770612.png',
|
||||
'生物': '/StaticResource/2026-06-11/shengwu1781163791768.png',
|
||||
'历史': '/StaticResource/2026-06-11/lishi1781163811822.png',
|
||||
'政治': '/StaticResource/2026-06-11/zhengzhi1781163750422.png',
|
||||
'地理': '/StaticResource/2026-06-11/dili1781163844311.png',
|
||||
};
|
||||
|
||||
/// 取学科图标完整 URL(无匹配时用数学兜底,对标 Vue getSubjectIcon)
|
||||
String subjectIconUrl(String subjectName) {
|
||||
final path = kSubjectIconMap[subjectName] ?? kSubjectIconMap['数学']!;
|
||||
return ImgUrl.build(path);
|
||||
}
|
||||
Reference in New Issue
Block a user