Merge branch 'master' into chenshuainan
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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>);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
+7
-2
@@ -9,6 +9,7 @@ import 'package:new_learn/core/config/app_config.dart';
|
||||
import 'package:new_learn/core/logger/app_talker.dart';
|
||||
import 'package:new_learn/core/providers/network_status_provider.dart';
|
||||
import 'package:new_learn/core/providers/privacy_provider.dart';
|
||||
import 'package:new_learn/core/report/report_redirect.dart';
|
||||
import 'package:new_learn/core/router/app_router.dart';
|
||||
import 'package:new_learn/core/services/local_notification_service.dart';
|
||||
import 'package:new_learn/core/utils/screen_adapt_template.dart'; // 复用 AppAdapt 适配配置
|
||||
@@ -89,8 +90,12 @@ class TabApp extends StatelessWidget {
|
||||
// 这样冷启动时不会先看到登录页/主页闪一下,再被路由重定向到隐私页,
|
||||
// 而是直接由全屏拦截页先接管。
|
||||
builder: (context, child) {
|
||||
final content =
|
||||
PrivacyStartupGate(child: child ?? const SizedBox.shrink());
|
||||
// ReportRedirectNotifier 必须包在 MaterialApp 里面(能拿到 GoRouterState)
|
||||
// 但在 PrivacyStartupGate 外层(隐私同意后的页面才能拦截)
|
||||
final withRedirect = ReportRedirectNotifier(
|
||||
child: child ?? const SizedBox.shrink(),
|
||||
);
|
||||
final content = PrivacyStartupGate(child: withRedirect);
|
||||
final bootstrapped = _LocalNotificationBootstrapper(child: content);
|
||||
if (kReleaseMode) return bootstrapped;
|
||||
return TalkerWrapper(
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
/// ============================================================
|
||||
/// 【SubjectTag】学科标签(对标 Vue components/SubjectTag/SubjectTag.vue)
|
||||
///
|
||||
/// 四学科配色(与 Vue subjectMap 一致),未知学科兜底数学蓝:
|
||||
/// 数学 #3D7DF5 / #E5F0FF 物理 #A45CFF / #F5ECFF
|
||||
/// 英语 #E5A301 / #FFF6E3 化学 #2DB985 / #E5FAF3
|
||||
///
|
||||
/// 描边:Vue 用 ::before + mask-composite 画 2rpx 的学科色渐变描边
|
||||
/// (两端 60% 学科色、中间 60% 近白色);Flutter 用"外层渐变容器 +
|
||||
/// 内层底色容器留 1px 边"的嵌套方案等价实现。
|
||||
///
|
||||
/// 原 class 模块私有组件,因错题本/我的收藏列表也需要,按项目规范
|
||||
/// 提升为 core/components 全局公共组件。
|
||||
/// ============================================================
|
||||
class SubjectTag extends StatelessWidget {
|
||||
const SubjectTag({super.key, required this.subject});
|
||||
|
||||
final String subject;
|
||||
|
||||
static const Map<String, _SubjectColor> _map = {
|
||||
// 数学:描边 rgba(90,158,255,.6) → rgba(242,248,255,.6)
|
||||
'数学': _SubjectColor(0xFF3D7DF5, 0xFFE5F0FF, 0x995A9EFF, 0x99F2F8FF),
|
||||
// 物理:描边 rgba(182,127,255,.6) → rgba(252,246,255,.6)
|
||||
'物理': _SubjectColor(0xFFA45CFF, 0xFFF5ECFF, 0x99B67FFF, 0x99FCF6FF),
|
||||
// 英语:描边 rgba(247,194,82,.6) → rgba(255,251,243,.6)
|
||||
'英语': _SubjectColor(0xFFE5A301, 0xFFFFF6E3, 0x99F7C252, 0x99FFFBF3),
|
||||
// 化学:描边 rgba(91,209,165,.6) → rgba(240,255,248,.6)
|
||||
'化学': _SubjectColor(0xFF2DB985, 0xFFE5FAF3, 0x995BD1A5, 0x99F0FFF8),
|
||||
};
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = subject.trim();
|
||||
if (t.isEmpty) return const SizedBox.shrink();
|
||||
final c =
|
||||
_map[t] ??
|
||||
const _SubjectColor(0xFF3D7DF5, 0xFFE5F0FF, 0x995A9EFF, 0x99F2F8FF);
|
||||
|
||||
// Vue border-radius: 12rpx
|
||||
final outerRadius = BorderRadius.circular(6.r);
|
||||
// Vue ::before padding: 2rpx(描边厚度)
|
||||
final borderWidth = 1.w;
|
||||
|
||||
return Container(
|
||||
// 外层:学科色横向渐变,露出的 1px 边即"渐变描边"
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [Color(c.borderA), Color(c.borderB), Color(c.borderA)],
|
||||
),
|
||||
borderRadius: outerRadius,
|
||||
),
|
||||
child: Container(
|
||||
margin: EdgeInsets.all(borderWidth),
|
||||
// Vue padding: 4rpx 14rpx,扣除 2rpx 描边后内层 2rpx 12rpx
|
||||
padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 1.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Color(c.bg),
|
||||
borderRadius: BorderRadius.circular(6.r - borderWidth),
|
||||
),
|
||||
// Vue white-space: nowrap:学科名不换行
|
||||
child: Text(
|
||||
t,
|
||||
softWrap: false,
|
||||
style: TextStyle(fontSize: 11, color: Color(c.fg)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SubjectColor {
|
||||
const _SubjectColor(this.fg, this.bg, this.borderA, this.borderB);
|
||||
|
||||
final int fg; // 文字色
|
||||
final int bg; // 底色
|
||||
final int borderA; // 渐变描边两端色(60% 学科色)
|
||||
final int borderB; // 渐变描边中间色(60% 近白)
|
||||
}
|
||||
@@ -30,6 +30,7 @@ class CurrentUserExtension {
|
||||
this.userAvatarReason = '',
|
||||
this.isShowGuide = false,
|
||||
this.isShowReport = false,
|
||||
this.isShowAfterReport = false,
|
||||
this.id = 0,
|
||||
});
|
||||
|
||||
@@ -48,6 +49,9 @@ class CurrentUserExtension {
|
||||
final String userAvatarReason;
|
||||
final bool isShowGuide;
|
||||
final bool isShowReport;
|
||||
|
||||
/// 是否显示出门测试报告(默认 false,后端置 true 后,用户完成报告查看后置回 false)
|
||||
final bool isShowAfterReport;
|
||||
final int id;
|
||||
|
||||
/// JSON → 对象(服务器响应)
|
||||
@@ -74,6 +78,7 @@ class CurrentUserExtension {
|
||||
userAvatarReason: payload['userAvatarReason'] as String? ?? '',
|
||||
isShowGuide: payload['isShowGuide'] as bool? ?? false,
|
||||
isShowReport: payload['isShowReport'] as bool? ?? false,
|
||||
isShowAfterReport: payload['isShowAfterReport'] as bool? ?? false,
|
||||
id: _toInt(payload['id']),
|
||||
);
|
||||
}
|
||||
@@ -96,12 +101,17 @@ class CurrentUserExtension {
|
||||
'userAvatarReason': userAvatarReason,
|
||||
'isShowGuide': isShowGuide,
|
||||
'isShowReport': isShowReport,
|
||||
'isShowAfterReport': isShowAfterReport,
|
||||
'id': id,
|
||||
};
|
||||
}
|
||||
|
||||
/// 局部更新(目前只用得上 avatar:头像上传成功后替换 URL)
|
||||
CurrentUserExtension copyWith({String? avatar}) {
|
||||
/// 局部更新(目前只用得上 avatar / isShowReport / isShowAfterReport)
|
||||
CurrentUserExtension copyWith({
|
||||
String? avatar,
|
||||
bool? isShowReport,
|
||||
bool? isShowAfterReport,
|
||||
}) {
|
||||
return CurrentUserExtension(
|
||||
userId: userId,
|
||||
userName: userName,
|
||||
@@ -117,7 +127,8 @@ class CurrentUserExtension {
|
||||
userAvatarStatus: userAvatarStatus,
|
||||
userAvatarReason: userAvatarReason,
|
||||
isShowGuide: isShowGuide,
|
||||
isShowReport: isShowReport,
|
||||
isShowReport: isShowReport ?? this.isShowReport,
|
||||
isShowAfterReport: isShowAfterReport ?? this.isShowAfterReport,
|
||||
id: id,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:new_learn/core/api/auth/api.dart';
|
||||
import 'package:new_learn/core/models/user_model.dart';
|
||||
import 'package:new_learn/core/storage/user_storage.dart';
|
||||
|
||||
@@ -86,6 +87,49 @@ class CurrentUserNotifier extends AsyncNotifier<CurrentUserExtension?> {
|
||||
// 也还是最新头像。
|
||||
await UserStorage.instance.saveUserInfo(updated);
|
||||
}
|
||||
|
||||
/// 强制从服务器拉最新用户信息(对齐 Vue App.vue 的 fetchCurrentUserExtension(true))
|
||||
///
|
||||
/// 使用场景:
|
||||
/// 1. App 回前台(WidgetsBindingObserver resumed):拿最新 isShowReport / isShowAfterReport;
|
||||
/// 2. 报告页调完 ChangeShowReport / ChangeShowAfterReport 后:刷新全局状态;
|
||||
/// 3. 登录成功后:确保后端最新数据。
|
||||
Future<CurrentUserExtension?> refresh() async {
|
||||
try {
|
||||
final ext = await UserApi.getCurrentUserExtension();
|
||||
state = AsyncData(ext);
|
||||
await UserStorage.instance.saveUserInfo(ext);
|
||||
return ext;
|
||||
} catch (_) {
|
||||
// 网络失败:保持现有状态,不要清空(可能本来就是 AsyncData)
|
||||
return state.value;
|
||||
}
|
||||
}
|
||||
|
||||
/// 只改本地报告标记为 false(studentInfo subjectReports 为空时降级处理)
|
||||
///
|
||||
/// 对应 Vue studentInfo.vue 的:
|
||||
/// userInfo.isShowReport = false; uni.setStorageSync('userInfo', userInfo);
|
||||
/// app.setUserInfo(Object.assign({}, app.globalData.userInfo, { isShowReport: false }));
|
||||
///
|
||||
/// 注意:不走后端接口!因为 subjectReports 为空意味着后端可能也没报告数据,
|
||||
/// 直接本地置 false 避免拦截器下次又打回来。
|
||||
Future<void> markShowReportFalse() async {
|
||||
final current = state.value;
|
||||
if (current == null) return;
|
||||
final updated = current.copyWith(isShowReport: false);
|
||||
state = AsyncData(updated);
|
||||
await UserStorage.instance.saveUserInfo(updated);
|
||||
}
|
||||
|
||||
/// 只改本地出门测试报告标记为 false(降级场景用)
|
||||
Future<void> markShowAfterReportFalse() async {
|
||||
final current = state.value;
|
||||
if (current == null) return;
|
||||
final updated = current.copyWith(isShowAfterReport: false);
|
||||
state = AsyncData(updated);
|
||||
await UserStorage.instance.saveUserInfo(updated);
|
||||
}
|
||||
}
|
||||
|
||||
/// 登录态变化信号(给 go_router 的 refreshListenable 用)
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:new_learn/core/logger/app_talker.dart';
|
||||
import 'package:new_learn/core/network/dio_client.dart';
|
||||
import 'package:new_learn/core/providers/user_provider.dart';
|
||||
|
||||
/// ============================================================
|
||||
/// 【报告状态全局拦截 + App 生命周期监听】
|
||||
///
|
||||
/// 对标 Vue App.vue 的 checkAndRedirectToReport() + onShow force 拉 userInfo。
|
||||
///
|
||||
/// 一、拦截时机(两处):
|
||||
/// 1. userProvider 状态变化(set() / refresh())→ _checkAndRedirect()
|
||||
/// :登录成功 / refresh 后都自动检查;
|
||||
/// 2. App 回前台(WidgetsBindingObserver.resumed)→ force refresh() → 自动检查
|
||||
/// :从胶囊面板/后台回来时拉最新 isShowReport;
|
||||
///
|
||||
/// 二、拦截条件(全部满足才跳):
|
||||
/// ✅ 有登录态(DioClient.accessToken != null)
|
||||
/// ✅ isShowReport=true 或 isShowAfterReport=true
|
||||
/// ✅ 当前路由在 tab 页面(动态判断,不硬编码列表)
|
||||
/// —— go_router 的 StatefulShellRoute.indexedStack 内的路由就是 tab 页
|
||||
///
|
||||
/// 三、动态判断 tab 页面的方法(不硬编码):
|
||||
/// 通过 GoRouterState 的 matchedLocation,判断是否能匹配到 ShellBranch 内的路径
|
||||
/// (即 /, /recite, /class, /my 这四个——但这些路径是由路由配置决定的,
|
||||
/// 所以判断逻辑跟着路由配置走,别人改 tab 路径这里自动生效)。
|
||||
///
|
||||
/// ============================================================
|
||||
|
||||
/// 是否为 tab 路由(动态判断:4 个 tab 的路径由路由配置决定)
|
||||
///
|
||||
/// go_router 的 StatefulShellBranch 内配置了 4 条路径,
|
||||
/// 直接从路由表读取所有 ShellBranch 的 path 集合,而不是硬编码。
|
||||
/// 但 ShellBranch 的 routes 里只有 GoRoute,path 是字段。
|
||||
/// 所以这里采用一个更稳的方式:判断当前是否在 StatefulShellRoute 里,
|
||||
/// 或者直接在 app_router.dart 里暴露一个 tabPaths 常量。
|
||||
///
|
||||
/// 目前路由表固定,这里用 tabPaths 常量(从 app_router 导出)。
|
||||
import 'package:new_learn/core/router/app_router.dart';
|
||||
|
||||
/// 拦截器:当 userProvider 变化时检查是否需要跳报告页
|
||||
class ReportRedirectNotifier extends ConsumerStatefulWidget {
|
||||
const ReportRedirectNotifier({super.key, required this.child});
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
ConsumerState<ReportRedirectNotifier> createState() =>
|
||||
_ReportRedirectNotifierState();
|
||||
}
|
||||
|
||||
class _ReportRedirectNotifierState extends ConsumerState<ReportRedirectNotifier>
|
||||
with WidgetsBindingObserver {
|
||||
/// 防止重复跳转标记
|
||||
bool _isRedirecting = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
talker.debug('[ReportRedirect] initState 🔥 被创建了');
|
||||
// 监听 userProvider 后续变化
|
||||
ref.listenManual(currentUserProvider, (prev, next) {
|
||||
talker.debug('[ReportRedirect] listenManual 触发!prev=$prev next=$next');
|
||||
_checkAndRedirect();
|
||||
});
|
||||
// 立即检查一次!listenManual 只监听变化不会主动触发,
|
||||
// 登录后 userProvider 已经是 AsyncData 状态了,必须主动调。
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
talker.debug(
|
||||
'[ReportRedirect] addPostFrameCallback → 主动调 _checkAndRedirect',
|
||||
);
|
||||
_checkAndRedirect();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// App 生命周期变化:resumed 时 force 拉最新 userInfo
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
_onAppResumed();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onAppResumed() async {
|
||||
// 只有已登录才 force 拉
|
||||
if (DioClient.instance.accessToken == null) return;
|
||||
talker.debug('[ReportRedirect] App resumed → force refresh userInfo');
|
||||
await ref.read(currentUserProvider.notifier).refresh();
|
||||
_checkAndRedirect();
|
||||
}
|
||||
|
||||
/// 核心拦截逻辑:判断当前路由 + 报告标记 → 决定跳不跳
|
||||
void _checkAndRedirect() {
|
||||
talker.debug('[ReportRedirect] ===== _checkAndRedirect 开始 ====');
|
||||
if (_isRedirecting) {
|
||||
talker.debug('[ReportRedirect] _isRedirecting=true,跳过');
|
||||
return;
|
||||
}
|
||||
|
||||
final user = ref.read(currentUserProvider).value;
|
||||
if (user == null) {
|
||||
talker.debug('[ReportRedirect] user=null(未登录),放行');
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: 调试报告用,把后端字段叠加调试开关,上线前删掉下面三行
|
||||
// const _debugExitTest = false;
|
||||
// const _debugPlacementTest = false;
|
||||
const _debugExitTest = false; // true = 强制调试出门测
|
||||
const _debugPlacementTest = false; // true = 强制调试入门测
|
||||
|
||||
talker.debug(
|
||||
'[ReportRedirect] user.isShowReport=${user.isShowReport} '
|
||||
'user.isShowAfterReport=${user.isShowAfterReport} '
|
||||
'debugExit=$_debugExitTest debugPlacement=$_debugPlacementTest',
|
||||
);
|
||||
|
||||
if (!user.isShowReport &&
|
||||
!user.isShowAfterReport &&
|
||||
!_debugExitTest &&
|
||||
!_debugPlacementTest) {
|
||||
talker.debug('[ReportRedirect] ❌ 无报告标记+无调试开关,放行');
|
||||
return;
|
||||
}
|
||||
|
||||
final isExitTest = user.isShowAfterReport || _debugExitTest;
|
||||
final reportType = isExitTest
|
||||
? 'exitTest'
|
||||
: (user.isShowReport || _debugPlacementTest ? 'placementTest' : null);
|
||||
if (reportType == null) {
|
||||
talker.debug('[ReportRedirect] ❌ reportType=null,放行');
|
||||
return;
|
||||
}
|
||||
talker.debug('[ReportRedirect] ✅ 通过标记检查,reportType=$reportType');
|
||||
|
||||
// 当前路由是否在 tab 页面(直接从全局 appRouter 拿,不需要 context)
|
||||
String location = appRouter.state?.matchedLocation ?? '';
|
||||
talker.debug('[ReportRedirect] appRouter.state matchedLocation=$location');
|
||||
final inTab = _isInTabRoute(location);
|
||||
talker.debug(
|
||||
'[ReportRedirect] location=$location kTabPaths=$kTabPaths inTab=$inTab',
|
||||
);
|
||||
if (!inTab) {
|
||||
talker.debug('[ReportRedirect] ❌ 不在 tab 页面,跳过拦截');
|
||||
return;
|
||||
}
|
||||
|
||||
talker.debug('[ReportRedirect] ✅✅✅ 触发跳转 → studentInfo?type=$reportType');
|
||||
_isRedirecting = true;
|
||||
|
||||
try {
|
||||
appRouter.go('/studentInfo', extra: {'type': reportType});
|
||||
talker.debug('[ReportRedirect] appRouter.go 调用成功');
|
||||
} catch (e) {
|
||||
talker.error('[ReportRedirect] appRouter.go 崩了!', e);
|
||||
}
|
||||
|
||||
// 重置标记(go_router 会重建 Widget,这个 State 会被 dispose)
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
_isRedirecting = false;
|
||||
});
|
||||
}
|
||||
|
||||
/// 判断当前路径是否在 tab 路由里(动态,不硬编码)
|
||||
///
|
||||
/// go_router 的 StatefulShellRoute 下的 4 个 ShellBranch 里,
|
||||
/// 第一个 GoRoute 的 path 就是 tab 路径。我们从 app_router 导出的
|
||||
/// tabPaths 集合判断,别人改 tab 路径这里自动生效。
|
||||
bool _isInTabRoute(String location) {
|
||||
return kTabPaths.contains(location);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child;
|
||||
}
|
||||
@@ -13,14 +13,20 @@ import 'package:new_learn/features/class/my_interact_page.dart';
|
||||
import 'package:new_learn/features/class/my_update_detail_page.dart';
|
||||
import 'package:new_learn/core/api/class/model.dart';
|
||||
import 'package:new_learn/features/my/my_page.dart';
|
||||
import 'package:new_learn/features/my/note/note_page.dart';
|
||||
import 'package:new_learn/features/conquerPackage/captureList/index.dart';
|
||||
import 'package:new_learn/features/conquerPackage/beforeDuringClass/index.dart';
|
||||
import 'package:new_learn/features/my/my_point/my_point_page.dart';
|
||||
import 'package:new_learn/features/my/question_list/question_list_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';
|
||||
import 'package:new_learn/features/report/report_computation_page.dart';
|
||||
import 'package:new_learn/features/report/report_web_view_page.dart';
|
||||
import 'package:new_learn/features/report/student_info_page.dart';
|
||||
import 'package:new_learn/features/task/task_page.dart';
|
||||
import 'package:new_learn/core/router/main_tab_page.dart';
|
||||
import 'package:new_learn/core/network/dio_client.dart';
|
||||
@@ -71,6 +77,12 @@ import 'package:new_learn/core/providers/user_provider.dart';
|
||||
/// 必须在创建 GoRouter 前全局开启,见 main.dart 的 main()。
|
||||
/// ============================================================
|
||||
|
||||
/// 【kTabPaths】四个底部 tab 路径集合(动态来源,别人改 tab 路由时同步改这里即可)
|
||||
///
|
||||
/// 全局拦截器 ReportRedirectNotifier 用它判断"当前是否在 tab 页面",
|
||||
/// 避免硬编码分散在多个地方。
|
||||
const kTabPaths = <String>{'/', '/recite', '/class', '/my'};
|
||||
|
||||
/// 全局路由表(对标 Vue 的 router/index.ts)
|
||||
final GoRouter appRouter = GoRouter(
|
||||
navigatorKey:
|
||||
@@ -129,7 +141,19 @@ final GoRouter appRouter = GoRouter(
|
||||
MyUpdateDetailPage(initialPost: state.extra as UserPostDto),
|
||||
),
|
||||
GoRoute(path: '/myPoint', builder: (_, _) => const MyPointPage()),
|
||||
// 我的笔记
|
||||
GoRoute(path: '/myNote', builder: (_, _) => const NotePage()),
|
||||
// 错题本(type=2)/ 我的收藏(type=1)共用 QuestionListPage
|
||||
GoRoute(
|
||||
path: '/myWrong',
|
||||
builder: (_, _) => const QuestionListPage(type: 2),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/myCollect',
|
||||
builder: (_, _) => const QuestionListPage(type: 1),
|
||||
),
|
||||
GoRoute(path: '/myReport', builder: (_, _) => const MyReportPage()),
|
||||
GoRoute(path: '/myExam', builder: (_, _) => const ExamPage()),
|
||||
GoRoute(
|
||||
path: '/report-detail',
|
||||
builder: (_, state) {
|
||||
@@ -143,6 +167,16 @@ final GoRouter appRouter = GoRouter(
|
||||
),
|
||||
GoRoute(path: '/settings', builder: (_, _) => const SettingsPage()),
|
||||
GoRoute(path: '/about-app', builder: (_, _) => const AboutAppPage()),
|
||||
// ---------- 诊断报告包三个页面(不在 ShellBranch 内,push 时不显示 tabbar) ----------
|
||||
GoRoute(path: '/studentInfo', builder: (_, _) => const StudentInfoPage()),
|
||||
GoRoute(
|
||||
path: '/reportWebView',
|
||||
builder: (_, _) => const ReportWebViewPage(),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/reportComputation',
|
||||
builder: (_, _) => const ReportComputationPage(),
|
||||
),
|
||||
// ---------- 首次启动隐私协议页 ----------
|
||||
GoRoute(
|
||||
path: '/privacy-consent',
|
||||
@@ -167,8 +201,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(
|
||||
|
||||
@@ -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