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

543 lines
17 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_riverpod/flutter_riverpod.dart';
import '../models/vocabulary_book_model.dart';
import '../models/review_models.dart';
import '../models/word_model.dart';
import '../providers/vocabulary_provider.dart';
import 'dart:math';
class SmartReviewScreen extends ConsumerStatefulWidget {
final VocabularyBook? vocabularyBook;
final ReviewMode reviewMode;
final int dailyTarget;
const SmartReviewScreen({
super.key,
this.vocabularyBook,
this.reviewMode = ReviewMode.adaptive,
this.dailyTarget = 20,
});
@override
ConsumerState<SmartReviewScreen> createState() => _SmartReviewScreenState();
}
class _SmartReviewScreenState extends ConsumerState<SmartReviewScreen> {
List<Word> _reviewWords = [];
int _currentIndex = 0;
bool _isLoading = true;
bool _showAnswer = false;
Map<int, int> _reviewResults = {}; // 0: 不记得, 1: 模糊, 2: 记得
@override
void initState() {
super.initState();
_loadWords();
}
Future<void> _loadWords() async {
setState(() => _isLoading = true);
try {
final notifier = ref.read(vocabularyProvider.notifier);
await notifier.loadReviewWords();
final state = ref.read(vocabularyProvider);
final words = state.reviewWords;
if (words.isEmpty) {
// 如果没有复习词汇,生成示例数据
_reviewWords = _generateSampleWords();
} else {
_reviewWords = words.take(widget.dailyTarget).toList();
}
} catch (e) {
_reviewWords = _generateSampleWords();
}
setState(() => _isLoading = false);
}
List<Word> _generateSampleWords() {
final sampleData = [
{'word': 'abandon', 'phonetic': 'ˈbændən/', 'translation': '放弃;遗弃'},
{'word': 'ability', 'phonetic': 'ˈbɪləti/', 'translation': '能力;才能'},
{'word': 'abroad', 'phonetic': 'ˈbrɔːd/', 'translation': '在国外;到国外'},
{'word': 'absence', 'phonetic': '/ˈæbsəns/', 'translation': '缺席;缺乏'},
{'word': 'absolute', 'phonetic': '/ˈæbsəluːt/', 'translation': '绝对的;完全的'},
{'word': 'absorb', 'phonetic': '/əbˈːrb/', 'translation': '吸收;吸引'},
{'word': 'abstract', 'phonetic': '/ˈæbstrækt/', 'translation': '抽象的;抽象概念'},
{'word': 'abundant', 'phonetic': 'ˈbʌndənt/', 'translation': '丰富的;充裕的'},
{'word': 'academic', 'phonetic': '/ˌækəˈdemɪk/', 'translation': '学术的;学院的'},
{'word': 'accept', 'phonetic': '/əkˈsept/', 'translation': '接受;承认'},
];
return List.generate(
widget.dailyTarget.clamp(1, sampleData.length),
(index) {
final data = sampleData[index % sampleData.length];
return Word(
id: '${index + 1}',
word: data['word']!,
phonetic: data['phonetic'],
difficulty: WordDifficulty.intermediate,
frequency: 1000,
definitions: [
WordDefinition(
type: WordType.noun,
definition: 'Example definition',
translation: data['translation']!,
),
],
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 (_currentIndex >= _reviewWords.length) {
return _buildCompleteScreen();
}
return Scaffold(
appBar: AppBar(
title: Text('智能复习 (${_currentIndex + 1}/${_reviewWords.length})'),
actions: [
TextButton(
onPressed: _showExitConfirmDialog,
child: const Text(
'退出',
style: TextStyle(color: Colors.white),
),
),
],
),
body: _buildReviewCard(),
);
}
Widget _buildReviewCard() {
final word = _reviewWords[_currentIndex];
final translation = word.definitions.isNotEmpty
? word.definitions.first.translation
: '示例释义';
return Column(
children: [
// 进度条
LinearProgressIndicator(
value: (_currentIndex + 1) / _reviewWords.length,
backgroundColor: Colors.grey[200],
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFF9C27B0)),
),
Expanded(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 卡片
Container(
width: double.infinity,
padding: const EdgeInsets.all(32),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 20,
offset: const Offset(0, 4),
),
],
),
child: Column(
children: [
// 单词
Text(
word.word,
style: const TextStyle(
fontSize: 42,
fontWeight: FontWeight.bold,
color: Color(0xFF9C27B0),
),
),
if (word.phonetic != null) ...[
const SizedBox(height: 12),
Text(
word.phonetic!,
style: const TextStyle(
fontSize: 20,
color: Colors.grey,
fontStyle: FontStyle.italic,
),
),
],
const SizedBox(height: 32),
// 音频按钮
IconButton(
onPressed: () => _playAudio(word.word),
icon: const Icon(
Icons.volume_up,
size: 48,
color: Color(0xFF9C27B0),
),
),
const SizedBox(height: 32),
// 答案区域
AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
height: _showAnswer ? null : 0,
child: _showAnswer
? Column(
children: [
const Divider(),
const SizedBox(height: 16),
const Text(
'释义',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
const SizedBox(height: 8),
Text(
translation,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
],
)
: const SizedBox.shrink(),
),
],
),
),
const SizedBox(height: 32),
// 提示文本
if (!_showAnswer)
const Text(
'回忆这个单词的意思,然后点击“显示答案”',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
),
textAlign: TextAlign.center,
),
],
),
),
),
),
// 底部按钮
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: _showAnswer
? Row(
children: [
Expanded(
child: _buildResultButton(
label: '不记得',
color: Colors.red,
result: 0,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildResultButton(
label: '模糊',
color: Colors.orange,
result: 1,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildResultButton(
label: '记得',
color: Colors.green,
result: 2,
),
),
],
)
: ElevatedButton(
onPressed: () {
setState(() {
_showAnswer = true;
});
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF9C27B0),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text(
'显示答案',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
);
}
Widget _buildResultButton({
required String label,
required Color color,
required int result,
}) {
return ElevatedButton(
onPressed: () => _recordResult(result),
style: ElevatedButton.styleFrom(
backgroundColor: color,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
label,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
);
}
void _recordResult(int result) {
setState(() {
_reviewResults[_currentIndex] = result;
_showAnswer = false;
_currentIndex++;
});
}
Widget _buildCompleteScreen() {
final remembered = _reviewResults.values.where((r) => r == 2).length;
final fuzzy = _reviewResults.values.where((r) => r == 1).length;
final forgotten = _reviewResults.values.where((r) => r == 0).length;
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: const Color(0xFF9C27B0).withOpacity(0.1),
shape: BoxShape.circle,
),
child: const Center(
child: Icon(
Icons.check_circle,
size: 64,
color: Color(0xFF9C27B0),
),
),
),
const SizedBox(height: 24),
const Text(
'复习完成!',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Text(
'共复习 ${_reviewWords.length} 个单词',
style: const TextStyle(
fontSize: 18,
color: Colors.grey,
),
),
const SizedBox(height: 32),
// 统计信息
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(
children: [
_buildStatRow(
'完全记得',
remembered,
Colors.green,
),
const SizedBox(height: 12),
_buildStatRow(
'模糊记得',
fuzzy,
Colors.orange,
),
const SizedBox(height: 12),
_buildStatRow(
'不记得',
forgotten,
Colors.red,
),
],
),
),
const SizedBox(height: 48),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF9C27B0),
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;
_reviewResults.clear();
_showAnswer = false;
});
_loadWords();
},
child: const Text('重新复习'),
),
],
),
),
),
);
}
Widget _buildStatRow(String label, int count, Color color) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
const SizedBox(width: 12),
Text(
label,
style: const TextStyle(
fontSize: 16,
),
),
],
),
Text(
'$count',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: color,
),
),
],
);
}
void _playAudio(String word) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('正在播放 "$word" 的发音'),
duration: const Duration(seconds: 1),
),
);
}
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('确定'),
),
],
),
);
}
}