300 lines
8.7 KiB
Dart
300 lines
8.7 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'package:ai_english_learning/core/network/api_client.dart';
|
||
import 'package:ai_english_learning/core/services/storage_service.dart';
|
||
import 'package:ai_english_learning/features/vocabulary/models/vocabulary_book_model.dart';
|
||
import 'package:ai_english_learning/features/vocabulary/models/word_model.dart';
|
||
import 'package:ai_english_learning/features/vocabulary/models/learning_session_model.dart';
|
||
import 'package:ai_english_learning/features/vocabulary/services/learning_service.dart';
|
||
import 'package:ai_english_learning/features/vocabulary/services/vocabulary_service.dart';
|
||
import 'package:ai_english_learning/features/vocabulary/widgets/study_card_widget.dart';
|
||
|
||
class StudyScreen extends StatefulWidget {
|
||
final VocabularyBook vocabularyBook;
|
||
final int dailyGoal;
|
||
|
||
const StudyScreen({
|
||
Key? key,
|
||
required this.vocabularyBook,
|
||
this.dailyGoal = 20,
|
||
}) : super(key: key);
|
||
|
||
@override
|
||
State<StudyScreen> createState() => _StudyScreenState();
|
||
}
|
||
|
||
class _StudyScreenState extends State<StudyScreen> {
|
||
late LearningService _learningService;
|
||
late VocabularyService _vocabularyService;
|
||
|
||
bool _isLoading = true;
|
||
String? _error;
|
||
|
||
LearningSession? _session;
|
||
DailyLearningTasks? _tasks;
|
||
List<Word> _wordsList = [];
|
||
int _currentIndex = 0;
|
||
|
||
int _studiedCount = 0;
|
||
DateTime? _studyStartTime;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_init();
|
||
}
|
||
|
||
Future<void> _init() async {
|
||
final storageService = await StorageService.getInstance();
|
||
_learningService = LearningService(apiClient: ApiClient.instance);
|
||
_vocabularyService = VocabularyService(
|
||
apiClient: ApiClient.instance,
|
||
storageService: storageService,
|
||
);
|
||
|
||
await _startLearning();
|
||
}
|
||
|
||
Future<void> _startLearning() async {
|
||
setState(() {
|
||
_isLoading = true;
|
||
_error = null;
|
||
});
|
||
|
||
try {
|
||
// 1. 开始学习会话
|
||
final result = await _learningService.startLearning(
|
||
widget.vocabularyBook.id,
|
||
widget.dailyGoal,
|
||
);
|
||
|
||
_session = result['session'] as LearningSession;
|
||
_tasks = result['tasks'] as DailyLearningTasks;
|
||
|
||
print('✅ 学习会话创建成功');
|
||
print('📝 新单词数量: ${_tasks!.newWords.length}');
|
||
print('🔄 复习单词数量: ${_tasks!.reviewWords.length}');
|
||
|
||
// 2. 加载单词详情 - 从词汇书API获取
|
||
if (_tasks!.newWords.isEmpty && _tasks!.reviewWords.isEmpty) {
|
||
setState(() {
|
||
_isLoading = false;
|
||
_wordsList = [];
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 合并新单词和复习单词的ID(学习逻辑:新词 + 所有到期复习词)
|
||
final allWordIds = <int>{
|
||
..._tasks!.newWords,
|
||
..._tasks!.reviewWords.map((r) => r['vocabulary_id'] as int),
|
||
};
|
||
|
||
final totalWords = allWordIds.length;
|
||
print('📚 今日学习任务: ${_tasks!.newWords.length}个新词 + ${_tasks!.reviewWords.length}个复习词 = $totalWords个单词');
|
||
|
||
// 使用词汇书的单词加载API - 动态设置limit以包含所有需要的单词
|
||
final limit = (totalWords / 50).ceil() * 50; // 向上取整到50的倍数
|
||
final bookWords = await _vocabularyService.getVocabularyBookWords(
|
||
widget.vocabularyBook.id,
|
||
page: 1,
|
||
limit: limit < 100 ? 100 : limit,
|
||
);
|
||
|
||
// 筛选出需要学习的单词(新词 + 复习词)
|
||
final words = <Word>[];
|
||
|
||
for (final bookWord in bookWords) {
|
||
if (bookWord.word == null) continue;
|
||
|
||
final wordId = int.tryParse(bookWord.word!.id);
|
||
if (wordId != null && allWordIds.contains(wordId)) {
|
||
words.add(bookWord.word!);
|
||
}
|
||
if (words.length >= totalWords) break;
|
||
}
|
||
|
||
print('✅ 加载了 ${words.length} 个单词,需要 $totalWords 个');
|
||
|
||
setState(() {
|
||
_wordsList = words;
|
||
_isLoading = false;
|
||
_studyStartTime = DateTime.now();
|
||
});
|
||
|
||
} catch (e, stackTrace) {
|
||
print('❌ 开始学习失败: $e');
|
||
print('Stack trace: $stackTrace');
|
||
setState(() {
|
||
_error = '加载学习内容失败,请稍后重试\n错误:$e';
|
||
_isLoading = false;
|
||
});
|
||
}
|
||
}
|
||
|
||
Future<void> _handleAnswer(StudyDifficulty difficulty) async {
|
||
if (_currentIndex >= _wordsList.length) return;
|
||
|
||
final word = _wordsList[_currentIndex];
|
||
final studyTime = _studyStartTime != null
|
||
? DateTime.now().difference(_studyStartTime!).inMilliseconds
|
||
: 0;
|
||
|
||
try {
|
||
// 提交学习结果
|
||
await _learningService.submitWordStudy(
|
||
word.id,
|
||
difficulty,
|
||
sessionId: int.tryParse(_session?.id ?? '0'),
|
||
);
|
||
|
||
setState(() {
|
||
_studiedCount++;
|
||
_currentIndex++;
|
||
_studyStartTime = DateTime.now();
|
||
});
|
||
|
||
// 检查是否完成
|
||
if (_currentIndex >= _wordsList.length) {
|
||
_showCompletionDialog();
|
||
}
|
||
|
||
} catch (e) {
|
||
print('提交学习结果失败: $e');
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
const SnackBar(content: Text('提交失败,请重试')),
|
||
);
|
||
}
|
||
}
|
||
|
||
void _showCompletionDialog() {
|
||
showDialog(
|
||
context: context,
|
||
barrierDismissible: false,
|
||
builder: (context) => AlertDialog(
|
||
title: const Text('🎉 今日学习完成!'),
|
||
content: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text('学习单词数:$_studiedCount'),
|
||
Text('新学单词:${_tasks?.newWords.length ?? 0}'),
|
||
Text('复习单词:${_tasks?.reviewWords.length ?? 0}'),
|
||
],
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () {
|
||
Navigator.of(context).pop();
|
||
Navigator.of(context).pop(true); // 返回到上一页
|
||
},
|
||
child: const Text('完成'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(
|
||
title: Text(widget.vocabularyBook.name),
|
||
actions: [
|
||
// 进度显示
|
||
Center(
|
||
child: Padding(
|
||
padding: const EdgeInsets.only(right: 16),
|
||
child: Text(
|
||
'$_studiedCount / ${_wordsList.length}',
|
||
style: const TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
body: _buildBody(),
|
||
);
|
||
}
|
||
|
||
Widget _buildBody() {
|
||
if (_isLoading) {
|
||
return const Center(child: CircularProgressIndicator());
|
||
}
|
||
|
||
if (_error != null) {
|
||
return Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(Icons.error_outline, size: 64, color: Colors.grey[400]),
|
||
const SizedBox(height: 16),
|
||
Text(
|
||
_error!,
|
||
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
||
),
|
||
const SizedBox(height: 16),
|
||
ElevatedButton(
|
||
onPressed: _startLearning,
|
||
child: const Text('重试'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
if (_wordsList.isEmpty) {
|
||
return Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(Icons.check_circle, size: 64, color: Colors.green[400]),
|
||
const SizedBox(height: 16),
|
||
const Text(
|
||
'今日任务已完成!',
|
||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'明天继续加油!',
|
||
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
||
),
|
||
const SizedBox(height: 24),
|
||
ElevatedButton(
|
||
onPressed: () => Navigator.of(context).pop(),
|
||
child: const Text('返回'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
if (_currentIndex >= _wordsList.length) {
|
||
return const Center(child: CircularProgressIndicator());
|
||
}
|
||
|
||
return Column(
|
||
children: [
|
||
// 进度条
|
||
LinearProgressIndicator(
|
||
value: _currentIndex / _wordsList.length,
|
||
backgroundColor: Colors.grey[200],
|
||
),
|
||
|
||
// 学习卡片
|
||
Expanded(
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: StudyCardWidget(
|
||
word: _wordsList[_currentIndex],
|
||
onAnswer: _handleAnswer,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|