修改iOS 权限弹框内容
This commit is contained in:
@@ -0,0 +1,959 @@
|
||||
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/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 空状态
|
||||
///
|
||||
/// 二、换算规则(本工程统一):Vue 的 `N rpx` → Flutter 的 `(N/2).w/.h/.sp/.r`
|
||||
/// 例:间距 28rpx → 14.w;圆角 32rpx → 16.r;字号 28rpx → 14.sp
|
||||
///
|
||||
/// 三、状态(StatefulWidget):
|
||||
/// - remark: 规则说明文字
|
||||
/// - data: 攻克包列表
|
||||
/// - refreshing: 下拉刷新状态
|
||||
/// - isDataEmpty: 空数据标记
|
||||
///
|
||||
/// 四、接口暂未对接,使用 Mock 数据展示页面效果。
|
||||
/// ============================================================
|
||||
|
||||
/// 攻克记录状态枚举
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 攻克包状态
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 攻克记录模型
|
||||
class CaptureRecord {
|
||||
final String id;
|
||||
final CaptureRecordStatus status;
|
||||
final int? accuracyRate; // 正确率百分比
|
||||
final int? overcomeTimeSecond; // 攻克用时(秒)
|
||||
|
||||
const CaptureRecord({
|
||||
required this.id,
|
||||
required this.status,
|
||||
this.accuracyRate,
|
||||
this.overcomeTimeSecond,
|
||||
});
|
||||
}
|
||||
|
||||
/// 攻克包模型
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
||||
class CaptureListPage extends StatefulWidget {
|
||||
const CaptureListPage({super.key});
|
||||
|
||||
@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 = '';
|
||||
List<CapturePack> _data = [];
|
||||
bool _refreshing = false;
|
||||
bool _isDataEmpty = false;
|
||||
|
||||
@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,
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
_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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 切换攻克详情展开/收起
|
||||
void _toggleCaptureInfo(CapturePack item) {
|
||||
final validRecords = item.records.where((r) => r.status != CaptureRecordStatus.none).toList();
|
||||
if (validRecords.isEmpty) return;
|
||||
setState(() {
|
||||
item.isShowCaptureInfo = !item.isShowCaptureInfo;
|
||||
});
|
||||
}
|
||||
|
||||
/// 点击按钮跳转(占位,不接接口)
|
||||
void _navigateToCaptureInfo(CapturePack item) {
|
||||
if (item.status == CapturePackStatus.judging) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('该攻克正在判题中,暂不能操作'), behavior: SnackBarBehavior.floating),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// TODO(业务逻辑): 对接 GetStartOvercome 接口后跳转
|
||||
}
|
||||
|
||||
/// 点击报告(占位,不接接口)
|
||||
void _getReport(CaptureRecord record, CapturePack pack) {
|
||||
if (record.status == CaptureRecordStatus.processing) return;
|
||||
// TODO(业务逻辑): 跳转报告页
|
||||
}
|
||||
|
||||
/// 数字转中文(1-9)
|
||||
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;
|
||||
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)),
|
||||
// 顶部背景图(对标 HeaderBac,bg-size: 100% auto → width 100%,高度自适应)
|
||||
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,
|
||||
),
|
||||
),
|
||||
// 主内容(安全区内,含透明 AppBar + 规则卡片 + 列表)
|
||||
SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// 顶部导航栏(对标 NavBar,bg-color 透明,白色文字)
|
||||
SizedBox(
|
||||
height: 44.h,
|
||||
child: Stack(
|
||||
children: [
|
||||
const Center(
|
||||
child: Text(
|
||||
'攻克列表',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 16.w,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_back_ios, color: Colors.white, size: 20),
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 规则卡片(透明底 + 白边,置于渐变背景之上)
|
||||
_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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 子组件构建方法
|
||||
// ============================================================
|
||||
|
||||
/// 攻克规则说明卡片(透明底 + 白色边框 + 阴影,对标 Vue rule-view)
|
||||
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() {
|
||||
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(CapturePack 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(CapturePack 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: LinearGradient(
|
||||
colors: const [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: 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),
|
||||
),
|
||||
),
|
||||
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,
|
||||
),
|
||||
SizedBox(width: 5.5.w),
|
||||
Text(
|
||||
'$standard%',
|
||||
style: TextStyle(fontSize: 15.sp, color: _success, fontWeight: FontWeight.bold),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 进度 + 按钮 + 详情展开区
|
||||
Widget _buildCaptureProgress(CapturePack 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(CapturePack 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(CapturePack 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) {
|
||||
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(CapturePack 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(CapturePack item) {
|
||||
switch (item.status) {
|
||||
case CapturePackStatus.notStarted:
|
||||
return _buildOutlineButton('开始攻克', _primary, () => _navigateToCaptureInfo(item));
|
||||
case CapturePackStatus.inProgress:
|
||||
final text = item.records.isNotEmpty ? '继续攻克' : '开始攻克';
|
||||
return _buildOutlineButton(text, _primary, () => _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),
|
||||
_buildOutlineButton('重新攻克', _primary, () => _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 _buildProgressInfo(CapturePack item) {
|
||||
final validRecords = item.records.where((r) => r.status != CaptureRecordStatus.none).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 ?? 0),
|
||||
style: TextStyle(fontSize: 12.sp, color: _subtitleColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (record.status == CaptureRecordStatus.processing)
|
||||
Text('判题中', style: TextStyle(fontSize: 12.sp, color: _processing))
|
||||
else
|
||||
_buildInfoRight(record, item),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRight(CaptureRecord record, CapturePack item) {
|
||||
final isSuccess = (record.accuracyRate ?? 0) >= item.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 ?? 0}%',
|
||||
style: TextStyle(fontSize: 12.sp, color: textColor, fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user