This commit is contained in:
sjk
2025-11-17 14:09:17 +08:00
commit 31e46c5bf6
479 changed files with 109324 additions and 0 deletions

View File

@@ -0,0 +1,274 @@
import 'package:flutter/material.dart';
import '../models/reading_article.dart';
/// 阅读文章卡片组件
class ReadingArticleCard extends StatelessWidget {
final ReadingArticle article;
final VoidCallback? onTap;
final VoidCallback? onFavorite;
final bool showProgress;
const ReadingArticleCard({
super.key,
required this.article,
this.onTap,
this.onFavorite,
this.showProgress = false,
});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 16),
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题和收藏按钮
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
article.title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
if (onFavorite != null)
IconButton(
icon: const Icon(
Icons.favorite_border,
color: Colors.grey,
size: 20,
),
onPressed: onFavorite,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(
minWidth: 24,
minHeight: 24,
),
),
],
),
const SizedBox(height: 8),
// 文章摘要
if (article.content.isNotEmpty)
Text(
_getExcerpt(article.content),
style: TextStyle(
fontSize: 14,
color: Colors.grey[600],
height: 1.4,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 12),
// 标签行
Row(
children: [
// 分类标签
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: const Color(0xFF2196F3).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
article.categoryLabel,
style: const TextStyle(
fontSize: 12,
color: Color(0xFF2196F3),
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(width: 8),
// 难度标签
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: _getDifficultyColor(article.difficulty).withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Text(
article.difficultyLabel,
style: TextStyle(
fontSize: 12,
color: _getDifficultyColor(article.difficulty),
fontWeight: FontWeight.w500,
),
),
),
const Spacer(),
// 字数和阅读时间
Text(
'${article.wordCount}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[500],
),
),
const SizedBox(width: 8),
Icon(
Icons.schedule,
size: 12,
color: Colors.grey[500],
),
const SizedBox(width: 2),
Text(
'${article.readingTime}分钟',
style: TextStyle(
fontSize: 12,
color: Colors.grey[500],
),
),
],
),
// 进度条(如果需要显示)
if (showProgress && article.isCompleted)
Padding(
padding: const EdgeInsets.only(top: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'已完成',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
if (article.comprehensionScore != null)
Text(
'得分: ${article.comprehensionScore}',
style: TextStyle(
fontSize: 12,
color: _getScoreColor(article.comprehensionScore!.toInt()),
fontWeight: FontWeight.w500,
),
),
],
),
const SizedBox(height: 4),
LinearProgressIndicator(
value: 1.0,
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation<Color>(
_getScoreColor((article.comprehensionScore ?? 0).toInt()),
),
),
],
),
),
// 标签(如果有)
if (article.tags.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8),
child: Wrap(
spacing: 6,
runSpacing: 4,
children: article.tags.take(3).map((tag) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Text(
'#$tag',
style: TextStyle(
fontSize: 10,
color: Colors.grey[600],
),
),
);
}).toList(),
),
),
],
),
),
),
);
}
/// 获取文章摘要
String _getExcerpt(String content) {
// 移除HTML标签和多余空白
String cleanContent = content
.replaceAll(RegExp(r'<[^>]*>'), '')
.replaceAll(RegExp(r'\s+'), ' ')
.trim();
// 截取前100个字符作为摘要
if (cleanContent.length <= 100) {
return cleanContent;
}
return '${cleanContent.substring(0, 100)}...';
}
/// 获取难度颜色
Color _getDifficultyColor(String difficulty) {
switch (difficulty.toLowerCase()) {
case 'a1':
case 'a2':
return Colors.green;
case 'b1':
case 'b2':
return Colors.orange;
case 'c1':
case 'c2':
return Colors.red;
default:
return Colors.grey;
}
}
/// 获取分数颜色
Color _getScoreColor(int score) {
if (score >= 90) {
return Colors.green;
} else if (score >= 70) {
return Colors.orange;
} else {
return Colors.red;
}
}
}

View File

@@ -0,0 +1,240 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/reading_provider.dart';
/// 阅读分类标签组件
class ReadingCategoryTabs extends StatelessWidget {
const ReadingCategoryTabs({super.key});
static const List<Map<String, String>> categories = [
{'key': '', 'label': '全部'},
{'key': 'cet4', 'label': '四级'},
{'key': 'cet6', 'label': '六级'},
{'key': 'toefl', 'label': '托福'},
{'key': 'ielts', 'label': '雅思'},
{'key': 'daily', 'label': '日常'},
{'key': 'business', 'label': '商务'},
{'key': 'academic', 'label': '学术'},
];
@override
Widget build(BuildContext context) {
return Container(
height: 50,
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
border: Border(
bottom: BorderSide(color: Colors.grey[200]!),
),
),
child: Consumer<ReadingProvider>(
builder: (context, provider, child) {
return ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: categories.length,
itemBuilder: (context, index) {
final category = categories[index];
final isSelected = provider.selectedCategory == category['key'] ||
(provider.selectedCategory == null && category['key'] == '');
return Padding(
padding: const EdgeInsets.only(right: 12),
child: _buildCategoryChip(
context,
category['label']!,
category['key']!,
isSelected,
provider,
),
);
},
);
},
),
);
}
/// 构建分类标签
Widget _buildCategoryChip(
BuildContext context,
String label,
String key,
bool isSelected,
ReadingProvider provider,
) {
return GestureDetector(
onTap: () {
final selectedKey = key.isEmpty ? null : key;
provider.setFilter(category: selectedKey);
provider.loadArticles(refresh: true);
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 6,
),
decoration: BoxDecoration(
color: isSelected
? const Color(0xFF2196F3)
: Colors.grey[100],
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isSelected
? const Color(0xFF2196F3)
: Colors.grey[300]!,
width: 1,
),
),
child: Text(
label,
style: TextStyle(
color: isSelected
? Colors.white
: Colors.grey[700],
fontSize: 14,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.normal,
),
),
),
);
}
}
/// 阅读难度筛选组件
class ReadingDifficultyFilter extends StatelessWidget {
const ReadingDifficultyFilter({super.key});
static const List<Map<String, String>> difficulties = [
{'key': '', 'label': '全部难度'},
{'key': 'a1', 'label': 'A1'},
{'key': 'a2', 'label': 'A2'},
{'key': 'b1', 'label': 'B1'},
{'key': 'b2', 'label': 'B2'},
{'key': 'c1', 'label': 'C1'},
{'key': 'c2', 'label': 'C2'},
];
@override
Widget build(BuildContext context) {
return Container(
height: 50,
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
border: Border(
bottom: BorderSide(color: Colors.grey[200]!),
),
),
child: Consumer<ReadingProvider>(
builder: (context, provider, child) {
return ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: difficulties.length,
itemBuilder: (context, index) {
final difficulty = difficulties[index];
final isSelected = provider.selectedDifficulty == difficulty['key'] ||
(provider.selectedDifficulty == null && difficulty['key'] == '');
return Padding(
padding: const EdgeInsets.only(right: 12),
child: _buildDifficultyChip(
context,
difficulty['label']!,
difficulty['key']!,
isSelected,
provider,
),
);
},
);
},
),
);
}
/// 构建难度标签
Widget _buildDifficultyChip(
BuildContext context,
String label,
String key,
bool isSelected,
ReadingProvider provider,
) {
final color = _getDifficultyColor(key);
return GestureDetector(
onTap: () {
final selectedKey = key.isEmpty ? null : key;
provider.setFilter(difficulty: selectedKey);
provider.loadArticles(refresh: true);
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 6,
),
decoration: BoxDecoration(
color: isSelected
? color
: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: color,
width: 1,
),
),
child: Text(
label,
style: TextStyle(
color: isSelected
? Colors.white
: color,
fontSize: 14,
fontWeight: isSelected
? FontWeight.w600
: FontWeight.normal,
),
),
),
);
}
/// 获取难度颜色
Color _getDifficultyColor(String difficulty) {
switch (difficulty.toLowerCase()) {
case 'a1':
case 'a2':
return Colors.green;
case 'b1':
case 'b2':
return Colors.orange;
case 'c1':
case 'c2':
return Colors.red;
default:
return const Color(0xFF2196F3);
}
}
}
/// 组合的分类和难度筛选组件
class ReadingFilterTabs extends StatelessWidget {
const ReadingFilterTabs({super.key});
@override
Widget build(BuildContext context) {
return Column(
children: [
const ReadingCategoryTabs(),
const ReadingDifficultyFilter(),
],
);
}
}

View File

@@ -0,0 +1,209 @@
import 'package:flutter/material.dart';
import '../models/reading_article.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimensions.dart';
import '../../../core/theme/app_text_styles.dart';
/// 阅读内容组件
class ReadingContentWidget extends StatefulWidget {
final ReadingArticle article;
final ScrollController? scrollController;
final VoidCallback? onWordTap;
final Function(String)? onTextSelection;
const ReadingContentWidget({
super.key,
required this.article,
this.scrollController,
this.onWordTap,
this.onTextSelection,
});
@override
State<ReadingContentWidget> createState() => _ReadingContentWidgetState();
}
class _ReadingContentWidgetState extends State<ReadingContentWidget> {
double _fontSize = 16.0;
double _lineHeight = 1.6;
bool _isDarkMode = false;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(AppDimensions.spacingMd),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 文章标题
Text(
widget.article.title,
style: AppTextStyles.headlineMedium.copyWith(
fontSize: _fontSize + 4,
fontWeight: FontWeight.bold,
color: _isDarkMode ? AppColors.onSurface : AppColors.onSurface,
),
),
const SizedBox(height: AppDimensions.spacingMd),
// 文章信息
Row(
children: [
Icon(
Icons.category_outlined,
size: 16,
color: AppColors.onSurfaceVariant,
),
const SizedBox(width: AppDimensions.spacingSm),
Text(
widget.article.category,
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.onSurfaceVariant,
),
),
const SizedBox(width: AppDimensions.spacingMd),
Icon(
Icons.schedule_outlined,
size: 16,
color: AppColors.onSurfaceVariant,
),
const SizedBox(width: AppDimensions.spacingSm),
Text(
'${widget.article.estimatedReadingTime} 分钟',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.onSurfaceVariant,
),
),
const SizedBox(width: AppDimensions.spacingMd),
Icon(
Icons.text_fields_outlined,
size: 16,
color: AppColors.onSurfaceVariant,
),
const SizedBox(width: AppDimensions.spacingSm),
Text(
'${widget.article.wordCount}',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.onSurfaceVariant,
),
),
],
),
const SizedBox(height: AppDimensions.spacingLg),
// 阅读设置工具栏
_buildReadingToolbar(),
const SizedBox(height: AppDimensions.spacingMd),
// 文章内容
Expanded(
child: SingleChildScrollView(
controller: widget.scrollController,
child: SelectableText(
widget.article.content,
style: AppTextStyles.bodyLarge.copyWith(
fontSize: _fontSize,
height: _lineHeight,
color: _isDarkMode ? AppColors.onSurface : AppColors.onSurface,
),
onSelectionChanged: (selection, cause) {
if (selection.isValid && widget.onTextSelection != null) {
final selectedText = widget.article.content
.substring(selection.start, selection.end);
widget.onTextSelection!(selectedText);
}
},
),
),
),
],
),
);
}
Widget _buildReadingToolbar() {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: AppDimensions.spacingSm,
vertical: AppDimensions.spacingXs,
),
decoration: BoxDecoration(
color: AppColors.surfaceVariant,
borderRadius: BorderRadius.circular(AppDimensions.radiusMd),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// 字体大小调节
Row(
children: [
IconButton(
onPressed: () {
setState(() {
if (_fontSize > 12) _fontSize -= 1;
});
},
icon: const Icon(Icons.text_decrease),
iconSize: 20,
),
Text(
'${_fontSize.toInt()}',
style: AppTextStyles.bodySmall,
),
IconButton(
onPressed: () {
setState(() {
if (_fontSize < 24) _fontSize += 1;
});
},
icon: const Icon(Icons.text_increase),
iconSize: 20,
),
],
),
// 行间距调节
Row(
children: [
IconButton(
onPressed: () {
setState(() {
if (_lineHeight > 1.2) _lineHeight -= 0.1;
});
},
icon: const Icon(Icons.format_line_spacing),
iconSize: 20,
),
Text(
'${(_lineHeight * 10).toInt() / 10}',
style: AppTextStyles.bodySmall,
),
IconButton(
onPressed: () {
setState(() {
if (_lineHeight < 2.0) _lineHeight += 0.1;
});
},
icon: const Icon(Icons.format_line_spacing),
iconSize: 20,
),
],
),
// 夜间模式切换
IconButton(
onPressed: () {
setState(() {
_isDarkMode = !_isDarkMode;
});
},
icon: Icon(
_isDarkMode ? Icons.light_mode : Icons.dark_mode,
),
iconSize: 20,
),
],
),
);
}
}

View File

@@ -0,0 +1,240 @@
import 'package:flutter/material.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimensions.dart';
import '../../../core/theme/app_text_styles.dart';
/// 阅读进度条组件
class ReadingProgressBar extends StatelessWidget {
final int current;
final int total;
final double? progress;
final String? label;
final bool showPercentage;
final Color? progressColor;
final Color? backgroundColor;
const ReadingProgressBar({
super.key,
required this.current,
required this.total,
this.progress,
this.label,
this.showPercentage = true,
this.progressColor,
this.backgroundColor,
});
@override
Widget build(BuildContext context) {
final progressValue = progress ?? (total > 0 ? current / total : 0.0);
final percentage = (progressValue * 100).toInt();
return Container(
padding: const EdgeInsets.all(AppDimensions.spacingMd),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题和进度信息
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label ?? '进度',
style: AppTextStyles.titleMedium,
),
Row(
children: [
Text(
'$current / $total',
style: AppTextStyles.bodyMedium.copyWith(
fontWeight: FontWeight.w500,
),
),
if (showPercentage) ...[
const SizedBox(width: AppDimensions.spacingSm),
Text(
'$percentage%',
style: AppTextStyles.bodyMedium.copyWith(
color: progressColor ?? AppColors.primary,
fontWeight: FontWeight.bold,
),
),
],
],
),
],
),
const SizedBox(height: AppDimensions.spacingSm),
// 进度条
LinearProgressIndicator(
value: progressValue,
backgroundColor: backgroundColor ?? AppColors.surfaceVariant,
valueColor: AlwaysStoppedAnimation<Color>(
progressColor ?? AppColors.primary,
),
minHeight: 6,
),
],
),
);
}
}
/// 圆形进度条组件
class ReadingCircularProgressBar extends StatelessWidget {
final int current;
final int total;
final double? progress;
final double size;
final double strokeWidth;
final Color? progressColor;
final Color? backgroundColor;
final Widget? child;
const ReadingCircularProgressBar({
super.key,
required this.current,
required this.total,
this.progress,
this.size = 80,
this.strokeWidth = 6,
this.progressColor,
this.backgroundColor,
this.child,
});
@override
Widget build(BuildContext context) {
final progressValue = progress ?? (total > 0 ? current / total : 0.0);
final percentage = (progressValue * 100).toInt();
return SizedBox(
width: size,
height: size,
child: Stack(
alignment: Alignment.center,
children: [
// 圆形进度条
CircularProgressIndicator(
value: progressValue,
strokeWidth: strokeWidth,
backgroundColor: backgroundColor ?? AppColors.surfaceVariant,
valueColor: AlwaysStoppedAnimation<Color>(
progressColor ?? AppColors.primary,
),
),
// 中心内容
child ??
Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'$percentage%',
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.bold,
color: progressColor ?? AppColors.primary,
),
),
Text(
'$current/$total',
style: AppTextStyles.bodySmall.copyWith(
color: AppColors.onSurfaceVariant,
),
),
],
),
],
),
);
}
}
/// 步骤进度条组件
class ReadingStepProgressBar extends StatelessWidget {
final int currentStep;
final int totalSteps;
final List<String>? stepLabels;
final Color? activeColor;
final Color? inactiveColor;
final Color? completedColor;
const ReadingStepProgressBar({
super.key,
required this.currentStep,
required this.totalSteps,
this.stepLabels,
this.activeColor,
this.inactiveColor,
this.completedColor,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(AppDimensions.spacingMd),
child: Row(
children: List.generate(totalSteps, (index) {
final stepNumber = index + 1;
final isCompleted = stepNumber < currentStep;
final isActive = stepNumber == currentStep;
final isInactive = stepNumber > currentStep;
Color stepColor;
if (isCompleted) {
stepColor = completedColor ?? AppColors.success;
} else if (isActive) {
stepColor = activeColor ?? AppColors.primary;
} else {
stepColor = inactiveColor ?? AppColors.surfaceVariant;
}
return Expanded(
child: Row(
children: [
// 步骤圆圈
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: stepColor,
),
child: Center(
child: isCompleted
? Icon(
Icons.check,
color: AppColors.onPrimary,
size: 16,
)
: Text(
stepNumber.toString(),
style: AppTextStyles.bodySmall.copyWith(
color: isInactive
? AppColors.onSurfaceVariant
: AppColors.onPrimary,
fontWeight: FontWeight.bold,
),
),
),
),
// 连接线(除了最后一个步骤)
if (index < totalSteps - 1)
Expanded(
child: Container(
height: 2,
color: isCompleted
? (completedColor ?? AppColors.success)
: (inactiveColor ?? AppColors.surfaceVariant),
),
),
],
),
);
}),
),
);
}
}

View File

@@ -0,0 +1,335 @@
import 'package:flutter/material.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimensions.dart';
import '../../../core/theme/app_text_styles.dart';
import '../models/reading_question.dart';
/// 阅读问题组件
class ReadingQuestionWidget extends StatelessWidget {
final ReadingQuestion question;
final String? selectedAnswer;
final bool showResult;
final Function(String) onAnswerSelected;
final VoidCallback? onNext;
final VoidCallback? onPrevious;
final bool isFirst;
final bool isLast;
const ReadingQuestionWidget({
super.key,
required this.question,
this.selectedAnswer,
this.showResult = false,
required this.onAnswerSelected,
this.onNext,
this.onPrevious,
this.isFirst = false,
this.isLast = false,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(AppDimensions.spacingLg),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 问题类型标签
Container(
padding: const EdgeInsets.symmetric(
horizontal: AppDimensions.spacingSm,
vertical: AppDimensions.spacingXs,
),
decoration: BoxDecoration(
color: _getQuestionTypeColor(question.type).withOpacity(0.1),
borderRadius: BorderRadius.circular(AppDimensions.radiusSm),
border: Border.all(
color: _getQuestionTypeColor(question.type),
width: 1,
),
),
child: Text(
_getQuestionTypeLabel(question.type),
style: AppTextStyles.bodySmall.copyWith(
color: _getQuestionTypeColor(question.type),
fontWeight: FontWeight.w500,
),
),
),
const SizedBox(height: AppDimensions.spacingMd),
// 问题内容
Text(
question.question,
style: AppTextStyles.titleMedium.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: AppDimensions.spacingLg),
// 选项列表
...question.options.asMap().entries.map((entry) {
final index = entry.key;
final option = entry.value;
final optionKey = String.fromCharCode(65 + index.toInt()); // A, B, C, D
final isSelected = selectedAnswer == optionKey;
final isCorrect = showResult && question.correctAnswer == optionKey;
final isWrong = showResult && isSelected && !isCorrect;
return Container(
margin: const EdgeInsets.only(bottom: AppDimensions.spacingSm),
child: InkWell(
onTap: showResult ? null : () => onAnswerSelected(optionKey),
borderRadius: BorderRadius.circular(AppDimensions.radiusMd),
child: Container(
padding: const EdgeInsets.all(AppDimensions.spacingMd),
decoration: BoxDecoration(
color: _getOptionBackgroundColor(
isSelected,
isCorrect,
isWrong,
showResult,
),
border: Border.all(
color: _getOptionBorderColor(
isSelected,
isCorrect,
isWrong,
showResult,
),
width: 2,
),
borderRadius: BorderRadius.circular(AppDimensions.radiusMd),
),
child: Row(
children: [
// 选项标识
Container(
width: 32,
height: 32,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: _getOptionLabelColor(
isSelected,
isCorrect,
isWrong,
showResult,
),
),
child: Center(
child: Text(
optionKey,
style: AppTextStyles.bodyMedium.copyWith(
color: _getOptionLabelTextColor(
isSelected,
isCorrect,
isWrong,
showResult,
),
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(width: AppDimensions.spacingMd),
// 选项内容
Expanded(
child: Text(
option,
style: AppTextStyles.bodyMedium.copyWith(
color: _getOptionTextColor(
isSelected,
isCorrect,
isWrong,
showResult,
),
),
),
),
// 结果图标
if (showResult && (isCorrect || isWrong))
Icon(
isCorrect ? Icons.check_circle : Icons.cancel,
color: isCorrect ? AppColors.success : AppColors.error,
size: 24,
),
],
),
),
),
);
}).toList(),
// 解析(如果显示结果且有解析)
if (showResult && question.explanation != null) ...[
const SizedBox(height: AppDimensions.spacingLg),
Container(
padding: const EdgeInsets.all(AppDimensions.spacingMd),
decoration: BoxDecoration(
color: AppColors.info.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppDimensions.radiusMd),
border: Border.all(
color: AppColors.info.withOpacity(0.3),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.lightbulb_outline,
color: AppColors.info,
size: 20,
),
const SizedBox(width: AppDimensions.spacingSm),
Text(
'解析',
style: AppTextStyles.titleSmall.copyWith(
color: AppColors.info,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: AppDimensions.spacingSm),
Text(
question.explanation!,
style: AppTextStyles.bodyMedium,
),
],
),
),
],
const SizedBox(height: AppDimensions.spacingXl),
// 导航按钮
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// 上一题按钮
if (!isFirst)
OutlinedButton.icon(
onPressed: onPrevious,
icon: const Icon(Icons.arrow_back),
label: const Text('上一题'),
)
else
const SizedBox.shrink(),
// 下一题/完成按钮
if (!isLast)
ElevatedButton.icon(
onPressed: selectedAnswer != null ? onNext : null,
icon: const Icon(Icons.arrow_forward),
label: const Text('下一题'),
)
else
ElevatedButton.icon(
onPressed: selectedAnswer != null ? onNext : null,
icon: const Icon(Icons.check),
label: const Text('完成'),
),
],
),
],
),
);
}
String _getQuestionTypeLabel(QuestionType type) {
switch (type) {
case QuestionType.multipleChoice:
return '选择题';
case QuestionType.trueFalse:
return '判断题';
case QuestionType.fillInBlank:
return '填空题';
case QuestionType.shortAnswer:
return '简答题';
}
}
Color _getQuestionTypeColor(QuestionType type) {
switch (type) {
case QuestionType.multipleChoice:
return AppColors.primary;
case QuestionType.trueFalse:
return AppColors.secondary;
case QuestionType.fillInBlank:
return AppColors.warning;
case QuestionType.shortAnswer:
return AppColors.info;
}
}
Color _getOptionBackgroundColor(
bool isSelected,
bool isCorrect,
bool isWrong,
bool showResult,
) {
if (showResult) {
if (isCorrect) return AppColors.success.withOpacity(0.1);
if (isWrong) return AppColors.error.withOpacity(0.1);
}
if (isSelected) return AppColors.primary.withOpacity(0.1);
return AppColors.surface;
}
Color _getOptionBorderColor(
bool isSelected,
bool isCorrect,
bool isWrong,
bool showResult,
) {
if (showResult) {
if (isCorrect) return AppColors.success;
if (isWrong) return AppColors.error;
}
if (isSelected) return AppColors.primary;
return AppColors.outline;
}
Color _getOptionLabelColor(
bool isSelected,
bool isCorrect,
bool isWrong,
bool showResult,
) {
if (showResult) {
if (isCorrect) return AppColors.success;
if (isWrong) return AppColors.error;
}
if (isSelected) return AppColors.primary;
return AppColors.surfaceVariant;
}
Color _getOptionLabelTextColor(
bool isSelected,
bool isCorrect,
bool isWrong,
bool showResult,
) {
if (showResult && (isCorrect || isWrong)) return AppColors.onPrimary;
if (isSelected) return AppColors.onPrimary;
return AppColors.onSurfaceVariant;
}
Color _getOptionTextColor(
bool isSelected,
bool isCorrect,
bool isWrong,
bool showResult,
) {
if (showResult) {
if (isCorrect) return AppColors.success;
if (isWrong) return AppColors.error;
}
if (isSelected) return AppColors.primary;
return AppColors.onSurface;
}
}

View File

@@ -0,0 +1,419 @@
import 'package:flutter/material.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimensions.dart';
import '../../../core/theme/app_text_styles.dart';
import '../models/reading_question.dart';
/// 阅读结果对话框
class ReadingResultDialog extends StatelessWidget {
final ReadingExercise exercise;
final VoidCallback? onReview;
final VoidCallback? onRetry;
final VoidCallback? onFinish;
const ReadingResultDialog({
super.key,
required this.exercise,
this.onReview,
this.onRetry,
this.onFinish,
});
ReadingExerciseResult get result {
// 计算练习结果
int correctCount = 0;
int totalCount = exercise.questions.length;
for (final question in exercise.questions) {
if (question.userAnswer == question.correctAnswer) {
correctCount++;
}
}
double score = totalCount > 0 ? (correctCount / totalCount) * 100 : 0;
return ReadingExerciseResult(
score: score,
correctCount: correctCount,
totalCount: totalCount,
timeSpent: Duration.zero, // 可以从练习中获取
accuracy: score,
);
}
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppDimensions.radiusLg),
),
child: Container(
padding: const EdgeInsets.all(AppDimensions.spacingLg),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// 结果图标和标题
_buildHeader(),
const SizedBox(height: AppDimensions.spacingLg),
// 分数展示
_buildScoreDisplay(),
const SizedBox(height: AppDimensions.spacingLg),
// 详细统计
_buildDetailedStats(),
const SizedBox(height: AppDimensions.spacingLg),
// 评价和建议
_buildFeedback(),
const SizedBox(height: AppDimensions.spacingXl),
// 操作按钮
_buildActionButtons(context),
],
),
),
);
}
Widget _buildHeader() {
final isExcellent = result.score >= 90;
final isGood = result.score >= 70;
final isPass = result.score >= 60;
IconData iconData;
Color iconColor;
String title;
if (isExcellent) {
iconData = Icons.emoji_events;
iconColor = AppColors.warning;
title = '优秀!';
} else if (isGood) {
iconData = Icons.thumb_up;
iconColor = AppColors.success;
title = '良好!';
} else if (isPass) {
iconData = Icons.check_circle;
iconColor = AppColors.info;
title = '及格!';
} else {
iconData = Icons.refresh;
iconColor = AppColors.error;
title = '需要加油!';
}
return Column(
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: iconColor.withOpacity(0.1),
),
child: Icon(
iconData,
size: 40,
color: iconColor,
),
),
const SizedBox(height: AppDimensions.spacingMd),
Text(
title,
style: AppTextStyles.headlineSmall.copyWith(
color: iconColor,
fontWeight: FontWeight.bold,
),
),
],
);
}
Widget _buildScoreDisplay() {
return Container(
padding: const EdgeInsets.all(AppDimensions.spacingLg),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppColors.primary.withOpacity(0.1),
AppColors.secondary.withOpacity(0.1),
],
),
borderRadius: BorderRadius.circular(AppDimensions.radiusMd),
border: Border.all(
color: AppColors.primary.withOpacity(0.3),
),
),
child: Column(
children: [
Text(
'总分',
style: AppTextStyles.bodyMedium.copyWith(
color: AppColors.onSurfaceVariant,
),
),
const SizedBox(height: AppDimensions.spacingSm),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
result.score.toString(),
style: AppTextStyles.displayMedium.copyWith(
color: AppColors.primary,
fontWeight: FontWeight.bold,
),
),
Text(
' / 100',
style: AppTextStyles.titleLarge.copyWith(
color: AppColors.onSurfaceVariant,
),
),
],
),
],
),
);
}
Widget _buildDetailedStats() {
return Container(
padding: const EdgeInsets.all(AppDimensions.spacingMd),
decoration: BoxDecoration(
color: AppColors.surfaceVariant.withOpacity(0.3),
borderRadius: BorderRadius.circular(AppDimensions.radiusMd),
),
child: Column(
children: [
_buildStatRow('正确题数', '${result.correctCount}', AppColors.success),
const SizedBox(height: AppDimensions.spacingSm),
_buildStatRow('错误题数', '${result.wrongCount}', AppColors.error),
const SizedBox(height: AppDimensions.spacingSm),
_buildStatRow('总题数', '${result.totalQuestions}', AppColors.onSurface),
const SizedBox(height: AppDimensions.spacingSm),
_buildStatRow(
'正确率',
'${((result.correctCount / result.totalQuestions) * 100).toInt()}%',
AppColors.primary,
),
const SizedBox(height: AppDimensions.spacingSm),
_buildStatRow(
'用时',
_formatDuration(result.timeSpent),
AppColors.info,
),
],
),
);
}
Widget _buildStatRow(String label, String value, Color valueColor) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: AppTextStyles.bodyMedium,
),
Text(
value,
style: AppTextStyles.bodyMedium.copyWith(
color: valueColor,
fontWeight: FontWeight.w600,
),
),
],
);
}
Widget _buildFeedback() {
String feedback = _getFeedbackMessage();
List<String> suggestions = _getSuggestions();
return Container(
padding: const EdgeInsets.all(AppDimensions.spacingMd),
decoration: BoxDecoration(
color: AppColors.info.withOpacity(0.1),
borderRadius: BorderRadius.circular(AppDimensions.radiusMd),
border: Border.all(
color: AppColors.info.withOpacity(0.3),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.psychology,
color: AppColors.info,
size: 20,
),
const SizedBox(width: AppDimensions.spacingSm),
Text(
'学习建议',
style: AppTextStyles.titleSmall.copyWith(
color: AppColors.info,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: AppDimensions.spacingSm),
Text(
feedback,
style: AppTextStyles.bodyMedium,
),
if (suggestions.isNotEmpty) ...[
const SizedBox(height: AppDimensions.spacingSm),
...suggestions.map((suggestion) => Padding(
padding: const EdgeInsets.only(top: AppDimensions.spacingXs),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'',
style: AppTextStyles.bodyMedium.copyWith(
color: AppColors.info,
),
),
Expanded(
child: Text(
suggestion,
style: AppTextStyles.bodyMedium,
),
),
],
),
)),
],
],
),
);
}
Widget _buildActionButtons(BuildContext context) {
return Row(
children: [
// 查看解析按钮
if (onReview != null)
Expanded(
child: OutlinedButton.icon(
onPressed: () {
onReview?.call();
},
icon: const Icon(Icons.visibility),
label: const Text('查看解析'),
),
),
if (onReview != null && (onRetry != null || onFinish != null))
const SizedBox(width: AppDimensions.spacingMd),
// 重新练习按钮
if (onRetry != null)
Expanded(
child: OutlinedButton.icon(
onPressed: () {
onRetry?.call();
},
icon: const Icon(Icons.refresh),
label: const Text('重新练习'),
),
),
if (onRetry != null && onFinish != null)
const SizedBox(width: AppDimensions.spacingMd),
// 完成按钮
if (onFinish != null)
Expanded(
child: ElevatedButton.icon(
onPressed: () {
onFinish?.call();
},
icon: const Icon(Icons.check),
label: const Text('完成'),
),
),
],
);
}
String _getFeedbackMessage() {
final accuracy = (result.correctCount / result.totalQuestions) * 100;
if (accuracy >= 90) {
return '太棒了!你的阅读理解能力非常出色,继续保持这种学习状态!';
} else if (accuracy >= 80) {
return '很好!你的阅读理解能力不错,再接再厉!';
} else if (accuracy >= 70) {
return '不错!你的阅读理解能力还可以,继续努力提升!';
} else if (accuracy >= 60) {
return '及格了!但还有很大的提升空间,建议多练习阅读理解。';
} else {
return '需要加强练习!建议从基础阅读开始,逐步提升理解能力。';
}
}
List<String> _getSuggestions() {
final accuracy = (result.correctCount / result.totalQuestions) * 100;
if (accuracy >= 80) {
return [
'可以尝试更高难度的阅读材料',
'注意总结阅读技巧和方法',
'保持每日阅读的好习惯',
];
} else if (accuracy >= 60) {
return [
'多练习不同类型的阅读题目',
'注意理解文章的主旨大意',
'学会从文中寻找关键信息',
'提高词汇量和语法理解',
];
} else {
return [
'从简单的阅读材料开始练习',
'重点提升基础词汇量',
'学习基本的阅读理解技巧',
'每天坚持阅读练习',
'可以寻求老师或同学的帮助',
];
}
}
String _formatDuration(Duration duration) {
final minutes = duration.inMinutes;
final seconds = duration.inSeconds % 60;
if (minutes > 0) {
return '${minutes}${seconds}';
} else {
return '${seconds}';
}
}
}
/// 显示阅读结果对话框
Future<void> showReadingResultDialog(
BuildContext context,
ReadingExercise exercise, {
VoidCallback? onRestart,
VoidCallback? onContinue,
VoidCallback? onClose,
}) {
return showDialog<void>(
context: context,
barrierDismissible: false,
builder: (context) => ReadingResultDialog(
exercise: exercise,
onReview: onRestart,
onRetry: onContinue,
onFinish: onClose,
),
);
}

View File

@@ -0,0 +1,303 @@
import 'package:flutter/material.dart';
/// 阅读搜索栏组件
class ReadingSearchBar extends StatefulWidget {
final Function(String) onSearch;
final String? initialQuery;
final String hintText;
const ReadingSearchBar({
super.key,
required this.onSearch,
this.initialQuery,
this.hintText = '搜索文章标题、内容或标签...',
});
@override
State<ReadingSearchBar> createState() => _ReadingSearchBarState();
}
class _ReadingSearchBarState extends State<ReadingSearchBar> {
late TextEditingController _controller;
final FocusNode _focusNode = FocusNode();
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.initialQuery);
// 自动聚焦
WidgetsBinding.instance.addPostFrameCallback((_) {
_focusNode.requestFocus();
});
}
@override
void dispose() {
_controller.dispose();
_focusNode.dispose();
super.dispose();
}
void _performSearch() {
final query = _controller.text.trim();
if (query.isNotEmpty) {
widget.onSearch(query);
}
}
@override
Widget build(BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
const Text(
'搜索文章',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 16),
// 搜索输入框
TextField(
controller: _controller,
focusNode: _focusNode,
decoration: InputDecoration(
hintText: widget.hintText,
hintStyle: TextStyle(color: Colors.grey[500]),
prefixIcon: const Icon(
Icons.search,
color: Color(0xFF2196F3),
),
suffixIcon: _controller.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_controller.clear();
setState(() {});
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey[300]!),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: Color(0xFF2196F3),
width: 2,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey[300]!),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
),
onChanged: (value) {
setState(() {});
},
onSubmitted: (_) => _performSearch(),
textInputAction: TextInputAction.search,
),
const SizedBox(height: 16),
// 搜索建议
_buildSearchSuggestions(),
const SizedBox(height: 20),
// 操作按钮
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text(
'取消',
style: TextStyle(color: Colors.grey[600]),
),
),
const SizedBox(width: 12),
ElevatedButton(
onPressed: _controller.text.trim().isNotEmpty
? _performSearch
: null,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF2196F3),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
),
),
child: const Text('搜索'),
),
],
),
],
),
),
);
}
/// 构建搜索建议
Widget _buildSearchSuggestions() {
final suggestions = [
'四级阅读',
'六级阅读',
'托福阅读',
'雅思阅读',
'商务英语',
'日常对话',
'科技文章',
'新闻报道',
];
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'热门搜索',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.grey[700],
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: suggestions.map((suggestion) {
return GestureDetector(
onTap: () {
_controller.text = suggestion;
setState(() {});
_performSearch();
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey[300]!),
),
child: Text(
suggestion,
style: TextStyle(
fontSize: 12,
color: Colors.grey[700],
),
),
),
);
}).toList(),
),
],
);
}
}
/// 简化的搜索栏组件(用于嵌入页面)
class ReadingSearchField extends StatefulWidget {
final Function(String) onSearch;
final Function()? onTap;
final String? initialQuery;
final bool enabled;
const ReadingSearchField({
super.key,
required this.onSearch,
this.onTap,
this.initialQuery,
this.enabled = true,
});
@override
State<ReadingSearchField> createState() => _ReadingSearchFieldState();
}
class _ReadingSearchFieldState extends State<ReadingSearchField> {
late TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.initialQuery);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.all(16),
child: TextField(
controller: _controller,
enabled: widget.enabled,
onTap: widget.onTap,
decoration: InputDecoration(
hintText: '搜索文章...',
hintStyle: TextStyle(color: Colors.grey[500]),
prefixIcon: const Icon(
Icons.search,
color: Color(0xFF2196F3),
),
suffixIcon: _controller.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () {
_controller.clear();
widget.onSearch('');
setState(() {});
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(25),
borderSide: BorderSide.none,
),
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 12,
),
),
onChanged: (value) {
setState(() {});
},
onSubmitted: widget.onSearch,
textInputAction: TextInputAction.search,
),
);
}
}

View File

@@ -0,0 +1,289 @@
import 'package:flutter/material.dart';
import '../models/reading_stats.dart';
/// 阅读统计卡片组件
class ReadingStatsCard extends StatelessWidget {
final ReadingStats stats;
const ReadingStatsCard({
super.key,
required this.stats,
});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF2196F3), Color(0xFF1976D2)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: const Color(0xFF2196F3).withOpacity(0.3),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
const Row(
children: [
Icon(
Icons.analytics,
color: Colors.white,
size: 24,
),
SizedBox(width: 8),
Text(
'阅读统计',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 16),
// 统计数据网格
Row(
children: [
Expanded(
child: _buildStatItem(
icon: Icons.article,
label: '已读文章',
value: stats.totalArticlesRead.toString(),
unit: '',
),
),
Expanded(
child: _buildStatItem(
icon: Icons.quiz,
label: '练习次数',
value: stats.practicesDone.toString(),
unit: '',
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildStatItem(
icon: Icons.score,
label: '平均分数',
value: stats.averageScore.toStringAsFixed(1),
unit: '',
),
),
Expanded(
child: _buildStatItem(
icon: Icons.speed,
label: '阅读速度',
value: stats.averageReadingSpeed.toStringAsFixed(0),
unit: '词/分',
),
),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: _buildStatItem(
icon: Icons.schedule,
label: '总时长',
value: '${stats.totalReadingTime}分钟',
unit: '',
),
),
Expanded(
child: _buildStatItem(
icon: Icons.local_fire_department,
label: '连续天数',
value: stats.consecutiveDays.toString(),
unit: '',
),
),
],
),
const SizedBox(height: 16),
// 理解准确度进度条
_buildAccuracyProgress(),
const SizedBox(height: 12),
// 词汇掌握进度条
_buildVocabularyProgress(),
],
),
);
}
/// 构建统计项目
Widget _buildStatItem({
required IconData icon,
required String label,
required String value,
required String unit,
}) {
return Column(
children: [
Icon(
icon,
color: Colors.white70,
size: 20,
),
const SizedBox(height: 4),
Text(
label,
style: const TextStyle(
color: Colors.white70,
fontSize: 12,
),
),
const SizedBox(height: 2),
Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.alphabetic,
children: [
Text(
value,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
if (unit.isNotEmpty)
Text(
unit,
style: const TextStyle(
color: Colors.white70,
fontSize: 12,
),
),
],
),
],
);
}
/// 构建理解准确度进度条
Widget _buildAccuracyProgress() {
final accuracy = stats.comprehensionAccuracy;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'理解准确度',
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
Text(
'${accuracy.toStringAsFixed(1)}%',
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 6),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: accuracy / 100,
backgroundColor: Colors.white.withOpacity(0.3),
valueColor: AlwaysStoppedAnimation<Color>(
_getAccuracyColor(accuracy),
),
minHeight: 6,
),
),
],
);
}
/// 构建词汇掌握进度条
Widget _buildVocabularyProgress() {
final vocabulary = stats.vocabularyMastered;
final maxVocabulary = 10000; // 假设最大词汇量为10000
final progress = (vocabulary / maxVocabulary).clamp(0.0, 1.0);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'词汇掌握',
style: TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
Text(
'$vocabulary',
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 6),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: progress,
backgroundColor: Colors.white.withOpacity(0.3),
valueColor: const AlwaysStoppedAnimation<Color>(
Colors.greenAccent,
),
minHeight: 6,
),
),
],
);
}
/// 获取准确度颜色
Color _getAccuracyColor(double accuracy) {
if (accuracy >= 90) {
return Colors.greenAccent;
} else if (accuracy >= 70) {
return Colors.yellowAccent;
} else {
return Colors.redAccent;
}
}
}

View File

@@ -0,0 +1,340 @@
import 'package:flutter/material.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimensions.dart';
import '../../../core/theme/app_text_styles.dart';
/// 阅读工具栏组件
class ReadingToolbar extends StatelessWidget {
final VoidCallback? onBookmark;
final VoidCallback? onShare;
final VoidCallback? onTranslate;
final VoidCallback? onHighlight;
final VoidCallback? onNote;
final VoidCallback? onSettings;
final bool isBookmarked;
final bool showProgress;
final double? progress;
const ReadingToolbar({
super.key,
this.onBookmark,
this.onShare,
this.onTranslate,
this.onHighlight,
this.onNote,
this.onSettings,
this.isBookmarked = false,
this.showProgress = false,
this.progress,
});
@override
Widget build(BuildContext context) {
return Container(
height: 60,
decoration: BoxDecoration(
color: AppColors.surface,
boxShadow: [
BoxShadow(
color: AppColors.shadow.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, -2),
),
],
),
child: Column(
children: [
// 进度条
if (showProgress && progress != null)
LinearProgressIndicator(
value: progress,
backgroundColor: AppColors.surfaceVariant,
valueColor: AlwaysStoppedAnimation<Color>(AppColors.primary),
minHeight: 2,
),
// 工具栏按钮
Expanded(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// 书签
_ToolbarButton(
icon: isBookmarked ? Icons.bookmark : Icons.bookmark_border,
label: '书签',
onTap: onBookmark,
isActive: isBookmarked,
),
// 分享
_ToolbarButton(
icon: Icons.share_outlined,
label: '分享',
onTap: onShare,
),
// 翻译
_ToolbarButton(
icon: Icons.translate_outlined,
label: '翻译',
onTap: onTranslate,
),
// 高亮
_ToolbarButton(
icon: Icons.highlight_outlined,
label: '高亮',
onTap: onHighlight,
),
// 笔记
_ToolbarButton(
icon: Icons.note_outlined,
label: '笔记',
onTap: onNote,
),
// 设置
_ToolbarButton(
icon: Icons.settings_outlined,
label: '设置',
onTap: onSettings,
),
],
),
),
],
),
);
}
}
class _ToolbarButton extends StatelessWidget {
final IconData icon;
final String label;
final VoidCallback? onTap;
final bool isActive;
const _ToolbarButton({
required this.icon,
required this.label,
this.onTap,
this.isActive = false,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppDimensions.radiusSm),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: AppDimensions.spacingXs,
vertical: AppDimensions.spacingXs,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 20,
color: isActive ? AppColors.primary : AppColors.onSurfaceVariant,
),
const SizedBox(height: 2),
Text(
label,
style: AppTextStyles.labelSmall.copyWith(
color: isActive ? AppColors.primary : AppColors.onSurfaceVariant,
fontSize: 10,
),
),
],
),
),
);
}
}
/// 阅读设置底部弹窗
class ReadingSettingsBottomSheet extends StatefulWidget {
final double fontSize;
final double lineHeight;
final bool isDarkMode;
final Function(double)? onFontSizeChanged;
final Function(double)? onLineHeightChanged;
final Function(bool)? onDarkModeChanged;
const ReadingSettingsBottomSheet({
super.key,
required this.fontSize,
required this.lineHeight,
required this.isDarkMode,
this.onFontSizeChanged,
this.onLineHeightChanged,
this.onDarkModeChanged,
});
@override
State<ReadingSettingsBottomSheet> createState() => _ReadingSettingsBottomSheetState();
}
class _ReadingSettingsBottomSheetState extends State<ReadingSettingsBottomSheet> {
late double _fontSize;
late double _lineHeight;
late bool _isDarkMode;
@override
void initState() {
super.initState();
_fontSize = widget.fontSize;
_lineHeight = widget.lineHeight;
_isDarkMode = widget.isDarkMode;
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(AppDimensions.spacingLg),
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(AppDimensions.radiusLg),
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'阅读设置',
style: AppTextStyles.headlineSmall,
),
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close),
),
],
),
const SizedBox(height: AppDimensions.spacingLg),
// 字体大小
Text(
'字体大小',
style: AppTextStyles.titleMedium,
),
const SizedBox(height: AppDimensions.spacingSm),
Row(
children: [
IconButton(
onPressed: () {
if (_fontSize > 12) {
setState(() {
_fontSize -= 1;
});
widget.onFontSizeChanged?.call(_fontSize);
}
},
icon: const Icon(Icons.remove),
),
Expanded(
child: Slider(
value: _fontSize,
min: 12,
max: 24,
divisions: 12,
label: '${_fontSize.toInt()}',
onChanged: (value) {
setState(() {
_fontSize = value;
});
widget.onFontSizeChanged?.call(value);
},
),
),
IconButton(
onPressed: () {
if (_fontSize < 24) {
setState(() {
_fontSize += 1;
});
widget.onFontSizeChanged?.call(_fontSize);
}
},
icon: const Icon(Icons.add),
),
],
),
// 行间距
Text(
'行间距',
style: AppTextStyles.titleMedium,
),
const SizedBox(height: AppDimensions.spacingSm),
Row(
children: [
IconButton(
onPressed: () {
if (_lineHeight > 1.2) {
setState(() {
_lineHeight -= 0.1;
});
widget.onLineHeightChanged?.call(_lineHeight);
}
},
icon: const Icon(Icons.remove),
),
Expanded(
child: Slider(
value: _lineHeight,
min: 1.2,
max: 2.0,
divisions: 8,
label: '${(_lineHeight * 10).toInt() / 10}',
onChanged: (value) {
setState(() {
_lineHeight = value;
});
widget.onLineHeightChanged?.call(value);
},
),
),
IconButton(
onPressed: () {
if (_lineHeight < 2.0) {
setState(() {
_lineHeight += 0.1;
});
widget.onLineHeightChanged?.call(_lineHeight);
}
},
icon: const Icon(Icons.add),
),
],
),
// 夜间模式
SwitchListTile(
title: Text(
'夜间模式',
style: AppTextStyles.titleMedium,
),
value: _isDarkMode,
onChanged: (value) {
setState(() {
_isDarkMode = value;
});
widget.onDarkModeChanged?.call(value);
},
),
const SizedBox(height: AppDimensions.spacingLg),
],
),
);
}
}