😋 初始化仓库

This commit is contained in:
2025-11-13 09:14:49 +08:00
commit 347d264437
133 changed files with 11214 additions and 0 deletions

2
lib/screens/.gitkeep Normal file
View File

@@ -0,0 +1,2 @@
# Screens directory
# This directory contains UI screens

View File

@@ -0,0 +1,272 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/auth_provider.dart';
import '../providers/evaluation_provider.dart';
import '../widgets/course_card.dart';
import 'progress_screen.dart';
import 'settings_screen.dart';
/// Home screen displaying course list and evaluation controls
///
/// Shows list of courses that need evaluation
/// Provides batch evaluation functionality
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
@override
void initState() {
super.initState();
// Defer loading until after the first frame
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadCourses();
});
}
Future<void> _loadCourses() async {
if (!mounted) return;
final authProvider = Provider.of<AuthProvider>(context, listen: false);
final evaluationProvider = Provider.of<EvaluationProvider>(
context,
listen: false,
);
print('🔍 _loadCourses called');
print('🔍 authProvider.connection: ${authProvider.connection}');
print('🔍 authProvider.isAuthenticated: ${authProvider.isAuthenticated}');
// Set connection for evaluation provider
if (authProvider.connection != null) {
print('🔍 Setting connection and loading courses...');
evaluationProvider.setConnection(authProvider.connection);
final success = await evaluationProvider.loadCourses();
print('🔍 loadCourses result: $success');
print('🔍 courses count: ${evaluationProvider.courses.length}');
if (!success) {
print('❌ Error: ${evaluationProvider.errorMessage}');
}
} else {
print('❌ No connection available');
}
}
Future<void> _handleRefresh() async {
await _loadCourses();
}
Future<void> _handleBatchEvaluation() async {
final evaluationProvider = Provider.of<EvaluationProvider>(
context,
listen: false,
);
// Check if there are pending courses
if (evaluationProvider.pendingCourses.isEmpty) {
if (!mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('没有待评课程')));
return;
}
// Navigate to progress screen
if (!mounted) return;
Navigator.of(
context,
).push(MaterialPageRoute(builder: (context) => const ProgressScreen()));
// Start concurrent batch evaluation
await evaluationProvider.startConcurrentBatchEvaluation();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('课程评教'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _handleRefresh,
tooltip: '刷新',
),
IconButton(
icon: const Icon(Icons.settings),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const SettingsScreen()),
);
},
tooltip: '设置',
),
],
),
body: Consumer<EvaluationProvider>(
builder: (context, evaluationProvider, child) {
if (evaluationProvider.state == EvaluationState.loading) {
return const Center(child: CircularProgressIndicator());
}
if (evaluationProvider.state == EvaluationState.error) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error_outline,
size: 64,
color: Theme.of(context).colorScheme.error,
),
const SizedBox(height: 16),
Text(
evaluationProvider.errorMessage ?? '加载失败',
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton.icon(
onPressed: _handleRefresh,
icon: const Icon(Icons.refresh),
label: const Text('重试'),
),
],
),
);
}
final courses = evaluationProvider.courses;
final pendingCount = evaluationProvider.pendingCourses.length;
final evaluatedCount = evaluationProvider.evaluatedCourses.length;
if (courses.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.check_circle_outline,
size: 64,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 16),
Text(
'暂无待评课程',
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 8),
Text(
'下拉刷新以检查新课程',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
);
}
return Column(
children: [
// Statistics card
Container(
margin: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildStatItem(
context,
'总计',
courses.length.toString(),
Icons.list_alt,
),
_buildStatItem(
context,
'待评',
pendingCount.toString(),
Icons.pending_actions,
),
_buildStatItem(
context,
'已评',
evaluatedCount.toString(),
Icons.check_circle,
),
],
),
),
// Course list
Expanded(
child: RefreshIndicator(
onRefresh: _handleRefresh,
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: courses.length,
itemBuilder: (context, index) {
final course = courses[index];
return CourseCard(course: course);
},
),
),
),
],
);
},
),
floatingActionButton: Consumer<EvaluationProvider>(
builder: (context, evaluationProvider, child) {
final hasPending = evaluationProvider.pendingCourses.isNotEmpty;
final isEvaluating =
evaluationProvider.state == EvaluationState.evaluating;
if (!hasPending || isEvaluating) {
return const SizedBox.shrink();
}
return FloatingActionButton.extended(
onPressed: _handleBatchEvaluation,
icon: const Icon(Icons.play_arrow),
label: const Text('批量评教'),
);
},
),
);
}
Widget _buildStatItem(
BuildContext context,
String label,
String value,
IconData icon,
) {
return Column(
children: [
Icon(icon, color: Theme.of(context).colorScheme.onPrimaryContainer),
const SizedBox(height: 4),
Text(
value,
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
color: Theme.of(context).colorScheme.onPrimaryContainer,
fontWeight: FontWeight.bold,
),
),
Text(
label,
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onPrimaryContainer,
),
),
],
);
}
}

View File

@@ -0,0 +1,252 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/auth_provider.dart';
import 'home_screen.dart';
/// Login screen for user authentication
///
/// Provides input fields for student ID, EC password, and UAAP password
/// Integrates with AuthProvider for authentication
class LoginScreen extends StatefulWidget {
const LoginScreen({super.key});
@override
State<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _userIdController = TextEditingController();
final _ecPasswordController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscureEcPassword = true;
bool _obscurePassword = true;
@override
void dispose() {
_userIdController.dispose();
_ecPasswordController.dispose();
_passwordController.dispose();
super.dispose();
}
Future<void> _handleLogin() async {
if (!_formKey.currentState!.validate()) {
return;
}
final authProvider = Provider.of<AuthProvider>(context, listen: false);
final success = await authProvider.login(
userId: _userIdController.text.trim(),
ecPassword: _ecPasswordController.text,
password: _passwordController.text,
);
if (!mounted) return;
if (success) {
// Navigate to home screen
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => const HomeScreen()),
);
} else {
// Show error message
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(authProvider.errorMessage ?? '登录失败'),
backgroundColor: Theme.of(context).colorScheme.error,
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(24.0),
child: Form(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// App logo/title
Icon(
Icons.school,
size: 80,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 16),
Text(
'自动评教系统',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'安徽财经大学',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 48),
// Student ID field
TextFormField(
controller: _userIdController,
decoration: const InputDecoration(
labelText: '学号',
hintText: '请输入学号',
prefixIcon: Icon(Icons.person),
),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '请输入学号';
}
return null;
},
),
const SizedBox(height: 16),
// EC password field
TextFormField(
controller: _ecPasswordController,
decoration: InputDecoration(
labelText: 'EC密码',
hintText: '请输入EC系统密码',
prefixIcon: const Icon(Icons.lock),
suffixIcon: IconButton(
icon: Icon(
_obscureEcPassword
? Icons.visibility_off
: Icons.visibility,
),
onPressed: () {
setState(() {
_obscureEcPassword = !_obscureEcPassword;
});
},
),
),
obscureText: _obscureEcPassword,
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入EC密码';
}
return null;
},
),
const SizedBox(height: 16),
// UAAP password field
TextFormField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'UAAP密码',
hintText: '请输入UAAP系统密码',
prefixIcon: const Icon(Icons.vpn_key),
suffixIcon: IconButton(
icon: Icon(
_obscurePassword
? Icons.visibility_off
: Icons.visibility,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
),
obscureText: _obscurePassword,
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入UAAP密码';
}
return null;
},
),
const SizedBox(height: 32),
// Login button
Consumer<AuthProvider>(
builder: (context, authProvider, child) {
final isLoading = authProvider.state == AuthState.loading;
return ElevatedButton(
onPressed: isLoading ? null : _handleLogin,
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
backgroundColor: Theme.of(
context,
).colorScheme.primary,
foregroundColor: Theme.of(
context,
).colorScheme.onPrimary,
),
child: isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: const Text('登录', style: TextStyle(fontSize: 16)),
);
},
),
const SizedBox(height: 16),
// Help text
Text(
'首次登录需要输入EC和UAAP系统密码\n登录信息将被安全加密存储',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 32),
// Signature
Column(
children: [
Text(
'❤ Created By LoveACE Team',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant.withOpacity(0.6),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 4),
Text(
'🌧 Powered By Sibuxiangx & Flutter',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant.withOpacity(0.6),
),
textAlign: TextAlign.center,
),
],
),
],
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,647 @@
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),
],
),
);
}
}

View File

@@ -0,0 +1,368 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import '../providers/auth_provider.dart';
import '../providers/theme_provider.dart';
import '../providers/evaluation_provider.dart';
import '../widgets/confirm_dialog.dart';
import 'login_screen.dart';
/// Settings screen for app configuration
///
/// Provides theme customization, logout, and data management
class SettingsScreen extends StatelessWidget {
const SettingsScreen({super.key});
Future<void> _handleLogout(BuildContext context) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => const ConfirmDialog(
title: '退出登录',
content: '确定要退出登录吗?',
confirmText: '退出',
cancelText: '取消',
),
);
if (confirmed != true || !context.mounted) return;
final authProvider = Provider.of<AuthProvider>(context, listen: false);
final evaluationProvider = Provider.of<EvaluationProvider>(
context,
listen: false,
);
await authProvider.logout();
evaluationProvider.reset();
if (!context.mounted) return;
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const LoginScreen()),
(route) => false,
);
}
Future<void> _handleClearData(BuildContext context) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => const ConfirmDialog(
title: '清除数据',
content: '确定要清除所有本地数据吗?\n这将删除评教历史记录。',
confirmText: '清除',
cancelText: '取消',
),
);
if (confirmed != true || !context.mounted) return;
final evaluationProvider = Provider.of<EvaluationProvider>(
context,
listen: false,
);
await evaluationProvider.clearEvaluationHistory();
if (!context.mounted) return;
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('数据已清除')));
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('设置')),
body: ListView(
children: [
// Theme section
_buildSectionHeader(context, '外观设置'),
_buildThemeModeSelector(context),
_buildColorSchemeSelector(context),
const Divider(height: 32),
// Account section
_buildSectionHeader(context, '账号管理'),
_buildAccountInfo(context),
_buildLogoutTile(context),
const Divider(height: 32),
// Data section
_buildSectionHeader(context, '数据管理'),
_buildClearDataTile(context),
const Divider(height: 32),
// About section
_buildSectionHeader(context, '关于'),
_buildAboutTile(context),
_buildFontLicenseTile(context),
],
),
);
}
Widget _buildSectionHeader(BuildContext context, String title) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Text(
title,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
);
}
Widget _buildThemeModeSelector(BuildContext context) {
return Consumer<ThemeProvider>(
builder: (context, themeProvider, child) {
return ListTile(
leading: const Icon(Icons.brightness_6),
title: const Text('主题模式'),
subtitle: Text(
themeProvider.getThemeModeName(themeProvider.themeMode),
),
onTap: () {
showDialog(
context: context,
builder: (context) => _ThemeModeDialog(
currentMode: themeProvider.themeMode,
onModeSelected: (mode) {
themeProvider.setThemeMode(mode);
Navigator.of(context).pop();
},
),
);
},
);
},
);
}
Widget _buildColorSchemeSelector(BuildContext context) {
return Consumer<ThemeProvider>(
builder: (context, themeProvider, child) {
return ListTile(
leading: const Icon(Icons.palette),
title: const Text('颜色方案'),
subtitle: Text(
themeProvider.getColorSchemeName(themeProvider.colorScheme),
),
onTap: () {
showDialog(
context: context,
builder: (context) => _ColorSchemeDialog(
currentScheme: themeProvider.colorScheme,
onSchemeSelected: (scheme) {
themeProvider.setColorScheme(scheme);
Navigator.of(context).pop();
},
),
);
},
);
},
);
}
Widget _buildAccountInfo(BuildContext context) {
return Consumer<AuthProvider>(
builder: (context, authProvider, child) {
final credentials = authProvider.credentials;
return ListTile(
leading: const Icon(Icons.account_circle),
title: const Text('当前账号'),
subtitle: Text(credentials?.userId ?? '未登录'),
);
},
);
}
Widget _buildLogoutTile(BuildContext context) {
return ListTile(
leading: Icon(Icons.logout, color: Theme.of(context).colorScheme.error),
title: Text(
'退出登录',
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
onTap: () => _handleLogout(context),
);
}
Widget _buildClearDataTile(BuildContext context) {
return ListTile(
leading: const Icon(Icons.delete_outline),
title: const Text('清除本地数据'),
subtitle: const Text('删除评教历史记录'),
onTap: () => _handleClearData(context),
);
}
Widget _buildAboutTile(BuildContext context) {
return ListTile(
leading: const Icon(Icons.info_outline),
title: const Text('关于应用'),
subtitle: const Text('版本 1.0.0'),
onTap: () {
showAboutDialog(
context: context,
applicationName: '自动评教系统',
applicationVersion: '1.0.0',
applicationIcon: Icon(
Icons.school,
size: 48,
color: Theme.of(context).colorScheme.primary,
),
children: [
const Text('AUFE自动评教工具'),
const SizedBox(height: 8),
const Text('帮助学生快速完成课程评教任务'),
],
);
},
);
}
Widget _buildFontLicenseTile(BuildContext context) {
return ListTile(
leading: const Icon(Icons.font_download),
title: const Text('字体许可'),
subtitle: const Text('MiSans 字体使用说明'),
onTap: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('MiSans 字体知识产权许可协议'),
content: const SingleChildScrollView(
child: Text(
'本应用在 Windows 平台使用 MiSans 字体。\n\n'
'根据小米科技有限责任公司的授权MiSans 字体可免费用于个人和商业用途。\n\n'
'使用条件:\n'
'• 应特别注明使用了 MiSans 字体\n'
'• 不得对字体进行改编或二次开发\n'
'• 不得单独分发或售卖字体文件\n'
'• 可自由分发使用该字体创作的作品\n\n'
'本应用遵守以上使用条款。',
style: TextStyle(fontSize: 14),
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('关闭'),
),
],
),
);
},
);
}
}
class _ThemeModeDialog extends StatelessWidget {
final ThemeMode currentMode;
final Function(ThemeMode) onModeSelected;
const _ThemeModeDialog({
required this.currentMode,
required this.onModeSelected,
});
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('选择主题模式'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
RadioListTile<ThemeMode>(
title: const Text('浅色模式'),
value: ThemeMode.light,
groupValue: currentMode,
onChanged: (mode) {
if (mode != null) onModeSelected(mode);
},
),
RadioListTile<ThemeMode>(
title: const Text('深色模式'),
value: ThemeMode.dark,
groupValue: currentMode,
onChanged: (mode) {
if (mode != null) onModeSelected(mode);
},
),
RadioListTile<ThemeMode>(
title: const Text('跟随系统'),
value: ThemeMode.system,
groupValue: currentMode,
onChanged: (mode) {
if (mode != null) onModeSelected(mode);
},
),
],
),
);
}
}
class _ColorSchemeDialog extends StatelessWidget {
final AppColorScheme currentScheme;
final Function(AppColorScheme) onSchemeSelected;
const _ColorSchemeDialog({
required this.currentScheme,
required this.onSchemeSelected,
});
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('选择颜色方案'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildColorOption(context, AppColorScheme.blue, '蓝色', Colors.blue),
_buildColorOption(context, AppColorScheme.green, '绿色', Colors.green),
_buildColorOption(
context,
AppColorScheme.purple,
'紫色',
Colors.purple,
),
_buildColorOption(
context,
AppColorScheme.orange,
'橙色',
Colors.orange,
),
],
),
);
}
Widget _buildColorOption(
BuildContext context,
AppColorScheme scheme,
String name,
Color color,
) {
return RadioListTile<AppColorScheme>(
title: Row(
children: [
Container(
width: 24,
height: 24,
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
),
const SizedBox(width: 12),
Text(name),
],
),
value: scheme,
groupValue: currentScheme,
onChanged: (scheme) {
if (scheme != null) onSchemeSelected(scheme);
},
);
}
}