Files
ai_english/client/lib/features/writing/screens/exam_writing_screen.dart
2025-11-17 13:39:05 +08:00

388 lines
13 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/writing_task.dart';
import '../providers/writing_provider.dart';
import 'writing_detail_screen.dart';
/// 考试写作页面
class ExamWritingScreen extends ConsumerStatefulWidget {
final ExamType? examType;
final String title;
const ExamWritingScreen({
super.key,
this.examType,
required this.title,
});
@override
ConsumerState<ExamWritingScreen> createState() => _ExamWritingScreenState();
}
class _ExamWritingScreenState extends ConsumerState<ExamWritingScreen> {
ExamType? selectedExamType;
@override
void initState() {
super.initState();
selectedExamType = widget.examType;
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF5F5F5),
appBar: AppBar(
title: Text(widget.title),
backgroundColor: Colors.white,
foregroundColor: Colors.black,
elevation: 0,
),
body: Column(
children: [
if (widget.examType == null) _buildExamTypeFilter(),
Expanded(
child: Builder(
builder: (context) {
if (selectedExamType != null) {
final tasksAsync = ref.watch(examWritingTasksProvider(selectedExamType!));
return tasksAsync.when(
data: (tasks) {
if (tasks.isEmpty) return _buildEmptyState();
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: tasks.length,
itemBuilder: (context, index) {
final task = tasks[index];
return _buildTaskCard(task);
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, st) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 48, color: Colors.grey[400]),
const SizedBox(height: 12),
Text('加载失败', style: TextStyle(fontSize: 14, color: Colors.grey[600])),
const SizedBox(height: 8),
TextButton(
onPressed: () {
ref.invalidate(examWritingTasksProvider(selectedExamType!));
},
child: const Text('重试'),
),
],
),
),
);
} else {
final service = ref.watch(writingServiceProvider);
return FutureBuilder<List<WritingTask>>(
future: service.getWritingTasks(limit: 100),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 48, color: Colors.grey[400]),
const SizedBox(height: 12),
Text('加载失败', style: TextStyle(fontSize: 14, color: Colors.grey[600])),
const SizedBox(height: 8),
TextButton(
onPressed: () {
setState(() {});
},
child: const Text('重试'),
),
],
),
);
}
final allTasks = snapshot.data ?? [];
final examTasks = allTasks.where((t) => t.examType != null).toList();
if (examTasks.isEmpty) return _buildEmptyState();
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: examTasks.length,
itemBuilder: (context, index) {
final task = examTasks[index];
return _buildTaskCard(task);
},
);
},
);
}
},
),
),
],
),
);
}
Widget _buildExamTypeFilter() {
return Container(
padding: const EdgeInsets.all(16),
color: Colors.white,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'选择考试类型',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
_buildFilterChip('全部', null),
const SizedBox(width: 8),
...ExamType.values.map((type) => Padding(
padding: const EdgeInsets.only(right: 8),
child: _buildFilterChip(type.displayName, type),
)),
],
),
),
],
),
);
}
Widget _buildFilterChip(String label, ExamType? type) {
final isSelected = selectedExamType == type;
return FilterChip(
label: Text(label),
selected: isSelected,
onSelected: (selected) {
setState(() {
selectedExamType = type;
});
},
backgroundColor: Colors.grey[100],
selectedColor: const Color(0xFF2196F3).withOpacity(0.2),
checkmarkColor: const Color(0xFF2196F3),
labelStyle: TextStyle(
color: isSelected ? const Color(0xFF2196F3) : Colors.grey[700],
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.school_outlined,
size: 80,
color: Colors.grey[400],
),
const SizedBox(height: 16),
Text(
'暂无考试写作题目',
style: TextStyle(
fontSize: 18,
color: Colors.grey[600],
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Text(
'请选择其他考试类型或稍后再试',
style: TextStyle(
fontSize: 14,
color: Colors.grey[500],
),
),
],
),
);
}
Widget _buildTaskCard(WritingTask task) {
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: InkWell(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => WritingDetailScreen(task: task),
),
);
},
borderRadius: BorderRadius.circular(16),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: Text(
task.title,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getExamTypeColor(task.examType!),
borderRadius: BorderRadius.circular(12),
),
child: Text(
task.examType!.displayName,
style: const TextStyle(
fontSize: 12,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 8),
Text(
task.description,
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
height: 1.4,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 12),
Row(
children: [
_buildInfoChip(
Icons.access_time,
'${task.timeLimit}分钟',
const Color(0xFF4CAF50),
),
const SizedBox(width: 12),
_buildInfoChip(
Icons.text_fields,
'${task.wordLimit}',
const Color(0xFFFF9800),
),
const SizedBox(width: 12),
_buildInfoChip(
Icons.star,
'${task.difficulty.level}',
_getDifficultyColor(task.difficulty),
),
],
),
if (task.keywords.isNotEmpty) ...[
const SizedBox(height: 12),
Wrap(
spacing: 6,
runSpacing: 6,
children: task.keywords.take(3).map((keyword) => Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Text(
keyword,
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
)).toList(),
),
],
],
),
),
),
);
}
Widget _buildInfoChip(IconData icon, String text, Color color) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
icon,
size: 14,
color: color,
),
const SizedBox(width: 4),
Text(
text,
style: TextStyle(
fontSize: 12,
color: color,
fontWeight: FontWeight.w500,
),
),
],
),
);
}
Color _getDifficultyColor(WritingDifficulty difficulty) {
switch (difficulty) {
case WritingDifficulty.beginner:
return const Color(0xFF4CAF50);
case WritingDifficulty.elementary:
return const Color(0xFF8BC34A);
case WritingDifficulty.intermediate:
return const Color(0xFFFF9800);
case WritingDifficulty.upperIntermediate:
return const Color(0xFFFF5722);
case WritingDifficulty.advanced:
return const Color(0xFFF44336);
}
}
Color _getExamTypeColor(ExamType examType) {
switch (examType) {
case ExamType.cet:
return const Color(0xFF2196F3);
case ExamType.kaoyan:
return const Color(0xFFF44336);
case ExamType.toefl:
return const Color(0xFF4CAF50);
case ExamType.ielts:
return const Color(0xFF9C27B0);
}
}
}