Files
AutoJudge-Flutter/lib/screens/progress_screen.dart
2025-11-13 09:14:49 +08:00

648 lines
21 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/evaluation_provider.dart';
/// Progress screen showing real-time evaluation progress
///
/// Displays progress percentage, current course, and statistics
/// Allows canceling the evaluation process
class ProgressScreen extends StatelessWidget {
const ProgressScreen({super.key});
Future<bool> _onWillPop(BuildContext context, EvaluationState state) async {
// 如果已完成或出错,允许直接返回
if (state == EvaluationState.completed || state == EvaluationState.error) {
return true;
}
// 如果正在评教,显示确认对话框
final shouldPop = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('确认中断'),
content: const Text('评教正在进行中,确定要中断吗?\n\n中断后当前进度将丢失。'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('继续评教'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
style: TextButton.styleFrom(
foregroundColor: Theme.of(context).colorScheme.error,
),
child: const Text('中断'),
),
],
),
);
return shouldPop ?? false;
}
@override
Widget build(BuildContext context) {
return Consumer<EvaluationProvider>(
builder: (context, evaluationProvider, child) {
final state = evaluationProvider.state;
final progress = evaluationProvider.progress;
final current = evaluationProvider.currentProgress;
final total = evaluationProvider.totalProgress;
final currentCourse = evaluationProvider.currentCourse;
final currentStatus = evaluationProvider.currentStatus;
final lastResult = evaluationProvider.lastResult;
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
final shouldPop = await _onWillPop(context, state);
if (shouldPop && context.mounted) {
Navigator.of(context).pop();
}
},
child: Scaffold(
appBar: AppBar(
title: const Text('评教进度'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () async {
final shouldPop = await _onWillPop(context, state);
if (shouldPop && context.mounted) {
Navigator.of(context).pop();
}
},
),
),
body: Builder(
builder: (context) {
// Show completion screen
if (state == EvaluationState.completed && lastResult != null) {
return _buildCompletionScreen(context, lastResult);
}
// Show error screen
if (state == EvaluationState.error) {
return _buildErrorScreen(
context,
evaluationProvider.errorMessage ?? '评教失败',
);
}
// Show progress screen
return _buildProgressScreen(
context,
progress,
current,
total,
currentCourse,
currentStatus,
);
},
),
),
);
},
);
}
Widget _buildProgressScreen(
BuildContext context,
double progress,
int current,
int total,
dynamic currentCourse,
String? status,
) {
return Column(
children: [
// Top: Overall progress info
Expanded(
flex: 2,
child: _buildProgressContent(
context,
progress,
current,
total,
currentCourse,
status,
),
),
// Bottom: Task list
Expanded(flex: 3, child: _buildTaskListPanel(context)),
],
);
}
Widget _buildProgressContent(
BuildContext context,
double progress,
int current,
int total,
dynamic currentCourse,
String? status,
) {
return Center(
child: Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Progress percentage
Text(
'${(progress * 100).toInt()}%',
style: Theme.of(context).textTheme.displayMedium?.copyWith(
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
const SizedBox(height: 16),
// Progress bar
ClipRRect(
borderRadius: BorderRadius.circular(12),
child: LinearProgressIndicator(
value: progress,
minHeight: 24,
backgroundColor: Theme.of(
context,
).colorScheme.onPrimaryContainer.withValues(alpha: 0.2),
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.onPrimaryContainer,
),
),
),
const SizedBox(height: 16),
// Complete / All
Text(
'$current / $total',
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
],
),
),
);
}
Widget _buildTaskListPanel(BuildContext context) {
return Consumer<EvaluationProvider>(
builder: (context, provider, child) {
final tasks = provider.concurrentTasks;
return Container(
margin: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: Theme.of(
context,
).colorScheme.outline.withValues(alpha: 0.2),
),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Header
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHigh,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(12),
topRight: Radius.circular(12),
),
),
child: Row(
children: [
Icon(
Icons.list_alt,
size: 20,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(width: 8),
Text(
'并发任务列表',
style: Theme.of(context).textTheme.titleSmall?.copyWith(
fontWeight: FontWeight.w600,
),
),
const Spacer(),
Text(
'${tasks.length} 个任务',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
// Task list
Expanded(
child: tasks.isEmpty
? Center(
child: Text(
'暂无任务',
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
),
)
: ListView.builder(
padding: const EdgeInsets.all(8),
itemCount: tasks.length,
itemBuilder: (context, index) {
final task = tasks[index];
return _buildTaskCard(context, task);
},
),
),
],
),
);
},
);
}
Widget _buildTaskCard(BuildContext context, dynamic task) {
final statusColor = _getTaskStatusColor(context, task.status);
final statusIcon = _getTaskStatusIcon(task.status);
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Task header
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: statusColor.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: statusColor.withValues(alpha: 0.3),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(statusIcon, size: 14, color: statusColor),
const SizedBox(width: 4),
Text(
'任务 ${task.taskId}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: statusColor,
fontWeight: FontWeight.bold,
),
),
],
),
),
const SizedBox(width: 8),
Expanded(
child: Text(
task.course.name,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
],
),
const SizedBox(height: 8),
// Teacher info
Row(
children: [
Icon(
Icons.person,
size: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(width: 4),
Text(
task.course.teacher,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
const SizedBox(height: 8),
// Status and progress
Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
task.statusText,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: statusColor,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 4),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: task.progress,
minHeight: 6,
backgroundColor: Theme.of(
context,
).colorScheme.surfaceContainerHighest,
valueColor: AlwaysStoppedAnimation<Color>(
statusColor,
),
),
),
],
),
),
const SizedBox(width: 8),
Text(
'${(task.progress * 100).toInt()}%',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: statusColor,
fontWeight: FontWeight.bold,
),
),
],
),
// Error message if failed
if (task.status.toString().contains('failed') &&
task.errorMessage != null) ...[
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.errorContainer,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Icon(
Icons.error_outline,
size: 16,
color: Theme.of(context).colorScheme.onErrorContainer,
),
const SizedBox(width: 8),
Expanded(
child: Text(
task.errorMessage,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onErrorContainer,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
),
),
],
],
),
),
);
}
Color _getTaskStatusColor(BuildContext context, dynamic status) {
final statusStr = status.toString();
if (statusStr.contains('completed')) {
return Colors.green;
} else if (statusStr.contains('failed')) {
return Theme.of(context).colorScheme.error;
} else if (statusStr.contains('countdown')) {
return Theme.of(context).colorScheme.tertiary;
} else if (statusStr.contains('submitting') ||
statusStr.contains('verifying')) {
return Theme.of(context).colorScheme.secondary;
} else if (statusStr.contains('preparing')) {
return Theme.of(context).colorScheme.primary;
} else {
return Theme.of(context).colorScheme.onSurfaceVariant;
}
}
IconData _getTaskStatusIcon(dynamic status) {
final statusStr = status.toString();
if (statusStr.contains('completed')) {
return Icons.check_circle;
} else if (statusStr.contains('failed')) {
return Icons.error;
} else if (statusStr.contains('countdown')) {
return Icons.timer;
} else if (statusStr.contains('submitting')) {
return Icons.upload;
} else if (statusStr.contains('verifying')) {
return Icons.verified;
} else if (statusStr.contains('preparing')) {
return Icons.settings;
} else {
return Icons.pending;
}
}
Widget _buildCompletionScreen(BuildContext context, dynamic result) {
final isAllSuccess = result.failed == 0;
return SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 40),
// Success/Warning icon
Icon(
isAllSuccess ? Icons.check_circle : Icons.warning,
size: 100,
color: isAllSuccess
? Colors.green
: Theme.of(context).colorScheme.error,
),
const SizedBox(height: 24),
// Title
Text(
isAllSuccess ? '评教完成' : '评教完成(部分失败)',
style: Theme.of(
context,
).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 32),
// Statistics
_buildResultCard(
context,
'总计',
result.total.toString(),
Icons.list_alt,
Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 12),
_buildResultCard(
context,
'成功',
result.success.toString(),
Icons.check_circle,
Colors.green,
),
if (result.failed > 0) ...[
const SizedBox(height: 12),
_buildResultCard(
context,
'失败',
result.failed.toString(),
Icons.error,
Theme.of(context).colorScheme.error,
),
],
const SizedBox(height: 12),
_buildResultCard(
context,
'耗时',
'${result.duration.inSeconds}',
Icons.timer,
Theme.of(context).colorScheme.secondary,
),
const SizedBox(height: 48),
// Action buttons
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (result.failed > 0)
ElevatedButton.icon(
onPressed: () {
final evaluationProvider = Provider.of<EvaluationProvider>(
context,
listen: false,
);
evaluationProvider.retryFailed();
},
icon: const Icon(Icons.refresh),
label: const Text('重试失败'),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error,
foregroundColor: Theme.of(context).colorScheme.onError,
),
),
if (result.failed > 0) const SizedBox(width: 16),
ElevatedButton.icon(
onPressed: () {
Navigator.of(context).pop();
},
icon: const Icon(Icons.home),
label: const Text('返回首页'),
),
],
),
const SizedBox(height: 40),
],
),
);
}
Widget _buildResultCard(
BuildContext context,
String label,
String value,
IconData icon,
Color color,
) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withValues(alpha: 0.3)),
),
child: Row(
children: [
Icon(icon, color: color, size: 32),
const SizedBox(width: 16),
Expanded(
child: Text(label, style: Theme.of(context).textTheme.titleMedium),
),
Text(
value,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
color: color,
),
),
],
),
);
}
Widget _buildErrorScreen(BuildContext context, String errorMessage) {
return SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 80),
Icon(
Icons.error_outline,
size: 100,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(height: 24),
Text(
'评教失败',
style: Theme.of(
context,
).textTheme.headlineMedium?.copyWith(fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Text(
errorMessage,
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 48),
ElevatedButton.icon(
onPressed: () {
Navigator.of(context).pop();
},
icon: const Icon(Icons.home),
label: const Text('返回首页'),
),
const SizedBox(height: 80),
],
),
);
}
}