import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import '../../../../core/theme/app_colors.dart'; import '../../data/providers/auth_provider.dart'; import 'package:it0_app/l10n/app_localizations.dart'; enum _LoginMode { password, otp } class LoginPage extends ConsumerStatefulWidget { const LoginPage({super.key}); @override ConsumerState createState() => _LoginPageState(); } class _LoginPageState extends ConsumerState { final _formKey = GlobalKey(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); final _phoneController = TextEditingController(); final _otpController = TextEditingController(); _LoginMode _mode = _LoginMode.password; int _smsCooldown = 0; bool _smsSending = false; Timer? _cooldownTimer; @override void dispose() { _emailController.dispose(); _passwordController.dispose(); _phoneController.dispose(); _otpController.dispose(); _cooldownTimer?.cancel(); super.dispose(); } Future _sendSmsCode() async { final phone = _phoneController.text.trim(); if (phone.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(AppLocalizations.of(context).enterPhoneFirstError)), ); return; } setState(() => _smsSending = true); try { await ref.read(authStateProvider.notifier).sendSmsCode(phone); setState(() => _smsCooldown = 60); _cooldownTimer = Timer.periodic(const Duration(seconds: 1), (t) { setState(() { if (_smsCooldown <= 1) { _smsCooldown = 0; t.cancel(); } else { _smsCooldown--; } }); }); } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(e.toString())), ); } } finally { if (mounted) setState(() => _smsSending = false); } } @override Widget build(BuildContext context) { final authState = ref.watch(authStateProvider); return Scaffold( backgroundColor: AppColors.background, body: Center( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 48), child: Form( key: _formKey, child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 360), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ SvgPicture.asset('assets/icons/logo.svg', width: 96, height: 96), const SizedBox(height: 12), Text( AppLocalizations.of(context).appTitle, style: const TextStyle( color: AppColors.textPrimary, fontSize: 28, fontWeight: FontWeight.bold, letterSpacing: 2, ), ), const SizedBox(height: 4), Text( AppLocalizations.of(context).appSubtitle, style: const TextStyle(color: AppColors.textSecondary, fontSize: 14), ), const SizedBox(height: 32), // Login mode toggle Container( decoration: BoxDecoration( color: AppColors.surface, borderRadius: BorderRadius.circular(8), border: Border.all(color: AppColors.surfaceLight), ), child: Row( children: [ Expanded(child: _modeButton(AppLocalizations.of(context).loginPasswordTab, _LoginMode.password)), Expanded(child: _modeButton(AppLocalizations.of(context).loginOtpTab, _LoginMode.otp)), ], ), ), const SizedBox(height: 24), if (_mode == _LoginMode.password) ...[ TextFormField( controller: _emailController, keyboardType: TextInputType.text, decoration: const InputDecoration( labelText: '邮箱 / 手机号', hintText: '请输入邮箱或手机号', prefixIcon: Icon(Icons.person_outline), ), validator: (v) { if (v == null || v.isEmpty) return '请输入邮箱或手机号'; return null; }, ), const SizedBox(height: 16), TextFormField( controller: _passwordController, obscureText: true, decoration: InputDecoration( labelText: AppLocalizations.of(context).passwordLabel, prefixIcon: const Icon(Icons.lock_outline), ), validator: (v) => (v == null || v.isEmpty) ? '请输入密码' : null, onFieldSubmitted: (_) => _handlePasswordLogin(), ), ] else ...[ TextFormField( controller: _phoneController, keyboardType: TextInputType.phone, decoration: InputDecoration( labelText: AppLocalizations.of(context).phoneLabel, hintText: AppLocalizations.of(context).phoneHint, prefixIcon: const Icon(Icons.phone_outlined), ), validator: (v) => (v == null || v.isEmpty) ? '请输入手机号' : null, ), const SizedBox(height: 16), Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: TextFormField( controller: _otpController, keyboardType: TextInputType.number, maxLength: 6, decoration: InputDecoration( labelText: AppLocalizations.of(context).otpLabel, hintText: AppLocalizations.of(context).otpHint, prefixIcon: const Icon(Icons.sms_outlined), counterText: '', ), validator: (v) => (v == null || v.isEmpty) ? '请输入验证码' : null, onFieldSubmitted: (_) => _handleOtpLogin(), ), ), const SizedBox(width: 8), SizedBox( height: 56, child: OutlinedButton( onPressed: (_smsSending || _smsCooldown > 0) ? null : _sendSmsCode, child: Text( _smsSending ? AppLocalizations.of(context).sendingLabel : _smsCooldown > 0 ? '${_smsCooldown}s' : AppLocalizations.of(context).getOtpButton, style: const TextStyle(fontSize: 13), ), ), ), ], ), ], if (authState.error != null) ...[ const SizedBox(height: 16), Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: AppColors.error.withAlpha(25), borderRadius: BorderRadius.circular(8), ), child: Row( children: [ const Icon(Icons.error_outline, color: AppColors.error, size: 18), const SizedBox(width: 8), Expanded( child: Text( authState.error!, style: const TextStyle(color: AppColors.error, fontSize: 13), ), ), ], ), ), ], const SizedBox(height: 24), SizedBox( width: double.infinity, height: 48, child: FilledButton( onPressed: authState.isLoading ? null : (_mode == _LoginMode.password ? _handlePasswordLogin : _handleOtpLogin), child: authState.isLoading ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white), ) : Text(AppLocalizations.of(context).loginButton, style: const TextStyle(fontSize: 16)), ), ), const SizedBox(height: 32), Text( AppLocalizations.of(context).accountCreationNote, style: const TextStyle(color: AppColors.textMuted, fontSize: 12), textAlign: TextAlign.center, ), ], ), ), ), ), ), ); } Widget _modeButton(String label, _LoginMode mode) { final selected = _mode == mode; return GestureDetector( onTap: () => setState(() => _mode = mode), child: Container( padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( color: selected ? AppColors.primary : Colors.transparent, borderRadius: BorderRadius.circular(7), ), child: Text( label, textAlign: TextAlign.center, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: selected ? Colors.white : AppColors.textSecondary, ), ), ), ); } Future _handlePasswordLogin() async { if (!_formKey.currentState!.validate()) return; final success = await ref.read(authStateProvider.notifier).login( _emailController.text.trim(), _passwordController.text, ); if (success && mounted) context.go('/home'); } Future _handleOtpLogin() async { if (!_formKey.currentState!.validate()) return; final success = await ref.read(authStateProvider.notifier).loginWithOtp( _phoneController.text.trim(), _otpController.text.trim(), ); if (success && mounted) context.go('/home'); } }