合并代买
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -23,3 +23,33 @@ plugins {
|
||||
}
|
||||
|
||||
include ":app"
|
||||
|
||||
// ============================================================
|
||||
// 【Gradle 兼容补丁】flutter_inappwebview_android 1.1.3 的 AGP 9 兼容问题
|
||||
//
|
||||
// 问题:AGP 9 不支持 proguard-android.txt,必须用 proguard-android-optimize.txt。
|
||||
// 插件 build.gradle 写死了 proguard-android.txt → Gradle 解析时就报错
|
||||
// (debug 构建也炸,因为 AGP 9 在解析阶段就校验了这个方法调用)。
|
||||
//
|
||||
// 修复:在 Gradle 解析插件 build.gradle 之前(beforeProject 钩子),
|
||||
// 把插件文件里的字符串替换掉。这个钩子比 Gradle 自带机制还早触发,
|
||||
// 所以能赶在 Gradle 报错前修好。
|
||||
//
|
||||
// 优势:
|
||||
// - Gradle 原生,不依赖 Python/PowerShell
|
||||
// - 跨平台(Windows/Mac/Linux,Gradle 必备)
|
||||
// - 所有同事拉代码后自动生效
|
||||
// - Gradle daemon 每次构建都会重新解析 build.gradle,不会有缓存问题
|
||||
// ============================================================
|
||||
gradle.beforeProject { Project project ->
|
||||
if (project.name == 'flutter_inappwebview_android') {
|
||||
def gradleFile = project.file('build.gradle')
|
||||
def text = gradleFile.text
|
||||
if (text.contains("proguard-android.txt")) {
|
||||
gradleFile.text = text.replace(
|
||||
"proguard-android.txt", "proguard-android-optimize.txt"
|
||||
)
|
||||
println "✅ Patched flutter_inappwebview_android/build.gradle (AGP 9 compat)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -148,7 +148,8 @@ final GoRouter appRouter = GoRouter(
|
||||
// ---------- 攻克包 - 截图名单 ----------
|
||||
GoRoute(
|
||||
path: '/conquerPackage/captureList',
|
||||
builder: (_, _) => const CaptureListPage(),
|
||||
builder: (_, state) =>
|
||||
CaptureListPage(courseId: state.extra as String),
|
||||
),
|
||||
// app_router.dart 的 routes 里加,不用管跳转逻辑
|
||||
// GoRoute(path: '/task/:id', builder: (_, _) => const TaskDetailPage()),
|
||||
|
||||
@@ -1,41 +1,28 @@
|
||||
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
|
||||
///
|
||||
/// 一、Vue 源结构:
|
||||
/// HeaderBac + NavBar(title: "攻克列表", bg-color: #ADD1FF00)
|
||||
/// ├── rule-view 攻克规则说明卡片(remark 富文本)
|
||||
/// └── capture-list-refresh-wrap
|
||||
/// └── up-pull-refresh
|
||||
/// ├── captureList 攻克项列表
|
||||
/// │ └── capture-item
|
||||
/// │ ├── capture-info (左:标题+学科标签;右:攻克标准百分比)
|
||||
/// │ └── capture-progress
|
||||
/// │ ├── progress-top (进度条 + 右侧按钮)
|
||||
/// │ └── capture-progress-info (展开的历次攻克详情)
|
||||
/// └── DataEmpty 空状态
|
||||
/// 入参:courseId(课程 id)
|
||||
///
|
||||
/// 二、换算规则(本工程统一):Vue 的 `N rpx` → Flutter 的 `(N/2).w/.h/.sp/.r`
|
||||
/// 例:间距 28rpx → 14.w;圆角 32rpx → 16.r;字号 28rpx → 14.sp
|
||||
/// 接口:
|
||||
/// - CaptureListApi.getCoursePack(courseId) → 列表 + remark
|
||||
/// - CaptureListApi.startOvercome(packId) → 开始/继续攻克,跳转对应页面
|
||||
///
|
||||
/// 三、状态(StatefulWidget):
|
||||
/// - remark: 规则说明文字
|
||||
/// - data: 攻克包列表
|
||||
/// - refreshing: 下拉刷新状态
|
||||
/// - isDataEmpty: 空数据标记
|
||||
///
|
||||
/// 四、接口暂未对接,使用 Mock 数据展示页面效果。
|
||||
/// 换算规则(本工程统一):Vue 的 `N rpx` → Flutter 的 `(N/2).w/.h/.sp/.r`
|
||||
/// ============================================================
|
||||
|
||||
/// 攻克记录状态枚举
|
||||
/// 攻克记录状态枚举(与后端 status 对齐:0未开始/1成功/2失败/3判题中)
|
||||
enum CaptureRecordStatus {
|
||||
none(0), // 未开始
|
||||
success(1), // 成功
|
||||
fail(2), // 失败
|
||||
none(0), // 未开始
|
||||
success(1), // 成功
|
||||
fail(2), // 失败
|
||||
processing(3); // 判题中
|
||||
|
||||
final int value;
|
||||
@@ -49,14 +36,14 @@ enum CaptureRecordStatus {
|
||||
}
|
||||
}
|
||||
|
||||
/// 攻克包状态
|
||||
/// 攻克包状态枚举(与后端 status 对齐)
|
||||
enum CapturePackStatus {
|
||||
notStarted(0), // 未开始
|
||||
inProgress(1), // 进行中
|
||||
conquered(2), // 已攻克
|
||||
failed(3), // 攻克失败
|
||||
laterStudy(4), // 后续学习
|
||||
judging(5); // 判题中
|
||||
notStarted(0), // 未开始
|
||||
inProgress(1), // 进行中
|
||||
conquered(2), // 已攻克
|
||||
failed(3), // 攻克失败
|
||||
laterStudy(4), // 后续学习
|
||||
judging(5); // 判题中
|
||||
|
||||
final int value;
|
||||
const CapturePackStatus(this.value);
|
||||
@@ -69,48 +56,27 @@ enum CapturePackStatus {
|
||||
}
|
||||
}
|
||||
|
||||
/// 攻克记录模型
|
||||
class CaptureRecord {
|
||||
final String id;
|
||||
final CaptureRecordStatus status;
|
||||
final int? accuracyRate; // 正确率百分比
|
||||
final int? overcomeTimeSecond; // 攻克用时(秒)
|
||||
/// 页面内部包装模型(在 CourseInfo 基础上增加 UI 状态 isShowCaptureInfo)
|
||||
class _CapturePackViewModel {
|
||||
final CourseInfo info;
|
||||
bool isShowCaptureInfo;
|
||||
|
||||
const CaptureRecord({
|
||||
required this.id,
|
||||
required this.status,
|
||||
this.accuracyRate,
|
||||
this.overcomeTimeSecond,
|
||||
});
|
||||
}
|
||||
_CapturePackViewModel(this.info, {this.isShowCaptureInfo = false});
|
||||
|
||||
/// 攻克包模型
|
||||
class CapturePack {
|
||||
final String id;
|
||||
final String name;
|
||||
final String baseSubjectName;
|
||||
final int overcomeStandard; // 攻克标准百分比
|
||||
final int maxOvercomeCount; // 最大攻克次数
|
||||
final int recordCount; // 已进行次数
|
||||
final CapturePackStatus status;
|
||||
final List<CaptureRecord> records;
|
||||
bool isShowCaptureInfo; // 是否展开详情
|
||||
|
||||
CapturePack({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.baseSubjectName,
|
||||
required this.overcomeStandard,
|
||||
required this.maxOvercomeCount,
|
||||
required this.recordCount,
|
||||
required this.status,
|
||||
required this.records,
|
||||
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});
|
||||
const CaptureListPage({super.key, required this.courseId});
|
||||
|
||||
final String courseId;
|
||||
|
||||
@override
|
||||
State<CaptureListPage> createState() => _CaptureListPageState();
|
||||
@@ -130,209 +96,195 @@ class _CaptureListPageState extends State<CaptureListPage> {
|
||||
|
||||
// ========== 状态 ==========
|
||||
String _remark = '';
|
||||
List<CapturePack> _data = [];
|
||||
bool _refreshing = false;
|
||||
bool _isDataEmpty = false;
|
||||
bool _navigating = false; // 防止连续点击重复请求/跳转
|
||||
List<String> _expandedPackIds = []; // 展开的包 id(刷新时保留展开状态)
|
||||
int _requestId = 0; // 请求序号,防止并发请求错乱
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadMockData();
|
||||
}
|
||||
|
||||
/// Mock 数据 —— 替代真实接口,仅用于展示页面效果
|
||||
void _loadMockData() {
|
||||
_remark = '1. 每次攻克需达到指定正确率才算成功;\n'
|
||||
'2. 攻克失败可重新挑战,最多可挑战 N 次;\n'
|
||||
'3. 判题中状态下不能进行操作;\n'
|
||||
'4. 详情可查看每次攻克的用时与正确率。';
|
||||
// ========== 接口 ==========
|
||||
|
||||
_data = [
|
||||
CapturePack(
|
||||
id: '1',
|
||||
name: '一元一次方程入门',
|
||||
baseSubjectName: '数学',
|
||||
overcomeStandard: 80,
|
||||
maxOvercomeCount: 3,
|
||||
recordCount: 2,
|
||||
status: CapturePackStatus.inProgress,
|
||||
records: [
|
||||
CaptureRecord(
|
||||
id: 'r1-1',
|
||||
status: CaptureRecordStatus.fail,
|
||||
accuracyRate: 60,
|
||||
overcomeTimeSecond: 480,
|
||||
),
|
||||
CaptureRecord(
|
||||
id: 'r1-2',
|
||||
status: CaptureRecordStatus.success,
|
||||
accuracyRate: 90,
|
||||
overcomeTimeSecond: 600,
|
||||
),
|
||||
],
|
||||
isShowCaptureInfo: true,
|
||||
),
|
||||
CapturePack(
|
||||
id: '2',
|
||||
name: '英语单词 Unit 3',
|
||||
baseSubjectName: '英语',
|
||||
overcomeStandard: 70,
|
||||
maxOvercomeCount: 5,
|
||||
recordCount: 0,
|
||||
status: CapturePackStatus.notStarted,
|
||||
records: [],
|
||||
),
|
||||
CapturePack(
|
||||
id: '3',
|
||||
name: '文言文翻译技巧',
|
||||
baseSubjectName: '语文',
|
||||
overcomeStandard: 75,
|
||||
maxOvercomeCount: 3,
|
||||
recordCount: 3,
|
||||
status: CapturePackStatus.conquered,
|
||||
records: [
|
||||
CaptureRecord(
|
||||
id: 'r3-1',
|
||||
status: CaptureRecordStatus.fail,
|
||||
accuracyRate: 50,
|
||||
overcomeTimeSecond: 300,
|
||||
),
|
||||
CaptureRecord(
|
||||
id: 'r3-2',
|
||||
status: CaptureRecordStatus.fail,
|
||||
accuracyRate: 65,
|
||||
overcomeTimeSecond: 420,
|
||||
),
|
||||
CaptureRecord(
|
||||
id: 'r3-3',
|
||||
status: CaptureRecordStatus.success,
|
||||
accuracyRate: 88,
|
||||
overcomeTimeSecond: 540,
|
||||
),
|
||||
],
|
||||
),
|
||||
CapturePack(
|
||||
id: '4',
|
||||
name: '物理力学综合',
|
||||
baseSubjectName: '物理',
|
||||
overcomeStandard: 85,
|
||||
maxOvercomeCount: 3,
|
||||
recordCount: 1,
|
||||
status: CapturePackStatus.judging,
|
||||
records: [
|
||||
CaptureRecord(
|
||||
id: 'r4-1',
|
||||
status: CaptureRecordStatus.processing,
|
||||
accuracyRate: null,
|
||||
overcomeTimeSecond: 700,
|
||||
),
|
||||
],
|
||||
isShowCaptureInfo: true,
|
||||
),
|
||||
CapturePack(
|
||||
id: '5',
|
||||
name: '化学方程式配平',
|
||||
baseSubjectName: '化学',
|
||||
overcomeStandard: 80,
|
||||
maxOvercomeCount: 3,
|
||||
recordCount: 3,
|
||||
status: CapturePackStatus.failed,
|
||||
records: [
|
||||
CaptureRecord(
|
||||
id: 'r5-1',
|
||||
status: CaptureRecordStatus.fail,
|
||||
accuracyRate: 40,
|
||||
overcomeTimeSecond: 200,
|
||||
),
|
||||
CaptureRecord(
|
||||
id: 'r5-2',
|
||||
status: CaptureRecordStatus.fail,
|
||||
accuracyRate: 55,
|
||||
overcomeTimeSecond: 300,
|
||||
),
|
||||
CaptureRecord(
|
||||
id: 'r5-3',
|
||||
status: CaptureRecordStatus.fail,
|
||||
accuracyRate: 60,
|
||||
overcomeTimeSecond: 400,
|
||||
),
|
||||
],
|
||||
),
|
||||
CapturePack(
|
||||
id: '6',
|
||||
name: '历史时间轴梳理',
|
||||
baseSubjectName: '历史',
|
||||
overcomeStandard: 70,
|
||||
maxOvercomeCount: 3,
|
||||
recordCount: 2,
|
||||
status: CapturePackStatus.laterStudy,
|
||||
records: [
|
||||
CaptureRecord(
|
||||
id: 'r6-1',
|
||||
status: CaptureRecordStatus.success,
|
||||
accuracyRate: 80,
|
||||
overcomeTimeSecond: 360,
|
||||
),
|
||||
CaptureRecord(
|
||||
id: 'r6-2',
|
||||
status: CaptureRecordStatus.success,
|
||||
accuracyRate: 95,
|
||||
overcomeTimeSecond: 300,
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
/// 分页加载:CommonPagedList 回调。接口无分页,当作一页返回,hasNext=false。
|
||||
Future<PagingResult<_CapturePackViewModel>> _loadPage(int pageKey) async {
|
||||
final requestId = ++_requestId;
|
||||
final expandedIds = _expandedPackIds.isNotEmpty
|
||||
? [..._expandedPackIds]
|
||||
: <String>[];
|
||||
|
||||
_isDataEmpty = _data.isEmpty;
|
||||
}
|
||||
|
||||
/// 下拉刷新
|
||||
Future<void> _onRefresh() async {
|
||||
if (_refreshing) return;
|
||||
setState(() => _refreshing = true);
|
||||
// 模拟网络请求延迟
|
||||
await Future.delayed(const Duration(milliseconds: 800));
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_loadMockData();
|
||||
_refreshing = false;
|
||||
});
|
||||
// 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(CapturePack item) {
|
||||
final validRecords = item.records.where((r) => r.status != CaptureRecordStatus.none).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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 点击按钮跳转(占位,不接接口)
|
||||
void _navigateToCaptureInfo(CapturePack item) {
|
||||
// ========== 点击事件 ==========
|
||||
|
||||
/// 点击按钮 → 开始/继续攻克(对接 startOvercome 接口)
|
||||
Future<void> _navigateToCaptureInfo(_CapturePackViewModel item) async {
|
||||
if (item.status == CapturePackStatus.judging) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('该攻克正在判题中,暂不能操作'), behavior: SnackBarBehavior.floating),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// TODO(业务逻辑): 对接 GetStartOvercome 接口后跳转
|
||||
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 _getReport(CaptureRecord record, CapturePack pack) {
|
||||
if (record.status == CaptureRecordStatus.processing) return;
|
||||
// TODO(业务逻辑): 跳转报告页
|
||||
/// 页面跳转(占位 —— 待路由统一后替换为正式路由名)
|
||||
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),
|
||||
);
|
||||
}
|
||||
|
||||
/// 数字转中文(1-9)
|
||||
/// 点击报告(跳转报告页)
|
||||
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)];
|
||||
}
|
||||
|
||||
/// 秒数格式化:mm分ss秒 / h时mm分
|
||||
String _formatDuration(int seconds) {
|
||||
if (seconds <= 0) return '0秒';
|
||||
final h = seconds ~/ 3600;
|
||||
@@ -343,15 +295,16 @@ class _CaptureListPageState extends State<CaptureListPage> {
|
||||
return '${s}秒';
|
||||
}
|
||||
|
||||
// ========== 构建 ==========
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: _bgColor,
|
||||
body: Stack(
|
||||
children: [
|
||||
// 页面底色
|
||||
const Positioned.fill(child: ColoredBox(color: _bgColor)),
|
||||
// 顶部背景图(对标 HeaderBac,bg-size: 100% auto → width 100%,高度自适应)
|
||||
// 顶部背景图
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
@@ -362,13 +315,15 @@ class _CaptureListPageState extends State<CaptureListPage> {
|
||||
),
|
||||
fit: BoxFit.fitWidth,
|
||||
width: double.infinity,
|
||||
placeholder: (context, url) => const SizedBox.shrink(),
|
||||
errorWidget: (context, url, error) => const SizedBox.shrink(),
|
||||
),
|
||||
),
|
||||
// 主内容(安全区内,含透明 AppBar + 规则卡片 + 列表)
|
||||
// 主内容
|
||||
SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// 顶部导航栏(对标 NavBar,bg-color 透明,白色文字)
|
||||
// 导航栏
|
||||
SizedBox(
|
||||
height: 44.h,
|
||||
child: Stack(
|
||||
@@ -377,7 +332,7 @@ class _CaptureListPageState extends State<CaptureListPage> {
|
||||
child: Text(
|
||||
'攻克列表',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
color: Color.fromRGBO(0, 0, 0, 0.8),
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
@@ -388,7 +343,8 @@ class _CaptureListPageState extends State<CaptureListPage> {
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios, color: Colors.white, size: 20),
|
||||
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(),
|
||||
@@ -397,31 +353,17 @@ class _CaptureListPageState extends State<CaptureListPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
// 规则卡片(透明底 + 白边,置于渐变背景之上)
|
||||
// 规则卡片
|
||||
_buildRuleView(),
|
||||
SizedBox(height: 19.h),
|
||||
// 列表区域
|
||||
// 列表
|
||||
Expanded(
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _onRefresh,
|
||||
child: CustomScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
if (_isDataEmpty)
|
||||
SliverFillRemaining(child: _buildEmpty())
|
||||
else
|
||||
SliverPadding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 14.w),
|
||||
sliver: SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) => _buildCaptureItem(_data[index]),
|
||||
childCount: _data.length,
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(child: SizedBox(height: 20.h)),
|
||||
],
|
||||
),
|
||||
child: CommonPagedList<_CapturePackViewModel>(
|
||||
load: _loadPage,
|
||||
buildItem: (context, item, index) => _buildCaptureItem(item),
|
||||
padding: EdgeInsets.symmetric(horizontal: 14.w),
|
||||
emptyWidget: _buildEmpty(),
|
||||
noMoreText: '', // 只有一页,不显示"没有更多"
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -436,7 +378,6 @@ class _CaptureListPageState extends State<CaptureListPage> {
|
||||
// 子组件构建方法
|
||||
// ============================================================
|
||||
|
||||
/// 攻克规则说明卡片(透明底 + 白色边框 + 阴影,对标 Vue rule-view)
|
||||
Widget _buildRuleView() {
|
||||
return Container(
|
||||
margin: EdgeInsets.fromLTRB(14.w, 16.h, 14.w, 0),
|
||||
@@ -514,6 +455,10 @@ class _CaptureListPageState extends State<CaptureListPage> {
|
||||
}
|
||||
|
||||
Widget _buildRuleContent() {
|
||||
if (_remark.isEmpty) {
|
||||
return Text('暂无规则说明',
|
||||
style: TextStyle(fontSize: 14.sp, color: _subtitleColor));
|
||||
}
|
||||
final lines = _remark.split('\n');
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -546,7 +491,6 @@ class _CaptureListPageState extends State<CaptureListPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 空状态
|
||||
Widget _buildEmpty() {
|
||||
return Center(
|
||||
child: Column(
|
||||
@@ -564,8 +508,7 @@ class _CaptureListPageState extends State<CaptureListPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 攻克项卡片
|
||||
Widget _buildCaptureItem(CapturePack item) {
|
||||
Widget _buildCaptureItem(_CapturePackViewModel item) {
|
||||
return Container(
|
||||
margin: EdgeInsets.only(bottom: 14.h),
|
||||
decoration: BoxDecoration(
|
||||
@@ -586,8 +529,7 @@ class _CaptureListPageState extends State<CaptureListPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 攻克项头部信息
|
||||
Widget _buildCaptureInfo(CapturePack item) {
|
||||
Widget _buildCaptureInfo(_CapturePackViewModel item) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(14.w, 18.5.h, 14.w, 0),
|
||||
child: Row(
|
||||
@@ -643,8 +585,8 @@ class _CaptureListPageState extends State<CaptureListPage> {
|
||||
padding: EdgeInsets.symmetric(horizontal: 1.w, vertical: 1.h),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6.r),
|
||||
gradient: LinearGradient(
|
||||
colors: const [Color(0xFF5A9EFF), Color(0xFFF2F8FF), Color(0xFF5A9EFF)],
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFF5A9EFF), Color(0xFFF2F8FF), Color(0xFF5A9EFF)],
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
),
|
||||
@@ -667,7 +609,6 @@ class _CaptureListPageState extends State<CaptureListPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 右侧攻克标准百分比盒子
|
||||
Widget _buildStandardBox(int standard) {
|
||||
return Container(
|
||||
width: 60.w,
|
||||
@@ -686,37 +627,48 @@ class _CaptureListPageState extends State<CaptureListPage> {
|
||||
Expanded(
|
||||
flex: 1,
|
||||
child: Center(
|
||||
child: Text(
|
||||
'攻克标准',
|
||||
style: TextStyle(fontSize: 12.sp, color: Colors.white),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
'攻克标准',
|
||||
style: TextStyle(fontSize: 12.sp, color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 1.5.h, horizontal: 4.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(6.r),
|
||||
bottomRight: Radius.circular(6.r),
|
||||
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: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
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,
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(width: 5.5.w),
|
||||
Text(
|
||||
'$standard%',
|
||||
style: TextStyle(fontSize: 15.sp, color: _success, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -724,8 +676,7 @@ class _CaptureListPageState extends State<CaptureListPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 进度 + 按钮 + 详情展开区
|
||||
Widget _buildCaptureProgress(CapturePack item) {
|
||||
Widget _buildCaptureProgress(_CapturePackViewModel item) {
|
||||
return Container(
|
||||
margin: EdgeInsets.only(top: 10.5.h),
|
||||
padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 16.h),
|
||||
@@ -742,7 +693,7 @@ class _CaptureListPageState extends State<CaptureListPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressTop(CapturePack item) {
|
||||
Widget _buildProgressTop(_CapturePackViewModel item) {
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -765,12 +716,12 @@ class _CaptureListPageState extends State<CaptureListPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressBars(CapturePack 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 (record?.status ?? CaptureRecordStatus.none) {
|
||||
switch (CaptureRecordStatus.fromInt(record?.status)) {
|
||||
case CaptureRecordStatus.success:
|
||||
color = _success;
|
||||
break;
|
||||
@@ -796,7 +747,7 @@ class _CaptureListPageState extends State<CaptureListPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProgressCount(CapturePack item) {
|
||||
Widget _buildProgressCount(_CapturePackViewModel item) {
|
||||
final hasRecords = item.recordCount > 0 && item.records.isNotEmpty;
|
||||
return Row(
|
||||
children: [
|
||||
@@ -808,7 +759,9 @@ class _CaptureListPageState extends State<CaptureListPage> {
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 4.5.w),
|
||||
child: Icon(
|
||||
item.isShowCaptureInfo ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down,
|
||||
item.isShowCaptureInfo
|
||||
? Icons.keyboard_arrow_up
|
||||
: Icons.keyboard_arrow_down,
|
||||
size: 12.sp,
|
||||
color: _mutedText,
|
||||
),
|
||||
@@ -817,18 +770,19 @@ class _CaptureListPageState extends State<CaptureListPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 右侧按钮组(根据状态不同显示)
|
||||
Widget _buildButtonGroup(CapturePack item) {
|
||||
Widget _buildButtonGroup(_CapturePackViewModel item) {
|
||||
switch (item.status) {
|
||||
case CapturePackStatus.notStarted:
|
||||
return _buildOutlineButton('开始攻克', _primary, () => _navigateToCaptureInfo(item));
|
||||
return _buildSolidButton('开始攻克', () => _navigateToCaptureInfo(item));
|
||||
case CapturePackStatus.inProgress:
|
||||
final text = item.records.isNotEmpty ? '继续攻克' : '开始攻克';
|
||||
return _buildOutlineButton(text, _primary, () => _navigateToCaptureInfo(item));
|
||||
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);
|
||||
return _buildOutlineButton('攻克失败', _error,
|
||||
() => _navigateToCaptureInfo(item),
|
||||
opacity: 0.5);
|
||||
case CapturePackStatus.laterStudy:
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -841,7 +795,7 @@ class _CaptureListPageState extends State<CaptureListPage> {
|
||||
),
|
||||
),
|
||||
SizedBox(width: 8.w),
|
||||
_buildOutlineButton('重新攻克', _primary, () => _navigateToCaptureInfo(item)),
|
||||
_buildSolidButton('重新攻克', () => _navigateToCaptureInfo(item)),
|
||||
],
|
||||
);
|
||||
case CapturePackStatus.judging:
|
||||
@@ -849,7 +803,8 @@ class _CaptureListPageState extends State<CaptureListPage> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildOutlineButton(String text, Color color, VoidCallback? onTap, {double opacity = 1.0}) {
|
||||
Widget _buildOutlineButton(String text, Color color, VoidCallback? onTap,
|
||||
{double opacity = 1.0}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
@@ -873,9 +828,34 @@ class _CaptureListPageState extends State<CaptureListPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 展开的历次攻克详情
|
||||
Widget _buildProgressInfo(CapturePack item) {
|
||||
final validRecords = item.records.where((r) => r.status != CaptureRecordStatus.none).toList();
|
||||
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(
|
||||
@@ -906,13 +886,15 @@ class _CaptureListPageState extends State<CaptureListPage> {
|
||||
),
|
||||
SizedBox(width: 13.w),
|
||||
Text(
|
||||
_formatDuration(record.overcomeTimeSecond ?? 0),
|
||||
_formatDuration(0), // TODO: 后端 record 没有用时字段,需确认
|
||||
style: TextStyle(fontSize: 12.sp, color: _subtitleColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (record.status == CaptureRecordStatus.processing)
|
||||
Text('判题中', style: TextStyle(fontSize: 12.sp, color: _processing))
|
||||
if (CaptureRecordStatus.fromInt(record.status) ==
|
||||
CaptureRecordStatus.processing)
|
||||
Text('判题中',
|
||||
style: TextStyle(fontSize: 12.sp, color: _processing))
|
||||
else
|
||||
_buildInfoRight(record, item),
|
||||
],
|
||||
@@ -924,9 +906,10 @@ class _CaptureListPageState extends State<CaptureListPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRight(CaptureRecord record, CapturePack item) {
|
||||
final isSuccess = (record.accuracyRate ?? 0) >= item.overcomeStandard;
|
||||
final bgColor = isSuccess ? const Color(0xFFE8FAF6) : const Color(0xFFFFF0F0);
|
||||
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'
|
||||
@@ -949,8 +932,9 @@ class _CaptureListPageState extends State<CaptureListPage> {
|
||||
),
|
||||
SizedBox(width: 5.5.w),
|
||||
Text(
|
||||
'${record.accuracyRate ?? 0}%',
|
||||
style: TextStyle(fontSize: 12.sp, color: textColor, fontWeight: FontWeight.w500),
|
||||
'${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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user