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

171 lines
5.7 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_screenutil/flutter_screenutil.dart';
/// ============================================================
/// 【AnswerSheetDialog】答题卡 —— 题号网格 + 跳题
///
/// 一、职责:
/// 1. 展示所有题号(GridView 4 列),颜色区分答题状态:
/// 已答=蓝 / 未答=灰 / 当前题=绿;
/// 2. 点击题号 → onJump(index) 跳转(由 ExamPage 调 jumpToPage)。
///
/// 二、为什么独立成组件:
/// 答题卡是"总览 → 跳转"的通用交互,参数化设计后可复用于
/// 错题重做、试卷回顾等场景;不直接碰 PageController,
/// 跳转动作通过 onJump 回调上抛,保持组件解耦。
///
/// 三、状态判定:answers 是 `Map<questionId, answer>`,
/// 已答 = 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<int> questionIds;
/// 已答答案表(questionId → answer)
final Map<int, String> answers;
/// 当前题索引(高亮绿色)
final int currentIndex;
/// 点击题号回调(传入索引,由页面执行跳转)
final ValueChanged<int> 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),
),
),
),
);
}
}