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

1761 lines
58 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 '../../../core/utils/responsive_utils.dart';
import '../../../core/routes/app_routes.dart';
import '../models/vocabulary_book_model.dart';
import '../models/vocabulary_book_category.dart';
import '../models/word_model.dart';
import '../data/vocabulary_book_factory.dart';
import '../providers/vocabulary_provider.dart';
import '../../../core/routes/app_routes.dart';
import '../../auth/providers/auth_provider.dart';
/// 词汇学习主页面
class VocabularyHomeScreen extends ConsumerStatefulWidget {
const VocabularyHomeScreen({super.key});
@override
ConsumerState<VocabularyHomeScreen> createState() => _VocabularyHomeScreenState();
}
class _VocabularyHomeScreenState extends ConsumerState<VocabularyHomeScreen> {
@override
void initState() {
super.initState();
// 初始化时加载统计数据
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadStats();
});
}
// 移除 _loadVocabularyBooks 方法,因为 provider 已经自动加载}
Future<void> _loadStats() async {
try {
final vocabularyNotifier = ref.read(vocabularyProvider.notifier);
// 获取当前用户ID以加载每日统计
final user = ref.read(currentUserProvider);
final userId = user?.id;
await vocabularyNotifier.loadUserVocabularyOverallStats();
await vocabularyNotifier.loadWeeklyStudyStats();
if (userId != null && userId.isNotEmpty) {
await vocabularyNotifier.loadTodayStudyWords(userId: userId);
} else {
// 无用户时仍加载今日单词与默认统计
await vocabularyNotifier.loadTodayStudyWords();
}
} catch (_) {
// 忽略错误在UI中以默认值显示
}
}
// 获取推荐词汇书数据
static List<VocabularyBook> get _recommendedBooks => VocabularyBookFactory.getRecommendedBooks(limit: 8);
@override
Widget build(BuildContext context) {
final vocabularyAsync = ref.watch(vocabularyProvider);
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.black87),
onPressed: () => Navigator.of(context).pop(),
),
title: const Text(
'词汇学习',
style: TextStyle(
color: Colors.black87,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
centerTitle: true,
),
body: SafeArea(
child: SingleChildScrollView(
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
child: Column(
children: [
const SizedBox(height: 12),
_buildTitle(),
const SizedBox(height: 12),
_buildCategoryNavigation(),
const SizedBox(height: 12),
_buildTodayWords(),
const SizedBox(height: 12),
// _buildAIRecommendation(),
// const SizedBox(height: 12),
_buildLearningStats(),
const SizedBox(height: 80), // 底部导航栏空间
],
),
),
),
);
}
Widget _buildTitle() {
return const Padding(
padding: EdgeInsets.symmetric(horizontal: 16),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
'选择词书开始学习',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
);
}
Widget _buildCategoryNavigation() {
// 从 API 加载分类数据
final state = ref.watch(vocabularyProvider);
final categories = state.categories;
print('📂 分类数据: ${categories.length} 个分类');
if (categories.isNotEmpty) {
print('📂 第一个分类: ${categories.first}');
}
// 如果API没有返回分类数据使用本地分类
final displayCategories = categories.isEmpty
? _getLocalCategories()
: categories;
return ResponsiveBuilder(
builder: (context, isMobile, isTablet, isDesktop) {
return Container(
margin: ResponsiveUtils.getResponsivePadding(context),
child: GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
mainAxisExtent: isMobile ? 110 : 130,
),
itemCount: displayCategories.length,
itemBuilder: (context, index) {
final category = displayCategories[index];
final categoryName = category['name'] as String;
final count = category['count'] as int;
// 从中文名称映射到枚举(用于显示图标和颜色)
final mainCategory = VocabularyBookCategoryHelper.getMainCategoryFromName(categoryName);
if (mainCategory == null) return const SizedBox.shrink();
// 根据分类获取图标和颜色
final iconData = _getCategoryIcon(mainCategory);
final color = _getCategoryColor(mainCategory);
return _buildCategoryCard(
mainCategory,
iconData,
color,
count: count,
);
},
),
);
},
);
}
// 获取本地分类数据作为后备方案
List<Map<String, dynamic>> _getLocalCategories() {
return [
{'name': '学段基础词汇', 'count': 4},
{'name': '国内应试类词汇', 'count': 4},
{'name': '出国考试类词汇', 'count': 3},
{'name': '职业与专业类词汇', 'count': 3},
{'name': '功能型词库', 'count': 3},
];
}
IconData _getCategoryIcon(VocabularyBookMainCategory category) {
switch (category) {
case VocabularyBookMainCategory.academicStage:
return Icons.school;
case VocabularyBookMainCategory.domesticTest:
return Icons.quiz;
case VocabularyBookMainCategory.internationalTest:
return Icons.public;
case VocabularyBookMainCategory.professional:
return Icons.work;
case VocabularyBookMainCategory.functional:
return Icons.functions;
}
}
Color _getCategoryColor(VocabularyBookMainCategory category) {
switch (category) {
case VocabularyBookMainCategory.academicStage:
return Colors.blue;
case VocabularyBookMainCategory.domesticTest:
return Colors.green;
case VocabularyBookMainCategory.internationalTest:
return Colors.orange;
case VocabularyBookMainCategory.professional:
return Colors.purple;
case VocabularyBookMainCategory.functional:
return Colors.teal;
}
}
Widget _buildCategoryCard(VocabularyBookMainCategory category, IconData icon, Color color, {int? count}) {
final categoryName = VocabularyBookCategoryHelper.getMainCategoryName(category);
return GestureDetector(
onTap: () => _navigateToCategory(category),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Icon(
icon,
color: color,
size: 22,
),
),
const SizedBox(height: 8),
Text(
categoryName,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (count != null) ...[
const SizedBox(height: 4),
Text(
'$count',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
],
),
),
);
}
Widget _buildVocabularyBooks() {
final systemBooks = ref.watch(systemVocabularyBooksProvider);
final userBooks = ref.watch(userVocabularyBooksProvider);
// 合并系统词汇书和用户词汇书
final allBooks = [...systemBooks, ...userBooks];
// 如果没有数据,显示推荐词书
final booksToShow = allBooks.isNotEmpty ? allBooks : _recommendedBooks;
return ResponsiveBuilder(
builder: (context, isMobile, isTablet, isDesktop) {
final crossAxisCount = ResponsiveUtils.getGridColumns(
context,
mobileColumns: 3,
tabletColumns: 4,
desktopColumns: 5,
);
final childAspectRatio = ResponsiveUtils.getValueForScreenSize(
context,
mobile: 1.3,
tablet: 1.4,
desktop: 1.5,
);
return Container(
margin: ResponsiveUtils.getResponsivePadding(context),
child: GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
childAspectRatio: childAspectRatio,
),
itemCount: booksToShow.length,
itemBuilder: (context, index) {
final book = booksToShow[index];
// 使用后端 Provider 获取词书学习进度
final progressAsync = ref.watch(vocabularyBookProgressProvider(book.id));
final progress = progressAsync.when(
data: (p) => (p.progressPercentage / 100.0).clamp(0.0, 1.0),
loading: () => null,
error: (e, st) => 0.0,
);
return _buildBookCard(
book,
progress,
);
},
),
);
},
);
}
Widget _buildBookCard(VocabularyBook book, double? progress) {
return ResponsiveBuilder(
builder: (context, isMobile, isTablet, isDesktop) {
final iconSize = ResponsiveUtils.getValueForScreenSize(
context,
mobile: 64.0,
tablet: 72.0,
desktop: 80.0,
);
final titleFontSize = ResponsiveUtils.getResponsiveFontSize(
context,
mobileSize: 14,
tabletSize: 16,
desktopSize: 18,
);
final subtitleFontSize = ResponsiveUtils.getResponsiveFontSize(
context,
mobileSize: 12,
tabletSize: 14,
desktopSize: 16,
);
final progressFontSize = ResponsiveUtils.getResponsiveFontSize(
context,
mobileSize: 10,
tabletSize: 12,
desktopSize: 14,
);
final cardPadding = ResponsiveUtils.getValueForScreenSize(
context,
mobile: 8.0,
tablet: 10.0,
desktop: 12.0,
);
return GestureDetector(
onTap: () => _navigateToVocabularyBook(book),
child: Container(
padding: EdgeInsets.all(cardPadding),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
children: [
Container(
width: iconSize,
height: iconSize,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
image: book.coverImageUrl != null
? DecorationImage(
image: NetworkImage(book.coverImageUrl!),
fit: BoxFit.cover,
)
: null,
color: book.coverImageUrl == null ? Colors.grey[300] : null,
),
child: book.coverImageUrl == null
? Icon(
Icons.book,
color: Colors.grey,
size: iconSize * 0.4,
)
: null,
),
const SizedBox(height: 4),
Text(
book.name,
style: TextStyle(
fontSize: titleFontSize,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 1),
Text(
'${book.totalWords}',
style: TextStyle(
fontSize: subtitleFontSize,
color: Colors.grey,
),
),
const SizedBox(height: 3),
LinearProgressIndicator(
value: progress,
backgroundColor: Colors.grey[200],
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFF2196F3)),
minHeight: 4,
),
const SizedBox(height: 1),
Text(
progress == null
? '加载中...'
: '${(progress * 100).toInt()}%',
style: TextStyle(
fontSize: ResponsiveUtils.getValueForScreenSize(
context,
mobile: 9.0,
tablet: 10.0,
desktop: 12.0,
),
color: Colors.grey[600],
),
textAlign: TextAlign.center,
),
],
),
),
);
},
);
}
void _navigateToVocabularyBook(VocabularyBook book) {
Navigator.of(context).pushNamed(
Routes.vocabularyList,
arguments: {'vocabularyBook': book},
);
}
void _startTodayWordLearning() {
// 导航到今日单词详情页面
Navigator.of(context).pushNamed(Routes.dailyWords);
}
void _showWordDetailFromModel(Word word) {
final meaning = word.definitions.isNotEmpty
? word.definitions.map((d) => d.translation).join('; ')
: '暂无释义';
final phonetic = word.phonetic ?? '';
_showWordDetail(word.word, meaning, phonetic, word);
}
void _showWordDetail(String wordText, String meaning, String phonetic, [Word? wordModel]) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => Container(
height: MediaQuery.of(context).size.height * 0.7,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
children: [
Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(top: 12),
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
wordText,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
phonetic,
style: const TextStyle(
fontSize: 16,
color: Color(0xFF2196F3),
fontStyle: FontStyle.italic,
),
),
],
),
),
IconButton(
onPressed: () => _playWordPronunciation(wordText),
icon: const Icon(
Icons.volume_up,
color: Color(0xFF2196F3),
size: 32,
),
),
],
),
const SizedBox(height: 24),
const Text(
'释义',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
meaning,
style: const TextStyle(
fontSize: 16,
color: Colors.black87,
),
),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: ElevatedButton(
onPressed: () {
Navigator.pop(context);
if (wordModel != null) {
_startWordLearningWithModel(wordModel);
} else {
_startWordLearning(wordText);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2196F3),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text('开始学习'),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton(
onPressed: () {
if (wordModel != null) {
_addWordToFavorites(wordModel);
} else {
_addToFavorites(wordText);
}
},
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF2196F3),
side: const BorderSide(color: Color(0xFF2196F3)),
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text('收藏'),
),
),
],
),
],
),
),
),
],
),
),
);
}
void _startWordLearningWithModel(Word word) {
Navigator.of(context).pushNamed(
Routes.wordLearning,
arguments: {
'words': [word],
'mode': LearningMode.normal,
'dailyTarget': 1,
},
);
}
void _startWordLearning(String word) {
// 为单个单词创建学习会话
final wordToLearn = Word(
id: '1',
word: word,
phonetic: '/example/',
difficulty: WordDifficulty.intermediate,
frequency: 1000,
definitions: [
WordDefinition(
type: WordType.noun,
definition: word,
translation: '示例释义',
),
],
examples: [
WordExample(
sentence: 'This is an example sentence.',
translation: '这是一个示例句子。',
),
],
createdAt: DateTime.now(),
updatedAt: DateTime.now(),
);
Navigator.of(context).pushNamed(
Routes.wordLearning,
arguments: {
'words': [wordToLearn],
'mode': LearningMode.normal,
'dailyTarget': 1,
},
);
}
void _playWordAudio(Word word) async {
if (word.audioUrl != null && word.audioUrl!.isNotEmpty) {
// TODO: 集成音频服务播放
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('正在播放 "${word.word}" 的发音'),
duration: const Duration(seconds: 1),
),
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('"${word.word}" 暂无音频'),
duration: const Duration(seconds: 1),
),
);
}
}
void _playWordPronunciation(String word) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('播放 "$word" 的发音'),
duration: const Duration(seconds: 1),
),
);
}
void _addWordToFavorites(Word word) async {
// TODO: 集成生词本服务
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('已将 "${word.word}" 添加到生词本'),
duration: const Duration(seconds: 2),
backgroundColor: Colors.green,
),
);
}
void _addToFavorites(String word) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('已将 "$word" 添加到收藏夹'),
duration: const Duration(seconds: 2),
),
);
}
Widget _buildTodayWords() {
final vocabularyAsync = ref.watch(vocabularyProvider);
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
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(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'今日单词',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
TextButton(
onPressed: _startTodayWordLearning,
child: const Text(
'开始学习',
style: TextStyle(
color: Color(0xFF2196F3),
fontSize: 14,
),
),
),
],
),
const SizedBox(height: 16),
Builder(
builder: (context) {
final state = ref.watch(vocabularyProvider);
final todayWords = state.todayWords;
if (todayWords.isEmpty) {
return const Center(
child: Padding(
padding: EdgeInsets.all(16.0),
child: Text(
'今日暂无学习单词',
style: TextStyle(
color: Colors.grey,
fontSize: 14,
),
),
),
);
}
// 显示前3个单词
final displayWords = todayWords.take(3).toList();
return Column(
children: displayWords.asMap().entries.map((entry) {
final index = entry.key;
final word = entry.value;
return Column(
children: [
if (index > 0) const SizedBox(height: 12),
_buildWordItemFromModel(word),
],
);
}).toList(),
);
},
),
],
),
);
}
Widget _buildWordItemFromModel(Word word) {
final meaning = word.definitions.isNotEmpty
? word.definitions.first.translation
: '暂无释义';
final phonetic = word.phonetic ?? '';
return GestureDetector(
onTap: () => _showWordDetailFromModel(word),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
word.word,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
if (phonetic.isNotEmpty) ...[
const SizedBox(height: 2),
Text(
phonetic,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF2196F3),
fontStyle: FontStyle.italic,
),
),
],
const SizedBox(height: 2),
Text(
meaning,
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
],
),
),
Row(
mainAxisSize: MainAxisSize.min,
children: [
if (word.audioUrl != null && word.audioUrl!.isNotEmpty)
IconButton(
onPressed: () => _playWordAudio(word),
icon: const Icon(
Icons.volume_up,
color: Color(0xFF2196F3),
size: 20,
),
),
// IconButton(
// onPressed: () => _addWordToFavorites(word),
// icon: const Icon(
// Icons.favorite_border,
// color: Colors.grey,
// size: 20,
// ),
// ),
],
),
],
),
),
);
}
Widget _buildAIRecommendation() {
final state = ref.watch(vocabularyProvider);
final reviewWords = state.reviewWords;
final todayWords = state.todayWords;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(left: 4, bottom: 12),
child: Text(
'推荐功能',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
Row(
children: [
// 智能复习卡片
Expanded(
child: _buildFeatureCard(
title: '智能复习',
subtitle: reviewWords.isEmpty
? '暂无复习内容'
: '${reviewWords.length} 个单词待复习',
icon: Icons.psychology_outlined,
color: const Color(0xFF9C27B0),
onTap: () => _startSmartReview(),
),
),
const SizedBox(width: 12),
// 词汇测试卡片
Expanded(
child: _buildFeatureCard(
title: '词汇测试',
subtitle: '测试你的词汇量',
icon: Icons.quiz_outlined,
color: const Color(0xFFFF9800),
onTap: () => _startVocabularyTest(),
),
),
],
),
],
),
);
}
Widget _buildFeatureCard({
required String title,
required String subtitle,
required IconData icon,
required Color color,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.all(16),
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(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
color: color,
size: 28,
),
),
const SizedBox(height: 12),
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF2C3E50),
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(
fontSize: 13,
color: Colors.grey[600],
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
// 启动智能复习
void _startSmartReview() async {
final state = ref.read(vocabularyProvider);
final reviewWords = state.reviewWords;
if (reviewWords.isEmpty) {
// 如果没有复习词汇,尝试加载
await ref.read(vocabularyProvider.notifier).loadReviewWords();
final updatedWords = ref.read(vocabularyProvider).reviewWords;
if (updatedWords.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('暂无需要复习的单词,赶紧去学习新单词吧!'),
duration: Duration(seconds: 2),
),
);
return;
}
}
// 导航到智能复习页面
Navigator.of(context).pushNamed(
Routes.smartReview,
arguments: {
'reviewMode': 'adaptive',
'dailyTarget': 20,
},
);
}
// 启动词汇测试
void _startVocabularyTest() {
// 显示测试选项对话框
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
builder: (context) => Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'选择测试类型',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 20),
_buildTestOption(
title: '快速测试',
subtitle: '10道题约 3 分钟',
icon: Icons.speed,
color: const Color(0xFF4CAF50),
onTap: () {
Navigator.pop(context);
_navigateToTest(10);
},
),
const SizedBox(height: 12),
_buildTestOption(
title: '标准测试',
subtitle: '20道题约 6 分钟',
icon: Icons.assessment,
color: const Color(0xFF2196F3),
onTap: () {
Navigator.pop(context);
_navigateToTest(20);
},
),
const SizedBox(height: 12),
_buildTestOption(
title: '完整测试',
subtitle: '50道题约 15 分钟',
icon: Icons.assignment,
color: const Color(0xFFFF9800),
onTap: () {
Navigator.pop(context);
_navigateToTest(50);
},
),
const SizedBox(height: 16),
TextButton(
onPressed: () => Navigator.pop(context),
child: const Center(
child: Text('取消'),
),
),
],
),
),
);
}
Widget _buildTestOption({
required String title,
required String subtitle,
required IconData icon,
required Color color,
required VoidCallback onTap,
}) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: Colors.grey[200]!),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
color: color,
size: 24,
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(
fontSize: 13,
color: Colors.grey[600],
),
),
],
),
),
Icon(
Icons.arrow_forward_ios,
size: 16,
color: Colors.grey[400],
),
],
),
),
);
}
void _navigateToTest(int questionCount) {
Navigator.of(context).pushNamed(
Routes.vocabularyTest,
arguments: {
'testType': 'vocabularyLevel',
'questionCount': questionCount,
},
);
}
Widget _buildLearningStats() {
final dailyStats = ref.watch(dailyVocabularyStatsProvider);
final todayStats = ref.watch(todayStatisticsProvider);
final overallStats = ref.watch(overallVocabularyStatsProvider);
final weeklyTotal = ref.watch(weeklyWordsStudiedProvider);
final currentUser = ref.watch(currentUserProvider);
final todayLearned = dailyStats?.wordsLearned ?? todayStats?.wordsStudied ?? 0;
final totalStudied = (overallStats != null && overallStats['total_studied'] != null)
? (overallStats['total_studied'] as num).toInt()
: 0;
final dailyWordGoal = currentUser?.settings?.dailyWordGoal ?? 20;
final weeklyTarget = (dailyWordGoal * 7).clamp(1, 100000);
final weeklyProgress = weeklyTarget > 0 ? (weeklyTotal / weeklyTarget) : 0.0;
final weeklyPercent = ((weeklyProgress * 100).clamp(0, 100)).round();
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16),
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(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'学习统计',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
TextButton(
onPressed: _showDetailedStats,
child: const Text(
'查看详情',
style: TextStyle(
color: Color(0xFF2196F3),
fontSize: 14,
),
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: GestureDetector(
onTap: _showTodayProgress,
child: _buildStatItem('今日学习', '$todayLearned', '个单词', Icons.today),
),
),
Expanded(
child: GestureDetector(
onTap: _showWeeklyProgress,
child: _buildStatItem('本周学习', '$weeklyTotal', '个单词', Icons.calendar_view_week),
),
),
Expanded(
child: GestureDetector(
onTap: _showVocabularyTest,
child: _buildStatItem('总词汇量', '$totalStudied', '个单词', Icons.library_books),
),
),
],
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFF2196F3).withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
const Icon(
Icons.trending_up,
color: Color(0xFF2196F3),
size: 20,
),
const SizedBox(width: 8),
const Text(
'本周学习进度:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(width: 8),
Expanded(
child: LinearProgressIndicator(
value: weeklyProgress.clamp(0.0, 1.0),
backgroundColor: Colors.grey[300],
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFF2196F3)),
),
),
const SizedBox(width: 8),
Text(
'$weeklyPercent%',
style: const TextStyle(
fontSize: 12,
color: Color(0xFF2196F3),
fontWeight: FontWeight.bold,
),
),
],
),
),
],
),
);
}
Widget _buildStatItem(String label, String value, String unit, IconData icon) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[50],
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Icon(
icon,
color: const Color(0xFF2196F3),
size: 24,
),
const SizedBox(height: 8),
Text(
value,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF2196F3),
),
),
const SizedBox(height: 2),
Text(
unit,
style: const TextStyle(
fontSize: 10,
color: Colors.grey,
),
),
const SizedBox(height: 4),
Text(
label,
style: const TextStyle(
fontSize: 12,
color: Colors.black87,
fontWeight: FontWeight.w500,
),
textAlign: TextAlign.center,
),
],
),
);
}
void _showDetailedStats() {
Navigator.of(context).pushNamed(Routes.learningStatsDetail);
}
void _showTodayProgress() {
// 显示今日学习进度详情
final dailyStats = ref.read(dailyVocabularyStatsProvider);
final todayStats = ref.read(todayStatisticsProvider);
final todayLearned = dailyStats?.wordsLearned ?? todayStats?.wordsStudied ?? 0;
final currentUser = ref.read(currentUserProvider);
final dailyWordGoal = currentUser?.settings?.dailyWordGoal ?? 20;
final progress = dailyWordGoal > 0 ? (todayLearned / dailyWordGoal) : 0.0;
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => Container(
height: MediaQuery.of(context).size.height * 0.6,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
children: [
// 拖拽条
Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(top: 12, bottom: 20),
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
// 内容
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: const Color(0xFF2196F3).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.today,
color: Color(0xFF2196F3),
size: 28,
),
),
const SizedBox(width: 16),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'今日学习进度',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 4),
Text(
'保持每日学习,让进步成为习惯',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
],
),
),
],
),
const SizedBox(height: 32),
// 进度环
Center(
child: SizedBox(
width: 160,
height: 160,
child: Stack(
alignment: Alignment.center,
children: [
SizedBox(
width: 160,
height: 160,
child: CircularProgressIndicator(
value: progress.clamp(0.0, 1.0),
strokeWidth: 12,
backgroundColor: Colors.grey[200],
valueColor: const AlwaysStoppedAnimation<Color>(
Color(0xFF2196F3),
),
),
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'$todayLearned',
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
color: Color(0xFF2196F3),
),
),
Text(
'/ $dailyWordGoal 个单词',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
],
),
),
),
const SizedBox(height: 32),
// 统计信息
_buildProgressStatRow('学习进度', '${(progress * 100).clamp(0, 100).toInt()}%', Icons.trending_up),
const SizedBox(height: 16),
_buildProgressStatRow('已学单词', '$todayLearned', Icons.check_circle),
const SizedBox(height: 16),
_buildProgressStatRow('剩余目标', '${(dailyWordGoal - todayLearned).clamp(0, dailyWordGoal)}', Icons.flag),
const SizedBox(height: 32),
// 鼓励文字
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF2196F3).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
const Icon(
Icons.lightbulb_outline,
color: Color(0xFF2196F3),
),
const SizedBox(width: 12),
Expanded(
child: Text(
progress >= 1.0
? '太棒了!今天的目标已经完成!'
: progress >= 0.5
? '再加把劲,马上就能完成今天的目标!'
: '加油!坚持学习,每天进步一点点!',
style: const TextStyle(
fontSize: 14,
color: Color(0xFF2196F3),
),
),
),
],
),
),
],
),
),
),
],
),
),
);
}
Widget _buildProgressStatRow(String label, String value, IconData icon) {
return Row(
children: [
Icon(
icon,
size: 20,
color: Colors.grey[600],
),
const SizedBox(width: 12),
Text(
label,
style: TextStyle(
fontSize: 16,
color: Colors.grey[700],
),
),
const Spacer(),
Text(
value,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF2196F3),
),
),
],
);
}
void _showWeeklyProgress() {
// 显示本周学习进度详情
final weeklyTotal = ref.read(weeklyWordsStudiedProvider);
final currentUser = ref.read(currentUserProvider);
final dailyWordGoal = currentUser?.settings?.dailyWordGoal ?? 20;
final weeklyTarget = (dailyWordGoal * 7).clamp(1, 100000);
final weeklyProgress = weeklyTarget > 0 ? (weeklyTotal / weeklyTarget) : 0.0;
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => Container(
height: MediaQuery.of(context).size.height * 0.6,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
children: [
// 拖拽条
Container(
width: 40,
height: 4,
margin: const EdgeInsets.only(top: 12, bottom: 20),
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
// 内容
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: const Color(0xFF4CAF50).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: const Icon(
Icons.calendar_view_week,
color: Color(0xFF4CAF50),
size: 28,
),
),
const SizedBox(width: 16),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'本周学习进度',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 4),
Text(
'坑持七天学习,养成好习惯',
style: TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
],
),
),
],
),
const SizedBox(height: 32),
// 进度环
Center(
child: SizedBox(
width: 160,
height: 160,
child: Stack(
alignment: Alignment.center,
children: [
SizedBox(
width: 160,
height: 160,
child: CircularProgressIndicator(
value: weeklyProgress.clamp(0.0, 1.0),
strokeWidth: 12,
backgroundColor: Colors.grey[200],
valueColor: const AlwaysStoppedAnimation<Color>(
Color(0xFF4CAF50),
),
),
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'$weeklyTotal',
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
color: Color(0xFF4CAF50),
),
),
Text(
'/ $weeklyTarget 个单词',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
],
),
),
),
const SizedBox(height: 32),
// 统计信息
_buildProgressStatRow('周学习进度', '${(weeklyProgress * 100).clamp(0, 100).toInt()}%', Icons.trending_up),
const SizedBox(height: 16),
_buildProgressStatRow('已学单词', '$weeklyTotal', Icons.check_circle),
const SizedBox(height: 16),
_buildProgressStatRow('周目标', '$weeklyTarget', Icons.flag),
const SizedBox(height: 16),
_buildProgressStatRow('剩余目标', '${(weeklyTarget - weeklyTotal).clamp(0, weeklyTarget)}', Icons.timer),
const SizedBox(height: 32),
// 鼓励文字
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF4CAF50).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
const Icon(
Icons.emoji_events,
color: Color(0xFF4CAF50),
),
const SizedBox(width: 12),
Expanded(
child: Text(
weeklyProgress >= 1.0
? '完美!本周目标已经达成!'
: weeklyProgress >= 0.7
? '太棒了!本周已经完成大部分目标!'
: '加油!距离本周目标还差一点点!',
style: const TextStyle(
fontSize: 14,
color: Color(0xFF4CAF50),
),
),
),
],
),
),
const SizedBox(height: 16),
// 查看详情按钮
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
Navigator.pop(context);
_showDetailedStats();
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF4CAF50),
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text(
'查看详细统计',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
),
],
),
),
);
}
void _showVocabularyTest() {
// TODO: 导航到词汇量测试页面
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('开始词汇量测试'),
duration: Duration(seconds: 1),
),
);
}
void _navigateToCategory(VocabularyBookMainCategory category) {
// 将枚举转换为中文名称传递
final categoryName = VocabularyBookCategoryHelper.getMainCategoryName(category);
Navigator.pushNamed(
context,
Routes.vocabularyCategory,
arguments: {
'category': categoryName,
},
);
}
}