1389 lines
58 KiB
Dart
1389 lines
58 KiB
Dart
import 'dart:async';
|
||
import 'dart:convert';
|
||
|
||
import 'package:flutter/material.dart';
|
||
|
||
import '../../../constants/app_constants.dart';
|
||
import '../../../controllers/shared_preferences_storage_controller.dart';
|
||
import '../guide/tongji.dart';
|
||
import 'level-jilu.dart';
|
||
import 'flow-anim.dart';
|
||
import 'distinguish.dart';
|
||
import '../settings/offline-data.dart';
|
||
|
||
/// 时间: 2026-03-28
|
||
/// 功能: 诗词答题页面
|
||
/// 介绍: 基于 API 接口实现的诗词答题系统,支持获取题目、提交答案、获取提示
|
||
/// 最新变化: 添加自动加载下一题开关,隐藏提示标签,使用独立逻辑管理器
|
||
|
||
class PoetryLevelPage extends StatefulWidget {
|
||
const PoetryLevelPage({super.key});
|
||
|
||
@override
|
||
State<PoetryLevelPage> createState() => _PoetryLevelPageState();
|
||
}
|
||
|
||
class _PoetryLevelPageState extends State<PoetryLevelPage>
|
||
with TickerProviderStateMixin {
|
||
final PoetryLevelManager _manager = PoetryLevelManager();
|
||
|
||
// 状态管理
|
||
bool _isLoading = true;
|
||
bool _isSubmitting = false;
|
||
Map<String, dynamic>? _currentQuestion;
|
||
String? _errorMessage;
|
||
int _score = 0;
|
||
bool _autoLoadNext = true;
|
||
|
||
// 答题状态
|
||
int? _selectedAnswer;
|
||
String? _feedbackMessage;
|
||
bool _showFeedback = false;
|
||
bool _isAnswerCorrect = false;
|
||
|
||
// 动画控制器
|
||
late AnimationController _successAnimationController;
|
||
late AnimationController _shakeAnimationController;
|
||
late Animation<double> _scaleAnimation;
|
||
late Animation<double> _shakeAnimation;
|
||
|
||
// 标签显示状态
|
||
bool _showTags = false;
|
||
// 计时器
|
||
Timer? _tagTimer;
|
||
|
||
// 答题记录统计
|
||
int _totalQuestions = 0;
|
||
int _correctAnswers = 0;
|
||
int _wrongAnswers = 0;
|
||
int _totalTime = 0;
|
||
int _hintCount = 0;
|
||
int _skipCount = 0;
|
||
DateTime? _questionStartTime;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_initializeAndLoadQuestion();
|
||
|
||
_successAnimationController = AnimationController(
|
||
duration: const Duration(milliseconds: 600),
|
||
vsync: this,
|
||
);
|
||
_scaleAnimation = Tween<double>(begin: 0.8, end: 1.2).animate(
|
||
CurvedAnimation(
|
||
parent: _successAnimationController,
|
||
curve: Curves.elasticOut,
|
||
),
|
||
);
|
||
|
||
_shakeAnimationController = AnimationController(
|
||
duration: const Duration(milliseconds: 500),
|
||
vsync: this,
|
||
);
|
||
_shakeAnimation = Tween<double>(begin: -5, end: 5).animate(
|
||
CurvedAnimation(
|
||
parent: _shakeAnimationController,
|
||
curve: Curves.easeInOut,
|
||
),
|
||
);
|
||
|
||
// 10秒后显示标签
|
||
Future.delayed(const Duration(seconds: 10), () {
|
||
if (mounted) {
|
||
setState(() {
|
||
_showTags = true;
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_successAnimationController.dispose();
|
||
_shakeAnimationController.dispose();
|
||
_tagTimer?.cancel();
|
||
super.dispose();
|
||
}
|
||
|
||
/// 初始化并加载题目
|
||
Future<void> _initializeAndLoadQuestion() async {
|
||
setState(() {
|
||
_isLoading = true;
|
||
_errorMessage = null;
|
||
});
|
||
|
||
// 初始化题目列表(调用 fetch 和 refresh)
|
||
final initResult = await _manager.initializeQuestions();
|
||
|
||
if (!mounted) return;
|
||
|
||
if (!initResult.success) {
|
||
setState(() {
|
||
_errorMessage = initResult.message ?? '初始化题目失败,请重试';
|
||
_isLoading = false;
|
||
});
|
||
|
||
// 如果需要下载,显示下载提示
|
||
if (initResult.needDownload) {
|
||
_showDownloadPrompt();
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 显示加载模式提示
|
||
if (initResult.isOffline) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text(initResult.message ?? '已加载离线缓存'),
|
||
backgroundColor: AppConstants.primaryColor,
|
||
duration: const Duration(seconds: 2),
|
||
),
|
||
);
|
||
}
|
||
|
||
// 加载第一题
|
||
await _loadQuestion();
|
||
}
|
||
|
||
/// 显示下载提示
|
||
void _showDownloadPrompt() {
|
||
showDialog(
|
||
context: context,
|
||
builder: (BuildContext context) {
|
||
return AlertDialog(
|
||
title: const Text('提示'),
|
||
content: const Text('当前无网络连接且无离线缓存数据,请先下载数据或检查网络设置。'),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () {
|
||
Navigator.of(context).pop();
|
||
},
|
||
child: const Text('取消'),
|
||
),
|
||
TextButton(
|
||
onPressed: () {
|
||
Navigator.of(context).pop();
|
||
// 跳转到离线数据下载页面
|
||
Navigator.push(
|
||
context,
|
||
MaterialPageRoute(builder: (_) => const OfflineDataPage()),
|
||
);
|
||
},
|
||
child: const Text('去下载'),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
/// 加载题目
|
||
Future<void> _loadQuestion() async {
|
||
// 开始计时
|
||
_questionStartTime = DateTime.now();
|
||
|
||
final result = await _manager.loadQuestion();
|
||
|
||
if (!mounted) return;
|
||
|
||
setState(() {
|
||
if (result.success) {
|
||
_currentQuestion = result.data;
|
||
_isLoading = false;
|
||
_errorMessage = null;
|
||
|
||
// 重置标签显示状态
|
||
_showTags = false;
|
||
|
||
// 取消之前的计时器
|
||
_tagTimer?.cancel();
|
||
|
||
// 10秒后显示标签
|
||
_tagTimer = Timer(const Duration(seconds: 10), () {
|
||
if (mounted) {
|
||
setState(() {
|
||
_showTags = true;
|
||
});
|
||
}
|
||
});
|
||
} else {
|
||
_errorMessage = result.message;
|
||
_isLoading = false;
|
||
}
|
||
_selectedAnswer = null;
|
||
_feedbackMessage = null;
|
||
_showFeedback = false;
|
||
_isAnswerCorrect = false;
|
||
});
|
||
}
|
||
|
||
/// 提交答案
|
||
Future<void> _submitAnswer(int answer) async {
|
||
if (_isSubmitting || _currentQuestion == null) return;
|
||
|
||
setState(() {
|
||
_isSubmitting = true;
|
||
_selectedAnswer = answer;
|
||
_showFeedback = false;
|
||
});
|
||
|
||
final result = await _manager.submitAnswer(_manager.currentId, answer);
|
||
|
||
if (!mounted) return;
|
||
|
||
// 记录今日答题
|
||
try {
|
||
await StatisticsManager().recordTodayQuestion();
|
||
} catch (e) {
|
||
// 忽略错误
|
||
}
|
||
|
||
setState(() {
|
||
if (result.success) {
|
||
_isAnswerCorrect = result.isCorrect;
|
||
if (result.message != null && result.message!.isNotEmpty) {
|
||
_feedbackMessage = result.message;
|
||
} else {
|
||
_feedbackMessage = _isAnswerCorrect ? '🎉 回答正确!' : '😔 回答错误,再想想吧!';
|
||
}
|
||
_showFeedback = true;
|
||
|
||
// 计算答题时间
|
||
if (_questionStartTime != null) {
|
||
final duration = DateTime.now().difference(_questionStartTime!);
|
||
_totalTime += duration.inSeconds;
|
||
}
|
||
|
||
// 更新统计数据
|
||
_totalQuestions++;
|
||
if (_isAnswerCorrect) {
|
||
_correctAnswers++;
|
||
_score++;
|
||
_successAnimationController.forward().then((_) {
|
||
_successAnimationController.reverse();
|
||
});
|
||
|
||
if (_autoLoadNext) {
|
||
Future.delayed(const Duration(seconds: 2), () {
|
||
if (mounted && _isAnswerCorrect) {
|
||
_nextQuestion();
|
||
}
|
||
});
|
||
}
|
||
} else {
|
||
_wrongAnswers++;
|
||
_shakeAnimationController.forward().then((_) {
|
||
_shakeAnimationController.reverse();
|
||
});
|
||
}
|
||
|
||
// 保存答题记录
|
||
_saveAnswerRecord(isCorrect: _isAnswerCorrect);
|
||
} else {
|
||
_feedbackMessage = result.message;
|
||
_showFeedback = true;
|
||
}
|
||
_isSubmitting = false;
|
||
});
|
||
}
|
||
|
||
/// 获取提示
|
||
Future<void> _getHint() async {
|
||
if (_isSubmitting || _currentQuestion == null) return;
|
||
|
||
setState(() {
|
||
_isSubmitting = true;
|
||
});
|
||
|
||
final result = await _manager.getHint(_manager.currentId);
|
||
|
||
if (!mounted) return;
|
||
|
||
setState(() {
|
||
if (result.success) {
|
||
_feedbackMessage = result.message;
|
||
_showFeedback = true;
|
||
// 增加提示次数
|
||
_hintCount++;
|
||
// 保存答题记录
|
||
_saveAnswerRecord();
|
||
} else {
|
||
_feedbackMessage = result.message;
|
||
_showFeedback = true;
|
||
}
|
||
_isSubmitting = false;
|
||
});
|
||
}
|
||
|
||
/// 下一题
|
||
void _nextQuestion() {
|
||
// 增加跳过次数
|
||
_skipCount++;
|
||
// 保存答题记录
|
||
_saveAnswerRecord();
|
||
|
||
_manager.nextQuestion();
|
||
_loadQuestion();
|
||
}
|
||
|
||
/// 上一题
|
||
void _previousQuestion() {
|
||
_manager.previousQuestion();
|
||
_loadQuestion();
|
||
}
|
||
|
||
/// 保存答题记录到本地存储
|
||
Future<void> _saveAnswerRecord({bool isCorrect = false}) async {
|
||
try {
|
||
// 保存统计数据
|
||
await SharedPreferencesStorageController.setInt(
|
||
'totalQuestions',
|
||
_totalQuestions,
|
||
);
|
||
await SharedPreferencesStorageController.setInt(
|
||
'correctAnswers',
|
||
_correctAnswers,
|
||
);
|
||
await SharedPreferencesStorageController.setInt(
|
||
'wrongAnswers',
|
||
_wrongAnswers,
|
||
);
|
||
await SharedPreferencesStorageController.setInt('totalTime', _totalTime);
|
||
await SharedPreferencesStorageController.setInt('hintCount', _hintCount);
|
||
await SharedPreferencesStorageController.setInt('skipCount', _skipCount);
|
||
|
||
// 保存当前题目的详细记录
|
||
if (_currentQuestion != null) {
|
||
// 构建标签列表
|
||
List<String> tags = [];
|
||
if (_currentQuestion!['type'] != null) {
|
||
tags.add(_currentQuestion!['type'].toString());
|
||
}
|
||
if (_currentQuestion!['grade'] != null) {
|
||
tags.add(_currentQuestion!['grade'].toString());
|
||
}
|
||
if (_currentQuestion!['dynasty'] != null) {
|
||
tags.add(_currentQuestion!['dynasty'].toString());
|
||
}
|
||
|
||
final record = {
|
||
'questionId': _manager.currentId,
|
||
'question': _currentQuestion!['question'] ?? '未知题目',
|
||
'author': _currentQuestion!['author'] ?? '未知作者',
|
||
'tags': tags,
|
||
'isCorrect': isCorrect,
|
||
'answerTime': DateTime.now().toIso8601String(),
|
||
};
|
||
|
||
// 获取已有的记录列表
|
||
List<String> records =
|
||
await SharedPreferencesStorageController.getStringList(
|
||
'poetryAnswerRecords',
|
||
defaultValue: [],
|
||
);
|
||
|
||
// 添加新记录(JSON格式)
|
||
records.add(jsonEncode(record));
|
||
|
||
// 保存更新后的列表
|
||
await SharedPreferencesStorageController.setStringList(
|
||
'poetryAnswerRecords',
|
||
records,
|
||
);
|
||
}
|
||
} catch (e) {
|
||
// 保存失败
|
||
}
|
||
}
|
||
|
||
/// 打开答题记录页面
|
||
void _openAnswerRecordPage() {
|
||
Navigator.push(
|
||
context,
|
||
MaterialPageRoute(builder: (context) => const DistinguishPage()),
|
||
);
|
||
}
|
||
|
||
/// 构建选项布局
|
||
Widget _buildOptionsLayout() {
|
||
if (_currentQuestion == null) {
|
||
return const SizedBox();
|
||
}
|
||
|
||
final options = _currentQuestion!['options'] as List?;
|
||
if (options == null || options.isEmpty) {
|
||
return const SizedBox();
|
||
}
|
||
|
||
// 检查是否所有选项都少于等于4个字
|
||
bool allShortOptions = options.every((option) {
|
||
final text = option['content'] ?? '';
|
||
return text.length <= 4;
|
||
});
|
||
|
||
if (allShortOptions && options.length >= 4) {
|
||
// 2*2布局
|
||
return Column(
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Expanded(child: _buildOptionItem(options[0])),
|
||
const SizedBox(width: 12),
|
||
Expanded(child: _buildOptionItem(options[1])),
|
||
],
|
||
),
|
||
const SizedBox(height: 12),
|
||
Row(
|
||
children: [
|
||
Expanded(child: _buildOptionItem(options[2])),
|
||
const SizedBox(width: 12),
|
||
Expanded(child: _buildOptionItem(options[3])),
|
||
],
|
||
),
|
||
],
|
||
);
|
||
} else {
|
||
// 1*4布局
|
||
final List<Widget> optionWidgets = [];
|
||
for (int i = 0; i < options.length; i++) {
|
||
optionWidgets.add(_buildOptionItem(options[i]));
|
||
if (i < options.length - 1) {
|
||
optionWidgets.add(const SizedBox(height: 12));
|
||
}
|
||
}
|
||
return Column(children: optionWidgets);
|
||
}
|
||
}
|
||
|
||
/// 构建标签
|
||
Widget _buildTag(String label, String value) {
|
||
if (value.isEmpty) return const SizedBox();
|
||
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||
decoration: BoxDecoration(
|
||
color: AppConstants.primaryColor.withAlpha(20),
|
||
borderRadius: BorderRadius.circular(4),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.center,
|
||
children: [
|
||
Text(
|
||
label,
|
||
style: TextStyle(
|
||
fontSize: 10,
|
||
color: AppConstants.primaryColor,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
Text(
|
||
value,
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
color: Colors.black87,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
/// 构建单个选项
|
||
Widget _buildOptionItem(dynamic option) {
|
||
final optionNum = option['index'] ?? option['num'] ?? 0;
|
||
final isSelected = _selectedAnswer == optionNum;
|
||
final isCorrect =
|
||
_showFeedback && _isAnswerCorrect && _selectedAnswer == optionNum;
|
||
final isWrong =
|
||
_showFeedback && !_isAnswerCorrect && _selectedAnswer == optionNum;
|
||
|
||
return AnimatedContainer(
|
||
duration: const Duration(milliseconds: 300),
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
gradient: isSelected
|
||
? LinearGradient(
|
||
colors: isCorrect
|
||
? [Colors.green[400]!, Colors.green[300]!]
|
||
: isWrong
|
||
? [Colors.red[400]!, Colors.red[300]!]
|
||
: [
|
||
AppConstants.primaryColor,
|
||
AppConstants.primaryColor.withAlpha(200),
|
||
],
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
)
|
||
: null,
|
||
color: isSelected ? null : Colors.white,
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(
|
||
color: isSelected
|
||
? Colors.transparent
|
||
: AppConstants.primaryColor.withAlpha(50),
|
||
width: 2,
|
||
),
|
||
boxShadow: isSelected
|
||
? [
|
||
BoxShadow(
|
||
color:
|
||
(isCorrect
|
||
? Colors.green
|
||
: isWrong
|
||
? Colors.red
|
||
: AppConstants.primaryColor)
|
||
.withAlpha(80),
|
||
blurRadius: 12,
|
||
offset: const Offset(0, 4),
|
||
),
|
||
]
|
||
: [
|
||
BoxShadow(
|
||
color: Colors.black.withAlpha(5),
|
||
blurRadius: 8,
|
||
offset: const Offset(0, 2),
|
||
),
|
||
],
|
||
),
|
||
child: Material(
|
||
color: Colors.transparent,
|
||
child: InkWell(
|
||
onTap: _isSubmitting || (_showFeedback && _isAnswerCorrect)
|
||
? null
|
||
: () {
|
||
if (_showFeedback) {
|
||
// 重置状态,允许重新选择
|
||
setState(() {
|
||
_showFeedback = false;
|
||
_selectedAnswer = null;
|
||
_feedbackMessage = null;
|
||
});
|
||
}
|
||
_submitAnswer(optionNum);
|
||
},
|
||
borderRadius: BorderRadius.circular(12),
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(16),
|
||
child: Row(
|
||
children: [
|
||
AnimatedContainer(
|
||
duration: const Duration(milliseconds: 300),
|
||
width: 32,
|
||
height: 32,
|
||
decoration: BoxDecoration(
|
||
gradient: isSelected
|
||
? LinearGradient(
|
||
colors: isCorrect
|
||
? [Colors.white, Colors.white.withAlpha(230)]
|
||
: isWrong
|
||
? [Colors.white, Colors.white.withAlpha(230)]
|
||
: [Colors.white, Colors.white.withAlpha(230)],
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
)
|
||
: null,
|
||
color: isSelected
|
||
? null
|
||
: AppConstants.primaryColor.withAlpha(20),
|
||
shape: BoxShape.circle,
|
||
boxShadow: isSelected
|
||
? [
|
||
BoxShadow(
|
||
color: Colors.black.withAlpha(20),
|
||
blurRadius: 4,
|
||
offset: const Offset(0, 2),
|
||
),
|
||
]
|
||
: null,
|
||
),
|
||
child: Center(
|
||
child: Text(
|
||
'$optionNum',
|
||
style: TextStyle(
|
||
color: isSelected
|
||
? (isCorrect
|
||
? Colors.green
|
||
: isWrong
|
||
? Colors.red
|
||
: AppConstants.primaryColor)
|
||
: AppConstants.primaryColor,
|
||
fontWeight: FontWeight.bold,
|
||
fontSize: 16,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 16),
|
||
Expanded(
|
||
child: Text(
|
||
option['content'] ?? option['text'] ?? '',
|
||
style: TextStyle(
|
||
fontSize: 17,
|
||
fontWeight: FontWeight.w500,
|
||
color: isSelected ? Colors.white : Colors.black87,
|
||
),
|
||
),
|
||
),
|
||
if (isSelected)
|
||
Icon(
|
||
isCorrect
|
||
? Icons.check_circle
|
||
: isWrong
|
||
? Icons.cancel
|
||
: Icons.radio_button_checked,
|
||
color: Colors.white,
|
||
size: 28,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(
|
||
title: const Text(
|
||
'诗词答题',
|
||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
|
||
),
|
||
backgroundColor: AppConstants.primaryColor,
|
||
foregroundColor: Colors.white,
|
||
elevation: 0,
|
||
flexibleSpace: Container(
|
||
decoration: BoxDecoration(
|
||
gradient: LinearGradient(
|
||
colors: [
|
||
AppConstants.primaryColor,
|
||
AppConstants.primaryColor.withAlpha(180),
|
||
],
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
),
|
||
),
|
||
),
|
||
actions: [
|
||
Padding(
|
||
padding: const EdgeInsets.only(right: 16),
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
color: Colors.white.withAlpha(30),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: ElevatedButton(
|
||
onPressed: _openAnswerRecordPage,
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: Colors.transparent,
|
||
shadowColor: Colors.transparent,
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 16,
|
||
vertical: 8,
|
||
),
|
||
),
|
||
child: const Text(
|
||
'记录',
|
||
style: TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
body: Container(
|
||
color: Colors.white,
|
||
child: SafeArea(
|
||
child: Padding(
|
||
padding: const EdgeInsets.only(
|
||
left: 16.0,
|
||
right: 16.0,
|
||
top: 8.0,
|
||
bottom: 16.0,
|
||
),
|
||
child: Stack(
|
||
children: [
|
||
Column(
|
||
children: [
|
||
const SizedBox(height: 8), // 减少顶部空白
|
||
// 分数显示
|
||
AnimatedBuilder(
|
||
animation: _successAnimationController,
|
||
builder: (context, child) {
|
||
return Transform.scale(
|
||
scale: _isAnswerCorrect ? _scaleAnimation.value : 1.0,
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 20,
|
||
vertical: 12,
|
||
),
|
||
decoration: BoxDecoration(
|
||
gradient: LinearGradient(
|
||
colors: [
|
||
AppConstants.primaryColor,
|
||
AppConstants.primaryColor.withAlpha(200),
|
||
],
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
),
|
||
borderRadius: BorderRadius.circular(16),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: AppConstants.primaryColor.withAlpha(
|
||
80,
|
||
),
|
||
blurRadius: 12,
|
||
offset: const Offset(0, 4),
|
||
),
|
||
],
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Container(
|
||
padding: const EdgeInsets.all(8),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white.withAlpha(30),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Icon(
|
||
Icons.quiz_outlined,
|
||
color: Colors.white,
|
||
size: 20,
|
||
),
|
||
),
|
||
const SizedBox(width: 12),
|
||
Text(
|
||
'题目: ${_manager.currentIndex + 1}/${_manager.total}',
|
||
style: const TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.w600,
|
||
color: Colors.white,
|
||
),
|
||
),
|
||
const SizedBox(width: 12),
|
||
// 奖杯图标和分数
|
||
Row(
|
||
children: [
|
||
Icon(
|
||
Icons.emoji_events_outlined,
|
||
color: Colors.amber[300],
|
||
size: 24,
|
||
),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
'$_score',
|
||
style: const TextStyle(
|
||
fontSize: 24,
|
||
fontWeight: FontWeight.bold,
|
||
color: Colors.white,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
Row(
|
||
children: [
|
||
// 开关按钮
|
||
Tooltip(
|
||
message: _autoLoadNext
|
||
? '已开启自动下一题'
|
||
: '已关闭自动下一题',
|
||
child: Switch(
|
||
value: _autoLoadNext,
|
||
onChanged: (value) {
|
||
setState(() {
|
||
_autoLoadNext = value;
|
||
});
|
||
},
|
||
activeColor: Colors.white,
|
||
activeTrackColor: Colors.white
|
||
.withAlpha(128),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
// 移除空白间距
|
||
const SizedBox(height: 10),
|
||
// 加载状态
|
||
if (_isLoading)
|
||
const Expanded(
|
||
child: Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
CircularProgressIndicator(
|
||
valueColor: AlwaysStoppedAnimation<Color>(
|
||
AppConstants.primaryColor,
|
||
),
|
||
strokeWidth: 3,
|
||
),
|
||
SizedBox(height: 20),
|
||
Text(
|
||
'加载题目中...',
|
||
style: TextStyle(
|
||
fontSize: 16,
|
||
color: Colors.grey,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
|
||
// 错误状态
|
||
if (!_isLoading && _errorMessage != null)
|
||
Expanded(
|
||
child: Center(
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Container(
|
||
padding: const EdgeInsets.all(24),
|
||
decoration: BoxDecoration(
|
||
color: Colors.red[50],
|
||
shape: BoxShape.circle,
|
||
),
|
||
child: Icon(
|
||
Icons.error_outline,
|
||
size: 64,
|
||
color: Colors.red[400],
|
||
),
|
||
),
|
||
const SizedBox(height: 24),
|
||
Text(
|
||
_errorMessage!,
|
||
textAlign: TextAlign.center,
|
||
style: const TextStyle(
|
||
fontSize: 16,
|
||
color: Colors.red,
|
||
),
|
||
),
|
||
const SizedBox(height: 32),
|
||
Container(
|
||
decoration: BoxDecoration(
|
||
gradient: LinearGradient(
|
||
colors: [
|
||
AppConstants.primaryColor,
|
||
AppConstants.primaryColor.withAlpha(200),
|
||
],
|
||
),
|
||
borderRadius: BorderRadius.circular(12),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: AppConstants.primaryColor
|
||
.withAlpha(80),
|
||
blurRadius: 8,
|
||
offset: const Offset(0, 4),
|
||
),
|
||
],
|
||
),
|
||
child: ElevatedButton(
|
||
onPressed: () => _loadQuestion(),
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: Colors.transparent,
|
||
shadowColor: Colors.transparent,
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 32,
|
||
vertical: 16,
|
||
),
|
||
),
|
||
child: const Text(
|
||
'重新加载',
|
||
style: TextStyle(
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.w600,
|
||
color: Colors.white,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
|
||
// 题目内容
|
||
if (!_isLoading && _currentQuestion != null)
|
||
Expanded(
|
||
child: Column(
|
||
children: [
|
||
// 可滚动区域
|
||
Expanded(
|
||
child: SingleChildScrollView(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// 题目信息
|
||
AnimatedBuilder(
|
||
animation: _shakeAnimationController,
|
||
builder: (context, child) {
|
||
return Transform.translate(
|
||
offset: Offset(
|
||
_isAnswerCorrect
|
||
? 0
|
||
: _shakeAnimation.value,
|
||
0,
|
||
),
|
||
child: Stack(
|
||
children: [
|
||
// 流动边框
|
||
Positioned.fill(
|
||
child: FlowingBorderContainer(
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
borderRadius:
|
||
BorderRadius.circular(
|
||
16,
|
||
),
|
||
color: Colors.white,
|
||
),
|
||
),
|
||
color:
|
||
AppConstants.primaryColor,
|
||
width: 4,
|
||
),
|
||
),
|
||
// 题目内容
|
||
Container(
|
||
padding: const EdgeInsets.all(
|
||
16,
|
||
),
|
||
decoration: BoxDecoration(
|
||
gradient: LinearGradient(
|
||
colors: [
|
||
Colors.white,
|
||
AppConstants.primaryColor
|
||
.withAlpha(5),
|
||
Colors.white,
|
||
],
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
),
|
||
borderRadius:
|
||
BorderRadius.circular(16),
|
||
backgroundBlendMode:
|
||
BlendMode.softLight,
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment:
|
||
CrossAxisAlignment.start,
|
||
children: [
|
||
// 装饰元素
|
||
Row(
|
||
children: [
|
||
Container(
|
||
width: 4,
|
||
height: 20,
|
||
decoration: BoxDecoration(
|
||
color: AppConstants
|
||
.primaryColor,
|
||
borderRadius:
|
||
BorderRadius.circular(
|
||
2,
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(
|
||
width: 12,
|
||
),
|
||
Text(
|
||
'诗词挑战',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight:
|
||
FontWeight.w600,
|
||
color: AppConstants
|
||
.primaryColor,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 12),
|
||
// 题目
|
||
Text(
|
||
_currentQuestion!['question'] ??
|
||
'题目加载失败',
|
||
style: const TextStyle(
|
||
fontSize: 20,
|
||
fontWeight:
|
||
FontWeight.bold,
|
||
height: 1.5,
|
||
color: Colors.black87,
|
||
),
|
||
),
|
||
// 标签信息
|
||
if (_showTags)
|
||
AnimatedOpacity(
|
||
duration:
|
||
const Duration(
|
||
milliseconds: 500,
|
||
),
|
||
opacity: 1,
|
||
child: Container(
|
||
margin:
|
||
const EdgeInsets.only(
|
||
top: 12,
|
||
),
|
||
padding:
|
||
const EdgeInsets.all(
|
||
12,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: AppConstants
|
||
.primaryColor
|
||
.withAlpha(10),
|
||
borderRadius:
|
||
BorderRadius.circular(
|
||
8,
|
||
),
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment:
|
||
MainAxisAlignment
|
||
.spaceBetween,
|
||
children: [
|
||
_buildTag(
|
||
'作者',
|
||
_currentQuestion!['author'] ??
|
||
'',
|
||
),
|
||
_buildTag(
|
||
'年代',
|
||
_currentQuestion!['dynasty'] ??
|
||
'',
|
||
),
|
||
_buildTag(
|
||
'类型',
|
||
_currentQuestion!['type'] ??
|
||
'',
|
||
),
|
||
_buildTag(
|
||
'阶段',
|
||
_currentQuestion!['grade'] ??
|
||
'',
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
),
|
||
const SizedBox(height: 10),
|
||
// 选项
|
||
_buildOptionsLayout(),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
// 固定位置的操作按钮卡片(在底部固定,不随内容滚动)
|
||
Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.all(16),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(16),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: Colors.black.withAlpha(10),
|
||
blurRadius: 12,
|
||
offset: const Offset(0, 4),
|
||
),
|
||
],
|
||
),
|
||
child: Column(
|
||
children: [
|
||
// 操作按钮 - 改为一行显示
|
||
Row(
|
||
children: [
|
||
// 上一题按钮
|
||
Expanded(
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
gradient: LinearGradient(
|
||
colors: [
|
||
Colors.white,
|
||
Colors.grey[50]!,
|
||
],
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
),
|
||
borderRadius: BorderRadius.circular(
|
||
12,
|
||
),
|
||
border: Border.all(
|
||
color: AppConstants.primaryColor
|
||
.withAlpha(50),
|
||
width: 1,
|
||
),
|
||
),
|
||
child: OutlinedButton(
|
||
onPressed: _previousQuestion,
|
||
style: OutlinedButton.styleFrom(
|
||
side: BorderSide.none,
|
||
padding:
|
||
const EdgeInsets.symmetric(
|
||
vertical: 14,
|
||
),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius:
|
||
BorderRadius.circular(12),
|
||
),
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment:
|
||
MainAxisAlignment.center,
|
||
children: [
|
||
Icon(
|
||
Icons.arrow_back,
|
||
color:
|
||
AppConstants.primaryColor,
|
||
size: 20,
|
||
),
|
||
const SizedBox(width: 8),
|
||
const Text(
|
||
'上一题',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w500,
|
||
color: Colors.black87,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 12),
|
||
// 提示按钮
|
||
Expanded(
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
gradient: LinearGradient(
|
||
colors: [
|
||
Colors.white,
|
||
Colors.grey[50]!,
|
||
],
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
),
|
||
borderRadius: BorderRadius.circular(
|
||
12,
|
||
),
|
||
border: Border.all(
|
||
color: AppConstants.primaryColor,
|
||
width: 2,
|
||
),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: AppConstants.primaryColor
|
||
.withAlpha(30),
|
||
blurRadius: 8,
|
||
offset: const Offset(0, 4),
|
||
),
|
||
],
|
||
),
|
||
child: ElevatedButton(
|
||
onPressed: _getHint,
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor:
|
||
Colors.transparent,
|
||
shadowColor: Colors.transparent,
|
||
padding:
|
||
const EdgeInsets.symmetric(
|
||
vertical: 14,
|
||
),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius:
|
||
BorderRadius.circular(12),
|
||
),
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment:
|
||
MainAxisAlignment.center,
|
||
children: [
|
||
Icon(
|
||
Icons.lightbulb_outline,
|
||
color:
|
||
AppConstants.primaryColor,
|
||
size: 20,
|
||
),
|
||
const SizedBox(width: 8),
|
||
const Text(
|
||
'提示',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w600,
|
||
color: Colors.black87,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 12),
|
||
// 下一题按钮
|
||
Expanded(
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
gradient: LinearGradient(
|
||
colors: [
|
||
AppConstants.primaryColor,
|
||
AppConstants.primaryColor
|
||
.withAlpha(200),
|
||
],
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
),
|
||
borderRadius: BorderRadius.circular(
|
||
12,
|
||
),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: AppConstants.primaryColor
|
||
.withAlpha(80),
|
||
blurRadius: 12,
|
||
offset: const Offset(0, 4),
|
||
),
|
||
],
|
||
),
|
||
child: ElevatedButton(
|
||
onPressed: _nextQuestion,
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor:
|
||
Colors.transparent,
|
||
shadowColor: Colors.transparent,
|
||
padding:
|
||
const EdgeInsets.symmetric(
|
||
vertical: 14,
|
||
),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius:
|
||
BorderRadius.circular(12),
|
||
),
|
||
),
|
||
child: Row(
|
||
mainAxisAlignment:
|
||
MainAxisAlignment.center,
|
||
children: [
|
||
const Text(
|
||
'下一题',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w600,
|
||
color: Colors.white,
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Icon(
|
||
Icons.arrow_forward,
|
||
color: Colors.white,
|
||
size: 20,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
// 反馈信息气泡(不占用布局)
|
||
if (_showFeedback && _feedbackMessage != null)
|
||
Positioned(
|
||
top: 0,
|
||
left: 16,
|
||
right: 16,
|
||
child: AnimatedContainer(
|
||
duration: const Duration(milliseconds: 500),
|
||
curve: Curves.easeOut,
|
||
transform: Matrix4.translationValues(0, 0, 0),
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 20,
|
||
vertical: 12,
|
||
),
|
||
decoration: BoxDecoration(
|
||
gradient: LinearGradient(
|
||
colors: _isAnswerCorrect
|
||
? [Colors.green[400]!, Colors.green[300]!]
|
||
: [Colors.orange[400]!, Colors.orange[300]!],
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
),
|
||
borderRadius: BorderRadius.circular(20),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color:
|
||
(_isAnswerCorrect
|
||
? Colors.green
|
||
: Colors.orange)
|
||
.withAlpha(80),
|
||
blurRadius: 12,
|
||
offset: const Offset(0, 4),
|
||
),
|
||
],
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Icon(
|
||
_isAnswerCorrect
|
||
? Icons.celebration
|
||
: Icons.lightbulb_outline,
|
||
color: Colors.white,
|
||
size: 24,
|
||
),
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
child: Text(
|
||
_feedbackMessage!,
|
||
style: const TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w600,
|
||
color: Colors.white,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|