feat: integrate undergraduate school workflows and period twelve

This commit is contained in:
Codex
2026-10-15 00:28:04 +08:00
parent 7939bd4c3c
commit b28f333720
15 changed files with 573 additions and 26 deletions
+100 -12
View File
@@ -15,6 +15,9 @@ import '../features/timetable/timetable_page.dart';
import '../features/profile/profile_page.dart';
import '../features/school/school_session.dart';
import '../features/school/school_selection_page.dart';
import '../features/school/undergraduate/session.dart';
import '../features/school/undergraduate/exam_query_picker.dart';
import '../domain/undergraduate_exam_query.dart';
class HepanApp extends StatelessWidget {
const HepanApp({
@@ -22,12 +25,16 @@ class HepanApp extends StatelessWidget {
required this.controller,
this.schoolBuilder,
this.school,
this.undergraduate,
this.undergraduateBuilder,
this.notificationSelection,
this.feedbackSender,
});
final AppController controller;
final Widget Function()? schoolBuilder;
final SchoolSession? school;
final UndergraduateSession? undergraduate;
final Widget Function()? undergraduateBuilder;
final ValueNotifier<String?>? notificationSelection;
final FeedbackSender? feedbackSender;
@override
@@ -42,6 +49,8 @@ class HepanApp extends StatelessWidget {
controller: controller,
schoolBuilder: schoolBuilder,
school: school,
undergraduate: undergraduate,
undergraduateBuilder: undergraduateBuilder,
notificationSelection: notificationSelection,
feedbackSender: feedbackSender ?? configuredFeedbackSender(),
),
@@ -53,12 +62,16 @@ class _Shell extends StatefulWidget {
required this.controller,
this.schoolBuilder,
this.school,
this.undergraduate,
this.undergraduateBuilder,
this.notificationSelection,
required this.feedbackSender,
});
final AppController controller;
final Widget Function()? schoolBuilder;
final SchoolSession? school;
final UndergraduateSession? undergraduate;
final Widget Function()? undergraduateBuilder;
final ValueNotifier<String?>? notificationSelection;
final FeedbackSender feedbackSender;
@override
@@ -68,11 +81,13 @@ class _Shell extends StatefulWidget {
class _ShellState extends State<_Shell> with WidgetsBindingObserver {
int _tab = 0;
bool _openingSchool = false;
SupportedSchool? _selectedSchool;
// Earlier imports all belong to UESTC graduate; reuse their existing term
// and session without requiring students to choose again on every refresh.
SupportedSchool? get _knownSchool =>
_selectedSchool ??
widget.controller.snapshot.selectedSchool ??
(widget.undergraduate?.canLogout == true
? SupportedSchool.uestcUndergraduate
: null) ??
(widget.controller.snapshot.schoolTerm != null ||
widget.school?.canLogout == true
? SupportedSchool.uestcGraduate
@@ -149,14 +164,15 @@ class _ShellState extends State<_Shell> with WidgetsBindingObserver {
),
);
if (!mounted || selected == null) return;
setState(() => _selectedSchool = selected);
final saved = await widget.controller.selectSchool(selected);
if (!mounted || !saved) return;
}
if (!mounted) return;
// The only current connector is the existing UESTC graduate workflow.
// A new catalog option must provide its own connector here.
switch (selected) {
case SupportedSchool.uestcGraduate:
await _openSchool(exams: exams, chooseTerm: chooseTerm);
case SupportedSchool.uestcUndergraduate:
await _openUndergraduate(exams: exams, chooseTerm: chooseTerm);
}
} finally {
_openingSchool = false;
@@ -168,14 +184,17 @@ class _ShellState extends State<_Shell> with WidgetsBindingObserver {
bool chooseTerm = false,
}) async {
if (widget.schoolBuilder != null) {
final term = widget.controller.snapshot.schoolTerm;
final savedTerm = widget.controller.snapshot.schoolTerm;
final term = savedTerm?.school == SupportedSchool.uestcGraduate
? savedTerm
: null;
if (exams) {
widget.school?.openExams(
await widget.school?.openExams(
term: term,
chooseTerm: chooseTerm || term == null,
);
} else {
widget.school?.openTimetable(
await widget.school?.openTimetable(
term: term,
chooseTerm: chooseTerm || term == null,
);
@@ -201,6 +220,51 @@ class _ShellState extends State<_Shell> with WidgetsBindingObserver {
}
}
Future<void> _openUndergraduate({
required bool exams,
required bool chooseTerm,
}) async {
final session = widget.undergraduate, builder = widget.undergraduateBuilder;
if (session == null || builder == null) {
await showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: const Text('学校连接'),
content: const Text('本科连接暂未准备好,请重试。'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('知道了'),
),
],
),
);
return;
}
if (exams) {
var query = UndergraduateExamQuery.fromSchedule(
widget.controller.snapshot.examSchedule,
);
if (query == null || chooseTerm) {
query = await showUndergraduateExamQueryPicker(
context,
initial: query,
now: widget.controller.clock(),
);
if (!mounted || query == null) return;
}
await session.openExams(query);
} else {
await session.openTimetable();
}
if (!mounted) return;
await Navigator.push(
context,
MaterialPageRoute<void>(builder: (_) => builder()),
);
session.setReadingActive(false);
}
Future<void> _logout() async {
final choice = await showDialog<int>(
context: context,
@@ -224,7 +288,17 @@ class _ShellState extends State<_Shell> with WidgetsBindingObserver {
),
);
if (choice == null || choice == 0) return;
await widget.school?.logout();
if (_knownSchool == SupportedSchool.uestcUndergraduate &&
widget.undergraduate?.canLogout == true) {
await widget.undergraduate!.logout();
widget.school?.invalidateAuthentication();
} else if (widget.school?.canLogout == true) {
await widget.school!.logout();
widget.undergraduate?.invalidateAuthentication();
} else if (widget.undergraduate?.canLogout == true) {
await widget.undergraduate!.logout();
widget.school?.invalidateAuthentication();
}
if (choice == 2) {
await widget.controller.clearCourses();
await widget.controller.clearExams();
@@ -261,6 +335,7 @@ class _ShellState extends State<_Shell> with WidgetsBindingObserver {
animation: Listenable.merge([
widget.controller,
if (widget.school != null) widget.school!,
if (widget.undergraduate != null) widget.undergraduate!,
]),
builder: (context, _) => Scaffold(
body: Container(
@@ -316,6 +391,11 @@ class _ShellState extends State<_Shell> with WidgetsBindingObserver {
onSync: () {
_school(exams: true);
},
onQueryOptions:
_knownSchool ==
SupportedSchool.uestcUndergraduate
? () => _school(exams: true, chooseTerm: true)
: null,
),
ProfilePage(
controller: widget.controller,
@@ -323,11 +403,19 @@ class _ShellState extends State<_Shell> with WidgetsBindingObserver {
onLogin: () => _school(selectSchool: true),
schoolName: _knownSchool?.label,
onUpdateTerm: () => _school(chooseTerm: true),
authStatus: widget.school?.statusLabel ?? '未登录',
logoutLabel: widget.school?.logoutFailed == true
authStatus:
_knownSchool ==
SupportedSchool.uestcUndergraduate
? widget.undergraduate?.statusLabel ?? '未登录'
: widget.school?.statusLabel ?? '未登录',
logoutLabel:
widget.school?.logoutFailed == true ||
widget.undergraduate?.logoutFailed == true
? '重试退出学校登录'
: '退出学校登录',
onLogout: widget.school?.canLogout == true
onLogout:
widget.school?.canLogout == true ||
widget.undergraduate?.canLogout == true
? _logout
: null,
),