feat: deliver RSchedule service login exams and elastic glass navigation
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user