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 _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( 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), ), ), ], ), ); }