feat: deliver RSchedule service login exams and elastic glass navigation

This commit is contained in:
Codex
2026-10-13 17:06:50 +08:00
parent 9193fd8e43
commit 8f20a341c4
35 changed files with 1742 additions and 453 deletions
+12
View File
@@ -1,6 +1,7 @@
import 'app_settings.dart';
import 'course.dart';
import 'semester.dart';
import 'exam_schedule.dart';
class ImportMetadata {
const ImportMetadata({
@@ -18,6 +19,7 @@ class AppSnapshot {
this.settings = const AppSettings(),
List<Course> courses = const [],
this.importMetadata,
this.examSchedule,
this.calendarCleanupPending = false,
Map<String, String> calendarEventIds = const {},
}) : courses = List.unmodifiable(courses),
@@ -25,6 +27,7 @@ class AppSnapshot {
final AppSettings settings;
final List<Course> courses;
final ImportMetadata? importMetadata;
final ExamSchedule? examSchedule;
final Map<String, String> calendarEventIds;
final bool calendarCleanupPending;
AppSnapshot copyWith({
@@ -33,6 +36,8 @@ class AppSnapshot {
ImportMetadata? importMetadata,
Map<String, String>? calendarEventIds,
bool clearMetadata = false,
ExamSchedule? examSchedule,
bool clearExams = false,
bool? calendarCleanupPending,
}) => AppSnapshot(
settings: settings ?? this.settings,
@@ -40,12 +45,14 @@ class AppSnapshot {
importMetadata: clearMetadata
? null
: importMetadata ?? this.importMetadata,
examSchedule: clearExams ? null : examSchedule ?? this.examSchedule,
calendarEventIds: calendarEventIds ?? this.calendarEventIds,
calendarCleanupPending:
calendarCleanupPending ?? this.calendarCleanupPending,
);
Map<String, dynamic> toJson() => {
'schemaVersion': 1,
'examSchedule': examSchedule?.toJson(),
'courses': courses.map((c) => c.toJson()).toList(),
'settings': {
'semester': settings.semester == null
@@ -109,6 +116,11 @@ class AppSnapshot {
);
final metadata = json['importMetadata'];
return AppSnapshot(
examSchedule: json['examSchedule'] == null
? null
: ExamSchedule.fromJson(
Map<String, dynamic>.from(json['examSchedule'] as Map),
),
calendarCleanupPending:
(json['calendarCleanupPending'] ?? false) as bool,
courses: values,
+82
View File
@@ -0,0 +1,82 @@
class ExamRecord {
const ExamRecord({
required this.code,
required this.name,
this.assessment = '',
this.candidate = '',
this.sequence = '',
this.room = '',
this.seat = '',
this.time = '',
});
final String code, name, assessment, candidate, sequence, room, seat, time;
Map<String, dynamic> toJson() => {
'code': code,
'name': name,
'assessment': assessment,
'candidate': candidate,
'sequence': sequence,
'room': room,
'seat': seat,
'time': time,
};
factory ExamRecord.fromJson(Map<String, dynamic> json) {
String field(String key) {
final value = json[key];
if (value is! String || value.length > 1000) {
throw const FormatException('考试信息无效');
}
return value;
}
final record = ExamRecord(
code: field('code'),
name: field('name'),
assessment: field('assessment'),
candidate: field('candidate'),
sequence: field('sequence'),
room: field('room'),
seat: field('seat'),
time: field('time'),
);
if (record.code.isEmpty || record.name.isEmpty) {
throw const FormatException('考试课程无效');
}
return record;
}
}
class ExamSchedule {
ExamSchedule({
required this.semesterLabel,
required this.termId,
required List<ExamRecord> records,
}) : records = List.unmodifiable(records);
final String semesterLabel, termId;
final List<ExamRecord> records;
Map<String, dynamic> toJson() => {
'semesterLabel': semesterLabel,
'termId': termId,
'records': records.map((r) => r.toJson()).toList(),
};
factory ExamSchedule.fromJson(Map<String, dynamic> json) {
final label = json['semesterLabel'],
id = json['termId'],
rows = json['records'];
if (label is! String ||
label.trim().isEmpty ||
id is! String ||
!RegExp(r'^\d+$').hasMatch(id) ||
rows is! List ||
rows.length > 500) {
throw const FormatException('考试列表无效');
}
final records = rows
.map((r) => ExamRecord.fromJson(Map<String, dynamic>.from(r as Map)))
.toList();
if (records.map((r) => r.code).toSet().length != records.length) {
throw const FormatException('考试课程重复');
}
return ExamSchedule(semesterLabel: label, termId: id, records: records);
}
}