😋 初始化仓库
This commit is contained in:
2
lib/screens/.gitkeep
Normal file
2
lib/screens/.gitkeep
Normal file
@@ -0,0 +1,2 @@
|
||||
# Screens directory
|
||||
# This directory contains UI screens
|
||||
272
lib/screens/home_screen.dart
Normal file
272
lib/screens/home_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
252
lib/screens/login_screen.dart
Normal file
252
lib/screens/login_screen.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
647
lib/screens/progress_screen.dart
Normal file
647
lib/screens/progress_screen.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
368
lib/screens/settings_screen.dart
Normal file
368
lib/screens/settings_screen.dart
Normal 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);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user