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

1854 lines
57 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 '../../../shared/widgets/custom_app_bar.dart';
import '../../../shared/widgets/loading_widget.dart';
class StudyPlanScreen extends ConsumerStatefulWidget {
const StudyPlanScreen({super.key});
@override
ConsumerState<StudyPlanScreen> createState() => _StudyPlanScreenState();
}
class _StudyPlanScreenState extends ConsumerState<StudyPlanScreen>
with TickerProviderStateMixin {
late TabController _tabController;
bool _isLoading = false;
List<StudyPlan> _studyPlans = [];
StudyPlan? _currentPlan;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
_loadStudyPlans();
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isMobile = ResponsiveUtils.isMobile(context);
return Scaffold(
appBar: CustomAppBar(
title: '学习计划',
actions: [
IconButton(
icon: const Icon(Icons.add),
onPressed: _showCreatePlanDialog,
),
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert),
onSelected: _handleMenuAction,
itemBuilder: (context) => [
const PopupMenuItem(
value: 'templates',
child: Row(
children: [
Icon(Icons.library_books),
SizedBox(width: 8),
Text('计划模板'),
],
),
),
const PopupMenuItem(
value: 'statistics',
child: Row(
children: [
Icon(Icons.analytics),
SizedBox(width: 8),
Text('学习统计'),
],
),
),
const PopupMenuItem(
value: 'settings',
child: Row(
children: [
Icon(Icons.settings),
SizedBox(width: 8),
Text('设置'),
],
),
),
],
),
],
),
body: Column(
children: [
_buildTabBar(context, isMobile),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_buildCurrentPlanTab(context, isMobile),
_buildAllPlansTab(context, isMobile),
_buildProgressTab(context, isMobile),
],
),
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: _showCreatePlanDialog,
backgroundColor: Colors.blue,
child: const Icon(Icons.add, color: Colors.white),
),
);
}
Widget _buildTabBar(BuildContext context, bool isMobile) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
border: Border(
bottom: BorderSide(color: Colors.grey.shade200),
),
),
child: TabBar(
controller: _tabController,
labelColor: Colors.blue,
unselectedLabelColor: Colors.grey,
indicatorColor: Colors.blue,
tabs: const [
Tab(
icon: Icon(Icons.today),
text: '当前计划',
),
Tab(
icon: Icon(Icons.list),
text: '所有计划',
),
Tab(
icon: Icon(Icons.trending_up),
text: '学习进度',
),
],
),
);
}
Widget _buildCurrentPlanTab(BuildContext context, bool isMobile) {
if (_isLoading) {
return const LoadingWidget();
}
if (_currentPlan == null) {
return _buildNoPlanState(context, isMobile);
}
return SingleChildScrollView(
padding: EdgeInsets.all(isMobile ? 16.0 : 24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildCurrentPlanHeader(context, _currentPlan!, isMobile),
const SizedBox(height: 24),
_buildTodayTasks(context, _currentPlan!, isMobile),
const SizedBox(height: 24),
_buildWeeklyProgress(context, _currentPlan!, isMobile),
const SizedBox(height: 24),
_buildQuickActions(context, isMobile),
],
),
);
}
Widget _buildAllPlansTab(BuildContext context, bool isMobile) {
if (_isLoading) {
return const LoadingWidget();
}
if (_studyPlans.isEmpty) {
return _buildEmptyPlansState(context, isMobile);
}
return ListView.builder(
padding: EdgeInsets.all(isMobile ? 16.0 : 24.0),
itemCount: _studyPlans.length,
itemBuilder: (context, index) {
final plan = _studyPlans[index];
return _buildPlanCard(context, plan, isMobile);
},
);
}
Widget _buildProgressTab(BuildContext context, bool isMobile) {
if (_isLoading) {
return const LoadingWidget();
}
return SingleChildScrollView(
padding: EdgeInsets.all(isMobile ? 16.0 : 24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildOverallProgress(context, isMobile),
const SizedBox(height: 24),
_buildWeeklyChart(context, isMobile),
const SizedBox(height: 24),
_buildMonthlyStats(context, isMobile),
const SizedBox(height: 24),
_buildAchievements(context, isMobile),
],
),
);
}
Widget _buildCurrentPlanHeader(BuildContext context, StudyPlan plan, bool isMobile) {
final progress = plan.completedTasks / plan.totalTasks;
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
padding: EdgeInsets.all(isMobile ? 20.0 : 24.0),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade400, Colors.blue.shade600],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
plan.title,
style: TextStyle(
color: Colors.white,
fontSize: isMobile ? 20 : 24,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
plan.description,
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: isMobile ? 14 : 16,
),
),
],
),
),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
_getPlanTypeIcon(plan.type),
color: Colors.white,
size: isMobile ? 28 : 32,
),
),
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'总体进度',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: isMobile ? 14 : 16,
),
),
const SizedBox(height: 8),
LinearProgressIndicator(
value: progress,
backgroundColor: Colors.white.withOpacity(0.3),
valueColor: const AlwaysStoppedAnimation<Color>(Colors.white),
minHeight: 6,
),
const SizedBox(height: 4),
Text(
'${(progress * 100).toInt()}% (${plan.completedTasks}/${plan.totalTasks})',
style: TextStyle(
color: Colors.white,
fontSize: isMobile ? 12 : 14,
fontWeight: FontWeight.w500,
),
),
],
),
),
const SizedBox(width: 20),
Column(
children: [
Text(
'剩余天数',
style: TextStyle(
color: Colors.white.withOpacity(0.9),
fontSize: isMobile ? 12 : 14,
),
),
Text(
'${plan.endDate.difference(DateTime.now()).inDays}',
style: TextStyle(
color: Colors.white,
fontSize: isMobile ? 24 : 28,
fontWeight: FontWeight.bold,
),
),
],
),
],
),
],
),
),
);
}
Widget _buildTodayTasks(BuildContext context, StudyPlan plan, bool isMobile) {
final todayTasks = plan.tasks.where((task) {
final today = DateTime.now();
return task.dueDate.year == today.year &&
task.dueDate.month == today.month &&
task.dueDate.day == today.day;
}).toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.today,
color: Colors.blue[700],
size: isMobile ? 24 : 28,
),
const SizedBox(width: 8),
Text(
'今日任务',
style: TextStyle(
fontSize: isMobile ? 18 : 20,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
'${todayTasks.where((t) => t.isCompleted).length}/${todayTasks.length}',
style: TextStyle(
fontSize: 12,
color: Colors.blue[700],
fontWeight: FontWeight.w500,
),
),
),
],
),
const SizedBox(height: 16),
if (todayTasks.isEmpty)
Container(
padding: EdgeInsets.all(isMobile ? 20.0 : 24.0),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.green.withOpacity(0.3)),
),
child: Row(
children: [
Icon(
Icons.check_circle,
color: Colors.green[600],
size: isMobile ? 24 : 28,
),
const SizedBox(width: 12),
Expanded(
child: Text(
'今日无任务,您可以休息或提前学习明天的内容',
style: TextStyle(
fontSize: isMobile ? 14 : 16,
color: Colors.green[700],
),
),
),
],
),
)
else
...todayTasks.map((task) => _buildTaskCard(context, task, isMobile)),
],
);
}
Widget _buildTaskCard(BuildContext context, StudyTask task, bool isMobile) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
child: Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
onTap: () => _toggleTaskCompletion(task),
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: EdgeInsets.all(isMobile ? 16.0 : 20.0),
child: Row(
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: task.isCompleted ? Colors.green : Colors.transparent,
border: Border.all(
color: task.isCompleted ? Colors.green : Colors.grey[400]!,
width: 2,
),
),
child: task.isCompleted
? const Icon(
Icons.check,
color: Colors.white,
size: 16,
)
: null,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
task.title,
style: TextStyle(
fontSize: isMobile ? 16 : 18,
fontWeight: FontWeight.w600,
color: task.isCompleted ? Colors.grey[500] : Colors.grey[800],
decoration: task.isCompleted ? TextDecoration.lineThrough : null,
),
),
if (task.description.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
task.description,
style: TextStyle(
fontSize: isMobile ? 14 : 16,
color: task.isCompleted ? Colors.grey[400] : Colors.grey[600],
),
),
],
const SizedBox(height: 8),
Row(
children: [
_buildTaskTypeChip(task.type, isMobile),
const SizedBox(width: 8),
if (task.estimatedMinutes > 0)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.access_time,
size: 12,
color: Colors.orange[700],
),
const SizedBox(width: 4),
Text(
'${task.estimatedMinutes}分钟',
style: TextStyle(
fontSize: 12,
color: Colors.orange[700],
),
),
],
),
),
],
),
],
),
),
if (!task.isCompleted)
IconButton(
icon: const Icon(Icons.play_arrow),
onPressed: () => _startTask(task),
color: Colors.blue,
),
],
),
),
),
),
);
}
Widget _buildTaskTypeChip(TaskType type, bool isMobile) {
Color color;
String text;
IconData icon;
switch (type) {
case TaskType.vocabulary:
color = Colors.blue;
text = '词汇';
icon = Icons.book;
break;
case TaskType.reading:
color = Colors.green;
text = '阅读';
icon = Icons.article;
break;
case TaskType.listening:
color = Colors.purple;
text = '听力';
icon = Icons.headphones;
break;
case TaskType.speaking:
color = Colors.orange;
text = '口语';
icon = Icons.mic;
break;
case TaskType.writing:
color = Colors.red;
text = '写作';
icon = Icons.edit;
break;
case TaskType.review:
color = Colors.teal;
text = '复习';
icon = Icons.refresh;
break;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 12, color: color),
const SizedBox(width: 4),
Text(
text,
style: TextStyle(
fontSize: 12,
color: color,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
Widget _buildWeeklyProgress(BuildContext context, StudyPlan plan, bool isMobile) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.calendar_view_week,
color: Colors.blue[700],
size: isMobile ? 24 : 28,
),
const SizedBox(width: 8),
Text(
'本周进度',
style: TextStyle(
fontSize: isMobile ? 18 : 20,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
),
],
),
const SizedBox(height: 16),
Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: EdgeInsets.all(isMobile ? 16.0 : 20.0),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: List.generate(7, (index) {
final date = DateTime.now().subtract(Duration(days: 6 - index));
final dayName = ['', '', '', '', '', '', ''][date.weekday - 1];
final isToday = date.day == DateTime.now().day;
final hasTask = plan.tasks.any((task) =>
task.dueDate.year == date.year &&
task.dueDate.month == date.month &&
task.dueDate.day == date.day);
final isCompleted = hasTask && plan.tasks
.where((task) =>
task.dueDate.year == date.year &&
task.dueDate.month == date.month &&
task.dueDate.day == date.day)
.every((task) => task.isCompleted);
return Column(
children: [
Text(
dayName,
style: TextStyle(
fontSize: 12,
color: isToday ? Colors.blue : Colors.grey[600],
fontWeight: isToday ? FontWeight.bold : FontWeight.normal,
),
),
const SizedBox(height: 8),
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isCompleted
? Colors.green
: hasTask
? Colors.orange.withOpacity(0.3)
: Colors.grey.withOpacity(0.2),
border: isToday
? Border.all(color: Colors.blue, width: 2)
: null,
),
child: Center(
child: isCompleted
? const Icon(
Icons.check,
color: Colors.white,
size: 16,
)
: hasTask
? Icon(
Icons.circle,
color: Colors.orange,
size: 8,
)
: null,
),
),
const SizedBox(height: 4),
Text(
'${date.day}',
style: TextStyle(
fontSize: 10,
color: Colors.grey[500],
),
),
],
);
}),
),
],
),
),
),
],
);
}
Widget _buildQuickActions(BuildContext context, bool isMobile) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'快速操作',
style: TextStyle(
fontSize: isMobile ? 18 : 20,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
),
const SizedBox(height: 16),
GridView.count(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: isMobile ? 2 : 4,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: isMobile ? 1.2 : 1.0,
children: [
_buildQuickActionCard(
context,
'开始学习',
Icons.play_arrow,
Colors.green,
() => _startStudySession(),
isMobile,
),
_buildQuickActionCard(
context,
'复习单词',
Icons.refresh,
Colors.blue,
() => _startReview(),
isMobile,
),
_buildQuickActionCard(
context,
'查看统计',
Icons.analytics,
Colors.purple,
() => _showStatistics(),
isMobile,
),
_buildQuickActionCard(
context,
'调整计划',
Icons.edit,
Colors.orange,
() => _editCurrentPlan(),
isMobile,
),
],
),
],
);
}
Widget _buildQuickActionCard(
BuildContext context,
String title,
IconData icon,
Color color,
VoidCallback onTap,
bool isMobile,
) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: EdgeInsets.all(isMobile ? 12.0 : 16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
icon,
color: color,
size: isMobile ? 24 : 28,
),
),
const SizedBox(height: 8),
Text(
title,
style: TextStyle(
fontSize: isMobile ? 12 : 14,
fontWeight: FontWeight.w600,
color: Colors.grey[800],
),
textAlign: TextAlign.center,
),
],
),
),
),
);
}
Widget _buildPlanCard(BuildContext context, StudyPlan plan, bool isMobile) {
final progress = plan.completedTasks / plan.totalTasks;
final isActive = plan == _currentPlan;
return Container(
margin: const EdgeInsets.only(bottom: 16),
child: Card(
elevation: isActive ? 4 : 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: isActive
? BorderSide(color: Colors.blue, width: 2)
: BorderSide.none,
),
child: InkWell(
onTap: () => _selectPlan(plan),
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: EdgeInsets.all(isMobile ? 16.0 : 20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: _getPlanTypeColor(plan.type).withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
_getPlanTypeIcon(plan.type),
color: _getPlanTypeColor(plan.type),
size: isMobile ? 20 : 24,
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
plan.title,
style: TextStyle(
fontSize: isMobile ? 16 : 18,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
),
),
if (isActive)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
),
child: const Text(
'当前',
style: TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.w500,
),
),
),
],
),
const SizedBox(height: 4),
Text(
plan.description,
style: TextStyle(
fontSize: isMobile ? 14 : 16,
color: Colors.grey[600],
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
PopupMenuButton<String>(
icon: Icon(
Icons.more_vert,
color: Colors.grey[600],
),
onSelected: (value) => _handlePlanAction(value, plan),
itemBuilder: (context) => [
if (!isActive)
const PopupMenuItem(
value: 'activate',
child: Row(
children: [
Icon(Icons.play_arrow, size: 18),
SizedBox(width: 8),
Text('设为当前'),
],
),
),
const PopupMenuItem(
value: 'edit',
child: Row(
children: [
Icon(Icons.edit, size: 18),
SizedBox(width: 8),
Text('编辑'),
],
),
),
const PopupMenuItem(
value: 'duplicate',
child: Row(
children: [
Icon(Icons.copy, size: 18),
SizedBox(width: 8),
Text('复制'),
],
),
),
const PopupMenuItem(
value: 'delete',
child: Row(
children: [
Icon(Icons.delete, size: 18, color: Colors.red),
SizedBox(width: 8),
Text('删除', style: TextStyle(color: Colors.red)),
],
),
),
],
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'进度',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
const SizedBox(height: 4),
LinearProgressIndicator(
value: progress,
backgroundColor: Colors.grey[300],
valueColor: AlwaysStoppedAnimation<Color>(
_getPlanTypeColor(plan.type),
),
minHeight: 4,
),
const SizedBox(height: 4),
Text(
'${(progress * 100).toInt()}% (${plan.completedTasks}/${plan.totalTasks})',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
),
),
const SizedBox(width: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'剩余',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
Text(
'${plan.endDate.difference(DateTime.now()).inDays}',
style: TextStyle(
fontSize: isMobile ? 16 : 18,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
),
],
),
],
),
],
),
),
),
),
);
}
Widget _buildOverallProgress(BuildContext context, bool isMobile) {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Container(
padding: EdgeInsets.all(isMobile ? 20.0 : 24.0),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.purple.shade400, Colors.purple.shade600],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.trending_up,
color: Colors.white,
size: isMobile ? 28 : 32,
),
const SizedBox(width: 12),
Expanded(
child: Text(
'学习统计',
style: TextStyle(
color: Colors.white,
fontSize: isMobile ? 20 : 24,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: _buildStatItem('连续学习', '7天', Icons.local_fire_department, isMobile),
),
Expanded(
child: _buildStatItem('总学习时长', '45小时', Icons.access_time, isMobile),
),
],
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildStatItem('掌握单词', '328个', Icons.book, isMobile),
),
Expanded(
child: _buildStatItem('完成任务', '156个', Icons.check_circle, isMobile),
),
],
),
],
),
),
);
}
Widget _buildStatItem(String label, String value, IconData icon, bool isMobile) {
return Column(
children: [
Icon(
icon,
color: Colors.white.withOpacity(0.8),
size: isMobile ? 24 : 28,
),
const SizedBox(height: 8),
Text(
value,
style: TextStyle(
color: Colors.white,
fontSize: isMobile ? 18 : 20,
fontWeight: FontWeight.bold,
),
),
Text(
label,
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: isMobile ? 12 : 14,
),
),
],
);
}
Widget _buildWeeklyChart(BuildContext context, bool isMobile) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: EdgeInsets.all(isMobile ? 16.0 : 20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'本周学习时长',
style: TextStyle(
fontSize: isMobile ? 18 : 20,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
),
const SizedBox(height: 20),
SizedBox(
height: 200,
child: Center(
child: Text(
'图表组件开发中...',
style: TextStyle(
fontSize: isMobile ? 16 : 18,
color: Colors.grey[500],
),
),
),
),
],
),
),
);
}
Widget _buildMonthlyStats(BuildContext context, bool isMobile) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: EdgeInsets.all(isMobile ? 16.0 : 20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'月度统计',
style: TextStyle(
fontSize: isMobile ? 18 : 20,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
),
const SizedBox(height: 16),
Row(
children: [
Expanded(
child: _buildMonthlyStatCard(
'学习天数',
'23/30',
Icons.calendar_today,
Colors.blue,
isMobile,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildMonthlyStatCard(
'平均时长',
'1.8小时',
Icons.access_time,
Colors.green,
isMobile,
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildMonthlyStatCard(
'新学单词',
'156个',
Icons.add_circle,
Colors.orange,
isMobile,
),
),
const SizedBox(width: 12),
Expanded(
child: _buildMonthlyStatCard(
'复习次数',
'89次',
Icons.refresh,
Colors.purple,
isMobile,
),
),
],
),
],
),
),
);
}
Widget _buildMonthlyStatCard(
String label,
String value,
IconData icon,
Color color,
bool isMobile,
) {
return Container(
padding: EdgeInsets.all(isMobile ? 12.0 : 16.0),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Icon(
icon,
color: color,
size: isMobile ? 24 : 28,
),
const SizedBox(height: 8),
Text(
value,
style: TextStyle(
fontSize: isMobile ? 16 : 18,
fontWeight: FontWeight.bold,
color: color,
),
),
Text(
label,
style: TextStyle(
fontSize: isMobile ? 12 : 14,
color: Colors.grey[600],
),
),
],
),
);
}
Widget _buildAchievements(BuildContext context, bool isMobile) {
final achievements = [
Achievement(
title: '连续学习7天',
description: '坚持每天学习,养成良好习惯',
icon: Icons.local_fire_department,
color: Colors.red,
isUnlocked: true,
),
Achievement(
title: '掌握100个单词',
description: '词汇量达到新的里程碑',
icon: Icons.book,
color: Colors.blue,
isUnlocked: true,
),
Achievement(
title: '学习时长达到50小时',
description: '累计学习时间的重要节点',
icon: Icons.access_time,
color: Colors.green,
isUnlocked: false,
),
Achievement(
title: '完成第一个学习计划',
description: '成功完成制定的学习目标',
icon: Icons.emoji_events,
color: Colors.orange,
isUnlocked: false,
),
];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'成就徽章',
style: TextStyle(
fontSize: isMobile ? 18 : 20,
fontWeight: FontWeight.bold,
color: Colors.grey[800],
),
),
const SizedBox(height: 16),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: isMobile ? 2 : 4,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
childAspectRatio: 1.0,
),
itemCount: achievements.length,
itemBuilder: (context, index) {
final achievement = achievements[index];
return _buildAchievementCard(context, achievement, isMobile);
},
),
],
);
}
Widget _buildAchievementCard(BuildContext context, Achievement achievement, bool isMobile) {
return Card(
elevation: achievement.isUnlocked ? 4 : 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Container(
padding: EdgeInsets.all(isMobile ? 12.0 : 16.0),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: achievement.isUnlocked
? achievement.color.withOpacity(0.1)
: Colors.grey.withOpacity(0.05),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: achievement.isUnlocked
? achievement.color
: Colors.grey.withOpacity(0.3),
),
child: Icon(
achievement.icon,
color: achievement.isUnlocked ? Colors.white : Colors.grey,
size: isMobile ? 24 : 28,
),
),
const SizedBox(height: 8),
Text(
achievement.title,
style: TextStyle(
fontSize: isMobile ? 12 : 14,
fontWeight: FontWeight.bold,
color: achievement.isUnlocked
? Colors.grey[800]
: Colors.grey[500],
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
achievement.description,
style: TextStyle(
fontSize: 10,
color: achievement.isUnlocked
? Colors.grey[600]
: Colors.grey[400],
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
Widget _buildNoPlanState(BuildContext context, bool isMobile) {
return Center(
child: Padding(
padding: EdgeInsets.all(isMobile ? 32.0 : 48.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.assignment,
size: isMobile ? 80 : 100,
color: Colors.grey[400],
),
const SizedBox(height: 24),
Text(
'暂无学习计划',
style: TextStyle(
fontSize: isMobile ? 20 : 24,
fontWeight: FontWeight.bold,
color: Colors.grey[600],
),
),
const SizedBox(height: 12),
Text(
'创建您的第一个学习计划,开始高效学习之旅',
style: TextStyle(
fontSize: isMobile ? 16 : 18,
color: Colors.grey[500],
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
ElevatedButton.icon(
onPressed: _showCreatePlanDialog,
icon: const Icon(Icons.add),
label: const Text('创建计划'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: isMobile ? 24 : 32,
vertical: isMobile ? 12 : 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
),
),
],
),
),
);
}
Widget _buildEmptyPlansState(BuildContext context, bool isMobile) {
return Center(
child: Padding(
padding: EdgeInsets.all(isMobile ? 32.0 : 48.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.list_alt,
size: isMobile ? 80 : 100,
color: Colors.grey[400],
),
const SizedBox(height: 24),
Text(
'暂无学习计划',
style: TextStyle(
fontSize: isMobile ? 20 : 24,
fontWeight: FontWeight.bold,
color: Colors.grey[600],
),
),
const SizedBox(height: 12),
Text(
'创建学习计划,制定个性化的学习目标',
style: TextStyle(
fontSize: isMobile ? 16 : 18,
color: Colors.grey[500],
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
onPressed: _showCreatePlanDialog,
icon: const Icon(Icons.add),
label: const Text('创建计划'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: EdgeInsets.symmetric(
horizontal: isMobile ? 20 : 24,
vertical: isMobile ? 12 : 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
),
),
const SizedBox(width: 16),
OutlinedButton.icon(
onPressed: _showPlanTemplates,
icon: const Icon(Icons.library_books),
label: const Text('使用模板'),
style: OutlinedButton.styleFrom(
padding: EdgeInsets.symmetric(
horizontal: isMobile ? 20 : 24,
vertical: isMobile ? 12 : 16,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(25),
),
),
),
],
),
],
),
),
);
}
Future<void> _loadStudyPlans() async {
setState(() {
_isLoading = true;
});
// 模拟加载数据
await Future.delayed(const Duration(milliseconds: 800));
setState(() {
_studyPlans = _generateMockPlans();
_currentPlan = _studyPlans.isNotEmpty ? _studyPlans.first : null;
_isLoading = false;
});
}
List<StudyPlan> _generateMockPlans() {
final now = DateTime.now();
return [
StudyPlan(
id: '1',
title: '30天词汇突破计划',
description: '每天学习20个新单词复习已学单词',
type: PlanType.vocabulary,
startDate: now.subtract(const Duration(days: 7)),
endDate: now.add(const Duration(days: 23)),
tasks: _generateMockTasks(),
totalTasks: 30,
completedTasks: 7,
isActive: true,
),
StudyPlan(
id: '2',
title: '雅思备考计划',
description: '全面提升听说读写能力目标7分',
type: PlanType.exam,
startDate: now.add(const Duration(days: 1)),
endDate: now.add(const Duration(days: 90)),
tasks: [],
totalTasks: 90,
completedTasks: 0,
isActive: false,
),
];
}
List<StudyTask> _generateMockTasks() {
final now = DateTime.now();
return [
StudyTask(
id: '1',
title: '学习商务词汇',
description: '学习20个商务相关单词',
type: TaskType.vocabulary,
dueDate: now,
estimatedMinutes: 30,
isCompleted: false,
),
StudyTask(
id: '2',
title: '复习昨日单词',
description: '复习昨天学习的单词',
type: TaskType.review,
dueDate: now,
estimatedMinutes: 15,
isCompleted: true,
),
];
}
void _showCreatePlanDialog() {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('创建学习计划'),
content: const Text('计划创建功能开发中...'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('关闭'),
),
],
);
},
);
}
void _showPlanTemplates() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('计划模板功能开发中')),
);
}
void _handleMenuAction(String action) {
switch (action) {
case 'templates':
_showPlanTemplates();
break;
case 'statistics':
_showStatistics();
break;
case 'settings':
_showSettings();
break;
}
}
void _handlePlanAction(String action, StudyPlan plan) {
switch (action) {
case 'activate':
_selectPlan(plan);
break;
case 'edit':
_editPlan(plan);
break;
case 'duplicate':
_duplicatePlan(plan);
break;
case 'delete':
_deletePlan(plan);
break;
}
}
void _selectPlan(StudyPlan plan) {
setState(() {
_currentPlan = plan;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('已切换到计划:${plan.title}')),
);
}
void _editPlan(StudyPlan plan) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('编辑计划功能开发中')),
);
}
void _duplicatePlan(StudyPlan plan) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('复制计划功能开发中')),
);
}
void _deletePlan(StudyPlan plan) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('确认删除'),
content: Text('确定要删除计划 "${plan.title}" 吗?'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('取消'),
),
TextButton(
onPressed: () {
Navigator.of(context).pop();
setState(() {
_studyPlans.removeWhere((p) => p.id == plan.id);
if (_currentPlan?.id == plan.id) {
_currentPlan = _studyPlans.isNotEmpty ? _studyPlans.first : null;
}
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('计划已删除')),
);
},
child: const Text('删除', style: TextStyle(color: Colors.red)),
),
],
);
},
);
}
void _toggleTaskCompletion(StudyTask task) {
setState(() {
task.isCompleted = !task.isCompleted;
if (_currentPlan != null) {
if (task.isCompleted) {
_currentPlan!.completedTasks++;
} else {
_currentPlan!.completedTasks--;
}
}
});
}
void _startTask(StudyTask task) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('开始任务:${task.title}')),
);
}
void _startStudySession() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('开始学习会话')),
);
}
void _startReview() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('开始复习')),
);
}
void _showStatistics() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('统计功能开发中')),
);
}
void _showSettings() {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('设置功能开发中')),
);
}
void _editCurrentPlan() {
if (_currentPlan != null) {
_editPlan(_currentPlan!);
}
}
IconData _getPlanTypeIcon(PlanType type) {
switch (type) {
case PlanType.vocabulary:
return Icons.book;
case PlanType.grammar:
return Icons.school;
case PlanType.speaking:
return Icons.mic;
case PlanType.listening:
return Icons.headphones;
case PlanType.reading:
return Icons.article;
case PlanType.writing:
return Icons.edit;
case PlanType.exam:
return Icons.quiz;
case PlanType.custom:
return Icons.settings;
}
}
Color _getPlanTypeColor(PlanType type) {
switch (type) {
case PlanType.vocabulary:
return Colors.blue;
case PlanType.grammar:
return Colors.green;
case PlanType.speaking:
return Colors.orange;
case PlanType.listening:
return Colors.purple;
case PlanType.reading:
return Colors.teal;
case PlanType.writing:
return Colors.red;
case PlanType.exam:
return Colors.indigo;
case PlanType.custom:
return Colors.grey;
}
}
}
enum PlanType {
vocabulary,
grammar,
speaking,
listening,
reading,
writing,
exam,
custom,
}
enum TaskType {
vocabulary,
reading,
listening,
speaking,
writing,
review,
}
class StudyPlan {
final String id;
final String title;
final String description;
final PlanType type;
final DateTime startDate;
final DateTime endDate;
final List<StudyTask> tasks;
final int totalTasks;
int completedTasks;
final bool isActive;
StudyPlan({
required this.id,
required this.title,
required this.description,
required this.type,
required this.startDate,
required this.endDate,
required this.tasks,
required this.totalTasks,
required this.completedTasks,
required this.isActive,
});
}
class StudyTask {
final String id;
final String title;
final String description;
final TaskType type;
final DateTime dueDate;
final int estimatedMinutes;
bool isCompleted;
StudyTask({
required this.id,
required this.title,
required this.description,
required this.type,
required this.dueDate,
required this.estimatedMinutes,
required this.isCompleted,
});
}
class Achievement {
final String title;
final String description;
final IconData icon;
final Color color;
final bool isUnlocked;
const Achievement({
required this.title,
required this.description,
required this.icon,
required this.color,
required this.isUnlocked,
});
}