import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:dio/dio.dart'; import '../../../../app/theme/app_colors.dart'; import '../../../../app/theme/app_typography.dart'; import '../../../../app/theme/app_spacing.dart'; import '../../../../shared/widgets/genex_button.dart'; import '../../../../shared/widgets/countdown_button.dart'; import '../../../../core/services/auth_service.dart' show SmsCodeType; import '../../../../app/i18n/app_localizations.dart'; import '../providers/auth_provider.dart'; /// A1. 登录页 - 密码登录 / 验证码快捷登录 class LoginPage extends ConsumerStatefulWidget { const LoginPage({super.key}); @override ConsumerState createState() => _LoginPageState(); } class _LoginPageState extends ConsumerState with SingleTickerProviderStateMixin { late TabController _tabController; final _phoneController = TextEditingController(); final _passwordController = TextEditingController(); final _smsPhoneController = TextEditingController(); final _codeController = TextEditingController(); bool _obscurePassword = true; bool _loading = false; String? _errorMessage; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); _tabController.addListener(() { if (mounted) setState(() => _errorMessage = null); }); } @override void dispose() { _tabController.dispose(); _phoneController.dispose(); _passwordController.dispose(); _smsPhoneController.dispose(); _codeController.dispose(); super.dispose(); } void _showError(String message) { setState(() => _errorMessage = message); } String _extractError(DioException e) { final data = e.response?.data; if (data is Map && data['message'] != null) { return data['message'].toString(); } return context.t('login.networkError'); } /* ── 密码登录 ── */ Future _handlePasswordLogin() async { final identifier = _phoneController.text.trim(); final password = _passwordController.text; if (identifier.isEmpty) { _showError(context.t('login.errorPhoneRequired')); return; } if (password.isEmpty || password.length < 8) { _showError(context.t('login.errorPasswordMin')); return; } setState(() { _loading = true; _errorMessage = null; }); try { await ref.read(authProvider.notifier).loginByPassword( identifier: identifier, password: password, ); if (mounted) Navigator.pushReplacementNamed(context, '/main'); } on DioException catch (e) { _showError(_extractError(e)); } catch (e) { _showError(e.toString()); } finally { if (mounted) setState(() => _loading = false); } } /* ── 发送 SMS 验证码 ── */ Future _handleSendLoginCode() async { final phone = _smsPhoneController.text.trim(); if (phone.isEmpty) { _showError(context.t('login.errorPhoneRequired')); throw Exception('phone required'); // 阻止倒计时启动 } setState(() => _errorMessage = null); try { await ref.read(authProvider.notifier).sendSmsCode(phone, SmsCodeType.login); } on DioException catch (e) { _showError(_extractError(e)); rethrow; } } /* ── 验证码登录 ── */ Future _handleCodeLogin() async { final phone = _smsPhoneController.text.trim(); final code = _codeController.text.trim(); if (phone.isEmpty) { _showError(context.t('login.errorPhoneRequired')); return; } if (code.length != 6) { _showError(context.t('login.errorCodeInvalid')); return; } setState(() { _loading = true; _errorMessage = null; }); try { await ref.read(authProvider.notifier).loginByPhone(phone: phone, smsCode: code); if (mounted) Navigator.pushReplacementNamed(context, '/main'); } on DioException catch (e) { _showError(_extractError(e)); } catch (e) { _showError(e.toString()); } finally { if (mounted) setState(() => _loading = false); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back_ios_new_rounded, size: 20), onPressed: () => Navigator.of(context).pop(), ), ), body: SafeArea( child: Padding( padding: AppSpacing.pagePadding, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 16), Text(context.t('login.title'), style: AppTypography.displayMedium), const SizedBox(height: 8), Text( context.t('login.subtitle'), style: AppTypography.bodyLarge.copyWith(color: AppColors.textSecondary), ), const SizedBox(height: 32), // Tab: Password / SMS Code Container( decoration: BoxDecoration( color: AppColors.gray50, borderRadius: AppSpacing.borderRadiusFull, ), child: TabBar( controller: _tabController, indicator: BoxDecoration( color: AppColors.surface, borderRadius: AppSpacing.borderRadiusFull, boxShadow: AppSpacing.shadowSm, ), indicatorSize: TabBarIndicatorSize.tab, dividerColor: Colors.transparent, labelColor: AppColors.textPrimary, unselectedLabelColor: AppColors.textTertiary, labelStyle: AppTypography.labelMedium, tabs: [ Tab(text: context.t('login.passwordTab')), Tab(text: context.t('login.codeTab')), ], ), ), const SizedBox(height: 24), // Error message if (_errorMessage != null) ...[ Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: AppColors.errorLight, borderRadius: AppSpacing.borderRadiusSm, ), child: Row( children: [ const Icon(Icons.error_outline, color: AppColors.error, size: 18), const SizedBox(width: 8), Expanded( child: Text( _errorMessage!, style: AppTypography.bodySmall.copyWith(color: AppColors.error), ), ), ], ), ), const SizedBox(height: 16), ], Expanded( child: TabBarView( controller: _tabController, children: [ _buildPasswordLogin(), _buildCodeLogin(), ], ), ), // Register link Center( child: GestureDetector( onTap: () => Navigator.pushNamed(context, '/register'), child: RichText( text: TextSpan( style: AppTypography.bodySmall.copyWith(color: AppColors.textSecondary), children: [ TextSpan(text: context.t('login.noAccount')), TextSpan( text: context.t('login.registerNow'), style: const TextStyle(color: AppColors.primary, fontWeight: FontWeight.w600), ), ], ), ), ), ), const SizedBox(height: 16), ], ), ), ), ); } Widget _buildPasswordLogin() { return SingleChildScrollView( child: Column( children: [ TextField( controller: _phoneController, keyboardType: TextInputType.phone, decoration: InputDecoration( hintText: context.t('login.phoneOrEmail'), prefixIcon: const Icon(Icons.person_outline_rounded, color: AppColors.textTertiary), ), ), const SizedBox(height: 16), TextField( controller: _passwordController, obscureText: _obscurePassword, decoration: InputDecoration( hintText: context.t('login.password'), prefixIcon: const Icon(Icons.lock_outline_rounded, color: AppColors.textTertiary), suffixIcon: IconButton( icon: Icon( _obscurePassword ? Icons.visibility_off_outlined : Icons.visibility_outlined, color: AppColors.textTertiary, size: 20, ), onPressed: () => setState(() => _obscurePassword = !_obscurePassword), ), ), ), const SizedBox(height: 12), Align( alignment: Alignment.centerRight, child: GestureDetector( onTap: () => Navigator.pushNamed(context, '/forgot-password'), child: Text( context.t('login.forgotPassword'), style: AppTypography.labelSmall.copyWith(color: AppColors.primary), ), ), ), const SizedBox(height: 24), GenexButton( label: context.t('login.submit'), isLoading: _loading, onPressed: _handlePasswordLogin, ), ], ), ); } Widget _buildCodeLogin() { return SingleChildScrollView( child: Column( children: [ TextField( controller: _smsPhoneController, keyboardType: TextInputType.phone, decoration: InputDecoration( hintText: context.t('login.phone'), prefixIcon: const Icon(Icons.phone_android_rounded, color: AppColors.textTertiary), ), ), const SizedBox(height: 16), Row( children: [ Expanded( child: TextField( controller: _codeController, keyboardType: TextInputType.number, maxLength: 6, decoration: InputDecoration( hintText: context.t('login.verifyCode'), counterText: '', prefixIcon: const Icon(Icons.shield_outlined, color: AppColors.textTertiary), ), ), ), const SizedBox(width: 12), CountdownButton( label: context.t('login.getCode'), onPressed: _handleSendLoginCode, ), ], ), const SizedBox(height: 24), GenexButton( label: context.t('login.submit'), isLoading: _loading, onPressed: _handleCodeLogin, ), ], ), ); } }