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

1328 lines
42 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 '../models/vocabulary_book_model.dart';
import '../models/word_model.dart';
import '../providers/vocabulary_provider.dart';
import '../services/vocabulary_service.dart';
import '../../../core/network/api_client.dart';
import '../../../core/services/storage_service.dart';
import '../../../shared/widgets/custom_app_bar.dart';
import '../../../shared/widgets/custom_card.dart';
import '../../../shared/widgets/loading_widget.dart';
import '../../../shared/widgets/error_widget.dart' as custom_error;
import 'word_learning_screen.dart';
import 'study_screen.dart';
/// 词汇书详情页面
class VocabularyBookScreen extends ConsumerStatefulWidget {
final VocabularyBook vocabularyBook;
const VocabularyBookScreen({
super.key,
required this.vocabularyBook,
});
@override
ConsumerState<VocabularyBookScreen> createState() => _VocabularyBookScreenState();
}
class _VocabularyBookScreenState extends ConsumerState<VocabularyBookScreen>
with SingleTickerProviderStateMixin {
late TabController _tabController;
ScrollController? _scrollController;
List<Word> _words = [];
List<Word> _filteredWords = []; // 过滤后的单词列表
bool _isLoading = false;
bool _isLoadingMore = false; // 是否正在加载更多
String? _error;
UserVocabularyBookProgress? _progress;
bool _isLoadingProgress = false;
// 搜索相关
bool _isSearching = false;
final TextEditingController _searchController = TextEditingController();
String _searchQuery = '';
// 分页相关
int _currentPage = 1;
final int _pageSize = 50; // 每频50个单词
bool _hasMore = true; // 是否还有更多数据
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
_scrollController = ScrollController();
_scrollController?.addListener(_onScroll);
_loadVocabularyBookWords(); // 加载第一页
_loadProgress();
// 监听搜索输入变化
_searchController.addListener(_onSearchChanged);
}
/// 监听滚动,实现懒加载
void _onScroll() {
if (_scrollController == null) return;
// 如果滚动到底部80%的位置,且不在加载中,且还有更多数据
if (_scrollController!.position.pixels >= _scrollController!.position.maxScrollExtent * 0.8 &&
!_isLoadingMore &&
_hasMore &&
_tabController.index == 0) { // 只在单词列表标签触发
_loadMoreWords();
}
}
/// 加载更多单词
Future<void> _loadMoreWords() async {
if (_isLoadingMore || !_hasMore) return;
setState(() {
_isLoadingMore = true;
});
try {
_currentPage++;
final vocabularyService = VocabularyService(
apiClient: ApiClient.instance,
storageService: await StorageService.getInstance(),
);
final bookWords = await vocabularyService.getVocabularyBookWords(
widget.vocabularyBook.id,
page: _currentPage,
limit: _pageSize,
);
final newWords = bookWords
.where((bw) => bw.word != null)
.map((bw) => bw.word!)
.toList();
setState(() {
_words.addAll(newWords);
_filteredWords = _searchQuery.isEmpty ? _words : _filterWords(_searchQuery);
_isLoadingMore = false;
// 如果返回的数据少于pageSize说明没有更多数据了
_hasMore = newWords.length >= _pageSize;
});
} catch (e) {
print('加载更多单词失败: $e');
setState(() {
_isLoadingMore = false;
_currentPage--; // 回退页码
});
}
}
/// 搜索输入变化监听
void _onSearchChanged() {
setState(() {
_searchQuery = _searchController.text;
_filteredWords = _searchQuery.isEmpty ? _words : _filterWords(_searchQuery);
});
}
/// 过滤单词
List<Word> _filterWords(String query) {
final lowerQuery = query.toLowerCase();
return _words.where((word) {
// 搜索单词、音标、释义
final wordMatch = word.word.toLowerCase().contains(lowerQuery);
final phoneticMatch = word.phonetic?.toLowerCase().contains(lowerQuery) ?? false;
final definitionMatch = word.definitions.any(
(def) => def.translation.toLowerCase().contains(lowerQuery),
);
return wordMatch || phoneticMatch || definitionMatch;
}).toList();
}
/// 切换搜索状态
void _toggleSearch() {
setState(() {
_isSearching = !_isSearching;
if (!_isSearching) {
_searchController.clear();
_searchQuery = '';
_filteredWords = _words;
}
});
}
@override
void dispose() {
_scrollController?.removeListener(_onScroll);
_scrollController?.dispose();
_tabController.dispose();
_searchController.dispose();
super.dispose();
}
/// 从后端API加载词汇书单词第一页
Future<void> _loadVocabularyBookWords() async {
setState(() {
_isLoading = true;
_error = null;
_currentPage = 1;
_hasMore = true;
});
try {
final vocabularyService = VocabularyService(
apiClient: ApiClient.instance,
storageService: await StorageService.getInstance(),
);
final bookWords = await vocabularyService.getVocabularyBookWords(
widget.vocabularyBook.id,
page: 1,
limit: _pageSize,
);
// 提取单词对象
final words = bookWords
.where((bw) => bw.word != null)
.map((bw) => bw.word!)
.toList();
setState(() {
_words = words;
_filteredWords = words; // 初始化过滤列表
_isLoading = false;
});
} catch (e) {
print('加载词汇书单词失败: $e');
setState(() {
_error = '加载失败,请稍后重试';
_isLoading = false;
// API失败时显示空状态不使用模拟数据
_words = [];
});
}
}
/// 加载学习进度
Future<void> _loadProgress({bool forceRefresh = false}) async {
setState(() {
_isLoadingProgress = true;
});
try {
final vocabularyService = VocabularyService(
apiClient: ApiClient.instance,
storageService: await StorageService.getInstance(),
);
final progress = await vocabularyService.getVocabularyBookProgress(
widget.vocabularyBook.id,
forceRefresh: forceRefresh, // 传递强制刷新参数
);
setState(() {
_progress = progress;
_isLoadingProgress = false;
});
} catch (e) {
print('加载学习进度失败: $e');
setState(() {
_isLoadingProgress = false;
// 使用默认进度
_progress = UserVocabularyBookProgress(
id: '0',
userId: '0',
vocabularyBookId: widget.vocabularyBook.id,
learnedWords: 0,
masteredWords: 0,
progressPercentage: 0.0,
streakDays: 0,
totalStudyDays: 0,
averageDailyWords: 0.0,
startedAt: DateTime.now(),
lastStudiedAt: null,
);
});
}
}
void _handleMenuAction(String action) {
switch (action) {
case 'start_learning':
_startLearning(context);
break;
case 'vocabulary_test':
_startVocabularyTest();
break;
case 'smart_review':
_startSmartReview();
break;
case 'export':
_exportWords();
break;
}
}
void _startLearning(BuildContext context) async {
// 显示学习目标选择对话框
double selectedGoal = 20.0;
final dailyGoal = await showDialog<int>(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
title: const Text('设置学习目标'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('请选择每日学习单词数:'),
const SizedBox(height: 8),
Text(
'${selectedGoal.toInt()}个单词',
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
const SizedBox(height: 16),
Slider(
value: selectedGoal,
min: 5,
max: 50,
divisions: 9,
label: '${selectedGoal.toInt()}个单词',
onChanged: (value) {
setState(() {
selectedGoal = value;
});
},
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('5', style: TextStyle(color: Colors.grey[600])),
Text('50', style: TextStyle(color: Colors.grey[600])),
],
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('取消'),
),
ElevatedButton(
onPressed: () => Navigator.of(context).pop(selectedGoal.toInt()),
child: const Text('开始学习'),
),
],
);
},
),
);
if (dailyGoal == null) return;
// 导航到学习页面
final result = await Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => StudyScreen(
vocabularyBook: widget.vocabularyBook,
dailyGoal: dailyGoal,
),
),
);
// 学习完成后强制刷新进度(跳过缓存)
if (result == true) {
_loadProgress(forceRefresh: true);
}
}
void _startVocabularyTest() {
Navigator.of(context).pushNamed(
'/vocabulary_test',
arguments: {
'vocabularyBook': widget.vocabularyBook,
'testType': 'bookTest',
'questionCount': 20,
},
);
}
void _startSmartReview() {
final reviewWords = _words.where((word) {
// 这里可以根据学习进度和遗忘曲线来筛选需要复习的单词
// 暂时返回所有单词
return true;
}).toList();
if (reviewWords.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('暂无需要复习的单词')),
);
return;
}
Navigator.of(context).pushNamed(
'/smart_review',
arguments: {
'vocabularyBook': widget.vocabularyBook,
'words': reviewWords,
'reviewMode': 'adaptive',
},
);
}
void _exportWords() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('导出单词'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
leading: const Icon(Icons.text_snippet),
title: const Text('导出为文本文件'),
onTap: () {
Navigator.of(context).pop();
_exportAsText();
},
),
ListTile(
leading: const Icon(Icons.table_chart),
title: const Text('导出为Excel文件'),
onTap: () {
Navigator.of(context).pop();
_exportAsExcel();
},
),
ListTile(
leading: const Icon(Icons.picture_as_pdf),
title: const Text('导出为PDF文件'),
onTap: () {
Navigator.of(context).pop();
_exportAsPDF();
},
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('取消'),
),
],
),
);
}
void _exportAsText() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('正在导出为文本文件...')),
);
// TODO: 实现文本导出功能
}
void _exportAsExcel() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('正在导出为Excel文件...')),
);
// TODO: 实现Excel导出功能
}
void _exportAsPDF() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('正在导出为PDF文件...')),
);
// TODO: 实现PDF导出功能
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: TabAppBar(
title: widget.vocabularyBook.name,
controller: _tabController,
tabs: const [
Tab(text: '单词列表'),
Tab(text: '学习进度'),
],
actions: [
IconButton(
icon: Icon(_isSearching ? Icons.close : Icons.search),
onPressed: _toggleSearch,
),
PopupMenuButton<String>(
onSelected: _handleMenuAction,
itemBuilder: (context) => [
const PopupMenuItem(
value: 'start_learning',
child: Row(
children: [
Icon(Icons.school),
SizedBox(width: 8),
Text('开始学习'),
],
),
),
const PopupMenuItem(
value: 'vocabulary_test',
child: Row(
children: [
Icon(Icons.quiz),
SizedBox(width: 8),
Text('词汇测试'),
],
),
),
const PopupMenuItem(
value: 'smart_review',
child: Row(
children: [
Icon(Icons.psychology),
SizedBox(width: 8),
Text('智能复习'),
],
),
),
const PopupMenuItem(
value: 'export',
child: Row(
children: [
Icon(Icons.download),
SizedBox(width: 8),
Text('导出单词'),
],
),
),
],
),
],
),
body: Column(
children: [
// 搜索框
if (_isSearching)
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: TextField(
controller: _searchController,
autofocus: true,
decoration: InputDecoration(
hintText: '搜索单词、音标或释义...',
prefixIcon: const Icon(Icons.search),
suffixIcon: _searchQuery.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey[300]!),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).primaryColor,
width: 2,
),
),
filled: true,
fillColor: Colors.grey[50],
),
),
),
// TabBarView
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildWordListTab(),
_buildProgressTab(),
],
),
),
],
),
floatingActionButton: _buildFloatingActionButton(),
);
}
Widget _buildFloatingActionButton() {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
FloatingActionButton.extended(
onPressed: () => _startLearning(context),
icon: const Icon(Icons.school),
label: const Text('开始学习'),
heroTag: 'start_learning',
),
const SizedBox(height: 8),
FloatingActionButton(
onPressed: () => _startVocabularyTest(),
child: const Icon(Icons.quiz),
heroTag: 'vocabulary_test',
),
],
);
}
Widget _buildWordListTab() {
if (_isLoading) {
return const Center(child: LoadingWidget());
}
if (_error != null) {
return Center(
child: custom_error.PageErrorWidget(
message: _error!,
onRetry: _loadVocabularyBookWords,
),
);
}
if (_words.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.book_outlined,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'暂无单词',
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
Text(
'该词汇书还没有单词',
style: TextStyle(
fontSize: 14,
color: Colors.grey[500],
),
),
],
),
);
}
return RefreshIndicator(
onRefresh: _loadVocabularyBookWords,
child: ListView.builder(
controller: _scrollController, // 可以为nullListView会自动处理
padding: const EdgeInsets.all(16),
itemCount: _filteredWords.length + (_hasMore && _searchQuery.isEmpty ? 1 : 0), // 搜索时不显示加载更多
itemBuilder: (context, index) {
// 搜索结果提示
if (_searchQuery.isNotEmpty && index == 0 && _filteredWords.isEmpty) {
return _buildNoResultsWidget();
}
// 如果是最后一项且还有更多数据,显示加载指示器
if (index == _filteredWords.length && _searchQuery.isEmpty) {
return _buildLoadingIndicator();
}
final word = _filteredWords[index];
return _buildWordCard(word, index);
},
),
);
}
/// 构建无搜索结果提示
Widget _buildNoResultsWidget() {
return Center(
child: Padding(
padding: const EdgeInsets.all(48),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.search_off,
size: 64,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'未找到相关单词',
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
),
),
const SizedBox(height: 8),
Text(
'请尝试使用其他关键词搜索',
style: TextStyle(
fontSize: 14,
color: Colors.grey[500],
),
),
],
),
),
);
}
/// 构建加载更多指示器
Widget _buildLoadingIndicator() {
return Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: Column(
children: [
const CircularProgressIndicator(),
const SizedBox(height: 8),
Text(
'正在加载更多...',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
),
);
}
Widget _buildWordCard(Word word, int index) {
return CustomCard(
margin: const EdgeInsets.only(bottom: 12),
child: InkWell(
onTap: () => _showWordDetail(word),
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
'${index + 1}',
style: TextStyle(
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
word.word,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
if (word.phonetic != null) ...[
const SizedBox(width: 8),
Text(
word.phonetic!,
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
],
),
if (word.definitions.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
word.definitions.first.translation,
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
),
IconButton(
icon: const Icon(Icons.volume_up),
onPressed: () => _playWordPronunciation(word),
),
],
),
if (word.examples.isNotEmpty) ...[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
word.examples.first.sentence,
style: const TextStyle(
fontSize: 13,
fontStyle: FontStyle.italic,
),
),
const SizedBox(height: 4),
Text(
word.examples.first.translation,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
),
],
],
),
),
),
);
}
void _showWordDetail(Word word) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => DraggableScrollableSheet(
initialChildSize: 0.7,
minChildSize: 0.5,
maxChildSize: 0.95,
builder: (context, scrollController) => Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
child: Column(
children: [
Container(
margin: const EdgeInsets.symmetric(vertical: 12),
width: 40,
height: 4,
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius: BorderRadius.circular(2),
),
),
Expanded(
child: ListView(
controller: scrollController,
padding: const EdgeInsets.all(24),
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
word.word,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
),
),
if (word.phonetic != null) ...[
const SizedBox(height: 4),
Text(
word.phonetic!,
style: TextStyle(
fontSize: 16,
color: Colors.grey[600],
),
),
],
],
),
),
IconButton(
icon: const Icon(Icons.volume_up, size: 32),
onPressed: () => _playWordPronunciation(word),
),
],
),
const SizedBox(height: 24),
const Text(
'释义',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
...word.definitions.map((def) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
_getWordTypeText(def.type),
style: TextStyle(
fontSize: 12,
color: Theme.of(context).primaryColor,
fontWeight: FontWeight.bold,
),
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
def.translation,
style: const TextStyle(fontSize: 16),
),
),
],
),
)),
if (word.examples.isNotEmpty) ...[
const SizedBox(height: 24),
const Text(
'例句',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
...word.examples.map((example) => Container(
margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
example.sentence,
style: const TextStyle(
fontSize: 15,
fontStyle: FontStyle.italic,
),
),
const SizedBox(height: 8),
Text(
example.translation,
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
),
),
],
),
)),
],
if (word.synonyms.isNotEmpty) ...[
const SizedBox(height: 24),
const Text(
'同义词',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: word.synonyms.map((synonym) => Chip(
label: Text(synonym),
backgroundColor: Colors.blue[50],
)).toList(),
),
],
if (word.antonyms.isNotEmpty) ...[
const SizedBox(height: 24),
const Text(
'反义词',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: word.antonyms.map((antonym) => Chip(
label: Text(antonym),
backgroundColor: Colors.orange[50],
)).toList(),
),
],
],
),
),
],
),
),
),
);
}
String _getWordTypeText(WordType type) {
switch (type) {
case WordType.noun:
return 'n.';
case WordType.verb:
return 'v.';
case WordType.adjective:
return 'adj.';
case WordType.adverb:
return 'adv.';
case WordType.pronoun:
return 'pron.';
case WordType.preposition:
return 'prep.';
case WordType.conjunction:
return 'conj.';
case WordType.interjection:
return 'interj.';
case WordType.article:
return 'art.';
case WordType.phrase:
return 'phrase';
}
}
void _playWordPronunciation(Word word) {
// TODO: 实现音频播放功能
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('播放 ${word.word} 的发音')),
);
}
Widget _buildProgressTab() {
if (_isLoadingProgress) {
return const Center(child: LoadingWidget());
}
if (_progress == null) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.trending_up, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text(
'暂无学习进度',
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
),
],
),
);
}
return RefreshIndicator(
onRefresh: () => _loadProgress(forceRefresh: true), // 下拉刷新强制跳过缓存
child: ListView(
padding: const EdgeInsets.all(16),
children: [
// 整体进度卡片
CustomCard(
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'整体进度',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: Theme.of(context).primaryColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
'${_progress!.progressPercentage.toStringAsFixed(1)}%',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Theme.of(context).primaryColor,
),
),
),
],
),
const SizedBox(height: 20),
// 进度条
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: LinearProgressIndicator(
value: _progress!.progressPercentage / 100,
minHeight: 20,
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).primaryColor,
),
),
),
const SizedBox(height: 16),
// 进度详情
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${_progress!.learnedWords} / ${widget.vocabularyBook.totalWords} 个单词',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
Text(
'剩余 ${widget.vocabularyBook.totalWords - _progress!.learnedWords}',
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
),
),
],
),
],
),
),
),
const SizedBox(height: 16),
// 单词状态统计
Row(
children: [
Expanded(
child: _buildStatCard(
'已学习',
_progress!.learnedWords.toString(),
Icons.school,
Colors.blue,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
'已掌握',
_progress!.masteredWords.toString(),
Icons.check_circle,
Colors.green,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildStatCard(
'待复习',
'0',
Icons.refresh,
Colors.orange,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildStatCard(
'未学习',
'${widget.vocabularyBook.totalWords - _progress!.learnedWords}',
Icons.fiber_new,
Colors.grey,
),
),
],
),
const SizedBox(height: 16),
// 学习信息
CustomCard(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'学习信息',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
_buildInfoRow(
Icons.calendar_today,
'累计学习',
'${_progress!.totalStudyDays}',
),
const Divider(height: 24),
_buildInfoRow(
Icons.trending_up,
'掌握率',
'${(_progress!.masteredWords / widget.vocabularyBook.totalWords * 100).toStringAsFixed(1)}%',
),
const Divider(height: 24),
_buildInfoRow(
Icons.access_time,
'上次学习',
_progress!.lastStudiedAt != null
? _formatDateTime(_progress!.lastStudiedAt!)
: '从未学习',
),
const Divider(height: 24),
_buildInfoRow(
Icons.play_circle_outline,
'开始时间',
_formatDateTime(_progress!.startedAt),
),
],
),
),
),
],
),
);
}
Widget _buildStatCard(String label, String value, IconData icon, Color color) {
return CustomCard(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, size: 24, color: color),
),
const SizedBox(height: 12),
Text(
value,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: color,
),
),
const SizedBox(height: 4),
Text(
label,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
),
);
}
Widget _buildInfoRow(IconData icon, String label, String value) {
return Row(
children: [
Icon(icon, size: 20, color: Colors.grey[600]),
const SizedBox(width: 12),
Expanded(
child: Text(
label,
style: TextStyle(
fontSize: 14,
color: Colors.grey[700],
),
),
),
Text(
value,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
],
);
}
String _formatDateTime(DateTime dateTime) {
final now = DateTime.now();
final difference = now.difference(dateTime);
if (difference.inDays == 0) {
if (difference.inHours == 0) {
if (difference.inMinutes == 0) {
return '刚刚';
}
return '${difference.inMinutes} 分钟前';
}
return '${difference.inHours} 小时前';
} else if (difference.inDays == 1) {
return '昨天';
} else if (difference.inDays < 7) {
return '${difference.inDays} 天前';
} else {
return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')}';
}
}
}