feat(auth): 添加登录/注册页面及令牌恢复流程
- 新增 SplashPage(启动页)和 LoginPage(登录页),支持路由跳转 - 扩展 AuthState,增加 submitting/alert/busy 状态及消费机制 - AuthController 增加 checkLoginState/register/showAlert 等方法 - 删除旧模型文件(binding_item, token_response, verify_response) - 删除旧版 LoginPage,迁移至 features/auth 目录 - 更新本地化字符串,增加注册相关文案 - 调整默认 API 地址为本地开发环境
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../../../core/network/api_exception.dart';
|
||||
import '../data/auth_providers.dart';
|
||||
import '../domain/auth_repository.dart';
|
||||
import '../domain/auth_state.dart';
|
||||
@@ -7,6 +8,9 @@ import '../domain/auth_state.dart';
|
||||
part 'auth_controller.g.dart';
|
||||
|
||||
/// 登录状态控制器。
|
||||
///
|
||||
/// 会话级持有(`keepAlive`),负责令牌恢复、登录 / 注册 / 退出,
|
||||
/// 以及启动期的登录态校验(verify + refresh 重试)。
|
||||
@Riverpod(keepAlive: true)
|
||||
class AuthController extends _$AuthController {
|
||||
AuthRepository get _repository => ref.read(authRepositoryProvider);
|
||||
@@ -19,38 +23,143 @@ class AuthController extends _$AuthController {
|
||||
repository.accessToken != null && await repository.hasRefreshToken();
|
||||
return AuthState(
|
||||
loggedIn: loggedIn,
|
||||
restoring: false,
|
||||
username: await repository.getUsername(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 登录:成功返回 true,失败返回 false 并写入错误信息。
|
||||
AuthState get _current => state.valueOrNull ?? const AuthState();
|
||||
|
||||
/// 校验本地令牌是否仍然有效(对应启动页流程)。
|
||||
///
|
||||
/// 无令牌直接返回 false;`verify` 失败时尝试 `refresh` 后重试一次,
|
||||
/// 仍失败则清空令牌。
|
||||
Future<bool> checkLoginState() async {
|
||||
final repository = _repository;
|
||||
await repository.restore();
|
||||
if (repository.accessToken == null) {
|
||||
if (!await repository.hasRefreshToken()) {
|
||||
await _reset();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (await _verifyQuietly()) {
|
||||
state = AsyncData(
|
||||
_current.copyWith(
|
||||
loggedIn: true,
|
||||
username: await repository.getUsername(),
|
||||
),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
// access token 失效:用 refreshToken 续期后再校验一次。
|
||||
try {
|
||||
await repository.refresh();
|
||||
} on Object {
|
||||
await _reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (await _verifyQuietly()) {
|
||||
state = AsyncData(
|
||||
_current.copyWith(
|
||||
loggedIn: true,
|
||||
username: await repository.getUsername(),
|
||||
),
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
await _reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<bool> _verifyQuietly() async {
|
||||
try {
|
||||
await _repository.verify();
|
||||
return true;
|
||||
} on Object {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _reset() async {
|
||||
final username = await _repository.getUsername();
|
||||
await _repository.clear();
|
||||
state = AsyncData(AuthState(username: username));
|
||||
}
|
||||
|
||||
/// 登录:成功返回 true,失败返回 false 并写入 [AuthState.error]。
|
||||
Future<bool> login(String username, String password) async {
|
||||
state = AsyncData(_current.copyWith(submitting: true, error: null));
|
||||
try {
|
||||
await _repository.login(username, password);
|
||||
state = AsyncData(
|
||||
AuthState(loggedIn: true, username: username),
|
||||
);
|
||||
state = AsyncData(AuthState(loggedIn: true, username: username));
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = AsyncData(
|
||||
AuthState(loggedIn: false, error: e.toString()),
|
||||
_current.copyWith(
|
||||
submitting: false,
|
||||
loggedIn: false,
|
||||
error: _describe(e, '登录失败'),
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 退出登录:清空令牌并复位状态。
|
||||
/// 注册:成功后不自动登录,仅通过 [AuthState.alert] 提示前往登录。
|
||||
Future<bool> register(String username, String password) async {
|
||||
state = AsyncData(_current.copyWith(submitting: true, error: null));
|
||||
try {
|
||||
await _repository.register(username, password);
|
||||
state = AsyncData(
|
||||
_current.copyWith(
|
||||
submitting: false,
|
||||
error: null,
|
||||
alert: '注册成功,请登录',
|
||||
),
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = AsyncData(
|
||||
_current.copyWith(
|
||||
submitting: false,
|
||||
error: _describe(e, '注册失败'),
|
||||
),
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 退出登录:清空令牌并复位状态(保留用户名用于回显)。
|
||||
Future<void> logout() async {
|
||||
await _repository.clear();
|
||||
state = const AsyncData(AuthState());
|
||||
await _reset();
|
||||
}
|
||||
|
||||
/// 展示一次性提示(如从主页因登录失效跳转时携带的文案)。
|
||||
void showAlert(String message) {
|
||||
state = AsyncData(_current.copyWith(alert: message));
|
||||
}
|
||||
|
||||
/// 消费一次性提示,避免重建时重复展示。
|
||||
void consumeAlert() {
|
||||
if (_current.alert != null) {
|
||||
state = AsyncData(_current.copyWith(alert: null));
|
||||
}
|
||||
}
|
||||
|
||||
/// 清空错误信息。
|
||||
void clearError() {
|
||||
final current = state.valueOrNull;
|
||||
if (current != null && current.error != null) {
|
||||
state = AsyncData(current.copyWith(error: null));
|
||||
if (_current.error != null) {
|
||||
state = AsyncData(_current.copyWith(error: null));
|
||||
}
|
||||
}
|
||||
|
||||
/// 优先展示服务端返回的错误码,其余异常回落到通用提示。
|
||||
String _describe(Object e, String fallback) {
|
||||
if (e is ApiException) return e.code;
|
||||
return '$fallback:$e';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user