Files
ai_english/client/lib/features/reading/screens/reading_exercise_screen.dart
2025-11-17 13:39:05 +08:00

781 lines
23 KiB
Dart

import 'package:flutter/material.dart';
import '../models/reading_exercise_model.dart';
/// 阅读练习详情页面
class ReadingExerciseScreen extends StatefulWidget {
final ReadingExercise exercise;
const ReadingExerciseScreen({
super.key,
required this.exercise,
});
@override
State<ReadingExerciseScreen> createState() => _ReadingExerciseScreenState();
}
class _ReadingExerciseScreenState extends State<ReadingExerciseScreen>
with TickerProviderStateMixin {
late TabController _tabController;
Map<String, int> userAnswers = {};
bool showResults = false;
int score = 0;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black87),
onPressed: () => Navigator.of(context).pop(),
),
title: Text(
widget.exercise.title,
style: const TextStyle(
color: Colors.black87,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
centerTitle: true,
bottom: TabBar(
controller: _tabController,
labelColor: const Color(0xFF2196F3),
unselectedLabelColor: Colors.grey,
indicatorColor: const Color(0xFF2196F3),
tabs: const [
Tab(text: '阅读文章'),
Tab(text: '练习题'),
],
),
),
body: TabBarView(
controller: _tabController,
children: [
_buildArticleTab(),
_buildQuestionsTab(),
],
),
);
}
Widget _buildArticleTab() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildArticleHeader(),
const SizedBox(height: 20),
_buildArticleContent(),
const SizedBox(height: 20),
_buildArticleFooter(),
],
),
);
}
Widget _buildArticleHeader() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
widget.exercise.title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getDifficultyColor(widget.exercise.difficulty),
borderRadius: BorderRadius.circular(12),
),
child: Text(
_getDifficultyLabel(widget.exercise.difficulty),
style: const TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 12),
Text(
widget.exercise.summary,
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
const SizedBox(height: 16),
Row(
children: [
_buildInfoChip(Icons.access_time, '${widget.exercise.estimatedTime}分钟'),
const SizedBox(width: 12),
_buildInfoChip(Icons.text_fields, '${widget.exercise.wordCount}'),
const SizedBox(width: 12),
_buildInfoChip(Icons.quiz, '${widget.exercise.questions.length}'),
],
),
if (widget.exercise.tags.isNotEmpty) ...[
const SizedBox(height: 12),
Wrap(
spacing: 6,
runSpacing: 6,
children: widget.exercise.tags.map((tag) => Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(12),
),
child: Text(
tag,
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
)).toList(),
),
],
],
),
);
}
Widget _buildInfoChip(IconData icon, String text) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 14,
color: Colors.grey[600],
),
const SizedBox(width: 4),
Text(
text,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
);
}
Widget _buildArticleContent() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'文章内容',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
widget.exercise.content,
style: const TextStyle(
fontSize: 16,
height: 1.6,
color: Colors.black87,
),
),
],
),
);
}
Widget _buildArticleFooter() {
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'文章信息',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Row(
children: [
const Text(
'来源:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
Text(
widget.exercise.source,
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
],
),
const SizedBox(height: 8),
Row(
children: [
const Text(
'发布时间:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
Text(
'${widget.exercise.publishDate.year}${widget.exercise.publishDate.month}${widget.exercise.publishDate.day}',
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
],
),
],
),
);
}
Widget _buildQuestionsTab() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
if (showResults) _buildResultsHeader(),
...widget.exercise.questions.asMap().entries.map((entry) {
final index = entry.key;
final question = entry.value;
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: _buildQuestionCard(question, index),
);
}).toList(),
const SizedBox(height: 20),
if (!showResults)
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _submitAnswers,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2196F3),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text(
'提交答案',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
if (showResults)
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _resetQuiz,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text(
'重新练习',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
],
),
);
}
Widget _buildResultsHeader() {
final percentage = (score / widget.exercise.questions.length * 100).round();
return Container(
margin: const EdgeInsets.only(bottom: 20),
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
const Text(
'练习结果',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Column(
children: [
Text(
'$score',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Color(0xFF2196F3),
),
),
const Text(
'正确题数',
style: TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
],
),
Column(
children: [
Text(
'${widget.exercise.questions.length}',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.grey,
),
),
const Text(
'总题数',
style: TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
],
),
Column(
children: [
Text(
'$percentage%',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: percentage >= 80 ? Colors.green :
percentage >= 60 ? Colors.orange : Colors.red,
),
),
const Text(
'正确率',
style: TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
],
),
],
),
],
),
);
}
Widget _buildQuestionCard(ReadingQuestion question, int index) {
final userAnswer = userAnswers[question.id];
final isCorrect = userAnswer == question.correctAnswer;
final showAnswer = showResults;
return Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: showAnswer
? Border.all(
color: isCorrect ? Colors.green : Colors.red,
width: 2,
)
: null,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
color: const Color(0xFF2196F3),
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
'${index + 1}',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
question.question,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
if (showAnswer)
Icon(
isCorrect ? Icons.check_circle : Icons.cancel,
color: isCorrect ? Colors.green : Colors.red,
),
],
),
const SizedBox(height: 16),
if (question.type == 'multiple_choice')
...question.options.asMap().entries.map((entry) {
final optionIndex = entry.key;
final option = entry.value;
final isSelected = userAnswer == optionIndex;
final isCorrectOption = optionIndex == question.correctAnswer;
Color? backgroundColor;
Color? textColor;
if (showAnswer) {
if (isCorrectOption) {
backgroundColor = Colors.green.withOpacity(0.1);
textColor = Colors.green;
} else if (isSelected && !isCorrectOption) {
backgroundColor = Colors.red.withOpacity(0.1);
textColor = Colors.red;
}
} else if (isSelected) {
backgroundColor = const Color(0xFF2196F3).withOpacity(0.1);
textColor = const Color(0xFF2196F3);
}
return GestureDetector(
onTap: showAnswer ? null : () {
setState(() {
userAnswers[question.id] = optionIndex;
});
},
child: Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: backgroundColor ?? Colors.grey[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: backgroundColor != null
? (textColor ?? Colors.grey)
: Colors.grey.withOpacity(0.3),
),
),
child: Row(
children: [
Container(
width: 20,
height: 20,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isSelected ? (textColor ?? const Color(0xFF2196F3)) : Colors.transparent,
border: Border.all(
color: textColor ?? Colors.grey,
),
),
child: isSelected
? const Icon(
Icons.check,
size: 12,
color: Colors.white,
)
: null,
),
const SizedBox(width: 12),
Expanded(
child: Text(
option,
style: TextStyle(
fontSize: 14,
color: textColor ?? Colors.black87,
),
),
),
],
),
),
);
}).toList(),
if (question.type == 'true_false')
Row(
children: [
Expanded(
child: _buildTrueFalseOption(question, true, 'True'),
),
const SizedBox(width: 12),
Expanded(
child: _buildTrueFalseOption(question, false, 'False'),
),
],
),
if (showAnswer && question.explanation.isNotEmpty) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'解析:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Color(0xFF2196F3),
),
),
const SizedBox(height: 4),
Text(
question.explanation,
style: const TextStyle(
fontSize: 14,
color: Colors.black87,
),
),
],
),
),
],
],
),
);
}
Widget _buildTrueFalseOption(ReadingQuestion question, bool value, String label) {
final userAnswer = userAnswers[question.id];
final isSelected = userAnswer == (value ? 0 : 1);
final isCorrect = (value ? 0 : 1) == question.correctAnswer;
final showAnswer = showResults;
Color? backgroundColor;
Color? textColor;
if (showAnswer) {
if (isCorrect) {
backgroundColor = Colors.green.withOpacity(0.1);
textColor = Colors.green;
} else if (isSelected && !isCorrect) {
backgroundColor = Colors.red.withOpacity(0.1);
textColor = Colors.red;
}
} else if (isSelected) {
backgroundColor = const Color(0xFF2196F3).withOpacity(0.1);
textColor = const Color(0xFF2196F3);
}
return GestureDetector(
onTap: showAnswer ? null : () {
setState(() {
userAnswers[question.id] = value ? 0 : 1;
});
},
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: backgroundColor ?? Colors.grey[50],
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: backgroundColor != null
? (textColor ?? Colors.grey)
: Colors.grey.withOpacity(0.3),
),
),
child: Center(
child: Text(
label,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: textColor ?? Colors.black87,
),
),
),
),
);
}
void _submitAnswers() {
if (userAnswers.length < widget.exercise.questions.length) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('请完成所有题目后再提交'),
backgroundColor: Colors.orange,
),
);
return;
}
int correctCount = 0;
for (final question in widget.exercise.questions) {
if (userAnswers[question.id] == question.correctAnswer) {
correctCount++;
}
}
setState(() {
score = correctCount;
showResults = true;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('练习完成!正确率:${(correctCount / widget.exercise.questions.length * 100).round()}%'),
backgroundColor: Colors.green,
),
);
}
void _resetQuiz() {
setState(() {
userAnswers.clear();
showResults = false;
score = 0;
});
}
String _getDifficultyLabel(ReadingDifficulty difficulty) {
switch (difficulty) {
case ReadingDifficulty.elementary:
return 'A1';
case ReadingDifficulty.intermediate:
return 'B1';
case ReadingDifficulty.upperIntermediate:
return 'B2';
case ReadingDifficulty.advanced:
return 'C1';
case ReadingDifficulty.proficient:
return 'C2';
}
}
Color _getDifficultyColor(ReadingDifficulty difficulty) {
switch (difficulty) {
case ReadingDifficulty.elementary:
return Colors.green;
case ReadingDifficulty.intermediate:
return Colors.orange;
case ReadingDifficulty.upperIntermediate:
return Colors.deepOrange;
case ReadingDifficulty.advanced:
return Colors.red;
case ReadingDifficulty.proficient:
return Colors.purple;
}
}
}