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:
TongTongStudio
2026-08-03 16:47:08 +08:00
parent bb488d216d
commit 79cffcb041
20 changed files with 642 additions and 451 deletions

View File

@@ -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';
}
}