470 lines
17 KiB
Dart
470 lines
17 KiB
Dart
|
|
import 'package:flutter/material.dart';
|
||
|
|
import '../models/conversation_scenario.dart';
|
||
|
|
|
||
|
|
/// 场景练习页面
|
||
|
|
class ScenarioPracticeScreen extends StatefulWidget {
|
||
|
|
final ConversationScenario scenario;
|
||
|
|
|
||
|
|
const ScenarioPracticeScreen({
|
||
|
|
Key? key,
|
||
|
|
required this.scenario,
|
||
|
|
}) : super(key: key);
|
||
|
|
|
||
|
|
@override
|
||
|
|
State<ScenarioPracticeScreen> createState() => _ScenarioPracticeScreenState();
|
||
|
|
}
|
||
|
|
|
||
|
|
class _ScenarioPracticeScreenState extends State<ScenarioPracticeScreen> {
|
||
|
|
int _currentStepIndex = 0;
|
||
|
|
List<String> _userResponses = [];
|
||
|
|
bool _isCompleted = false;
|
||
|
|
|
||
|
|
@override
|
||
|
|
void initState() {
|
||
|
|
super.initState();
|
||
|
|
_userResponses = List.filled(widget.scenario.steps.length, '');
|
||
|
|
}
|
||
|
|
|
||
|
|
ScenarioStep get _currentStep => widget.scenario.steps[_currentStepIndex];
|
||
|
|
|
||
|
|
void _selectOption(String option) {
|
||
|
|
setState(() {
|
||
|
|
_userResponses[_currentStepIndex] = option;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
void _nextStep() {
|
||
|
|
if (_currentStepIndex < widget.scenario.steps.length - 1) {
|
||
|
|
setState(() {
|
||
|
|
_currentStepIndex++;
|
||
|
|
});
|
||
|
|
} else {
|
||
|
|
setState(() {
|
||
|
|
_isCompleted = true;
|
||
|
|
});
|
||
|
|
_showCompletionDialog();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void _previousStep() {
|
||
|
|
if (_currentStepIndex > 0) {
|
||
|
|
setState(() {
|
||
|
|
_currentStepIndex--;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void _showCompletionDialog() {
|
||
|
|
showDialog(
|
||
|
|
context: context,
|
||
|
|
barrierDismissible: false,
|
||
|
|
builder: (context) => AlertDialog(
|
||
|
|
title: const Text('🎉 恭喜完成!'),
|
||
|
|
content: Column(
|
||
|
|
mainAxisSize: MainAxisSize.min,
|
||
|
|
children: [
|
||
|
|
Text('您已成功完成「${widget.scenario.title}」场景练习!'),
|
||
|
|
const SizedBox(height: 16),
|
||
|
|
const Text('继续练习可以提高您的英语口语水平。'),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
actions: [
|
||
|
|
TextButton(
|
||
|
|
onPressed: () {
|
||
|
|
Navigator.of(context).pop(); // 关闭对话框
|
||
|
|
Navigator.of(context).pop(); // 返回主页
|
||
|
|
},
|
||
|
|
child: const Text('返回主页'),
|
||
|
|
),
|
||
|
|
ElevatedButton(
|
||
|
|
onPressed: () {
|
||
|
|
Navigator.of(context).pop(); // 关闭对话框
|
||
|
|
_restartScenario();
|
||
|
|
},
|
||
|
|
child: const Text('重新练习'),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
void _restartScenario() {
|
||
|
|
setState(() {
|
||
|
|
_currentStepIndex = 0;
|
||
|
|
_userResponses = List.filled(widget.scenario.steps.length, '');
|
||
|
|
_isCompleted = false;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
void _showScenarioInfo() {
|
||
|
|
showModalBottomSheet(
|
||
|
|
context: context,
|
||
|
|
isScrollControlled: true,
|
||
|
|
builder: (context) => DraggableScrollableSheet(
|
||
|
|
initialChildSize: 0.7,
|
||
|
|
maxChildSize: 0.9,
|
||
|
|
minChildSize: 0.5,
|
||
|
|
builder: (context, scrollController) => Container(
|
||
|
|
padding: const EdgeInsets.all(20),
|
||
|
|
decoration: const BoxDecoration(
|
||
|
|
color: Colors.white,
|
||
|
|
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||
|
|
),
|
||
|
|
child: SingleChildScrollView(
|
||
|
|
controller: scrollController,
|
||
|
|
child: Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
Row(
|
||
|
|
children: [
|
||
|
|
Text(
|
||
|
|
widget.scenario.type.icon,
|
||
|
|
style: const TextStyle(fontSize: 32),
|
||
|
|
),
|
||
|
|
const SizedBox(width: 12),
|
||
|
|
Expanded(
|
||
|
|
child: Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
Text(
|
||
|
|
widget.scenario.title,
|
||
|
|
style: const TextStyle(
|
||
|
|
fontSize: 20,
|
||
|
|
fontWeight: FontWeight.bold,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
Text(
|
||
|
|
widget.scenario.subtitle,
|
||
|
|
style: const TextStyle(
|
||
|
|
fontSize: 14,
|
||
|
|
color: Colors.grey,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
const SizedBox(height: 20),
|
||
|
|
Text(
|
||
|
|
widget.scenario.description,
|
||
|
|
style: const TextStyle(fontSize: 16),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 20),
|
||
|
|
const Text(
|
||
|
|
'学习目标',
|
||
|
|
style: TextStyle(
|
||
|
|
fontSize: 18,
|
||
|
|
fontWeight: FontWeight.bold,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 8),
|
||
|
|
...widget.scenario.objectives.map(
|
||
|
|
(objective) => Padding(
|
||
|
|
padding: const EdgeInsets.only(bottom: 4),
|
||
|
|
child: Row(
|
||
|
|
children: [
|
||
|
|
const Icon(Icons.check_circle,
|
||
|
|
color: Colors.green, size: 16),
|
||
|
|
const SizedBox(width: 8),
|
||
|
|
Expanded(child: Text(objective)),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 20),
|
||
|
|
const Text(
|
||
|
|
'关键短语',
|
||
|
|
style: TextStyle(
|
||
|
|
fontSize: 18,
|
||
|
|
fontWeight: FontWeight.bold,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 8),
|
||
|
|
Wrap(
|
||
|
|
spacing: 8,
|
||
|
|
runSpacing: 8,
|
||
|
|
children: widget.scenario.keyPhrases.map(
|
||
|
|
(phrase) => Container(
|
||
|
|
padding: const EdgeInsets.symmetric(
|
||
|
|
horizontal: 12,
|
||
|
|
vertical: 6,
|
||
|
|
),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: Colors.blue.withOpacity(0.1),
|
||
|
|
borderRadius: BorderRadius.circular(16),
|
||
|
|
border: Border.all(color: Colors.blue.withOpacity(0.3)),
|
||
|
|
),
|
||
|
|
child: Text(
|
||
|
|
phrase,
|
||
|
|
style: const TextStyle(
|
||
|
|
fontSize: 12,
|
||
|
|
color: Colors.blue,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
).toList(),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
@override
|
||
|
|
Widget build(BuildContext context) {
|
||
|
|
return Scaffold(
|
||
|
|
appBar: AppBar(
|
||
|
|
title: Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
Text(
|
||
|
|
widget.scenario.title,
|
||
|
|
style: const TextStyle(fontSize: 16),
|
||
|
|
),
|
||
|
|
Text(
|
||
|
|
'步骤 ${_currentStepIndex + 1}/${widget.scenario.steps.length}',
|
||
|
|
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
actions: [
|
||
|
|
IconButton(
|
||
|
|
onPressed: _showScenarioInfo,
|
||
|
|
icon: const Icon(Icons.info_outline),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
body: Column(
|
||
|
|
children: [
|
||
|
|
// 进度条
|
||
|
|
LinearProgressIndicator(
|
||
|
|
value: (_currentStepIndex + 1) / widget.scenario.steps.length,
|
||
|
|
backgroundColor: Colors.grey[300],
|
||
|
|
valueColor: const AlwaysStoppedAnimation<Color>(Colors.blue),
|
||
|
|
),
|
||
|
|
Expanded(
|
||
|
|
child: Padding(
|
||
|
|
padding: const EdgeInsets.all(20),
|
||
|
|
child: Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
// 步骤标题
|
||
|
|
Container(
|
||
|
|
width: double.infinity,
|
||
|
|
padding: const EdgeInsets.all(16),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: Colors.blue.withOpacity(0.1),
|
||
|
|
borderRadius: BorderRadius.circular(12),
|
||
|
|
),
|
||
|
|
child: Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
Text(
|
||
|
|
_currentStep.title,
|
||
|
|
style: const TextStyle(
|
||
|
|
fontSize: 18,
|
||
|
|
fontWeight: FontWeight.bold,
|
||
|
|
color: Colors.blue,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 4),
|
||
|
|
Text(
|
||
|
|
_currentStep.description,
|
||
|
|
style: const TextStyle(
|
||
|
|
fontSize: 14,
|
||
|
|
color: Colors.grey,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 20),
|
||
|
|
|
||
|
|
// 对话内容
|
||
|
|
Expanded(
|
||
|
|
child: Container(
|
||
|
|
width: double.infinity,
|
||
|
|
padding: const EdgeInsets.all(16),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: _currentStep.role == 'npc'
|
||
|
|
? Colors.grey[100]
|
||
|
|
: Colors.blue[50],
|
||
|
|
borderRadius: BorderRadius.circular(12),
|
||
|
|
border: Border.all(
|
||
|
|
color: _currentStep.role == 'npc'
|
||
|
|
? Colors.grey.withOpacity(0.3)
|
||
|
|
: Colors.blue.withOpacity(0.3),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
child: Column(
|
||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
|
children: [
|
||
|
|
Row(
|
||
|
|
children: [
|
||
|
|
CircleAvatar(
|
||
|
|
radius: 16,
|
||
|
|
backgroundColor: _currentStep.role == 'npc'
|
||
|
|
? Colors.grey
|
||
|
|
: Colors.blue,
|
||
|
|
child: Text(
|
||
|
|
_currentStep.role == 'npc' ? 'NPC' : 'YOU',
|
||
|
|
style: const TextStyle(
|
||
|
|
fontSize: 10,
|
||
|
|
color: Colors.white,
|
||
|
|
fontWeight: FontWeight.bold,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(width: 12),
|
||
|
|
Text(
|
||
|
|
_currentStep.role == 'npc' ? '对方说:' : '您说:',
|
||
|
|
style: const TextStyle(
|
||
|
|
fontSize: 14,
|
||
|
|
fontWeight: FontWeight.bold,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
const SizedBox(height: 12),
|
||
|
|
Text(
|
||
|
|
_currentStep.content,
|
||
|
|
style: const TextStyle(fontSize: 16),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
|
||
|
|
const SizedBox(height: 20),
|
||
|
|
|
||
|
|
// 选项
|
||
|
|
if (_currentStep.options.isNotEmpty) ...[
|
||
|
|
const Text(
|
||
|
|
'请选择您的回应:',
|
||
|
|
style: TextStyle(
|
||
|
|
fontSize: 16,
|
||
|
|
fontWeight: FontWeight.bold,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(height: 12),
|
||
|
|
...(_currentStep.options.asMap().entries.map(
|
||
|
|
(entry) {
|
||
|
|
final index = entry.key;
|
||
|
|
final option = entry.value;
|
||
|
|
final isSelected = _userResponses[_currentStepIndex] == option;
|
||
|
|
|
||
|
|
return Padding(
|
||
|
|
padding: const EdgeInsets.only(bottom: 8),
|
||
|
|
child: GestureDetector(
|
||
|
|
onTap: () => _selectOption(option),
|
||
|
|
child: Container(
|
||
|
|
width: double.infinity,
|
||
|
|
padding: const EdgeInsets.all(16),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: isSelected
|
||
|
|
? Colors.blue.withOpacity(0.1)
|
||
|
|
: Colors.white,
|
||
|
|
borderRadius: BorderRadius.circular(12),
|
||
|
|
border: Border.all(
|
||
|
|
color: isSelected
|
||
|
|
? Colors.blue
|
||
|
|
: Colors.grey.withOpacity(0.3),
|
||
|
|
width: isSelected ? 2 : 1,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
child: Row(
|
||
|
|
children: [
|
||
|
|
Container(
|
||
|
|
width: 24,
|
||
|
|
height: 24,
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
shape: BoxShape.circle,
|
||
|
|
color: isSelected
|
||
|
|
? Colors.blue
|
||
|
|
: Colors.grey[300],
|
||
|
|
),
|
||
|
|
child: Center(
|
||
|
|
child: Text(
|
||
|
|
String.fromCharCode(65 + index), // A, B, C
|
||
|
|
style: TextStyle(
|
||
|
|
color: isSelected
|
||
|
|
? Colors.white
|
||
|
|
: Colors.grey[600],
|
||
|
|
fontWeight: FontWeight.bold,
|
||
|
|
fontSize: 12,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
const SizedBox(width: 12),
|
||
|
|
Expanded(
|
||
|
|
child: Text(
|
||
|
|
option,
|
||
|
|
style: TextStyle(
|
||
|
|
fontSize: 14,
|
||
|
|
color: isSelected
|
||
|
|
? Colors.blue
|
||
|
|
: Colors.black,
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
);
|
||
|
|
},
|
||
|
|
)),
|
||
|
|
],
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
|
||
|
|
// 底部按钮
|
||
|
|
Container(
|
||
|
|
padding: const EdgeInsets.all(20),
|
||
|
|
decoration: BoxDecoration(
|
||
|
|
color: Colors.white,
|
||
|
|
boxShadow: [
|
||
|
|
BoxShadow(
|
||
|
|
color: Colors.black.withOpacity(0.05),
|
||
|
|
blurRadius: 10,
|
||
|
|
offset: const Offset(0, -2),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
child: Row(
|
||
|
|
children: [
|
||
|
|
if (_currentStepIndex > 0)
|
||
|
|
Expanded(
|
||
|
|
child: OutlinedButton(
|
||
|
|
onPressed: _previousStep,
|
||
|
|
child: const Text('上一步'),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
if (_currentStepIndex > 0) const SizedBox(width: 12),
|
||
|
|
Expanded(
|
||
|
|
child: ElevatedButton(
|
||
|
|
onPressed: _userResponses[_currentStepIndex].isNotEmpty ||
|
||
|
|
_currentStep.options.isEmpty
|
||
|
|
? _nextStep
|
||
|
|
: null,
|
||
|
|
child: Text(
|
||
|
|
_currentStepIndex == widget.scenario.steps.length - 1
|
||
|
|
? '完成练习'
|
||
|
|
: '下一步',
|
||
|
|
),
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
),
|
||
|
|
],
|
||
|
|
),
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|