init
This commit is contained in:
360
client/lib/features/vocabulary/widgets/study_card_widget.dart
Normal file
360
client/lib/features/vocabulary/widgets/study_card_widget.dart
Normal file
@@ -0,0 +1,360 @@
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user