flutter架 构项目基础
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user