feat(auth): 对接后端 open 模块认证接口并完善登录页
- 更新接口路径、字段和短信验证码场景,新增忘记密码/重置密码流程 - 增加 401 自动刷新 token 与统一错误处理 - 重构登录页 UI,支持退出确认、协议与法律文档入口
This commit is contained in:
@@ -2,6 +2,8 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../../../core/network/api_exception.dart';
|
||||
import '../../../core/network/error_message.dart';
|
||||
import '../../../core/storage/token_storage.dart';
|
||||
import '../data/auth_providers.dart';
|
||||
import '../domain/auth_models.dart';
|
||||
@@ -64,26 +66,64 @@ class AuthController extends Notifier<AuthState> {
|
||||
}
|
||||
|
||||
/// 发送短信验证码并启动 60s 倒计时。
|
||||
///
|
||||
/// 发送前先做本地校验:手机号为空或格式不正确时直接提示,不发起请求、
|
||||
/// 不启动倒计时,避免出现「空号也能点但弹报错」的体验问题。
|
||||
Future<void> sendSmsCode({
|
||||
required String phone,
|
||||
required SmsScene scene,
|
||||
}) async {
|
||||
if (state.isSendingCode || state.countdownSeconds > 0) return;
|
||||
final trimmed = phone.trim();
|
||||
final phoneError = _validatePhone(trimmed);
|
||||
if (phoneError != null) {
|
||||
state = state.copyWith(alert: phoneError);
|
||||
return;
|
||||
}
|
||||
state = state.copyWith(isSendingCode: true, clearAlert: true);
|
||||
try {
|
||||
await ref.read(authRepositoryProvider).sendSmsCode(
|
||||
phone: phone,
|
||||
phone: trimmed,
|
||||
scene: scene,
|
||||
);
|
||||
_startCountdown();
|
||||
} catch (e) {
|
||||
state = state.copyWith(
|
||||
isSendingCode: false,
|
||||
alert: _messageOf(e),
|
||||
alert: userMessageOf(e),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 校验手机号:空或不符合 11 位中国大陆手机号规则时返回提示文案。
|
||||
String? _validatePhone(String phone) {
|
||||
if (phone.isEmpty) return '请输入手机号';
|
||||
// 1 开头的 11 位手机号。
|
||||
if (!RegExp(r'^1\d{10}$').hasMatch(phone)) return '手机号格式不正确';
|
||||
return null;
|
||||
}
|
||||
|
||||
/// 重置密码(忘记密码流程)。
|
||||
Future<bool> resetPassword({
|
||||
required String phone,
|
||||
required String code,
|
||||
required String password,
|
||||
}) async {
|
||||
state = state.copyWith(isLoading: true, clearAlert: true);
|
||||
try {
|
||||
await ref.read(authRepositoryProvider).resetPassword(
|
||||
phone: phone,
|
||||
code: code,
|
||||
password: password,
|
||||
);
|
||||
state = state.copyWith(isLoading: false);
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = state.copyWith(isLoading: false, alert: userMessageOf(e));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 消费一次性提示。
|
||||
void consumeAlert() {
|
||||
if (state.alert != null) {
|
||||
@@ -105,11 +145,21 @@ class AuthController extends Notifier<AuthState> {
|
||||
state = state.copyWith(isLoading: false);
|
||||
return true;
|
||||
} catch (e) {
|
||||
state = state.copyWith(isLoading: false, alert: _messageOf(e));
|
||||
state = state.copyWith(isLoading: false, alert: userMessageOf(e));
|
||||
// 鉴权失效:本地令牌可能已作废,主动登出清理。
|
||||
if (_isUnauthorized(e)) {
|
||||
logout();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 判断异常是否为鉴权失效(401)。
|
||||
bool _isUnauthorized(Object e) {
|
||||
if (e is ApiException) return e.code == '401';
|
||||
return false;
|
||||
}
|
||||
|
||||
void _startCountdown() {
|
||||
state = state.copyWith(isSendingCode: false, countdownSeconds: 60);
|
||||
_countdownTimer?.cancel();
|
||||
@@ -125,10 +175,7 @@ class AuthController extends Notifier<AuthState> {
|
||||
}
|
||||
|
||||
String _messageOf(Object e) {
|
||||
if (e is Exception) {
|
||||
return e.toString().replaceFirst('Exception: ', '');
|
||||
}
|
||||
return '操作失败,请稍后重试';
|
||||
return userMessageOf(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user