Compare commits
7
Commits
5e4cfe9ac6
...
e0b95a8b43
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0b95a8b43 | ||
|
|
ee14373c6d | ||
|
|
0ce3c0b3ef | ||
|
|
de3457c86e | ||
|
|
7acf745295 | ||
|
|
592e576319 | ||
|
|
44fb5f86e2 |
@@ -0,0 +1,33 @@
|
||||
import 'package:new_learn/core/api/conquerPackage/captureList/model.dart';
|
||||
import 'package:new_learn/core/network/dio_client.dart';
|
||||
|
||||
/// ============================================================
|
||||
/// 【captureList 模块接口】攻克学习包 —— 对标 Vue3 端 api/task/index.ts
|
||||
///
|
||||
/// getCoursePack —— 获取课程下的攻克学习包列表
|
||||
/// GET /api/v1/StudyPack/GetCoursePacks/{courseId}
|
||||
///
|
||||
/// startOvercome —— 开始 / 继续攻克学习包
|
||||
/// POST /api/v1/StudyPack/StartOvercome/{packId}
|
||||
/// ============================================================
|
||||
class CaptureListApi {
|
||||
CaptureListApi._();
|
||||
|
||||
static final CaptureListApi instance = CaptureListApi._();
|
||||
|
||||
/// 获取课程下的攻克学习包列表
|
||||
Future<CoursePackResponse> getCoursePack(String courseId) async {
|
||||
final data = await DioClient.instance.get(
|
||||
'/api/v1/StudyPack/GetCoursePacks/$courseId',
|
||||
);
|
||||
return CoursePackResponse.fromJson(data as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
/// 开始 / 继续攻克学习包
|
||||
Future<TaskDetail> startOvercome(String packId) async {
|
||||
final data = await DioClient.instance.post(
|
||||
'/api/v1/StudyPack/StartOvercome/$packId',
|
||||
);
|
||||
return TaskDetail.fromJson(data as Map<String, dynamic>);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
/// ============================================================
|
||||
/// 【captureList 模块模型】攻克学习包 —— 对标 Vue3 端 api/task/type.ts
|
||||
///
|
||||
/// CoursePackResponse / CourseInfo / CourseRecord
|
||||
/// TaskDetail(开始/继续攻克返回)
|
||||
///
|
||||
/// 序列化:手动 fromJson(兼容 {code, message, data: {...}} 响应壳)
|
||||
/// ============================================================
|
||||
library;
|
||||
|
||||
/// 攻克记录(单次攻克的结果)
|
||||
class CourseRecord {
|
||||
const CourseRecord({
|
||||
this.id = 0,
|
||||
this.createTime = '',
|
||||
this.accuracyRate = 0,
|
||||
this.status = 0,
|
||||
this.statusName = '',
|
||||
});
|
||||
|
||||
final int id; // 记录 id
|
||||
final String createTime; // 创建时间
|
||||
final int accuracyRate; // 正确率(0-100)
|
||||
final int status; // 状态:0 未开始 / 1 进行中 / 2 成功 / 3 失败 等(按后端枚举)
|
||||
final String statusName; // 状态名称
|
||||
|
||||
factory CourseRecord.fromJson(Map<String, dynamic> json) {
|
||||
return CourseRecord(
|
||||
id: _parseInt(json['id']),
|
||||
createTime: json['createTime'] as String? ?? '',
|
||||
accuracyRate: _parseInt(json['accuracyRate']),
|
||||
status: _parseInt(json['status']),
|
||||
statusName: json['statusName'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 单个学习包(攻克包)信息
|
||||
class CourseInfo {
|
||||
const CourseInfo({
|
||||
this.id = 0,
|
||||
this.name = '',
|
||||
this.baseSubjectName = '',
|
||||
this.overcomeStandard = 0,
|
||||
this.estimatedTime = 0,
|
||||
this.maxOvercomeCount = 0,
|
||||
this.recordCount = 0,
|
||||
this.records = const [],
|
||||
this.status = 0,
|
||||
this.statusName = '',
|
||||
});
|
||||
|
||||
final int id; // 学习包 id
|
||||
final String name; // 名称
|
||||
final String baseSubjectName; // 学科名称
|
||||
final int overcomeStandard; // 攻克标准(百分比)
|
||||
final int estimatedTime; // 预计用时(分钟)
|
||||
final int maxOvercomeCount; // 最大可攻克次数
|
||||
final int recordCount; // 已攻克次数
|
||||
final List<CourseRecord> records; // 历次攻克记录
|
||||
final int status; // 状态:0 未开始 / 1 进行中 / 2 已攻克 / 3 失败 / 4 后续学习 / 5 判题中(按后端枚举)
|
||||
final String statusName; // 状态名称
|
||||
|
||||
factory CourseInfo.fromJson(Map<String, dynamic> json) {
|
||||
return CourseInfo(
|
||||
id: _parseInt(json['id']),
|
||||
name: json['name'] as String? ?? '',
|
||||
baseSubjectName: json['baseSubjectName'] as String? ?? '',
|
||||
overcomeStandard: _parseInt(json['overcomeStandard']),
|
||||
estimatedTime: _parseInt(json['estimatedTime']),
|
||||
maxOvercomeCount: _parseInt(json['maxOvercomeCount']),
|
||||
recordCount: _parseInt(json['recordCount']),
|
||||
records: (json['records'] as List<dynamic>? ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(CourseRecord.fromJson)
|
||||
.toList(),
|
||||
status: _parseInt(json['status']),
|
||||
statusName: json['statusName'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 学习包列表响应(接口返回 {packs: [...], remark: '...'})
|
||||
class CoursePackResponse {
|
||||
const CoursePackResponse({
|
||||
this.packs = const [],
|
||||
this.remark = '',
|
||||
});
|
||||
|
||||
final List<CourseInfo> packs; // 学习包列表
|
||||
final String remark; // 规则说明文字
|
||||
|
||||
/// 兼容业务响应壳 {code, message, data: {...}}:data 存在就取 data
|
||||
factory CoursePackResponse.fromJson(Map<String, dynamic> json) {
|
||||
final payload = (json['data'] as Map<String, dynamic>?) ?? json;
|
||||
return CoursePackResponse(
|
||||
packs: (payload['packs'] as List<dynamic>? ??
|
||||
payload['courses'] as List<dynamic>? ??
|
||||
const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(CourseInfo.fromJson)
|
||||
.toList(),
|
||||
remark: payload['remark'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 开始 / 继续攻克 接口返回(精简版,包含进入下一步所需字段)
|
||||
class TaskDetail {
|
||||
const TaskDetail({
|
||||
this.id = 0,
|
||||
this.name = '',
|
||||
this.overcomeStandard = 0,
|
||||
this.planTimeMinute = 0,
|
||||
this.recordId = 0,
|
||||
this.nowStudyObjectType = 0,
|
||||
this.nowStudyObjectId = 0,
|
||||
this.hasHandout = false,
|
||||
this.hasQuestion = false,
|
||||
});
|
||||
|
||||
final int id; // 学习包 id
|
||||
final String name; // 名称
|
||||
final int overcomeStandard; // 攻克标准
|
||||
final int planTimeMinute; // 计划用时(分钟)
|
||||
final int recordId; // 学习记录 id(跳转到习题/讲义页用)
|
||||
final int nowStudyObjectType; // 当前学习对象类型:0 讲义 / 11 习题 等
|
||||
final int nowStudyObjectId; // 当前学习对象 id
|
||||
final bool hasHandout; // 是否有讲义
|
||||
final bool hasQuestion; // 是否有题目
|
||||
|
||||
factory TaskDetail.fromJson(Map<String, dynamic> json) {
|
||||
final payload = (json['data'] as Map<String, dynamic>?) ?? json;
|
||||
return TaskDetail(
|
||||
id: _parseInt(payload['id']),
|
||||
name: payload['name'] as String? ?? '',
|
||||
overcomeStandard: _parseInt(payload['overcomeStandard']),
|
||||
planTimeMinute: _parseInt(payload['planTimeMinute']),
|
||||
recordId: _parseInt(payload['recordId']),
|
||||
nowStudyObjectType: _parseInt(payload['nowStudyObjectType']),
|
||||
nowStudyObjectId: _parseInt(payload['nowStudyObjectId']),
|
||||
hasHandout: payload['hasHandout'] as bool? ?? false,
|
||||
hasQuestion: payload['hasQuestion'] as bool? ?? false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 安全解析 int:兼容数字和字符串数字,解析失败返回 0
|
||||
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;
|
||||
}
|
||||
@@ -13,6 +13,7 @@ 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/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';
|
||||
@@ -181,7 +182,14 @@ final GoRouter appRouter = GoRouter(
|
||||
),
|
||||
// ---------- 登录/注册页(未登录可访问) ----------
|
||||
GoRoute(path: '/login', builder: (_, _) => const LoginPage()),
|
||||
// ---------- 攻克包 - 截图名单 ----------
|
||||
GoRoute(
|
||||
path: '/conquerPackage/captureList',
|
||||
builder: (_, state) =>
|
||||
CaptureListPage(courseId: state.extra as String),
|
||||
),
|
||||
GoRoute(path: '/register', builder: (_, _) => const RegisterPage()),
|
||||
|
||||
// app_router.dart 的 routes 里加,不用管跳转逻辑
|
||||
// GoRoute(path: '/task/:id', builder: (_, _) => const TaskDetailPage()),
|
||||
// ---------- 以后新业务页在这里加,一行一个 ----------
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
|
||||
class CaptureListPage extends StatelessWidget {
|
||||
const CaptureListPage({super.key});
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Align 对齐布局 Demo11111')),
|
||||
// ---------- 下方即为核心代码,可直接复制到 Scaffold body ----------
|
||||
body: Container(
|
||||
width: 300.w,
|
||||
height: 300.h,
|
||||
color: Colors.grey[300],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,943 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:new_learn/core/api/conquerPackage/captureList/api.dart';
|
||||
import 'package:new_learn/core/api/conquerPackage/captureList/model.dart';
|
||||
import 'package:new_learn/core/components/common_paged_list.dart';
|
||||
import 'package:new_learn/core/utils/img_url.dart';
|
||||
|
||||
/// ============================================================
|
||||
/// 【CaptureListPage】攻克列表页 —— 对标 Vue pages/conquerPackage/captureList/index.vue
|
||||
///
|
||||
/// 入参:courseId(课程 id)
|
||||
///
|
||||
/// 接口:
|
||||
/// - CaptureListApi.getCoursePack(courseId) → 列表 + remark
|
||||
/// - CaptureListApi.startOvercome(packId) → 开始/继续攻克,跳转对应页面
|
||||
///
|
||||
/// 换算规则(本工程统一):Vue 的 `N rpx` → Flutter 的 `(N/2).w/.h/.sp/.r`
|
||||
/// ============================================================
|
||||
|
||||
/// 攻克记录状态枚举(与后端 status 对齐:0未开始/1成功/2失败/3判题中)
|
||||
enum CaptureRecordStatus {
|
||||
none(0), // 未开始
|
||||
success(1), // 成功
|
||||
fail(2), // 失败
|
||||
processing(3); // 判题中
|
||||
|
||||
final int value;
|
||||
const CaptureRecordStatus(this.value);
|
||||
|
||||
factory CaptureRecordStatus.fromInt(int? v) {
|
||||
return CaptureRecordStatus.values.firstWhere(
|
||||
(e) => e.value == v,
|
||||
orElse: () => CaptureRecordStatus.none,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 攻克包状态枚举(与后端 status 对齐)
|
||||
enum CapturePackStatus {
|
||||
notStarted(0), // 未开始
|
||||
inProgress(1), // 进行中
|
||||
conquered(2), // 已攻克
|
||||
failed(3), // 攻克失败
|
||||
laterStudy(4), // 后续学习
|
||||
judging(5); // 判题中
|
||||
|
||||
final int value;
|
||||
const CapturePackStatus(this.value);
|
||||
|
||||
factory CapturePackStatus.fromInt(int? v) {
|
||||
return CapturePackStatus.values.firstWhere(
|
||||
(e) => e.value == v,
|
||||
orElse: () => CapturePackStatus.notStarted,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 页面内部包装模型(在 CourseInfo 基础上增加 UI 状态 isShowCaptureInfo)
|
||||
class _CapturePackViewModel {
|
||||
final CourseInfo info;
|
||||
bool isShowCaptureInfo;
|
||||
|
||||
_CapturePackViewModel(this.info, {this.isShowCaptureInfo = false});
|
||||
|
||||
String get id => '${info.id}';
|
||||
String get name => info.name;
|
||||
String get baseSubjectName => info.baseSubjectName;
|
||||
int get overcomeStandard => info.overcomeStandard;
|
||||
int get maxOvercomeCount => info.maxOvercomeCount;
|
||||
int get recordCount => info.recordCount;
|
||||
CapturePackStatus get status => CapturePackStatus.fromInt(info.status);
|
||||
List<CourseRecord> get records => info.records;
|
||||
}
|
||||
|
||||
class CaptureListPage extends StatefulWidget {
|
||||
const CaptureListPage({super.key, required this.courseId});
|
||||
|
||||
final String courseId;
|
||||
|
||||
@override
|
||||
State<CaptureListPage> createState() => _CaptureListPageState();
|
||||
}
|
||||
|
||||
class _CaptureListPageState extends State<CaptureListPage> {
|
||||
// ========== 颜色常量 ==========
|
||||
static const Color _bgColor = Color(0xFFF5F8FA);
|
||||
static const Color _titleColor = Color(0xFF1D2B47);
|
||||
static const Color _subtitleColor = Color(0xFF495A7A);
|
||||
static const Color _primary = Color(0xFF3D7DF5);
|
||||
static const Color _success = Color(0xFF00CC96);
|
||||
static const Color _error = Color(0xFFFF4D4D);
|
||||
static const Color _processing = Color(0xFFF5963D);
|
||||
static const Color _barBg = Color(0xFFE3ECFF);
|
||||
static const Color _mutedText = Color(0xFF687FAD);
|
||||
|
||||
// ========== 状态 ==========
|
||||
String _remark = '';
|
||||
bool _navigating = false; // 防止连续点击重复请求/跳转
|
||||
List<String> _expandedPackIds = []; // 展开的包 id(刷新时保留展开状态)
|
||||
int _requestId = 0; // 请求序号,防止并发请求错乱
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
// ========== 接口 ==========
|
||||
|
||||
/// 分页加载:CommonPagedList 回调。接口无分页,当作一页返回,hasNext=false。
|
||||
Future<PagingResult<_CapturePackViewModel>> _loadPage(int pageKey) async {
|
||||
final requestId = ++_requestId;
|
||||
final expandedIds = _expandedPackIds.isNotEmpty
|
||||
? [..._expandedPackIds]
|
||||
: <String>[];
|
||||
|
||||
// courseId 为空直接返回空列表
|
||||
if (widget.courseId.isEmpty) {
|
||||
if (!mounted) return const PagingResult(data: [], hasNext: false);
|
||||
setState(() => _remark = '');
|
||||
return const PagingResult(data: [], hasNext: false);
|
||||
}
|
||||
|
||||
final res = await CaptureListApi.instance.getCoursePack(widget.courseId);
|
||||
if (requestId != _requestId) {
|
||||
// 已被新请求覆盖,丢弃结果(返回空让库忽略也可)
|
||||
return const PagingResult(data: [], hasNext: false);
|
||||
}
|
||||
|
||||
final list = _restoreExpandedState(res.packs, expandedIds);
|
||||
if (!mounted) return PagingResult(data: list, hasNext: false);
|
||||
|
||||
setState(() {
|
||||
_remark = res.remark;
|
||||
_expandedPackIds = list
|
||||
.where((item) => item.isShowCaptureInfo)
|
||||
.map((item) => item.id)
|
||||
.toList();
|
||||
});
|
||||
|
||||
// 只有一页
|
||||
return PagingResult(data: list, hasNext: false);
|
||||
}
|
||||
|
||||
// ========== 展开状态管理(对标 Vue saveExpandedState / restoreExpandedState) ==========
|
||||
|
||||
/// 恢复展开状态 + 计算 recordCount(status != 0 的记录数)
|
||||
List<_CapturePackViewModel> _restoreExpandedState(
|
||||
List<CourseInfo> list,
|
||||
List<String> expandedIds,
|
||||
) {
|
||||
final expandedIdSet = expandedIds.toSet();
|
||||
return list.map((info) {
|
||||
final records = info.records;
|
||||
final validCount = records.where((r) => r.status != 0).length;
|
||||
// 构造一个新的 CourseInfo(recordCount 取有效记录数)
|
||||
final adjustedInfo = CourseInfo(
|
||||
id: info.id,
|
||||
name: info.name,
|
||||
baseSubjectName: info.baseSubjectName,
|
||||
overcomeStandard: info.overcomeStandard,
|
||||
estimatedTime: info.estimatedTime,
|
||||
maxOvercomeCount: info.maxOvercomeCount,
|
||||
recordCount: validCount,
|
||||
records: records,
|
||||
status: info.status,
|
||||
statusName: info.statusName,
|
||||
);
|
||||
final hasValidRecords = records.any((r) =>
|
||||
r.status == 1 || r.status == 2 || r.status == 3);
|
||||
final shouldExpand =
|
||||
expandedIdSet.contains('${info.id}') || hasValidRecords;
|
||||
return _CapturePackViewModel(adjustedInfo,
|
||||
isShowCaptureInfo: shouldExpand);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
/// 切换攻克详情展开/收起
|
||||
void _toggleCaptureInfo(_CapturePackViewModel item) {
|
||||
if (item.records.isEmpty) return;
|
||||
final validRecords = item.records.where((r) => r.status != 0).toList();
|
||||
if (validRecords.isEmpty) return;
|
||||
setState(() {
|
||||
item.isShowCaptureInfo = !item.isShowCaptureInfo;
|
||||
if (item.isShowCaptureInfo) {
|
||||
if (!_expandedPackIds.contains(item.id)) {
|
||||
_expandedPackIds.add(item.id);
|
||||
}
|
||||
} else {
|
||||
_expandedPackIds.remove(item.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ========== 点击事件 ==========
|
||||
|
||||
/// 点击按钮 → 开始/继续攻克(对接 startOvercome 接口)
|
||||
Future<void> _navigateToCaptureInfo(_CapturePackViewModel item) async {
|
||||
if (item.status == CapturePackStatus.judging) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('该攻克正在判题中,暂不能操作'), behavior: SnackBarBehavior.floating),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (_navigating) return;
|
||||
_navigating = true;
|
||||
|
||||
// 显示加载
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => const Center(child: CircularProgressIndicator(strokeWidth: 2)),
|
||||
);
|
||||
|
||||
try {
|
||||
final res = await CaptureListApi.instance.startOvercome(item.id);
|
||||
if (!mounted) return;
|
||||
Navigator.of(context).pop(); // 关闭加载
|
||||
|
||||
// 跳转分支(对标 Vue navigateToCaptureInfo)
|
||||
if (res.hasHandout) {
|
||||
// 有讲义 → 课前课中页
|
||||
_navigateTo('/conquer/beforeDuringClass', {
|
||||
'status': '1',
|
||||
'packId': item.id,
|
||||
'courseId': widget.courseId,
|
||||
'recordId': '${res.recordId}',
|
||||
if (res.nowStudyObjectType == 11)
|
||||
'nowStudyObjectId': '${res.nowStudyObjectId}',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (res.hasQuestion) {
|
||||
// 有题目 → 过渡页 或 习题页
|
||||
final pagePath = res.nowStudyObjectType == 0
|
||||
? '/conquer/transitionPage'
|
||||
: '/conquer/classExercises';
|
||||
_navigateTo(pagePath, {
|
||||
'status': '3',
|
||||
'courseId': widget.courseId,
|
||||
'packId': item.id,
|
||||
'recordId': '${res.recordId}',
|
||||
if (res.nowStudyObjectType == 12)
|
||||
'nowStudyObjectId': '${res.nowStudyObjectId}',
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 既没讲义也没题目
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('该攻克暂无讲义或题目'), behavior: SnackBarBehavior.floating),
|
||||
);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop(); // 关闭加载
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('加载失败:${e.toString()}'), behavior: SnackBarBehavior.floating),
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
_navigating = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 页面跳转(占位 —— 待路由统一后替换为正式路由名)
|
||||
void _navigateTo(String path, Map<String, String> params) {
|
||||
// TODO(路由): 替换为项目统一的路由跳转方式
|
||||
debugPrint('navigateTo: $path $params');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('跳转 $path ${params.toString()}'), behavior: SnackBarBehavior.floating),
|
||||
);
|
||||
}
|
||||
|
||||
/// 点击报告(跳转报告页)
|
||||
void _getReport(CourseRecord record, _CapturePackViewModel pack) {
|
||||
if (CaptureRecordStatus.fromInt(record.status) ==
|
||||
CaptureRecordStatus.processing) return;
|
||||
// TODO(路由): 跳转报告页
|
||||
debugPrint('跳转报告页 recordId=${record.id}, packId=${pack.id}');
|
||||
}
|
||||
|
||||
// ========== 工具方法 ==========
|
||||
|
||||
String _numberToChinese(int num) {
|
||||
const chineseNum = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
|
||||
return chineseNum[num.clamp(0, 9)];
|
||||
}
|
||||
|
||||
String _formatDuration(int seconds) {
|
||||
if (seconds <= 0) return '0秒';
|
||||
final h = seconds ~/ 3600;
|
||||
final m = (seconds % 3600) ~/ 60;
|
||||
final s = seconds % 60;
|
||||
if (h > 0) return '${h}时${m}分';
|
||||
if (m > 0) return '${m}分${s}秒';
|
||||
return '${s}秒';
|
||||
}
|
||||
|
||||
// ========== 构建 ==========
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: _bgColor,
|
||||
body: Stack(
|
||||
children: [
|
||||
const Positioned.fill(child: ColoredBox(color: _bgColor)),
|
||||
// 顶部背景图
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: ImgUrl.build(
|
||||
'/StaticResource/2026-05-26/my-header-bg-default1779775984375.png',
|
||||
),
|
||||
fit: BoxFit.fitWidth,
|
||||
width: double.infinity,
|
||||
placeholder: (context, url) => const SizedBox.shrink(),
|
||||
errorWidget: (context, url, error) => const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
// 主内容
|
||||
SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// 导航栏
|
||||
SizedBox(
|
||||
height: 44.h,
|
||||
child: Stack(
|
||||
children: [
|
||||
const Center(
|
||||
child: Text(
|
||||
'攻克列表',
|
||||
style: TextStyle(
|
||||
color: Color.fromRGBO(0, 0, 0, 0.8),
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 16.w,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios,
|
||||
color: Color.fromRGBO(0, 0, 0, 0.8), size: 20),
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 规则卡片
|
||||
_buildRuleView(),
|
||||
SizedBox(height: 19.h),
|
||||
// 列表
|
||||
Expanded(
|
||||
child: CommonPagedList<_CapturePackViewModel>(
|
||||
load: _loadPage,
|
||||
buildItem: (context, item, index) => _buildCaptureItem(item),
|
||||
padding: EdgeInsets.symmetric(horizontal: 14.w),
|
||||
emptyWidget: _buildEmpty(),
|
||||
noMoreText: '', // 只有一页,不显示"没有更多"
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 子组件构建方法
|
||||
// ============================================================
|
||||
|
||||
Widget _buildRuleView() {
|
||||
return Container(
|
||||
margin: EdgeInsets.fromLTRB(14.w, 16.h, 14.w, 0),
|
||||
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 16.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
border: Border.all(color: Colors.white, width: 1.w),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: const Color.fromARGB(255, 255, 255, 255).withOpacity(0.05),
|
||||
blurRadius: 20.r,
|
||||
offset: Offset(0, 10.h),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildRuleTitle(),
|
||||
SizedBox(height: 16.h),
|
||||
_buildRuleContent(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRuleTitle() {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
CachedNetworkImage(
|
||||
imageUrl: ImgUrl.build('/StaticResource/2026-04-10/capture-list-rule.png'),
|
||||
width: 18.w,
|
||||
height: 18.w,
|
||||
fit: BoxFit.fitWidth,
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 2.h),
|
||||
child: Text(
|
||||
'攻克规则说明',
|
||||
style: TextStyle(
|
||||
fontSize: 17.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _titleColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
bottom: -1.h,
|
||||
right: 0,
|
||||
child: Container(
|
||||
width: 59.5.w,
|
||||
height: 9.h,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [
|
||||
Color(0x003D7DF5),
|
||||
Color(0x331D9CFF),
|
||||
Color(0x808AECFD),
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(50.r),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildRuleContent() {
|
||||
if (_remark.isEmpty) {
|
||||
return Text('暂无规则说明',
|
||||
style: TextStyle(fontSize: 14.sp, color: _subtitleColor));
|
||||
}
|
||||
final lines = _remark.split('\n');
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: lines.asMap().entries.map((entry) {
|
||||
final isLast = entry.key == lines.length - 1;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: isLast ? 0 : 10.h),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 4.w,
|
||||
height: 4.w,
|
||||
margin: EdgeInsets.fromLTRB(6.5.w, 7.5.h, 13.w, 0),
|
||||
decoration: const BoxDecoration(
|
||||
color: _primary,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
entry.value,
|
||||
style: TextStyle(fontSize: 14.sp, color: _subtitleColor, height: 1.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmpty() {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CachedNetworkImage(
|
||||
imageUrl: ImgUrl.build('/StaticResource/2026-04-11/noCoursesss.png'),
|
||||
width: 200.w,
|
||||
height: 200.w,
|
||||
),
|
||||
SizedBox(height: 16.h),
|
||||
Text('暂无攻克', style: TextStyle(fontSize: 14.sp, color: _mutedText)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCaptureItem(_CapturePackViewModel item) {
|
||||
return Container(
|
||||
margin: EdgeInsets.only(bottom: 14.h),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
border: Border.all(color: Colors.white, width: 1.w),
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFFFAFEFF), Color(0xFFEDF7FF)],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildCaptureInfo(item),
|
||||
_buildCaptureProgress(item),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCaptureInfo(_CapturePackViewModel item) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(14.w, 18.5.h, 14.w, 0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 4,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildCaptureTitle(item.name),
|
||||
SizedBox(height: 10.h),
|
||||
_buildSubjectTag(item.baseSubjectName),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10.w),
|
||||
_buildStandardBox(item.overcomeStandard),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCaptureTitle(String name) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 3.w,
|
||||
height: 14.h,
|
||||
margin: EdgeInsets.only(right: 7.5.w),
|
||||
decoration: BoxDecoration(
|
||||
color: _primary,
|
||||
borderRadius: BorderRadius.circular(10.r),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
name,
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _titleColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSubjectTag(String subject) {
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 1.w, vertical: 1.h),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6.r),
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF5A9EFF), Color(0xFFF2F8FF), Color(0xFF5A9EFF)],
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 2.h),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFE5F0FF),
|
||||
borderRadius: BorderRadius.circular(4.r),
|
||||
),
|
||||
child: Text(
|
||||
subject,
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp,
|
||||
color: _primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStandardBox(int standard) {
|
||||
return Container(
|
||||
width: 60.w,
|
||||
height: 51.h,
|
||||
padding: EdgeInsets.all(2.w),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6.r),
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFF60D0C0), Color(0xFF6EE9DB)],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Center(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
'攻克标准',
|
||||
style: TextStyle(fontSize: 12.sp, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Container(
|
||||
margin: EdgeInsets.fromLTRB(1.5.w, 0, 1.5.w, 1.5.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(6.r),
|
||||
bottomRight: Radius.circular(6.r),
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CachedNetworkImage(
|
||||
imageUrl:
|
||||
ImgUrl.build('/StaticResource/2026-04-10/capture-list-success.png'),
|
||||
width: 14.w,
|
||||
height: 14.w,
|
||||
fit: BoxFit.fitWidth,
|
||||
),
|
||||
SizedBox(width: 5.5.w),
|
||||
Text(
|
||||
'$standard%',
|
||||
style: TextStyle(
|
||||
fontSize: 15.sp, color: _success, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCaptureProgress(_CapturePackViewModel item) {
|
||||
return Container(
|
||||
margin: EdgeInsets.only(top: 10.5.h),
|
||||
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 16.h),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16.r),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildProgressTop(item),
|
||||
if (item.isShowCaptureInfo) _buildProgressInfo(item),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressTop(_CapturePackViewModel item) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => _toggleCaptureInfo(item),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Row(
|
||||
children: [
|
||||
Text('攻克次数', style: TextStyle(fontSize: 12.sp, color: _mutedText)),
|
||||
SizedBox(width: 10.w),
|
||||
_buildProgressBars(item),
|
||||
SizedBox(width: 10.w),
|
||||
_buildProgressCount(item),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildButtonGroup(item),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressBars(_CapturePackViewModel item) {
|
||||
return Row(
|
||||
children: List.generate(item.maxOvercomeCount, (idx) {
|
||||
final record = idx < item.records.length ? item.records[idx] : null;
|
||||
Color color;
|
||||
switch (CaptureRecordStatus.fromInt(record?.status)) {
|
||||
case CaptureRecordStatus.success:
|
||||
color = _success;
|
||||
break;
|
||||
case CaptureRecordStatus.fail:
|
||||
color = _error;
|
||||
break;
|
||||
case CaptureRecordStatus.processing:
|
||||
color = _processing;
|
||||
break;
|
||||
default:
|
||||
color = _barBg;
|
||||
}
|
||||
return Container(
|
||||
width: 20.w,
|
||||
height: 4.h,
|
||||
margin: EdgeInsets.only(right: idx == item.maxOvercomeCount - 1 ? 0 : 4.w),
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
borderRadius: BorderRadius.circular(4.r),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressCount(_CapturePackViewModel item) {
|
||||
final hasRecords = item.recordCount > 0 && item.records.isNotEmpty;
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
'${item.recordCount}/${item.maxOvercomeCount}',
|
||||
style: TextStyle(fontSize: 12.sp, color: _mutedText),
|
||||
),
|
||||
if (hasRecords)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 4.5.w),
|
||||
child: Icon(
|
||||
item.isShowCaptureInfo
|
||||
? Icons.keyboard_arrow_up
|
||||
: Icons.keyboard_arrow_down,
|
||||
size: 12.sp,
|
||||
color: _mutedText,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildButtonGroup(_CapturePackViewModel item) {
|
||||
switch (item.status) {
|
||||
case CapturePackStatus.notStarted:
|
||||
return _buildSolidButton('开始攻克', () => _navigateToCaptureInfo(item));
|
||||
case CapturePackStatus.inProgress:
|
||||
final text = item.records.isNotEmpty ? '继续攻克' : '开始攻克';
|
||||
return _buildSolidButton(text, () => _navigateToCaptureInfo(item));
|
||||
case CapturePackStatus.conquered:
|
||||
return _buildOutlineButton('已攻克', _success, null, opacity: 0.5);
|
||||
case CapturePackStatus.failed:
|
||||
return _buildOutlineButton('攻克失败', _error,
|
||||
() => _navigateToCaptureInfo(item),
|
||||
opacity: 0.5);
|
||||
case CapturePackStatus.laterStudy:
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => _navigateToCaptureInfo(item),
|
||||
child: Text(
|
||||
'后续学习',
|
||||
style: TextStyle(fontSize: 11.sp, color: _primary),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
_buildSolidButton('重新攻克', () => _navigateToCaptureInfo(item)),
|
||||
],
|
||||
);
|
||||
case CapturePackStatus.judging:
|
||||
return _buildOutlineButton('判题中', _success, null, opacity: 0.5);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildOutlineButton(String text, Color color, VoidCallback? onTap,
|
||||
{double opacity = 1.0}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Opacity(
|
||||
opacity: opacity,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h),
|
||||
height: 30.h,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: color, width: 1.w),
|
||||
borderRadius: BorderRadius.circular(30.r),
|
||||
color: onTap == null ? Colors.transparent : color.withOpacity(0.08),
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(fontSize: 13.sp, color: color, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSolidButton(String text, VoidCallback? onTap,
|
||||
{double opacity = 1.0}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Opacity(
|
||||
opacity: opacity,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 4.h),
|
||||
height: 30.h,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(30.r),
|
||||
color: _primary,
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 13.sp, color: Colors.white, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressInfo(_CapturePackViewModel item) {
|
||||
final validRecords =
|
||||
item.records.where((r) => r.status != 0).toList();
|
||||
if (validRecords.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return Container(
|
||||
margin: EdgeInsets.only(top: 16.h),
|
||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 10.5.h),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFAFBFD),
|
||||
borderRadius: BorderRadius.circular(6.r),
|
||||
border: Border.all(color: _barBg, width: 0.5.w),
|
||||
),
|
||||
child: Column(
|
||||
children: validRecords.asMap().entries.map((entry) {
|
||||
final infoIndex = entry.key;
|
||||
final record = entry.value;
|
||||
return GestureDetector(
|
||||
onTap: () => _getReport(record, item),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 5.h),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'第${_numberToChinese(infoIndex + 1)}次攻克',
|
||||
style: TextStyle(fontSize: 12.sp, color: _mutedText),
|
||||
),
|
||||
SizedBox(width: 13.w),
|
||||
Text(
|
||||
_formatDuration(0), // TODO: 后端 record 没有用时字段,需确认
|
||||
style: TextStyle(fontSize: 12.sp, color: _subtitleColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (CaptureRecordStatus.fromInt(record.status) ==
|
||||
CaptureRecordStatus.processing)
|
||||
Text('判题中',
|
||||
style: TextStyle(fontSize: 12.sp, color: _processing))
|
||||
else
|
||||
_buildInfoRight(record, item),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRight(CourseRecord record, _CapturePackViewModel pack) {
|
||||
final isSuccess = record.accuracyRate >= pack.overcomeStandard;
|
||||
final bgColor =
|
||||
isSuccess ? const Color(0xFFE8FAF6) : const Color(0xFFFFF0F0);
|
||||
final textColor = isSuccess ? _success : _error;
|
||||
final iconPath = isSuccess
|
||||
? '/StaticResource/2026-04-22/capture-list-success.png'
|
||||
: '/StaticResource/2026-04-22/capture-list-fail.png';
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 6.w, vertical: 1.5.h),
|
||||
decoration: BoxDecoration(
|
||||
color: bgColor,
|
||||
borderRadius: BorderRadius.circular(4.r),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CachedNetworkImage(
|
||||
imageUrl: ImgUrl.build(iconPath),
|
||||
width: 14.w,
|
||||
height: 14.w,
|
||||
fit: BoxFit.fitWidth,
|
||||
),
|
||||
SizedBox(width: 5.5.w),
|
||||
Text(
|
||||
'${record.accuracyRate}%',
|
||||
style: TextStyle(
|
||||
fontSize: 12.sp, color: textColor, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:new_learn/core/components/video_player_page.dart';
|
||||
import 'package:new_learn/features/auth/login_page.dart';
|
||||
import 'package:new_learn/features/conquerPackage/captureList/index.dart';
|
||||
import 'package:new_learn/pages/demo/align_demo.dart';
|
||||
import 'package:new_learn/pages/demo/animation_demo.dart';
|
||||
import 'package:new_learn/pages/demo/form_demo.dart';
|
||||
@@ -228,6 +230,21 @@ class TaskPage extends StatelessWidget {
|
||||
Icons.description_outlined,
|
||||
_page(ExamDemoPage.new),
|
||||
),
|
||||
_DemoItem(
|
||||
'攻克列表',
|
||||
'课程攻克包列表 + 进度条 + 展开详情(go_router 跳转 + extra 传参)',
|
||||
Icons.emoji_events_outlined,
|
||||
_page(() => const Scaffold()),
|
||||
onTap: (context) {
|
||||
// 延时到当前帧结束后再导航,避免 build 过程中触发 Router rebuild
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (context.mounted) {
|
||||
context.push('/conquerPackage/captureList',
|
||||
extra: '841985856751366');
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
];
|
||||
|
||||
@override
|
||||
@@ -278,9 +295,13 @@ class TaskPage extends StatelessWidget {
|
||||
),
|
||||
// 点击 → 路由跳转到对应 Demo 页面
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: demo.builder),
|
||||
);
|
||||
if (demo.onTap != null) {
|
||||
demo.onTap!(context);
|
||||
} else {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: demo.builder),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -308,10 +329,17 @@ WidgetBuilder _page(Widget Function() create) {
|
||||
|
||||
/// Demo 列表项数据模型
|
||||
class _DemoItem {
|
||||
const _DemoItem(this.title, this.subtitle, this.icon, this.builder);
|
||||
const _DemoItem(
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.icon,
|
||||
this.builder, {
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final IconData icon;
|
||||
final WidgetBuilder builder;
|
||||
final void Function(BuildContext context)? onTap;
|
||||
}
|
||||
|
||||
+337
-425
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user