Files
ttstd_family_care/lib/features/auth/presentation/auth_controller.dart
tongtongstudio 41d815336a feat(home): 实现管理平板信息页与管理消息页
- 新增首页平板信息卡片(定位、截图、使用时长、设备操作)
- 将消息 Tab 改为管理 Tab,增加横版菜单与消息列表
- 新增相册、闹钟、联系人、应用子页面路由
- 统一主色为桐桐绿并优化调试日志输出
2026-08-17 16:44:45 +08:00

182 lines
5.1 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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';
import '../domain/auth_state.dart';
/// 认证控制器。
///
/// 承载登录 / 注册 / 发送验证码 / 倒计时等全部业务逻辑UI 仅触发动作并消费状态。
/// 使用 keepAlive避免登录流程中页面切换导致状态丢失。
class AuthController extends Notifier<AuthState> {
AuthController();
Timer? _countdownTimer;
@override
AuthState build() {
ref.keepAlive();
ref.onDispose(() => _countdownTimer?.cancel());
return const AuthState();
}
String get _deviceId => TokenStorage.deviceId;
/// 密码登录。
Future<bool> loginByPassword({
required String phone,
required String password,
}) async {
return _run(() => ref.read(authRepositoryProvider).loginByPassword(
phone: phone,
password: password,
deviceId: _deviceId,
));
}
/// 短信登录。
Future<bool> loginBySms({
required String phone,
required String code,
}) async {
return _run(() => ref.read(authRepositoryProvider).loginBySms(
phone: phone,
code: code,
deviceId: _deviceId,
));
}
/// 注册。
Future<bool> register({
required String phone,
required String code,
required String password,
}) async {
return _run(() => ref.read(authRepositoryProvider).register(
phone: phone,
code: code,
password: password,
deviceId: _deviceId,
));
}
/// 发送短信验证码并启动 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: trimmed,
scene: scene,
);
_startCountdown();
} catch (e) {
state = state.copyWith(
isSendingCode: false,
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) {
state = state.copyWith(clearAlert: true);
}
}
/// 登出。
void logout() {
_countdownTimer?.cancel();
TokenStorage.clear();
state = const AuthState();
}
Future<bool> _run(Future<LoginResult> Function() action) async {
state = state.copyWith(isLoading: true, clearAlert: true);
try {
await action();
state = state.copyWith(isLoading: false);
return true;
} catch (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();
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
final left = state.countdownSeconds - 1;
if (left <= 0) {
timer.cancel();
state = state.copyWith(countdownSeconds: 0);
} else {
state = state.copyWith(countdownSeconds: left);
}
});
}
}
/// 认证控制器 Provider会话级 keepAlive 在 build 内通过 ref.keepAlive 实现)。
final authControllerProvider =
NotifierProvider<AuthController, AuthState>(AuthController.new);