Files
ai_english/client/lib/features/vocabulary/screens/vocabulary_test_screen.dart
2025-11-17 14:09:17 +08:00

508 lines
15 KiB
Dart
Raw 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_riverpod/flutter_riverpod.dart';
import '../models/vocabulary_book_model.dart';
import '../models/word_model.dart';
import '../providers/vocabulary_provider.dart';
import 'dart:math';
enum TestType {
vocabularyLevel,
listening,
reading,
}
class VocabularyTestScreen extends ConsumerStatefulWidget {
final VocabularyBook? vocabularyBook;
final TestType testType;
final int questionCount;
const VocabularyTestScreen({
super.key,
this.vocabularyBook,
this.testType = TestType.vocabularyLevel,
this.questionCount = 20,
});
@override
ConsumerState<VocabularyTestScreen> createState() => _VocabularyTestScreenState();
}
class _VocabularyTestScreenState extends ConsumerState<VocabularyTestScreen> {
List<Word> _testWords = [];
int _currentIndex = 0;
Map<int, String> _userAnswers = {};
bool _isLoading = true;
bool _isTestComplete = false;
@override
void initState() {
super.initState();
_initTest();
}
Future<void> _initTest() async {
setState(() => _isLoading = true);
try {
final notifier = ref.read(vocabularyProvider.notifier);
await notifier.loadTodayStudyWords();
final state = ref.read(vocabularyProvider);
final allWords = state.todayWords;
if (allWords.isEmpty) {
// 如果没有今日单词,生成示例数据
_testWords = _generateSampleWords();
} else {
// 随机选取指定数量的单词
final random = Random();
final selectedWords = <Word>[];
final wordsCopy = List<Word>.from(allWords);
final count = widget.questionCount.clamp(1, wordsCopy.length);
for (var i = 0; i < count; i++) {
if (wordsCopy.isEmpty) break;
final index = random.nextInt(wordsCopy.length);
selectedWords.add(wordsCopy.removeAt(index));
}
_testWords = selectedWords;
}
} catch (e) {
_testWords = _generateSampleWords();
}
setState(() => _isLoading = false);
}
List<Word> _generateSampleWords() {
// 生成示例测试数据
final sampleWords = [
'abandon', 'ability', 'abroad', 'absence', 'absolute',
'absorb', 'abstract', 'abundant', 'academic', 'accept',
];
return List.generate(
widget.questionCount.clamp(1, sampleWords.length),
(index) => Word(
id: '${index + 1}',
word: sampleWords[index % sampleWords.length],
phonetic: '/ˈsæmpl/',
difficulty: WordDifficulty.intermediate,
frequency: 1000,
definitions: [
WordDefinition(
type: WordType.noun,
definition: 'Example definition',
translation: '示例释义 ${index + 1}',
),
],
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
),
);
}
@override
Widget build(BuildContext context) {
if (_isLoading) {
return Scaffold(
appBar: AppBar(
title: const Text('词汇测试'),
),
body: const Center(
child: CircularProgressIndicator(),
),
);
}
if (_isTestComplete) {
return _buildResultScreen();
}
return Scaffold(
appBar: AppBar(
title: Text('词汇测试 (${_currentIndex + 1}/${_testWords.length})'),
actions: [
TextButton(
onPressed: _showExitConfirmDialog,
child: const Text(
'退出',
style: TextStyle(color: Colors.white),
),
),
],
),
body: _buildTestQuestion(),
);
}
Widget _buildTestQuestion() {
if (_currentIndex >= _testWords.length) {
return const Center(child: Text('测试已完成'));
}
final word = _testWords[_currentIndex];
final correctAnswer = word.definitions.isNotEmpty
? word.definitions.first.translation
: '示例释义';
// 生成选项(一个正确答案 + 三个干扰项)
final options = _generateOptions(correctAnswer, _currentIndex);
return Column(
children: [
// 进度条
LinearProgressIndicator(
value: (_currentIndex + 1) / _testWords.length,
backgroundColor: Colors.grey[200],
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFF2196F3)),
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 问题区域
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: const Color(0xFF2196F3).withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
),
child: Column(
children: [
const Text(
'请选择下列单词的正确释义',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
const SizedBox(height: 16),
Text(
word.word,
style: const TextStyle(
fontSize: 36,
fontWeight: FontWeight.bold,
color: Color(0xFF2196F3),
),
),
if (word.phonetic != null) ...[
const SizedBox(height: 8),
Text(
word.phonetic!,
style: const TextStyle(
fontSize: 18,
color: Colors.grey,
fontStyle: FontStyle.italic,
),
),
],
],
),
),
const SizedBox(height: 32),
// 选项
...options.asMap().entries.map((entry) {
final index = entry.key;
final option = entry.value;
final optionLabel = String.fromCharCode(65 + index); // A, B, C, D
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _buildOptionCard(
label: optionLabel,
text: option,
isSelected: _userAnswers[_currentIndex] == option,
onTap: () => _selectAnswer(option),
),
);
}),
],
),
),
),
// 底部按钮
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, -2),
),
],
),
child: SafeArea(
child: ElevatedButton(
onPressed: _userAnswers.containsKey(_currentIndex) ? _nextQuestion : null,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2196F3),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
_currentIndex < _testWords.length - 1 ? '下一题' : '完成测试',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
);
}
Widget _buildOptionCard({
required String label,
required String text,
required bool isSelected,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: isSelected
? const Color(0xFF2196F3).withOpacity(0.1)
: Colors.white,
border: Border.all(
color: isSelected
? const Color(0xFF2196F3)
: Colors.grey[300]!,
width: isSelected ? 2 : 1,
),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: isSelected
? const Color(0xFF2196F3)
: Colors.grey[200],
shape: BoxShape.circle,
),
child: Center(
child: Text(
label,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: isSelected ? Colors.white : Colors.grey[600],
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: Text(
text,
style: TextStyle(
fontSize: 16,
color: isSelected ? const Color(0xFF2196F3) : Colors.black87,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
),
),
),
],
),
),
);
}
List<String> _generateOptions(String correctAnswer, int questionIndex) {
final distractors = [
'放弃;遗弃',
'能力;才能',
'在国外;到国外',
'缺席;缺乏',
'绝对的;完全的',
'吸收;吸引',
'抽象的;抽象概念',
'丰富的;充裕的',
'学术的;学院的',
'接受;承认',
];
final options = <String>[correctAnswer];
final random = Random(questionIndex); // 使用问题索引作为种子以保证一致性
while (options.length < 4 && distractors.isNotEmpty) {
final index = random.nextInt(distractors.length);
final distractor = distractors[index];
if (distractor != correctAnswer && !options.contains(distractor)) {
options.add(distractor);
}
distractors.removeAt(index);
}
// 打乱选项顺序
options.shuffle(random);
return options;
}
void _selectAnswer(String answer) {
setState(() {
_userAnswers[_currentIndex] = answer;
});
}
void _nextQuestion() {
if (_currentIndex < _testWords.length - 1) {
setState(() {
_currentIndex++;
});
} else {
setState(() {
_isTestComplete = true;
});
}
}
Widget _buildResultScreen() {
int correctCount = 0;
for (var i = 0; i < _testWords.length; i++) {
final word = _testWords[i];
final correctAnswer = word.definitions.isNotEmpty
? word.definitions.first.translation
: '示例释义';
if (_userAnswers[i] == correctAnswer) {
correctCount++;
}
}
final score = (correctCount / _testWords.length * 100).round();
return Scaffold(
appBar: AppBar(
title: const Text('测试结果'),
automaticallyImplyLeading: false,
),
body: Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: score >= 60
? Colors.green.withOpacity(0.1)
: Colors.orange.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Center(
child: Text(
'$score',
style: TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
color: score >= 60 ? Colors.green : Colors.orange,
),
),
),
),
const SizedBox(height: 24),
Text(
score >= 80 ? '太棒了!' : score >= 60 ? '不错哦!' : '加油!',
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
'你的分数:$score',
style: const TextStyle(
fontSize: 18,
color: Colors.grey,
),
),
const SizedBox(height: 8),
Text(
'正确率:$correctCount/${_testWords.length}',
style: const TextStyle(
fontSize: 16,
color: Colors.grey,
),
),
const SizedBox(height: 48),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2196F3),
padding: const EdgeInsets.symmetric(
horizontal: 48,
vertical: 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text(
'完成',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(height: 16),
TextButton(
onPressed: () {
setState(() {
_currentIndex = 0;
_userAnswers.clear();
_isTestComplete = false;
});
_initTest();
},
child: const Text('重新测试'),
),
],
),
),
),
);
}
void _showExitConfirmDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('退出测试'),
content: const Text('确定要退出吗?当前进度将不会保存。'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('取消'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
Navigator.of(context).pop();
},
child: const Text('确定'),
),
],
),
);
}
}