import 'dart:async'; import 'package:flutter/material.dart'; import 'package:patbond_flutter/core/network/api_exception.dart'; import 'package:patbond_flutter/core/theme/app_theme.dart'; import 'package:patbond_flutter/core/widgets/brand_mark.dart'; import 'package:patbond_flutter/features/auth/auth_repository.dart'; import 'package:patbond_flutter/features/auth/session_manager.dart'; enum _SplashState { checking, failed } /// 启动 Splash(12 号组装稿 §7):静默恢复会话。 /// /// 流程约束:最短停留 500ms;刷新超时 5s;spinner 等待超过 300ms 才出现; /// 网络失败不清除本地 refresh token(只有服务端明确 401/40102 才清)。 class SplashPage extends StatefulWidget { const SplashPage({ required this.authRepository, required this.sessionManager, super.key, }); final AuthRepository authRepository; final SessionManager sessionManager; @override State createState() => _SplashPageState(); } class _SplashPageState extends State { _SplashState _state = _SplashState.checking; bool _showSpinner = false; Timer? _spinnerTimer; @override void initState() { super.initState(); _restore(); } @override void dispose() { _spinnerTimer?.cancel(); super.dispose(); } Future _restore() async { _spinnerTimer?.cancel(); _spinnerTimer = Timer(const Duration(milliseconds: 300), () { if (mounted) setState(() => _showSpinner = true); }); final minStay = Future.delayed(const Duration(milliseconds: 500)); try { final result = await widget.authRepository.restoreSession().timeout( const Duration(seconds: 5), ); await minStay; if (!mounted) return; switch (result) { case SessionRestoreResult.authenticated: widget.sessionManager.markAuthenticated(); case SessionRestoreResult.noSession: widget.sessionManager.markUnauthenticated(); } } on SessionExpiredException { // refresh 已被服务端判失效,会话已清、状态机已切换,无需处理。 } catch (_) { // 网络失败 / 超时:token 保留,转错误态供重试。 await minStay; if (!mounted) return; setState(() { _state = _SplashState.failed; _showSpinner = false; }); } } void _retryRefresh() { setState(() { _state = _SplashState.checking; _showSpinner = false; }); _restore(); } Future _clearCredentialsAndGoLogin() async { // 逃生通道:放弃本地凭证,改用账号登录。 await widget.sessionManager.clearSession(); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.canvas, body: Center( child: _state == _SplashState.checking ? Column( mainAxisSize: MainAxisSize.min, children: [ const BrandMark(), const SizedBox(height: 32), // 占位保高度,spinner 出现时不跳动。 SizedBox.square( dimension: 20, child: _showSpinner ? const CircularProgressIndicator( strokeWidth: 2.5, color: AppColors.primary, ) : null, ), ], ) : Column( mainAxisSize: MainAxisSize.min, children: [ const BrandMark(), const SizedBox(height: 24), const Text( '网络连接失败,无法恢复登录', style: TextStyle(color: AppColors.ink, fontSize: 14), ), const SizedBox(height: 16), OutlinedButton( style: OutlinedButton.styleFrom( minimumSize: const Size(120, 44), foregroundColor: AppColors.primaryStrong, side: const BorderSide(color: AppColors.border), ), onPressed: _retryRefresh, child: const Text('重试'), ), const SizedBox(height: 8), TextButton( style: TextButton.styleFrom( foregroundColor: AppColors.primaryStrong, minimumSize: const Size(44, 44), ), onPressed: _clearCredentialsAndGoLogin, child: const Text('改用账号登录'), ), ], ), ), ); } }