Files
ai_english/client/lib/features/vocabulary/screens/vocabulary_home_screen.dart

1761 lines
58 KiB
Dart
Raw Normal View History

2025-11-17 14:09:17 +08:00
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,
},
);
}
}