648 lines
21 KiB
Dart
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),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|