feat(conquerPackage, upload): 新增课前课中页面与多图上传组件,修复路由配置
- 实现攻克包模块的课前课中学习页面,包含学习计时、笔记上传、分页切换等完整逻辑 - 新增通用多图上传组件,支持编辑/查看两种业务模式 - 配置新页面的路由规则,并更新捕获列表页面的跳转逻辑为正式的go_router跳转 - 修复捕获列表页面跳转路径不匹配的问题
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
import 'package:new_learn/core/api/conquerPackage/beforeDuringClass/model.dart';
|
||||
import 'package:new_learn/core/network/dio_client.dart';
|
||||
|
||||
/// ============================================================
|
||||
/// 【课前课中模块接口】讲义列表 / 学习时长更新 / 提交报告
|
||||
///
|
||||
/// 对标 Vue 端 api/task/index.ts 中的相关接口:
|
||||
///
|
||||
/// getListByRecordHandout
|
||||
/// —— 根据学习记录获取讲义列表
|
||||
/// GET /api/v1/Handout/GetListByRecord/{recordId}
|
||||
///
|
||||
/// startOvercome
|
||||
/// —— 开始/继续攻克学习包(返回 recordId 等)
|
||||
/// POST /api/v1/StudyPack/StartOvercome/{packId}
|
||||
///
|
||||
/// renewHandoutTime
|
||||
/// —— 更新讲义学习时长
|
||||
/// POST /api/v1/Handout/RenewTime
|
||||
///
|
||||
/// getSubmitReport
|
||||
/// —— 获取攻克报告(view 模式数据源)
|
||||
/// GET /api/v1/StudyPack/GetReport/{recordId}
|
||||
///
|
||||
/// 注意:
|
||||
/// - 页面只调这里,不直接碰 Dio;
|
||||
/// - 异常不在 catch,交页面统一处理;
|
||||
/// - view 模式下接口返回的 handouts 列表格式一致,统一走 HandoutListResponse。
|
||||
/// ============================================================
|
||||
class BeforeDuringClassApi {
|
||||
BeforeDuringClassApi._();
|
||||
|
||||
static final BeforeDuringClassApi instance = BeforeDuringClassApi._();
|
||||
|
||||
/// 根据学习记录获取讲义列表 → GET /api/v1/Handout/GetListByRecord/{recordId}
|
||||
Future<HandoutListResponse> getListByRecordHandout(String recordId) async {
|
||||
final data = await DioClient.instance.get(
|
||||
'/api/v1/Handout/GetListByRecord/$recordId',
|
||||
);
|
||||
return HandoutListResponse.fromJson(data);
|
||||
}
|
||||
|
||||
/// 更新讲义学习时长 → POST /api/v1/Handout/RenewTime
|
||||
///
|
||||
/// [id] 讲义 id
|
||||
/// [courseId] 课程 id
|
||||
/// [startTime] 开始时间 ISO 字符串
|
||||
/// [endTime] 结束时间 ISO 字符串
|
||||
Future<void> renewHandoutTime({
|
||||
required String id,
|
||||
required String courseId,
|
||||
required String startTime,
|
||||
required String endTime,
|
||||
}) async {
|
||||
await DioClient.instance.post(
|
||||
'/api/v1/Handout/RenewTime',
|
||||
data: {
|
||||
'id': id,
|
||||
'courseId': courseId,
|
||||
'startTime': startTime,
|
||||
'endTime': endTime,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 获取攻克报告(view 模式) → GET /api/v1/StudyPack/GetReport/{recordId}
|
||||
///
|
||||
/// 返回结构中 handouts 字段对应讲义列表。
|
||||
Future<HandoutListResponse> getSubmitReport(String recordId) async {
|
||||
final data = await DioClient.instance.get(
|
||||
'/api/v1/StudyPack/GetReport/$recordId',
|
||||
);
|
||||
// 报告里 handouts 可能在 data.handouts 下
|
||||
if (data is Map<String, dynamic>) {
|
||||
final payload = data['data'] as Map<String, dynamic>? ?? data;
|
||||
return HandoutListResponse.fromJson(payload['handouts']);
|
||||
}
|
||||
return const HandoutListResponse();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/// ============================================================
|
||||
/// 【课前课中模块模型】讲义列表 / 材料 / 笔记
|
||||
///
|
||||
/// 对标 Vue 端 api/task/type.ts 中的 Handout 相关类型。
|
||||
/// 序列化:手动 fromJson,兼容 {code, message, data: {...}} 响应壳。
|
||||
/// ============================================================
|
||||
library;
|
||||
|
||||
import 'package:new_learn/core/api/public_upload/model.dart';
|
||||
|
||||
/// 讲义材料类型
|
||||
enum HandoutMaterialType {
|
||||
video(1), // 视频
|
||||
audio(2), // 音频
|
||||
text(3), // 文本
|
||||
unknown(0);
|
||||
|
||||
final int value;
|
||||
const HandoutMaterialType(this.value);
|
||||
|
||||
factory HandoutMaterialType.fromInt(int? v) =>
|
||||
HandoutMaterialType.values.firstWhere(
|
||||
(e) => e.value == v,
|
||||
orElse: () => HandoutMaterialType.unknown,
|
||||
);
|
||||
}
|
||||
|
||||
/// 讲义材料项(视频/音频/文本)
|
||||
class HandoutMaterial {
|
||||
const HandoutMaterial({
|
||||
this.id = '',
|
||||
this.pageIndex = 0,
|
||||
this.materialType = HandoutMaterialType.unknown,
|
||||
this.materialContent = '',
|
||||
});
|
||||
|
||||
final String id; // 材料 id
|
||||
final int pageIndex; // 页码
|
||||
final HandoutMaterialType materialType; // 材料类型
|
||||
final String materialContent; // 内容(URL 或 HTML 文本)
|
||||
|
||||
factory HandoutMaterial.fromJson(Map<String, dynamic> json) {
|
||||
return HandoutMaterial(
|
||||
id: json['id']?.toString() ?? '',
|
||||
pageIndex: _parseInt(json['pageIndex']),
|
||||
materialType:
|
||||
HandoutMaterialType.fromInt(_parseInt(json['materialType'])),
|
||||
materialContent: json['materialContent'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'HandoutMaterial{id: $id, pageIndex: $pageIndex, '
|
||||
'type: ${materialType.name}, content: '
|
||||
'${materialContent.length > 50 ? '${materialContent.substring(0, 50)}...' : materialContent}}';
|
||||
}
|
||||
}
|
||||
|
||||
/// 单页讲义
|
||||
class HandoutItem {
|
||||
const HandoutItem({
|
||||
this.id = '',
|
||||
this.handoutPageId = '',
|
||||
this.name = '',
|
||||
this.title = '',
|
||||
this.content = '',
|
||||
this.planStudyTimeSecond = 0,
|
||||
this.planStudyRemainTimeSecond = 0,
|
||||
this.studyTimeSecond = 0,
|
||||
this.isFinishPlanTime = false,
|
||||
this.isUploadNotes = false,
|
||||
this.materials = const [],
|
||||
this.notes = const [],
|
||||
});
|
||||
|
||||
final String id; // 讲义 id(用于 RenewTime / NoteAdd)
|
||||
final String handoutPageId; // 讲义页 id
|
||||
final String name; // 名称(页面顶部标题)
|
||||
final String title; // 标题(NavBar 显示)
|
||||
final String content; // 正文(富文本 HTML)
|
||||
final int planStudyTimeSecond; // 建议学习时长(秒)
|
||||
final int planStudyRemainTimeSecond; // 剩余学习时长(秒)
|
||||
final int studyTimeSecond; // 已学习时长(秒)
|
||||
final bool isFinishPlanTime; // 是否已完成建议时长
|
||||
final bool isUploadNotes; // 是否必须上传笔记
|
||||
final List<HandoutMaterial> materials; // 材料列表
|
||||
final List<NoteImage> notes; // 笔记图片列表
|
||||
|
||||
/// 深拷贝(用于修改某字段后触发 setState)
|
||||
HandoutItem copyWith({
|
||||
String? id,
|
||||
String? handoutPageId,
|
||||
String? name,
|
||||
String? title,
|
||||
String? content,
|
||||
int? planStudyTimeSecond,
|
||||
int? planStudyRemainTimeSecond,
|
||||
int? studyTimeSecond,
|
||||
bool? isFinishPlanTime,
|
||||
bool? isUploadNotes,
|
||||
List<HandoutMaterial>? materials,
|
||||
List<NoteImage>? notes,
|
||||
}) {
|
||||
return HandoutItem(
|
||||
id: id ?? this.id,
|
||||
handoutPageId: handoutPageId ?? this.handoutPageId,
|
||||
name: name ?? this.name,
|
||||
title: title ?? this.title,
|
||||
content: content ?? this.content,
|
||||
planStudyTimeSecond: planStudyTimeSecond ?? this.planStudyTimeSecond,
|
||||
planStudyRemainTimeSecond:
|
||||
planStudyRemainTimeSecond ?? this.planStudyRemainTimeSecond,
|
||||
studyTimeSecond: studyTimeSecond ?? this.studyTimeSecond,
|
||||
isFinishPlanTime: isFinishPlanTime ?? this.isFinishPlanTime,
|
||||
isUploadNotes: isUploadNotes ?? this.isUploadNotes,
|
||||
materials: materials ?? this.materials,
|
||||
notes: notes ?? this.notes,
|
||||
);
|
||||
}
|
||||
|
||||
factory HandoutItem.fromJson(Map<String, dynamic> json) {
|
||||
final materials = (json['materials'] as List<dynamic>? ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(HandoutMaterial.fromJson)
|
||||
.toList();
|
||||
final notes = (json['notes'] as List<dynamic>? ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(NoteImage.fromJson)
|
||||
.toList();
|
||||
return HandoutItem(
|
||||
id: (json['id']?.toString()) ?? '',
|
||||
handoutPageId: (json['handoutPageId']?.toString()) ?? '',
|
||||
name: json['name'] as String? ?? json['title'] as String? ?? '',
|
||||
title: json['title'] as String? ?? json['name'] as String? ?? '',
|
||||
content: json['content'] as String? ?? '',
|
||||
planStudyTimeSecond: _parseInt(json['planStudyTimeSecond']),
|
||||
planStudyRemainTimeSecond: _parseInt(json['planStudyRemainTimeSecond']),
|
||||
studyTimeSecond: _parseInt(json['studyTimeSecond']),
|
||||
isFinishPlanTime:
|
||||
json['ISFinishPlanTime'] as bool? ?? json['isFinishPlanTime'] as bool? ?? false,
|
||||
isUploadNotes: json['isUploadNotes'] as bool? ?? false,
|
||||
materials: materials,
|
||||
notes: notes,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'HandoutItem{id: $id, name: $name, planTime: $planStudyTimeSecond, '
|
||||
'studyTime: $studyTimeSecond, isFinishPlanTime: $isFinishPlanTime, '
|
||||
'isUploadNotes: $isUploadNotes, materials: ${materials.length}, '
|
||||
'notes: ${notes.length}}';
|
||||
}
|
||||
}
|
||||
|
||||
/// 讲义列表响应(Handout 接口返回的列表)
|
||||
class HandoutListResponse {
|
||||
const HandoutListResponse({this.items = const []});
|
||||
|
||||
final List<HandoutItem> items;
|
||||
|
||||
/// 兼容响应壳 {code, message, data: [...]},也兼容直接返回数组
|
||||
factory HandoutListResponse.fromJson(dynamic data) {
|
||||
List<dynamic> list;
|
||||
if (data is List) {
|
||||
list = data;
|
||||
} else if (data is Map<String, dynamic>) {
|
||||
final payload = data['data'] ?? data['items'] ?? data['handouts'];
|
||||
list = (payload is List) ? payload : const [];
|
||||
} else {
|
||||
list = const [];
|
||||
}
|
||||
return HandoutListResponse(
|
||||
items: list
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(HandoutItem.fromJson)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 安全解析 int
|
||||
int _parseInt(dynamic value) {
|
||||
if (value == null) return 0;
|
||||
if (value is int) return value;
|
||||
if (value is double) return value.toInt();
|
||||
if (value is num) return value.toInt();
|
||||
if (value is String) return int.tryParse(value) ?? 0;
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user