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 get records => info.records; } class CaptureListPage extends StatefulWidget { const CaptureListPage({super.key, required this.courseId}); final String courseId; @override State createState() => _CaptureListPageState(); } class _CaptureListPageState extends State { // ========== 颜色常量 ========== 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 _expandedPackIds = []; // 展开的包 id(刷新时保留展开状态) int _requestId = 0; // 请求序号,防止并发请求错乱 @override void initState() { super.initState(); } // ========== 接口 ========== /// 分页加载:CommonPagedList 回调。接口无分页,当作一页返回,hasNext=false。 Future> _loadPage(int pageKey) async { final requestId = ++_requestId; final expandedIds = _expandedPackIds.isNotEmpty ? [..._expandedPackIds] : []; // 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 list, List 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 _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 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), ), ], ), ); } }