flutter架 构项目基础

This commit is contained in:
崔宝新
2026-09-04 15:22:20 +08:00
parent 70967229d0
commit 0c5aee59c0
265 changed files with 35484 additions and 0 deletions
+198
View File
@@ -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),
),
),
],
),
),
],
),
);
}
}