Files
TPSchedule/mobile/lib/features/exams/exams_page.dart
T

203 lines
6.9 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(
'考试',
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),
),
),
],
),
);
}