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,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),
),
],
),
);
}
}