Files
2026-09-04 15:22:20 +08:00

199 lines
6.8 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_riverpod/flutter_riverpod.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:new_learn/features/exam/exam_page.dart';
import 'package:new_learn/features/exam/paper_provider.dart';
/// ============================================================
/// 【ExamDemoPage】试卷练习 Demo 入口 —— 大数据滑动验证页
///
/// 一、本 Demo 验证什么(对标 Vue3 swiper 性能瓶颈):
/// PageView.builder 懒加载:100 道题只构建视口 ±1 页,
/// 滑出的页面 Element 自动回收 → 大数组不卡;
/// 同时验证题型分发(单选/多选/判断/解答)与续答持久化。
///
/// 二、演示要点:
/// 1. 选择题目数量(50/100/200)→ 开始练习;
/// 2. 快速左右滑动感受流畅度(allowImplicitScrolling 预构建);
/// 3. 答几题后退出页面 → 重新进入 → SnackBar 提示恢复进度;
/// 4. 交卷 → 自动判分(客观题)+ 清理本地进度。
///
/// 三、续答持久化说明:
/// 答案主状态在 Riverpod(内存),Isar AnswerRecord 是备份层;
/// Web 调试时 store 为空实现 → 自动降级为纯内存(不落盘)。
/// ============================================================
class ExamDemoPage extends ConsumerWidget {
const ExamDemoPage({super.key});
static const List<int> _sizes = [50, 100, 200];
@override
Widget build(BuildContext context, WidgetRef ref) {
final size = ref.watch(paperSizeProvider);
return Scaffold(
appBar: AppBar(title: const Text('试卷练习 Demo(大数据滑动)')),
body: ListView(
padding: EdgeInsets.all(16.w),
children: [
_buildTipCard(),
SizedBox(height: 16.h),
_buildSizeCard(context, ref, size),
SizedBox(height: 16.h),
FilledButton.icon(
style: FilledButton.styleFrom(
padding: EdgeInsets.symmetric(vertical: 14.h),
),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => const ExamPage()),
);
},
icon: const Icon(Icons.play_arrow),
label: Text(
'开始练习($size 题)',
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
),
),
SizedBox(height: 16.h),
const Text(
'架构要点',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xFF3B82F6),
),
),
SizedBox(height: 8.h),
const _ArchPoint(
icon: Icons.bolt,
title: 'PageView.builder 懒加载',
desc: '只构建视口 ±1 页,滑出即回收;allowImplicitScrolling 预构建相邻页,'
'100 题滑动不掉帧 —— 规避 Vue3 swiper 全量 DOM 挂载的瓶颈。',
),
SizedBox(height: 10.h),
const _ArchPoint(
icon: Icons.account_tree_outlined,
title: '题型分发(QuestionCard)',
desc: '按 question.type switch 到单选/多选/判断/解答视图;'
'组件纯展示不碰 provider,ExamPage 唯一桥接 Riverpod。',
),
SizedBox(height: 10.h),
const _ArchPoint(
icon: Icons.save_outlined,
title: '续答持久化(Riverpod + Isar)',
desc: '答案主状态在内存,每次作答异步写 AnswerRecord(Isar);'
'退出重进自动恢复进度,交卷成功后按 paperId 清理。',
),
],
),
);
}
Widget _buildTipCard() {
return Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: const Color(0xFFE8F0FE),
borderRadius: BorderRadius.circular(12.r),
),
child: const Text(
'模拟真实试卷:100 题含单选/多选/判断/解答四种题型,'
'题干为 HTML 富文本(RichTextContent 渲染)。'
'快速滑动体验大数据流畅度;答几题后退出一键返回,重新进入可恢复进度。',
style: TextStyle(fontSize: 13, height: 1.6, color: Color(0xFF3B82F6)),
),
);
}
Widget _buildSizeCard(BuildContext context, WidgetRef ref, int size) {
return Container(
padding: EdgeInsets.all(16.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(color: const Color(0xFFEEF1F5)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'试卷题目数量(大数据规模)',
style: TextStyle(
fontSize: 14.sp,
fontWeight: FontWeight.w600,
color: const Color(0xFF333333),
),
),
SizedBox(height: 12.h),
SegmentedButton<int>(
segments: [
for (final s in _sizes)
ButtonSegment(value: s, label: Text('$s 题')),
],
selected: {size},
onSelectionChanged: (selection) {
ref.read(paperSizeProvider.notifier).setSize(selection.first);
},
),
],
),
);
}
}
/// 架构要点行(图标 + 标题 + 描述)
class _ArchPoint extends StatelessWidget {
const _ArchPoint({
required this.icon,
required this.title,
required this.desc,
});
final IconData icon;
final String title;
final String desc;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(12.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12.r),
border: Border.all(color: const Color(0xFFEEF1F5)),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, size: 20, color: const Color(0xFF3B82F6)),
SizedBox(width: 10.w),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Color(0xFF333333),
),
),
SizedBox(height: 4.h),
Text(
desc,
style: TextStyle(
fontSize: 12.sp,
height: 1.6,
color: const Color(0xFF666666),
),
),
],
),
),
],
),
);
}
}