Files
2026-09-28 20:12:34 +08:00

940 lines
30 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:go_router/go_router.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';
import 'package:new_learn/features/conquerPackage/classExercises/controllers/exercise_timing.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('/conquerPackage/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
? '/conquerPackage/transitionPage'
: '/conquerPackage/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;
}
}
/// 页面跳转(go_router push + extra 传参)
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),
// );
context.push(path, extra: params);
}
/// 点击报告(跳转报告页,对标 Vue getReport)
/// recordId=攻克记录 id,packId=攻克包 id;判题中(status==3)不可点
void _getReport(CourseRecord record, _CapturePackViewModel pack) {
if (CaptureRecordStatus.fromInt(record.status) ==
CaptureRecordStatus.processing) return;
_navigateTo('/conquerPackage/report', {
'recordId': '${record.id}',
'packId': pack.id,
});
}
// ========== 工具方法 ==========
String _numberToChinese(int num) {
const chineseNum = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九'];
return chineseNum[num.clamp(0, 9)];
}
// ========== 构建 ==========
@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(record.overcomeTimeSecond),
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),
),
],
),
);
}
}