import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; /// ============================================================ /// 【AnswerSheetDialog】答题卡 —— 题号网格 + 跳题 /// /// 一、职责: /// 1. 展示所有题号(GridView 4 列),颜色区分答题状态: /// 已答=蓝 / 未答=灰 / 当前题=绿; /// 2. 点击题号 → onJump(index) 跳转(由 ExamPage 调 jumpToPage)。 /// /// 二、为什么独立成组件: /// 答题卡是"总览 → 跳转"的通用交互,参数化设计后可复用于 /// 错题重做、试卷回顾等场景;不直接碰 PageController, /// 跳转动作通过 onJump 回调上抛,保持组件解耦。 /// /// 三、状态判定:answers 是 `Map`, /// 已答 = questionId 存在于 map(解答题空字符串算未答?—— /// 本 Demo 约定:answer 非空字符串才算已答)。 /// ============================================================ class AnswerSheetDialog extends StatelessWidget { const AnswerSheetDialog({ super.key, required this.questionIds, required this.answers, required this.currentIndex, required this.onJump, }); /// 题目列表(顺序即试卷顺序,用于定位题号) final List questionIds; /// 已答答案表(questionId → answer) final Map answers; /// 当前题索引(高亮绿色) final int currentIndex; /// 点击题号回调(传入索引,由页面执行跳转) final ValueChanged onJump; int get _answeredCount => questionIds .where((id) => (answers[id] ?? '').isNotEmpty) .length; @override Widget build(BuildContext context) { return Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16.r)), child: ConstrainedBox( constraints: BoxConstraints(maxHeight: 420.h), child: Padding( padding: EdgeInsets.all(16.w), child: Column( mainAxisSize: MainAxisSize.min, children: [ // ---------- 标题 + 已答统计 ---------- Row( children: [ const Text( '答题卡', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Color(0xFF333333), ), ), const Spacer(), Container( padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.h), decoration: BoxDecoration( color: const Color(0xFFEFF6FF), borderRadius: BorderRadius.circular(20.r), ), child: Text( '已答 $_answeredCount / ${questionIds.length}', style: const TextStyle( fontSize: 12, color: Color(0xFF3B82F6), fontWeight: FontWeight.w600, ), ), ), ], ), SizedBox(height: 12.h), // ---------- 图例 ---------- Row( children: [ _buildLegend(const Color(0xFF3B82F6), '已答'), SizedBox(width: 16.w), _buildLegend(const Color(0xFF22C55E), '当前'), SizedBox(width: 16.w), _buildLegend(const Color(0xFFE5E7EB), '未答'), ], ), SizedBox(height: 12.h), // ---------- 题号网格 ---------- Flexible( child: GridView.builder( shrinkWrap: true, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 5, mainAxisSpacing: 10, crossAxisSpacing: 10, childAspectRatio: 1.3, ), itemCount: questionIds.length, itemBuilder: (context, index) { final id = questionIds[index]; final answered = (answers[id] ?? '').isNotEmpty; final isCurrent = index == currentIndex; return _buildCell(index, answered, isCurrent); }, ), ), ], ), ), ), ); } Widget _buildLegend(Color color, String label) { return Row( children: [ Container( width: 12.w, height: 12.w, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(3.r), ), ), SizedBox(width: 4.w), Text( label, style: const TextStyle(fontSize: 11, color: Color(0xFF666666)), ), ], ); } Widget _buildCell(int index, bool answered, bool isCurrent) { final color = isCurrent ? const Color(0xFF22C55E) : answered ? const Color(0xFF3B82F6) : const Color(0xFFE5E7EB); return InkWell( onTap: () => onJump(index), borderRadius: BorderRadius.circular(6.r), child: Container( alignment: Alignment.center, decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(6.r), ), child: Text( '${index + 1}', style: TextStyle( fontSize: 13.sp, fontWeight: FontWeight.w600, color: isCurrent || answered ? Colors.white : const Color(0xFF666666), ), ), ), ); } }