feat: add school authentication entry with guarded import
This commit is contained in:
@@ -5,24 +5,36 @@ import 'app_theme.dart';
|
||||
import '../widgets/glass_dock.dart';
|
||||
import '../features/timetable/timetable_page.dart';
|
||||
import '../features/profile/profile_page.dart';
|
||||
import '../features/school/school_session.dart';
|
||||
|
||||
class HepanApp extends StatelessWidget {
|
||||
const HepanApp({super.key, required this.controller, this.schoolBuilder});
|
||||
const HepanApp({
|
||||
super.key,
|
||||
required this.controller,
|
||||
this.schoolBuilder,
|
||||
this.school,
|
||||
});
|
||||
final AppController controller;
|
||||
final Widget Function()? schoolBuilder;
|
||||
final SchoolSession? school;
|
||||
@override
|
||||
Widget build(BuildContext context) => MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
title: '河畔Everyday',
|
||||
theme: hepanTheme(),
|
||||
home: _Shell(controller: controller, schoolBuilder: schoolBuilder),
|
||||
home: _Shell(
|
||||
controller: controller,
|
||||
schoolBuilder: schoolBuilder,
|
||||
school: school,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _Shell extends StatefulWidget {
|
||||
const _Shell({required this.controller, this.schoolBuilder});
|
||||
const _Shell({required this.controller, this.schoolBuilder, this.school});
|
||||
final AppController controller;
|
||||
final Widget Function()? schoolBuilder;
|
||||
final SchoolSession? school;
|
||||
@override
|
||||
State<_Shell> createState() => _ShellState();
|
||||
}
|
||||
@@ -71,9 +83,41 @@ class _ShellState extends State<_Shell> with WidgetsBindingObserver {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _logout() async {
|
||||
final choice = await showDialog<int>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('退出学校登录'),
|
||||
content: const Text('已导入的课表是否保留在手机中?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, 0),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, 1),
|
||||
child: const Text('保留课表'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, 2),
|
||||
child: const Text('清除课表'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (choice == null || choice == 0) return;
|
||||
await widget.school?.logout();
|
||||
if (choice == 2) {
|
||||
await widget.controller.clearCourses();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => AnimatedBuilder(
|
||||
animation: widget.controller,
|
||||
animation: Listenable.merge([
|
||||
widget.controller,
|
||||
if (widget.school != null) widget.school!,
|
||||
]),
|
||||
builder: (context, _) => Scaffold(
|
||||
body: Container(
|
||||
decoration: const BoxDecoration(
|
||||
@@ -118,6 +162,10 @@ class _ShellState extends State<_Shell> with WidgetsBindingObserver {
|
||||
ProfilePage(
|
||||
controller: widget.controller,
|
||||
onLogin: _school,
|
||||
authStatus: widget.school?.statusLabel ?? '未登录',
|
||||
onLogout: widget.school?.hasOpened == true
|
||||
? _logout
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
import '../../app/app_controller.dart';
|
||||
import '../../app/app_theme.dart';
|
||||
import '../profile/import_preview.dart';
|
||||
import 'school_session.dart';
|
||||
|
||||
class SchoolAuthPage extends StatefulWidget {
|
||||
const SchoolAuthPage({
|
||||
super.key,
|
||||
required this.session,
|
||||
required this.controller,
|
||||
});
|
||||
final SchoolSession session;
|
||||
final AppController controller;
|
||||
@override
|
||||
State<SchoolAuthPage> createState() => _SchoolAuthPageState();
|
||||
}
|
||||
|
||||
class _SchoolAuthPageState extends State<SchoolAuthPage> {
|
||||
final _username = TextEditingController(),
|
||||
_password = TextEditingController();
|
||||
bool _showWeb = false, _previewing = false;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.session.addListener(_changed);
|
||||
if (!widget.session.hasOpened) {
|
||||
widget.session.open();
|
||||
}
|
||||
}
|
||||
|
||||
void _changed() {
|
||||
if (!mounted) return;
|
||||
setState(() {});
|
||||
final draft = widget.session.pendingDraft;
|
||||
if (draft != null && !_previewing) {
|
||||
_previewing = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
if (!mounted) return;
|
||||
await showModalBottomSheet<bool>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
showDragHandle: true,
|
||||
builder: (_) =>
|
||||
ImportPreview(draft: draft, controller: widget.controller),
|
||||
);
|
||||
widget.session.pendingDraft = null;
|
||||
_previewing = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.session.removeListener(_changed);
|
||||
_password.clear();
|
||||
_username.clear();
|
||||
_password.dispose();
|
||||
_username.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
if (_username.text.trim().isEmpty || _password.text.isEmpty) {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(const SnackBar(content: Text('请输入学号和密码')));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await widget.session.submitCredentials(
|
||||
_username.text.trim(),
|
||||
_password.text,
|
||||
);
|
||||
} finally {
|
||||
_password.clear();
|
||||
if (mounted) {
|
||||
setState(() => _showWeb = true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final s = widget.session;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'学校认证',
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.w700),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: _showWeb ? '河畔登录界面' : '学校页面',
|
||||
onPressed: () => setState(() => _showWeb = !_showWeb),
|
||||
icon: Icon(
|
||||
_showWeb ? Icons.person_outline : Icons.language_rounded,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: '打开学校课表',
|
||||
onPressed: () {
|
||||
setState(() => _showWeb = true);
|
||||
s.openTimetable();
|
||||
},
|
||||
icon: const Icon(Icons.calendar_month_outlined),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
if (s.busy) const LinearProgressIndicator(minHeight: 2),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(13),
|
||||
color: river.withValues(alpha: 0.07),
|
||||
child: Text(
|
||||
s.message,
|
||||
style: const TextStyle(fontSize: 12, height: 1.5, color: ink),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: _showWeb
|
||||
? WebViewWidget(controller: s.web)
|
||||
: ListView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
const Icon(Icons.school_outlined, size: 48, color: river),
|
||||
const SizedBox(height: 20),
|
||||
const Text(
|
||||
'登录,连接你的每一天。',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 23,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'电子科技大学统一身份认证',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: ink.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 30),
|
||||
SurfaceCard(
|
||||
child: Column(
|
||||
children: [
|
||||
TextField(
|
||||
controller: _username,
|
||||
textInputAction: TextInputAction.next,
|
||||
autocorrect: false,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '学号/工号',
|
||||
prefixIcon: Icon(Icons.person_outline),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _password,
|
||||
obscureText: true,
|
||||
enableSuggestions: false,
|
||||
autocorrect: false,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '学校密码',
|
||||
prefixIcon: Icon(Icons.lock_outline),
|
||||
),
|
||||
onSubmitted: (_) => _submit(),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton(
|
||||
onPressed: !s.busy && s.formReady
|
||||
? _submit
|
||||
: null,
|
||||
child: Text(s.busy ? '正在准备学校认证…' : '登录并打开课表'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_password.clear();
|
||||
setState(() => _showWeb = true);
|
||||
s.openAuthentication();
|
||||
},
|
||||
child: const Text('使用学校页面登录'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'密码仅交给学校处理,不在河畔保存。需要验证码或其他验证时,将由学校页面继续处理。',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
height: 1.7,
|
||||
color: ink.withValues(alpha: 0.45),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Text(
|
||||
'学校课表识别待登录后联调;也可通过「我的」导入课表文件。',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
height: 1.6,
|
||||
color: ink.withValues(alpha: 0.45),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
class SchoolNavigationPolicy {
|
||||
static bool canNavigate(Uri uri, {required bool isMainFrame}) => isMainFrame
|
||||
? isSchoolPage(uri)
|
||||
: (uri.scheme == 'https' && uri.userInfo.isEmpty) ||
|
||||
(uri.scheme == 'about' && uri.path == 'blank');
|
||||
static bool _https(Uri uri) =>
|
||||
uri.scheme == 'https' && uri.port == 443 && uri.userInfo.isEmpty;
|
||||
static bool canInjectCredentials(Uri uri) =>
|
||||
_https(uri) &&
|
||||
uri.host == 'idas.uestc.edu.cn' &&
|
||||
uri.path == '/authserver/login';
|
||||
static bool isTimetable(Uri uri) =>
|
||||
_https(uri) &&
|
||||
uri.host == 'yjsjy.uestc.edu.cn' &&
|
||||
uri.path == '/pyxx/pygl/xskbcx/index/1';
|
||||
static bool isSchoolPage(Uri uri) =>
|
||||
_https(uri) &&
|
||||
const {'idas.uestc.edu.cn', 'yjsjy.uestc.edu.cn'}.contains(uri.host);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import '../../data/json_schedule_import.dart';
|
||||
import 'school_navigation_policy.dart';
|
||||
|
||||
class UnsupportedSchoolLayout implements Exception {
|
||||
@override
|
||||
String toString() => '学校课表识别待联调,当前课表保持不变。';
|
||||
}
|
||||
|
||||
class SchoolPageAdapter {
|
||||
ImportDraft extract(String html, Uri source) {
|
||||
// A real authenticated timetable fixture is required before registering a parser.
|
||||
// Unknown HTML must never erase the student's last valid imported timetable.
|
||||
throw UnsupportedSchoolLayout();
|
||||
}
|
||||
}
|
||||
|
||||
class SchoolReader {
|
||||
SchoolReader(this.adapter);
|
||||
final SchoolPageAdapter adapter;
|
||||
Future<ImportDraft>? _pending;
|
||||
bool get busy => _pending != null;
|
||||
Future<ImportDraft> read(Uri origin, Future<String> Function() getHtml) {
|
||||
if (_pending != null) return _pending!;
|
||||
_pending = Future<ImportDraft>(() async {
|
||||
try {
|
||||
if (!SchoolNavigationPolicy.isTimetable(origin)) {
|
||||
throw UnsupportedSchoolLayout();
|
||||
}
|
||||
return adapter.extract(await getHtml(), origin);
|
||||
} finally {
|
||||
_pending = null;
|
||||
}
|
||||
});
|
||||
return _pending!;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:webview_flutter/webview_flutter.dart';
|
||||
|
||||
import '../../data/json_schedule_import.dart';
|
||||
import 'school_navigation_policy.dart';
|
||||
import 'school_page_adapter.dart';
|
||||
|
||||
enum SchoolConnectionState {
|
||||
signedOut,
|
||||
authenticating,
|
||||
connected,
|
||||
expired,
|
||||
unknown,
|
||||
}
|
||||
|
||||
class SchoolSession extends ChangeNotifier {
|
||||
SchoolSession() {
|
||||
web = WebViewController()
|
||||
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onNavigationRequest: (request) {
|
||||
final uri = Uri.tryParse(request.url);
|
||||
if (uri == null ||
|
||||
!SchoolNavigationPolicy.canNavigate(
|
||||
uri,
|
||||
isMainFrame: request.isMainFrame,
|
||||
)) {
|
||||
message = '学校跳转到了其他地址,已停止自动交接登录信息。';
|
||||
notifyListeners();
|
||||
return NavigationDecision.prevent;
|
||||
}
|
||||
return NavigationDecision.navigate;
|
||||
},
|
||||
onPageStarted: (_) {
|
||||
if (!hasOpened) {
|
||||
return;
|
||||
}
|
||||
_loading = true;
|
||||
formReady = false;
|
||||
notifyListeners();
|
||||
},
|
||||
onPageFinished: _loaded,
|
||||
onWebResourceError: (error) {
|
||||
if (error.isForMainFrame == true) {
|
||||
_loading = false;
|
||||
message = '学校页面暂时未能加载,请检查网络或使用统一认证入口。';
|
||||
notifyListeners();
|
||||
}
|
||||
},
|
||||
onHttpError: (error) async {
|
||||
final current = Uri.tryParse(await web.currentUrl() ?? '');
|
||||
if (!hasOpened ||
|
||||
error.request == null ||
|
||||
error.request!.uri != current) {
|
||||
return;
|
||||
}
|
||||
_loading = false;
|
||||
message = '学校页面请求未完成,可稍后重试。';
|
||||
notifyListeners();
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
late final WebViewController web;
|
||||
final _reader = SchoolReader(SchoolPageAdapter());
|
||||
SchoolConnectionState connectionState = SchoolConnectionState.signedOut;
|
||||
bool _loading = false,
|
||||
_submitting = false,
|
||||
hasOpened = false,
|
||||
formReady = false;
|
||||
bool get busy => _loading || _submitting || _reader.busy;
|
||||
ImportDraft? pendingDraft;
|
||||
String message = '请通过学校统一身份认证登录。';
|
||||
String get statusLabel => switch (connectionState) {
|
||||
SchoolConnectionState.signedOut => '未登录',
|
||||
SchoolConnectionState.authenticating => '请在学校页面完成认证',
|
||||
SchoolConnectionState.connected => '学校认证已确认',
|
||||
SchoolConnectionState.expired => '学校登录已过期',
|
||||
SchoolConnectionState.unknown => '学校页面已打开 · 课表识别待联调',
|
||||
};
|
||||
static final authUrl = Uri.parse(
|
||||
'https://idas.uestc.edu.cn/authserver/login',
|
||||
);
|
||||
static final timetableUrl = Uri.parse(
|
||||
'https://yjsjy.uestc.edu.cn/pyxx/pygl/xskbcx/index/1',
|
||||
);
|
||||
Future<void> open() => openTimetable();
|
||||
Future<void> openTimetable() async {
|
||||
hasOpened = true;
|
||||
_loading = true;
|
||||
pendingDraft = null;
|
||||
message = '正在打开研究生课表页面…';
|
||||
notifyListeners();
|
||||
try {
|
||||
await web.loadRequest(timetableUrl);
|
||||
} catch (_) {
|
||||
_loading = false;
|
||||
message = '学校课表页面无法打开,请检查校园网络。';
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> openAuthentication() async {
|
||||
hasOpened = true;
|
||||
_loading = true;
|
||||
message = '正在准备学校认证…';
|
||||
notifyListeners();
|
||||
try {
|
||||
await web.loadRequest(authUrl);
|
||||
} catch (_) {
|
||||
_loading = false;
|
||||
message = '学校认证页面暂时不可用。';
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
static const _readyScript = """(() => {
|
||||
if (location.origin !== 'https://idas.uestc.edu.cn' || location.pathname !== '/authserver/login') return false;
|
||||
const f = document.querySelector('.login-main #pwdFromId');
|
||||
if (!f || f.method.toLowerCase() !== 'post') return false;
|
||||
const action = new URL(f.action, location.href);
|
||||
return action.origin === location.origin && action.pathname === '/authserver/login'
|
||||
&& !!f.querySelector('#username') && !!f.querySelector('#password')
|
||||
&& !!f.querySelector('#saltPassword') && !!f.querySelector('#pwdEncryptSalt')
|
||||
&& !!f.querySelector('#login_submit') && typeof window.startLogin === 'function'
|
||||
&& typeof window.encryptPassword === 'function' && typeof window.checkUserCaptcha === 'function';
|
||||
})()""";
|
||||
bool _truth(Object value) => value == true || value.toString() == 'true';
|
||||
Future<void> _loaded(String url) async {
|
||||
if (!hasOpened) {
|
||||
return;
|
||||
}
|
||||
_loading = false;
|
||||
final uri = Uri.tryParse(url);
|
||||
if (uri != null && SchoolNavigationPolicy.canInjectCredentials(uri)) {
|
||||
connectionState = connectionState == SchoolConnectionState.connected
|
||||
? SchoolConnectionState.expired
|
||||
: SchoolConnectionState.authenticating;
|
||||
try {
|
||||
formReady = _truth(
|
||||
await web.runJavaScriptReturningResult(_readyScript),
|
||||
);
|
||||
} catch (_) {
|
||||
formReady = false;
|
||||
}
|
||||
message = formReady ? '可以使用河畔界面登录;验证码会在学校页面完成。' : '请使用学校原页面完成认证。';
|
||||
} else if (uri != null && SchoolNavigationPolicy.isTimetable(uri)) {
|
||||
connectionState = SchoolConnectionState.unknown;
|
||||
message = '已打开课表页面,正在核对课程结构。';
|
||||
try {
|
||||
pendingDraft = await readTimetable();
|
||||
connectionState = SchoolConnectionState.connected;
|
||||
} catch (error) {
|
||||
message = error is UnsupportedSchoolLayout
|
||||
? error.toString()
|
||||
: '课表读取未完成,已有课表保持不变。';
|
||||
}
|
||||
} else {
|
||||
connectionState = SchoolConnectionState.unknown;
|
||||
message = '可点击右上角日历打开学校课表。';
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> submitCredentials(String username, String password) async {
|
||||
if (busy) return;
|
||||
_submitting = true;
|
||||
message = '正在交接学校认证…';
|
||||
notifyListeners();
|
||||
try {
|
||||
final url = Uri.tryParse(await web.currentUrl() ?? '');
|
||||
if (url == null ||
|
||||
!SchoolNavigationPolicy.canInjectCredentials(url) ||
|
||||
!_truth(await web.runJavaScriptReturningResult(_readyScript))) {
|
||||
throw StateError('学校表单尚未准备好,请使用学校页面登录');
|
||||
}
|
||||
final result = await web.runJavaScriptReturningResult('''(() => {
|
||||
if (!($_readyScript)) return false;
|
||||
const f = document.querySelector('.login-main #pwdFromId');
|
||||
const u = f.querySelector('#username'), p = f.querySelector('#password');
|
||||
u.value = ${jsonEncode(username)}; p.value = ${jsonEncode(password)};
|
||||
p.removeAttribute('readonly');
|
||||
u.dispatchEvent(new Event('input', {bubbles:true}));
|
||||
p.dispatchEvent(new Event('input', {bubbles:true}));
|
||||
window.checkUserCaptcha(); return true;
|
||||
})()''');
|
||||
if (!_truth(result)) throw StateError('学校表单已变化,请使用学校页面登录');
|
||||
var settled = false;
|
||||
for (var i = 0; i < 20; i++) {
|
||||
if (_truth(
|
||||
await web.runJavaScriptReturningResult(
|
||||
'!!window.jQuery && window.jQuery.active === 0',
|
||||
),
|
||||
)) {
|
||||
settled = true;
|
||||
break;
|
||||
}
|
||||
await Future<void>.delayed(const Duration(milliseconds: 200));
|
||||
}
|
||||
final current = Uri.tryParse(await web.currentUrl() ?? '');
|
||||
if (!settled ||
|
||||
current == null ||
|
||||
!SchoolNavigationPolicy.canInjectCredentials(current)) {
|
||||
message = '请在学校页面继续完成验证。';
|
||||
return;
|
||||
}
|
||||
await web.runJavaScript('''(() => {
|
||||
if (!($_readyScript)) return;
|
||||
window.startLogin(document.querySelector('.login-main #pwdFromId #login_submit'));
|
||||
})()''');
|
||||
message = '已交给学校处理;如有验证码,请在学校页面完成。';
|
||||
} catch (_) {
|
||||
message = '登录交接未完成,请使用学校页面继续。';
|
||||
} finally {
|
||||
_submitting = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
Future<ImportDraft> readTimetable() async {
|
||||
final uri = Uri.tryParse(await web.currentUrl() ?? '');
|
||||
if (uri == null) throw UnsupportedSchoolLayout();
|
||||
return _reader.read(uri, () async {
|
||||
final raw = await web.runJavaScriptReturningResult(
|
||||
'JSON.stringify(document.documentElement.outerHTML)',
|
||||
);
|
||||
final value = raw is String ? raw : raw.toString();
|
||||
try {
|
||||
final decoded = jsonDecode(value);
|
||||
return decoded is String ? decoded : value;
|
||||
} catch (_) {
|
||||
return value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
hasOpened = false;
|
||||
await WebViewCookieManager().clearCookies();
|
||||
await web.clearCache();
|
||||
await web.clearLocalStorage();
|
||||
await web.loadHtmlString('<html></html>');
|
||||
_loading = false;
|
||||
hasOpened = false;
|
||||
pendingDraft = null;
|
||||
formReady = false;
|
||||
connectionState = SchoolConnectionState.signedOut;
|
||||
message = '已退出学校登录。';
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user