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 _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( 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), ), ), ], ), ), ], ), ); } }