修改iOS 权限弹框内容
This commit is contained in:
@@ -12,6 +12,7 @@ import 'package:new_learn/features/class/my_interact_page.dart';
|
|||||||
import 'package:new_learn/features/class/my_update_detail_page.dart';
|
import 'package:new_learn/features/class/my_update_detail_page.dart';
|
||||||
import 'package:new_learn/core/api/class/model.dart';
|
import 'package:new_learn/core/api/class/model.dart';
|
||||||
import 'package:new_learn/features/my/my_page.dart';
|
import 'package:new_learn/features/my/my_page.dart';
|
||||||
|
import 'package:new_learn/features/conquerPackage/captureList/index.dart';
|
||||||
import 'package:new_learn/features/my/my_point/my_point_page.dart';
|
import 'package:new_learn/features/my/my_point/my_point_page.dart';
|
||||||
import 'package:new_learn/features/recite/recite_page.dart';
|
import 'package:new_learn/features/recite/recite_page.dart';
|
||||||
import 'package:new_learn/features/task/task_page.dart';
|
import 'package:new_learn/features/task/task_page.dart';
|
||||||
@@ -144,6 +145,11 @@ final GoRouter appRouter = GoRouter(
|
|||||||
),
|
),
|
||||||
// ---------- 登录页 ----------
|
// ---------- 登录页 ----------
|
||||||
GoRoute(path: '/login', builder: (_, _) => const LoginPage()),
|
GoRoute(path: '/login', builder: (_, _) => const LoginPage()),
|
||||||
|
// ---------- 攻克包 - 截图名单 ----------
|
||||||
|
GoRoute(
|
||||||
|
path: '/conquerPackage/captureList',
|
||||||
|
builder: (_, _) => const CaptureListPage(),
|
||||||
|
),
|
||||||
// app_router.dart 的 routes 里加,不用管跳转逻辑
|
// app_router.dart 的 routes 里加,不用管跳转逻辑
|
||||||
// GoRoute(path: '/task/:id', builder: (_, _) => const TaskDetailPage()),
|
// GoRoute(path: '/task/:id', builder: (_, _) => const TaskDetailPage()),
|
||||||
// ---------- 以后新业务页在这里加,一行一个 ----------
|
// ---------- 以后新业务页在这里加,一行一个 ----------
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
|
|
||||||
|
class CaptureListPage extends StatelessWidget {
|
||||||
|
const CaptureListPage({super.key});
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Align 对齐布局 Demo11111')),
|
||||||
|
// ---------- 下方即为核心代码,可直接复制到 Scaffold body ----------
|
||||||
|
body: Container(
|
||||||
|
width: 300.w,
|
||||||
|
height: 300.h,
|
||||||
|
color: Colors.grey[300],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+329
-417
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user