361 lines
10 KiB
Dart
361 lines
10 KiB
Dart
|
|
import 'package:flutter/material.dart';
|
||
|
|
import 'package:ai_english_learning/features/vocabulary/models/word_model.dart';
|
||
|
|
import 'package:ai_english_learning/features/vocabulary/models/learning_session_model.dart';
|
||
|
|
|
||
|
|
class StudyCardWidget extends StatefulWidget {
|
||
|
|
final Word word;
|
||
|
|
final Function(StudyDifficulty) onAnswer;
|
||
|
|
final VoidCallback? onNext;
|
||
|
|
|
||
|
|
const StudyCardWidget({
|
||
|
|
Key? key,
|
||
|
|
required this.word,
|
||
|
|
required this.onAnswer,
|
||
|
|
this.onNext,
|
||
|
|
}) : super(key: key);
|
||
|
|
|
||
|
|
@override
|
||
|
|
State<StudyCardWidget> createState() => _StudyCardWidgetState();
|
||
|
|
}
|
||
|
|
|
||
|
|
class _StudyCardWidgetState extends State<StudyCardWidget>
|
||
|
|
with SingleTickerProviderStateMixin {
|
||
|
|
bool _showAnswer = false;
|
||
|
|
late AnimationController _flipController;
|
||
|
|
late Animation<double> _flipAnimation;
|
||
|
|
|
||
|
|
@override
|
||
|
|
void initState() {
|
||
|
|
super.initState();
|
||
|
|
_flipController = AnimationController(
|
||
|
|
duration: const Duration(milliseconds: 300),
|
||
|
|
vsync: this,
|
||
|
|
);
|
||
|
|
_flipAnimation = Tween<double>(begin: 0, end: 1).animate(
|
||
|
|
CurvedAnimation(parent: _flipController, curve: Curves.easeInOut),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
void dispose() {
|
||
|
|
_flipController.dispose();
|
||
|
|
super.dispose();
|
||
|
|
}
|
||
|
|
|
||
|
|
void _toggleAnswer() {
|
||
|
|
setState(() {
|
||
|
|
_showAnswer = !_showAnswer;
|
||
|
|
if (_showAnswer) {
|
||
|
|
_flipController.forward();
|
||
|
|
} else {
|
||
|
|
_flipController.reverse();
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
return Column(
|
||
|
|
children: [
|
||
|
|
// 卡片主体
|
||
|
|
Expanded(
|
||
|
|
child: GestureDetector(
|
||
|
|
onTap: _toggleAnswer,
|
||
|
|
child: AnimatedBuilder(
|
||
|
|
animation: _flipAnimation,
|
||
|
|
builder: (context, child) {
|
||
|
|
final angle = _flipAnimation.value * 3.14159;
|
||
|
|
final transform = Matrix4.identity()
|
||
|
|
..setEntry(3, 2, 0.001)
|
||
|
|
..rotateY(angle);
|
||
|
|
|
||
|
|
return Transform(
|
||
|
|
transform: transform,
|
||
|
|
alignment: Alignment.center,
|
||
|
|
child: angle < 1.57
|
||
|
|
? _buildFrontCard()
|
||
|
|
: Transform(
|
||
|
|
transform: Matrix4.identity()..rotateY(3.14159),
|
||
|
|
alignment: Alignment.center,
|
||
|
|
child: _buildBackCard(),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
|
||
|
|
const SizedBox(height: 16),
|
||
|
|
|
||
|
|
// 答案按钮
|
||
|
|
if (_showAnswer) _buildAnswerButtons(),
|
||
|
|
|
||
|
|
// 提示文本
|
||
|
|
if (!_showAnswer)
|
||
|
|
Padding(
|
||
|
|
padding: const EdgeInsets.all(16),
|
||
|
|
child: Text(
|
||
|
|
'点击卡片查看答案',
|
||
|
|
style: TextStyle(
|
||
|
|
fontSize: 14,
|
||
|
|
color: Colors.grey[600],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildFrontCard() {
|
||
|
|
return Card(
|
||
|
|
elevation: 8,
|
||
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||
|
|
child: Container(
|
||
|
|
width: double.infinity,
|
||
|
|
padding: const EdgeInsets.all(32),
|
||
|
|
child: Column(
|
||
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
||
|
|
children: [
|
||
|
|
// 单词
|
||
|
|
Text(
|
||
|
|
widget.word.word,
|
||
|
|
style: const TextStyle(
|
||
|
|
fontSize: 48,
|
||
|
|
fontWeight: FontWeight.bold,
|
||
|
|
),
|
||
|
|
textAlign: TextAlign.center,
|
||
|
|
),
|
||
|
|
|
||
|
|
const SizedBox(height: 16),
|
||
|
|
|
||
|
|
// 音标
|
||
|
|
if (widget.word.phonetic != null)
|
||
|
|
Text(
|
||
|
|
widget.word.phonetic!,
|
||
|
|
style: TextStyle(
|
||
|
|
fontSize: 18,
|
||
|
|
color: Colors.grey[600],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
|
||
|
|
const SizedBox(height: 32),
|
||
|
|
|
||
|
|
// 提示:点击查看释义
|
||
|
|
Icon(
|
||
|
|
Icons.flip,
|
||
|
|
size: 32,
|
||
|
|
color: Colors.grey[400],
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildBackCard() {
|
||
|
|
return Card(
|
||
|
|
elevation: 8,
|
||
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||
|
|
child: Container(
|
||
|
|
width: double.infinity,
|
||
|
|
padding: const EdgeInsets.all(24),
|
||
|
|
child: SingleChildScrollView(
|
||
|
|
child: Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
// 单词和音标
|
||
|
|
Center(
|
||
|
|
child: Column(
|
||
|
|
children: [
|
||
|
|
Text(
|
||
|
|
widget.word.word,
|
||
|
|
style: const TextStyle(
|
||
|
|
fontSize: 32,
|
||
|
|
fontWeight: FontWeight.bold,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
if (widget.word.phonetic != null)
|
||
|
|
Text(
|
||
|
|
widget.word.phonetic!,
|
||
|
|
style: TextStyle(
|
||
|
|
fontSize: 16,
|
||
|
|
color: Colors.grey[600],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
|
||
|
|
const SizedBox(height: 24),
|
||
|
|
const Divider(),
|
||
|
|
const SizedBox(height: 16),
|
||
|
|
|
||
|
|
// 释义
|
||
|
|
if (widget.word.definitions.isNotEmpty) ...[
|
||
|
|
const Text(
|
||
|
|
'释义',
|
||
|
|
style: TextStyle(
|
||
|
|
fontSize: 16,
|
||
|
|
fontWeight: FontWeight.bold,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 8),
|
||
|
|
...widget.word.definitions.map((def) => Padding(
|
||
|
|
padding: const EdgeInsets.only(bottom: 8),
|
||
|
|
child: Text(
|
||
|
|
'• ${def.translation}',
|
||
|
|
style: const TextStyle(fontSize: 14),
|
||
|
|
),
|
||
|
|
)),
|
||
|
|
const SizedBox(height: 16),
|
||
|
|
],
|
||
|
|
|
||
|
|
// 例句
|
||
|
|
if (widget.word.examples.isNotEmpty) ...[
|
||
|
|
const Text(
|
||
|
|
'例句',
|
||
|
|
style: TextStyle(
|
||
|
|
fontSize: 16,
|
||
|
|
fontWeight: FontWeight.bold,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 8),
|
||
|
|
...widget.word.examples.take(2).map((example) => Padding(
|
||
|
|
padding: const EdgeInsets.only(bottom: 12),
|
||
|
|
child: Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
Text(
|
||
|
|
example.sentence,
|
||
|
|
style: const TextStyle(
|
||
|
|
fontSize: 14,
|
||
|
|
fontStyle: FontStyle.italic,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 4),
|
||
|
|
Text(
|
||
|
|
example.translation,
|
||
|
|
style: TextStyle(
|
||
|
|
fontSize: 13,
|
||
|
|
color: Colors.grey[700],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
)),
|
||
|
|
],
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
Widget _buildAnswerButtons() {
|
||
|
|
return Padding(
|
||
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||
|
|
child: Column(
|
||
|
|
children: [
|
||
|
|
Row(
|
||
|
|
children: [
|
||
|
|
// 完全忘记
|
||
|
|
Expanded(
|
||
|
|
child: _DifficultyButton(
|
||
|
|
label: '完全忘记',
|
||
|
|
icon: Icons.close,
|
||
|
|
color: Colors.red,
|
||
|
|
onPressed: () => widget.onAnswer(StudyDifficulty.forgot),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(width: 8),
|
||
|
|
// 困难
|
||
|
|
Expanded(
|
||
|
|
child: _DifficultyButton(
|
||
|
|
label: '困难',
|
||
|
|
icon: Icons.sentiment_dissatisfied,
|
||
|
|
color: Colors.orange,
|
||
|
|
onPressed: () => widget.onAnswer(StudyDifficulty.hard),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
const SizedBox(height: 8),
|
||
|
|
Row(
|
||
|
|
children: [
|
||
|
|
// 一般
|
||
|
|
Expanded(
|
||
|
|
child: _DifficultyButton(
|
||
|
|
label: '一般',
|
||
|
|
icon: Icons.sentiment_neutral,
|
||
|
|
color: Colors.blue,
|
||
|
|
onPressed: () => widget.onAnswer(StudyDifficulty.good),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(width: 8),
|
||
|
|
// 容易
|
||
|
|
Expanded(
|
||
|
|
child: _DifficultyButton(
|
||
|
|
label: '容易',
|
||
|
|
icon: Icons.sentiment_satisfied,
|
||
|
|
color: Colors.green,
|
||
|
|
onPressed: () => widget.onAnswer(StudyDifficulty.easy),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(width: 8),
|
||
|
|
// 完美
|
||
|
|
Expanded(
|
||
|
|
child: _DifficultyButton(
|
||
|
|
label: '完美',
|
||
|
|
icon: Icons.sentiment_very_satisfied,
|
||
|
|
color: Colors.purple,
|
||
|
|
onPressed: () => widget.onAnswer(StudyDifficulty.perfect),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
class _DifficultyButton extends StatelessWidget {
|
||
|
|
final String label;
|
||
|
|
final IconData icon;
|
||
|
|
final Color color;
|
||
|
|
final VoidCallback onPressed;
|
||
|
|
|
||
|
|
const _DifficultyButton({
|
||
|
|
required this.label,
|
||
|
|
required this.icon,
|
||
|
|
required this.color,
|
||
|
|
required this.onPressed,
|
||
|
|
});
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
return ElevatedButton(
|
||
|
|
onPressed: onPressed,
|
||
|
|
style: ElevatedButton.styleFrom(
|
||
|
|
backgroundColor: color,
|
||
|
|
foregroundColor: Colors.white,
|
||
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||
|
|
shape: RoundedRectangleBorder(
|
||
|
|
borderRadius: BorderRadius.circular(8),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
child: Column(
|
||
|
|
mainAxisSize: MainAxisSize.min,
|
||
|
|
children: [
|
||
|
|
Icon(icon, size: 20),
|
||
|
|
const SizedBox(height: 4),
|
||
|
|
Text(
|
||
|
|
label,
|
||
|
|
style: const TextStyle(fontSize: 12),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|