508 lines
15 KiB
Dart
508 lines
15 KiB
Dart
|
|
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('确定'),
|
|||
|
|
),
|
|||
|
|
],
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|