203 lines
6.8 KiB
Dart
203 lines
6.8 KiB
Dart
import 'dart:convert';
|
||
import 'dart:typed_data';
|
||
|
||
import 'package:file_picker/file_picker.dart';
|
||
import 'package:flutter/material.dart';
|
||
|
||
import '../../app/app_controller.dart';
|
||
import '../../app/app_theme.dart';
|
||
import '../school/school_exam_adapter.dart';
|
||
|
||
class ExamsPage extends StatelessWidget {
|
||
const ExamsPage({super.key, required this.controller, required this.onSync});
|
||
final AppController controller;
|
||
final VoidCallback onSync;
|
||
Future<void> _import(BuildContext context) async {
|
||
try {
|
||
final file = await FilePicker.pickFile(
|
||
type: FileType.custom,
|
||
allowedExtensions: ['html', 'htm'],
|
||
);
|
||
if (file == null) return;
|
||
final bytes = BytesBuilder(copy: false);
|
||
var length = 0;
|
||
await for (final chunk in file.readAsByteStream()) {
|
||
length += chunk.length;
|
||
if (length > 2 * 1024 * 1024) {
|
||
throw const FormatException('文件不能超过 2 MB');
|
||
}
|
||
bytes.add(chunk);
|
||
}
|
||
final draft = SchoolExamAdapter.extract(utf8.decode(bytes.takeBytes()));
|
||
if (!context.mounted) return;
|
||
final confirmed = await showDialog<bool>(
|
||
context: context,
|
||
builder: (context) => AlertDialog(
|
||
title: const Text('导入考试信息'),
|
||
content: Text(
|
||
'${draft.semesterLabel} · ${draft.records.length} 门课程\n将更新本机考试列表,尚未公布的字段显示为“尚未安排”。',
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.pop(context, false),
|
||
child: const Text('取消'),
|
||
),
|
||
FilledButton(
|
||
onPressed: () => Navigator.pop(context, true),
|
||
child: const Text('确认导入'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
if (confirmed == true) {
|
||
await controller.applyExams(draft);
|
||
if (context.mounted) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(content: Text(controller.lastError ?? '考试信息已导入')),
|
||
);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
if (context.mounted) {
|
||
ScaffoldMessenger.of(context)
|
||
.showSnackBar(SnackBar(content: Text('导入未完成:$error')));
|
||
}
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final schedule = controller.snapshot.examSchedule;
|
||
return ListView(
|
||
key: const PageStorageKey('exams-scroll'),
|
||
padding: EdgeInsets.fromLTRB(
|
||
22,
|
||
10,
|
||
22,
|
||
140 + MediaQuery.paddingOf(context).bottom,
|
||
),
|
||
children: [
|
||
const Text(
|
||
'RSchedule',
|
||
style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700),
|
||
),
|
||
const SizedBox(height: 18),
|
||
Row(
|
||
children: [
|
||
const Expanded(
|
||
child: Text(
|
||
'考试安排',
|
||
style: TextStyle(fontSize: 25, fontWeight: FontWeight.w700),
|
||
),
|
||
),
|
||
IconButton(
|
||
tooltip: '从学校更新考试',
|
||
onPressed: onSync,
|
||
icon: const Icon(Icons.sync_rounded),
|
||
),
|
||
IconButton(
|
||
tooltip: '导入考试 HTML',
|
||
onPressed: () => _import(context),
|
||
icon: const Icon(Icons.file_download_outlined),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
if (schedule == null) ...[
|
||
const SizedBox(height: 54),
|
||
const Icon(
|
||
Icons.assignment_outlined,
|
||
size: 64,
|
||
color: Color(0xFF9ABBE8),
|
||
),
|
||
const SizedBox(height: 20),
|
||
const Text(
|
||
'等待学校公布考试安排',
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
||
),
|
||
const SizedBox(height: 12),
|
||
const Text(
|
||
'连接研究生系统,或导入学校考试页面的 HTML。',
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(fontSize: 13, height: 1.8),
|
||
),
|
||
const SizedBox(height: 24),
|
||
Center(
|
||
child: FilledButton.icon(
|
||
onPressed: onSync,
|
||
icon: const Icon(Icons.school_outlined),
|
||
label: const Text('连接学校考试查询'),
|
||
),
|
||
),
|
||
] else ...[
|
||
Text(
|
||
'${schedule.semesterLabel} · ${schedule.records.length} 门课程',
|
||
style: TextStyle(fontSize: 13, color: ink.withValues(alpha: 0.55)),
|
||
),
|
||
const SizedBox(height: 18),
|
||
if (schedule.records.isEmpty) const Text('该学期暂无考试课程'),
|
||
for (final exam in schedule.records)
|
||
Padding(
|
||
padding: const EdgeInsets.only(bottom: 14),
|
||
child: SurfaceCard(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
exam.name,
|
||
style: const TextStyle(
|
||
fontSize: 17,
|
||
fontWeight: FontWeight.w700,
|
||
),
|
||
),
|
||
const SizedBox(height: 14),
|
||
_field(Icons.schedule_rounded, '考试时间', exam.time),
|
||
_field(Icons.location_on_outlined, '考场', exam.room),
|
||
_field(Icons.event_seat_outlined, '座位号', exam.seat),
|
||
if (exam.assessment.isNotEmpty)
|
||
_field(
|
||
Icons.assignment_outlined,
|
||
'考核方式',
|
||
exam.assessment,
|
||
),
|
||
if (exam.candidate.isNotEmpty)
|
||
_field(Icons.badge_outlined, '考生编号', exam.candidate),
|
||
if (exam.sequence.isNotEmpty)
|
||
_field(
|
||
Icons.format_list_numbered,
|
||
'考场顺序号',
|
||
exam.sequence,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
const Text(
|
||
'排考信息以学校最新公布为准。',
|
||
textAlign: TextAlign.center,
|
||
style: TextStyle(fontSize: 11, color: Color(0xFF8493A5)),
|
||
),
|
||
],
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _field(IconData icon, String label, String value) => Padding(
|
||
padding: const EdgeInsets.only(bottom: 8),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Icon(icon, size: 17, color: const Color(0xFF007AFF)),
|
||
const SizedBox(width: 9),
|
||
Expanded(
|
||
child: Text(
|
||
'$label:${value.isEmpty ? '尚未安排' : value}',
|
||
style: const TextStyle(fontSize: 13, height: 1.5),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|