import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'dart:async'; import '../models/test_models.dart'; /// 题目展示组件 class QuestionWidget extends StatefulWidget { final TestQuestion question; final UserAnswer? userAnswer; final Function(UserAnswer) onAnswerChanged; const QuestionWidget({ super.key, required this.question, this.userAnswer, required this.onAnswerChanged, }); @override State createState() => _QuestionWidgetState(); } class _QuestionWidgetState extends State { late List _selectedAnswers; late TextEditingController _textController; Timer? _debounceTimer; @override void initState() { super.initState(); _selectedAnswers = widget.userAnswer?.selectedAnswers ?? []; _textController = TextEditingController(text: widget.userAnswer?.textAnswer ?? ''); } @override void dispose() { _textController.dispose(); _debounceTimer?.cancel(); super.dispose(); } void _updateAnswer() { final answer = UserAnswer( questionId: widget.question.id, selectedAnswers: _selectedAnswers, textAnswer: _textController.text.isNotEmpty ? _textController.text : null, answeredAt: DateTime.now(), timeSpent: 0, // 这里应该计算实际用时 ); widget.onAnswerChanged(answer); } void _onTextChanged(String text) { _debounceTimer?.cancel(); _debounceTimer = Timer(const Duration(milliseconds: 500), () { _updateAnswer(); }); } @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.grey.withOpacity(0.1), blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildQuestionContent(), const SizedBox(height: 24), _buildAnswerSection(), if (widget.question.explanation != null && widget.userAnswer != null) ...[ const SizedBox(height: 16), _buildExplanation(), ], ], ), ); } Widget _buildQuestionContent() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( widget.question.content, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w600, height: 1.4, ), ), if (widget.question.imageUrl != null) ...[ const SizedBox(height: 16), ClipRRect( borderRadius: BorderRadius.circular(8), child: Image.network( widget.question.imageUrl!, width: double.infinity, height: 200, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Container( width: double.infinity, height: 200, color: Colors.grey[200], child: const Icon( Icons.image_not_supported, size: 48, color: Colors.grey, ), ); }, ), ), ], if (widget.question.audioUrl != null) ...[ const SizedBox(height: 16), _buildAudioPlayer(), ], ], ); } Widget _buildAudioPlayer() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.blue.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.blue.withOpacity(0.3)), ), child: Row( children: [ Icon( Icons.headphones, color: Colors.blue[700], size: 24, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '音频材料', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.blue[700], ), ), Text( '点击播放按钮收听音频', style: TextStyle( fontSize: 12, color: Colors.blue[600], ), ), ], ), ), IconButton( onPressed: () { // 这里应该实现音频播放功能 ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('音频播放功能正在开发中...'), backgroundColor: Colors.blue, ), ); }, icon: Icon( Icons.play_circle_filled, color: Colors.blue[700], size: 32, ), ), ], ), ); } Widget _buildAnswerSection() { switch (widget.question.type) { case QuestionType.multipleChoice: return _buildMultipleChoice(); case QuestionType.multipleSelect: return _buildMultipleSelect(); case QuestionType.fillInBlank: return _buildFillInBlank(); case QuestionType.reading: return _buildMultipleChoice(); // 阅读理解通常是选择题 case QuestionType.listening: return _buildMultipleChoice(); // 听力理解通常是选择题 case QuestionType.speaking: return _buildSpeaking(); case QuestionType.writing: return _buildWriting(); } } Widget _buildMultipleChoice() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '请选择正确答案:', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 12), ...widget.question.options.asMap().entries.map((entry) { final index = entry.key; final option = entry.value; final optionLabel = String.fromCharCode(65 + index); // A, B, C, D final isSelected = _selectedAnswers.contains(option); return Container( margin: const EdgeInsets.only(bottom: 8), child: InkWell( onTap: () { setState(() { _selectedAnswers = [option]; }); _updateAnswer(); }, borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isSelected ? Colors.blue.withOpacity(0.1) : Colors.grey[50], borderRadius: BorderRadius.circular(12), border: Border.all( color: isSelected ? Colors.blue : Colors.grey[300]!, width: isSelected ? 2 : 1, ), ), child: Row( children: [ Container( width: 24, height: 24, decoration: BoxDecoration( shape: BoxShape.circle, color: isSelected ? Colors.blue : Colors.transparent, border: Border.all( color: isSelected ? Colors.blue : Colors.grey[400]!, width: 2, ), ), child: isSelected ? const Icon( Icons.check, size: 16, color: Colors.white, ) : null, ), const SizedBox(width: 12), Text( '$optionLabel.', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: isSelected ? Colors.blue : Colors.grey[600], ), ), const SizedBox(width: 8), Expanded( child: Text( option, style: TextStyle( fontSize: 16, color: isSelected ? Colors.blue[700] : Colors.black87, ), ), ), ], ), ), ), ); }).toList(), ], ); } Widget _buildMultipleSelect() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '请选择所有正确答案:', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 12), ...widget.question.options.asMap().entries.map((entry) { final index = entry.key; final option = entry.value; final optionLabel = String.fromCharCode(65 + index); // A, B, C, D final isSelected = _selectedAnswers.contains(option); return Container( margin: const EdgeInsets.only(bottom: 8), child: InkWell( onTap: () { setState(() { if (isSelected) { _selectedAnswers.remove(option); } else { _selectedAnswers.add(option); } }); _updateAnswer(); }, borderRadius: BorderRadius.circular(12), child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isSelected ? Colors.green.withOpacity(0.1) : Colors.grey[50], borderRadius: BorderRadius.circular(12), border: Border.all( color: isSelected ? Colors.green : Colors.grey[300]!, width: isSelected ? 2 : 1, ), ), child: Row( children: [ Container( width: 24, height: 24, decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), color: isSelected ? Colors.green : Colors.transparent, border: Border.all( color: isSelected ? Colors.green : Colors.grey[400]!, width: 2, ), ), child: isSelected ? const Icon( Icons.check, size: 16, color: Colors.white, ) : null, ), const SizedBox(width: 12), Text( '$optionLabel.', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: isSelected ? Colors.green : Colors.grey[600], ), ), const SizedBox(width: 8), Expanded( child: Text( option, style: TextStyle( fontSize: 16, color: isSelected ? Colors.green[700] : Colors.black87, ), ), ), ], ), ), ), ); }).toList(), ], ); } Widget _buildFillInBlank() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '请填入正确答案:', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 12), TextField( controller: _textController, onChanged: _onTextChanged, decoration: InputDecoration( hintText: '请输入答案...', border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey[300]!), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Colors.blue, width: 2), ), contentPadding: const EdgeInsets.all(16), ), style: const TextStyle(fontSize: 16), ), if (widget.question.options.isNotEmpty) ...[ const SizedBox(height: 12), const Text( '提示选项:', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.grey, ), ), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: widget.question.options.map((option) { return InkWell( onTap: () { _textController.text = option; _updateAnswer(); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.grey[300]!), ), child: Text( option, style: TextStyle( fontSize: 14, color: Colors.grey[700], ), ), ), ); }).toList(), ), ], ], ); } Widget _buildSpeaking() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '口语回答:', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 12), Container( width: double.infinity, padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: Colors.red.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.red.withOpacity(0.3)), ), child: Column( children: [ Icon( Icons.mic, size: 48, color: Colors.red[600], ), const SizedBox(height: 12), Text( '点击开始录音', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.red[700], ), ), const SizedBox(height: 8), Text( '建议录音时长:${widget.question.timeLimit}秒', style: TextStyle( fontSize: 14, color: Colors.red[600], ), ), const SizedBox(height: 16), ElevatedButton.icon( onPressed: () { // 这里应该实现录音功能 ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('录音功能正在开发中...'), backgroundColor: Colors.red, ), ); }, icon: const Icon(Icons.fiber_manual_record), label: const Text('开始录音'), style: ElevatedButton.styleFrom( backgroundColor: Colors.red, foregroundColor: Colors.white, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), ), ), ], ), ), ], ); } Widget _buildWriting() { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '请写出您的答案:', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w600, ), ), const SizedBox(height: 12), TextField( controller: _textController, onChanged: _onTextChanged, maxLines: 8, decoration: InputDecoration( hintText: '请在此输入您的答案...', border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide(color: Colors.grey[300]!), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: const BorderSide(color: Colors.teal, width: 2), ), contentPadding: const EdgeInsets.all(16), ), style: const TextStyle(fontSize: 16, height: 1.5), ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '字数:${_textController.text.length}', style: TextStyle( fontSize: 12, color: Colors.grey[600], ), ), Text( '建议时长:${widget.question.timeLimit ~/ 60}分钟', style: TextStyle( fontSize: 12, color: Colors.grey[600], ), ), ], ), ], ); } Widget _buildExplanation() { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.blue.withOpacity(0.05), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.blue.withOpacity(0.2)), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.lightbulb_outline, color: Colors.blue[600], size: 20, ), const SizedBox(width: 8), Text( '解析', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.blue[700], ), ), ], ), const SizedBox(height: 8), Text( widget.question.explanation!, style: TextStyle( fontSize: 14, color: Colors.blue[700], height: 1.4, ), ), ], ), ); } }