import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:google_fonts/google_fonts.dart'; import '../theme/app_colors.dart'; import '../widgets/gradient_button.dart'; class LoginPage extends StatefulWidget { const LoginPage({super.key}); @override State createState() => _LoginPageState(); } class _LoginPageState extends State with TickerProviderStateMixin { // State bool _agreed = false; bool _isLoading = false; bool _showSmsView = false; // SMS Login State final TextEditingController _phoneController = TextEditingController(); final TextEditingController _codeController = TextEditingController(); int _countdown = 0; Timer? _countdownTimer; bool _isSmsSubmitting = false; @override void dispose() { _phoneController.dispose(); _codeController.dispose(); _countdownTimer?.cancel(); super.dispose(); } // ========== Agreement Dialog ========== void _showAgreementDialog({required String action}) { showDialog( context: context, barrierColor: Colors.black.withOpacity(0.5), builder: (context) => _buildAgreementModal(action), ); } Widget _buildAgreementModal(String action) { return Dialog( backgroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), child: Container( width: 320, padding: const EdgeInsets.fromLTRB(24, 28, 24, 24), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Title Text( '服务协议', style: GoogleFonts.inter( fontSize: 18, fontWeight: FontWeight.w600, color: const Color(0xFF1F2937), ), ), const SizedBox(height: 12), // Content Text.rich( TextSpan( text: '请先阅读并同意', style: GoogleFonts.inter( fontSize: 14, color: const Color(0xFF6B7280), height: 1.6, ), children: [ TextSpan( text: '《用户协议》', style: GoogleFonts.inter(color: const Color(0xFF6366F1)), ), const TextSpan(text: '和'), TextSpan( text: '《隐私政策》', style: GoogleFonts.inter(color: const Color(0xFF6366F1)), ), const TextSpan(text: ',以便为您提供更好的服务。'), ], ), textAlign: TextAlign.center, ), const SizedBox(height: 24), // Buttons Row( children: [ // Cancel Expanded( child: GestureDetector( onTap: () => Navigator.pop(context), child: Container( height: 48, decoration: BoxDecoration( color: const Color(0xFFF3F4F6), borderRadius: BorderRadius.circular(24), ), alignment: Alignment.center, child: Text( '再想想', style: GoogleFonts.inter( fontSize: 15, fontWeight: FontWeight.w500, color: const Color(0xFF6B7280), ), ), ), ), ), const SizedBox(width: 12), // Confirm Expanded( child: GestureDetector( onTap: () { setState(() => _agreed = true); Navigator.pop(context); if (action == 'oneclick') { _doOneClickLogin(); } else if (action == 'sms') { setState(() => _showSmsView = true); } }, child: Container( height: 48, decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF6366F1), Color(0xFF8B5CF6)], ), borderRadius: BorderRadius.circular(24), ), alignment: Alignment.center, child: Text( '同意并继续', style: GoogleFonts.inter( fontSize: 15, fontWeight: FontWeight.w500, color: Colors.white, ), ), ), ), ), ], ), ], ), ), ); } // ========== One-Click Login ========== void _handleOneClickLogin() { if (!_agreed) { _showAgreementDialog(action: 'oneclick'); return; } _doOneClickLogin(); } void _doOneClickLogin() { setState(() => _isLoading = true); _showToast('正在获取本机号码...'); Future.delayed(const Duration(milliseconds: 1500), () { if (mounted) { _showToast('登录成功'); Future.delayed(const Duration(seconds: 1), () { if (mounted) { setState(() => _isLoading = false); Navigator.of(context).pushReplacementNamed('/home'); } }); } }); } // ========== SMS Login ========== void _handleSmsLinkTap() { if (!_agreed) { _showAgreementDialog(action: 'sms'); return; } setState(() => _showSmsView = true); } bool _isValidPhone(String phone) { return RegExp(r'^1[3-9]\d{9}$').hasMatch(phone); } bool get _canSubmitSms { return _isValidPhone(_phoneController.text) && _codeController.text.length == 6; } void _sendCode() { if (!_isValidPhone(_phoneController.text)) { _showToast('请输入正确的手机号'); return; } setState(() => _countdown = 60); _showToast('验证码已发送'); _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { if (_countdown <= 1) { timer.cancel(); if (mounted) setState(() => _countdown = 0); } else { if (mounted) setState(() => _countdown--); } }); } void _submitSmsLogin() { if (!_canSubmitSms) return; setState(() => _isSmsSubmitting = true); Future.delayed(const Duration(milliseconds: 1500), () { if (mounted) { _showToast('登录成功'); Future.delayed(const Duration(seconds: 1), () { if (mounted) { setState(() => _isSmsSubmitting = false); Navigator.of(context).pushReplacementNamed('/home'); } }); } }); } // ========== Toast ========== void _showToast(String message) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), duration: const Duration(seconds: 2), behavior: SnackBarBehavior.floating, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), margin: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), ), ); } // ========== Build ========== @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, resizeToAvoidBottomInset: true, body: Stack( children: [ // Background _buildGradientBackground(), // Main Login View _buildMainLoginView(), // SMS View (overlay) if (_showSmsView) _buildSmsView(), ], ), ); } // ========== Gradient Background ========== Widget _buildGradientBackground() { final size = MediaQuery.of(context).size; return Positioned.fill( child: Stack( children: [ // Layer 1 - Pink (bottom-left) Positioned( bottom: -size.width * 0.5, left: -size.width * 0.5, width: size.width * 2, height: size.width * 2, child: Container( decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient( colors: [ const Color(0xFFFFC8DC).withOpacity(0.6), Colors.transparent, ], radius: 0.5, ), ), ), ), // Layer 2 - Cyan (top-right) Positioned( top: -size.width * 0.5, right: -size.width * 0.5, width: size.width * 2, height: size.width * 2, child: Container( decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient( colors: [ const Color(0xFFB4F0F0).withOpacity(0.5), Colors.transparent, ], radius: 0.5, ), ), ), ), // Layer 3 - Lavender (center) Positioned( top: size.height * 0.2, left: size.width * 0.1, width: size.width * 1.2, height: size.width * 1.2, child: Container( decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient( colors: [ const Color(0xFFE6D2FA).withOpacity(0.45), Colors.transparent, ], radius: 0.5, ), ), ), ), ], ), ); } // ========== Main Login View ========== Widget _buildMainLoginView() { final bottomPadding = MediaQuery.of(context).padding.bottom; final topPadding = MediaQuery.of(context).padding.top; return SafeArea( child: Column( children: [ // Logo - padding-top: calc(env(safe-area-inset-top) + 20px) Padding( padding: const EdgeInsets.only(top: 20), child: Text( 'Airhub', style: GoogleFonts.pressStart2p( fontSize: 26, // Exact match color: const Color(0xFF4B2E83), shadows: [ Shadow( offset: const Offset(0, 2), blurRadius: 10, color: const Color(0xFF8B5CF6).withOpacity(0.3), ), Shadow( offset: const Offset(0, 0), blurRadius: 40, color: const Color(0xFF8B5CF6).withOpacity(0.15), ), ], ), ), ), // Mascot - flex: 1, centered Expanded(child: Center(child: _FloatingMascot())), // Bottom Form Padding( padding: EdgeInsets.fromLTRB(32, 0, 32, bottomPadding + 40), child: Column( children: [ // Primary Button - height: 56px, border-radius: 28px GradientButton( text: '本机号码一键登录', onPressed: _handleOneClickLogin, isLoading: _isLoading, height: 56, ), // SMS Link - margin-top: 20px, font-size: 14px const SizedBox(height: 20), GestureDetector( onTap: _handleSmsLinkTap, child: Text( '使用验证码登录', style: GoogleFonts.inter( fontSize: 14, color: const Color(0xFF4B2E83).withOpacity(0.7), ), ), ), // Agreement - margin-top: 28px const SizedBox(height: 28), _buildAgreementCheckbox(), ], ), ), ], ), ); } // ========== Agreement Checkbox ========== Widget _buildAgreementCheckbox() { return GestureDetector( onTap: () => setState(() => _agreed = !_agreed), child: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ // Checkbox - 18x18, border-radius: 5px Container( width: 18, height: 18, margin: const EdgeInsets.only(top: 1), // Fine-tune alignment decoration: BoxDecoration( gradient: _agreed ? const LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFF6366F1), Color(0xFF8B5CF6)], ) : null, color: _agreed ? null : const Color(0x99FFFFFF), borderRadius: BorderRadius.circular(5), border: Border.all( color: _agreed ? Colors.transparent : const Color(0xFF4B2E83).withOpacity(0.3), width: 1.5, ), ), child: _agreed ? const Center( child: Text( '✓', style: TextStyle( color: Colors.white, fontSize: 11, fontWeight: FontWeight.w700, ), ), ) : null, ), const SizedBox(width: 10), // gap: 10px // Text - font-size: 12px, line-height: 1.6 Flexible( child: Text.rich( TextSpan( text: '我已阅读并同意', style: GoogleFonts.inter( fontSize: 12, color: const Color(0xFF4B2E83).withOpacity(0.6), height: 1.6, ), children: [ TextSpan( text: '《用户协议》', style: GoogleFonts.inter(color: const Color(0xFF6366F1)), ), const TextSpan(text: '和'), TextSpan( text: '《隐私政策》', style: GoogleFonts.inter(color: const Color(0xFF6366F1)), ), ], ), ), ), ], ), ); } // ========== SMS View ========== Widget _buildSmsView() { final topPadding = MediaQuery.of(context).padding.top; final bottomPadding = MediaQuery.of(context).padding.bottom; return Positioned.fill( child: Container( color: Colors.white, child: Stack( children: [ // Background _buildGradientBackground(), // Content Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header - padding-top: 60px (fixed) Padding( padding: const EdgeInsets.fromLTRB(24, 60, 24, 16), child: _buildBackButton(), ), // Body Expanded( child: SingleChildScrollView( padding: EdgeInsets.fromLTRB( 32, 60, 32, bottomPadding + 40, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Heading - font-size: 32px, font-weight: 700 Text( '欢迎使用 Airhub', style: GoogleFonts.inter( fontSize: 32, fontWeight: FontWeight.w700, color: const Color(0xFF4B2E83), letterSpacing: -0.5, ), ), const SizedBox(height: 12), // Subheading - font-size: 15px Text( '请输入您的手机号验证登录', style: GoogleFonts.inter( fontSize: 15, fontWeight: FontWeight.w400, color: const Color(0xFF4B2E83).withOpacity(0.6), ), ), const SizedBox(height: 48), // Phone Input _buildPhoneInput(), const SizedBox(height: 24), // Code Input _buildCodeInput(), const SizedBox(height: 48), // Submit Button - height: 60px, border-radius: 30px _buildSmsSubmitButton(), ], ), ), ), ], ), ], ), ), ); } Widget _buildBackButton() { return GestureDetector( onTap: () => setState(() => _showSmsView = false), child: Container( width: 40, height: 40, decoration: BoxDecoration( shape: BoxShape.circle, color: const Color(0x66FFFFFF), border: Border.all(color: const Color(0x99FFFFFF)), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.03), offset: const Offset(0, 4), blurRadius: 12, ), ], ), child: const Center( child: Icon(Icons.arrow_back, size: 22, color: Color(0xFF4B2E83)), ), ), ); } Widget _buildPhoneInput() { return Container( height: 64, decoration: BoxDecoration( color: const Color(0x8CFFFFFF), border: Border.all(color: const Color(0xCCFFFFFF)), borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: const Color(0xFF8B5CF6).withOpacity(0.03), offset: const Offset(0, 2), blurRadius: 10, ), ], ), padding: const EdgeInsets.symmetric(horizontal: 24), child: Row( children: [ // Prefix Container( padding: const EdgeInsets.only(right: 16), margin: const EdgeInsets.only(right: 16), decoration: const BoxDecoration( border: Border( right: BorderSide(color: Color(0x1A4B2E83), width: 1), ), ), child: Text( '+86', style: GoogleFonts.inter( fontSize: 16, fontWeight: FontWeight.w600, color: const Color(0xFF4B2E83), ), ), ), // Input Expanded( child: TextField( controller: _phoneController, keyboardType: TextInputType.phone, maxLength: 11, style: GoogleFonts.inter( fontSize: 17, fontWeight: FontWeight.w500, color: const Color(0xFF1F2937), ), decoration: InputDecoration( border: InputBorder.none, hintText: '请输入手机号', hintStyle: GoogleFonts.inter( fontSize: 17, fontWeight: FontWeight.w400, color: const Color(0xFF4B2E83).withOpacity(0.35), ), counterText: '', ), cursorColor: const Color(0xFF8B5CF6), onChanged: (_) => setState(() {}), ), ), ], ), ); } Widget _buildCodeInput() { return Container( height: 64, decoration: BoxDecoration( color: const Color(0x8CFFFFFF), border: Border.all(color: const Color(0xCCFFFFFF)), borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: const Color(0xFF8B5CF6).withOpacity(0.03), offset: const Offset(0, 2), blurRadius: 10, ), ], ), padding: const EdgeInsets.symmetric(horizontal: 24), child: Row( children: [ // Input Expanded( child: TextField( controller: _codeController, keyboardType: TextInputType.number, maxLength: 6, style: GoogleFonts.inter( fontSize: 17, fontWeight: FontWeight.w500, color: const Color(0xFF1F2937), ), decoration: InputDecoration( border: InputBorder.none, hintText: '输入验证码', hintStyle: GoogleFonts.inter( fontSize: 17, fontWeight: FontWeight.w400, color: const Color(0xFF4B2E83).withOpacity(0.35), ), counterText: '', ), cursorColor: const Color(0xFF8B5CF6), onChanged: (_) => setState(() {}), ), ), // Send Button Container( padding: const EdgeInsets.only(left: 14), margin: const EdgeInsets.only(left: 10), decoration: const BoxDecoration( border: Border( left: BorderSide(color: Color(0x1A4B2E83), width: 1), ), ), child: GestureDetector( onTap: _countdown > 0 ? null : _sendCode, child: Text( _countdown > 0 ? '${_countdown}s' : '获取验证码', style: GoogleFonts.inter( fontSize: 14, fontWeight: FontWeight.w600, color: _countdown > 0 ? const Color(0xFF9CA3AF) : const Color(0xFF6366F1), ), ), ), ), ], ), ); } Widget _buildSmsSubmitButton() { final bool enabled = _canSubmitSms && !_isSmsSubmitting; return GestureDetector( onTap: enabled ? _submitSmsLogin : null, child: AnimatedOpacity( duration: const Duration(milliseconds: 200), opacity: enabled ? 1.0 : 0.6, child: Container( width: double.infinity, height: 60, decoration: BoxDecoration( gradient: AppColors.btnPrimaryGradient, borderRadius: BorderRadius.circular(30), boxShadow: enabled ? [ BoxShadow( color: const Color(0xFF6366F1).withOpacity(0.3), offset: const Offset(0, 10), blurRadius: 30, ), ] : null, ), alignment: Alignment.center, child: _isSmsSubmitting ? const SizedBox( width: 24, height: 24, child: CircularProgressIndicator( color: Colors.white, strokeWidth: 2.5, ), ) : Text( '立即登录', style: GoogleFonts.inter( fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white, ), ), ), ), ); } } // ========== Floating Mascot Widget ========== class _FloatingMascot extends StatefulWidget { @override State<_FloatingMascot> createState() => _FloatingMascotState(); } class _FloatingMascotState extends State<_FloatingMascot> with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _animation; @override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(seconds: 5), vsync: this, )..repeat(reverse: true); _animation = Tween( begin: 0, end: -15, ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _animation, builder: (context, child) { return Transform.translate( offset: Offset(0, _animation.value), child: child, ); }, child: Container( width: 220, height: 220, decoration: BoxDecoration( boxShadow: [ BoxShadow( color: const Color(0xFF8B5CF6).withOpacity(0.25), offset: const Offset(0, 20), blurRadius: 40, ), ], ), child: Image.asset( 'assets/www/mascot.png', width: 220, height: 220, fit: BoxFit.contain, ), ), ); } }