This commit is contained in:
sjk
2025-11-17 13:39:05 +08:00
commit d4cfe2b9de
479 changed files with 109324 additions and 0 deletions

View File

@@ -0,0 +1,507 @@
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('确定'),
),
],
),
);
}
}