import 'dart:ui' show ImageFilter; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../api/auth_api.dart'; import '../../core/network/api_exception.dart'; import '../../core/storage/local_storage.dart'; import '../../providers/auth_provider.dart'; import '../../utils/crypto.dart'; import '../../utils/toast.dart'; import '../shell/main_shell.dart'; /// 登录/注册页(复刻 uniApp pages/login/login.vue) class LoginPage extends ConsumerStatefulWidget { const LoginPage({super.key}); @override ConsumerState createState() => _LoginPageState(); } class _LoginPageState extends ConsumerState with SingleTickerProviderStateMixin { bool _isRegister = false; bool _showPassword = false; bool _loading = false; bool _agreeProtocol = false; bool _rememberMe = false; final _usernameController = TextEditingController(); final _passwordController = TextEditingController(); final _nicknameController = TextEditingController(); final _formKey = GlobalKey(); late final AnimationController _floatController = AnimationController( vsync: this, duration: const Duration(seconds: 6), )..repeat(reverse: true); @override void initState() { super.initState(); final saved = LocalStorage.instance.getRememberedAccount(); if (saved != null) { _usernameController.text = saved.username; _passwordController.text = decryptPassword(saved.password); _rememberMe = true; } } @override void dispose() { _floatController.dispose(); _usernameController.dispose(); _passwordController.dispose(); _nicknameController.dispose(); super.dispose(); } Future _submit() async { if (_loading) return; final username = _usernameController.text.trim(); final password = _passwordController.text; if (username.isEmpty || password.isEmpty) { showToast(context, '请输入用户名和密码', error: true); return; } if (_isRegister) { if (password.length < 6) { showToast(context, '密码至少6位', error: true); return; } if (!_agreeProtocol) { showToast(context, '请先阅读并同意用户协议'); return; } } setState(() => _loading = true); try { final api = ref.read(authApiProvider); final response = _isRegister ? await api.register( username, password, _nicknameController.text.trim().isEmpty ? username : _nicknameController.text.trim(), ) : await api.login(username, password); // 记住密码 if (!_isRegister && _rememberMe) { await LocalStorage.instance.saveRememberedPassword( username, encryptPassword(password), ); } else if (!_isRegister && !_rememberMe) { await LocalStorage.instance.clearRememberedPassword(); } await ref.read(authProvider.notifier).loginSuccess(response); if (!mounted) return; showToast(context, _isRegister ? '🎉 注册成功' : '🚀 登录成功'); Navigator.of(context).pushAndRemoveUntil( MaterialPageRoute(builder: (_) => const MainShell()), (route) => false, ); } on ApiException catch (e) { if (mounted) showToast(context, e.message, error: true); } catch (_) { if (mounted) showToast(context, '操作失败,请重试', error: true); } finally { if (mounted) setState(() => _loading = false); } } @override Widget build(BuildContext context) { return Scaffold( body: Container( width: double.infinity, height: double.infinity, decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFFFFE5CC), Color(0xFFFFD4A8), Color(0xFFFFC08A)], ), ), child: Stack( children: [ ..._buildBackgroundBalls(), SafeArea( child: Center( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 20), child: Column( mainAxisSize: MainAxisSize.min, children: [ _buildHeader(), const SizedBox(height: 32), _buildFormCard(), ], ), ), ), ), ], ), ), ); } // ---- 背景装饰圆球 ---- List _buildBackgroundBalls() { final t = _floatController.value; Offset offset(double phase) => Offset(20 * _wave(t + phase), 30 * _wave(t + phase + 0.25)); Widget ball({ required double size, required List colors, required Offset pos, required double phase, }) { return Positioned( left: pos.dx, top: pos.dy, child: Opacity( opacity: 0.3, child: AnimatedBuilder( animation: _floatController, builder: (context, _) => Transform.translate( offset: offset(phase), child: Container( width: size, height: size, decoration: BoxDecoration( shape: BoxShape.circle, gradient: LinearGradient(colors: colors), ), ), ), ), ), ); } return [ ball( size: 280, colors: const [Color(0xFFC8956E), Color(0xFFFFB6C1)], pos: const Offset(-70, -70), phase: 0, ), ball( size: 200, colors: const [Color(0xFFF5D59E), Color(0xFFFFD700)], pos: const Offset(-40, 180), phase: 0.25, ), ball( size: 240, colors: const [Color(0xFFFFB6C1), Color(0xFFC8956E)], pos: const Offset(40, -180), phase: 0.5, ), ball( size: 170, colors: const [Color(0xFFFFD700), Color(0xFFF5D59E)], pos: const Offset(240, 420), phase: 0.75, ), ]; } double _wave(double x) { final v = (x % 1) * 2; return v <= 1 ? v : 2 - v; } // ---- Logo + 标题 ---- Widget _buildHeader() { return Column( children: [ Container( width: 120, height: 120, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white, boxShadow: [ BoxShadow( color: const Color(0xFF5D4037).withValues(alpha: 0.3), blurRadius: 20, offset: const Offset(0, 8), ), ], ), padding: const EdgeInsets.all(10), child: Image.asset('assets/images/logo.png', fit: BoxFit.contain), ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _decoLine(), const SizedBox(width: 10), const Text( '强宝爱记账', style: TextStyle( fontSize: 28, fontWeight: FontWeight.bold, color: Color(0xFF5D4037), letterSpacing: 2, shadows: [ Shadow( color: Color(0x66FFD700), blurRadius: 10, offset: Offset(0, 3), ), ], ), ), const SizedBox(width: 10), _decoLine(), ], ), const SizedBox(height: 6), const Text( '智能记账,轻松管理', style: TextStyle(fontSize: 14, color: Color(0xFF8D6E63)), ), ], ); } Widget _decoLine() => Container( width: 40, height: 4, decoration: BoxDecoration( borderRadius: BorderRadius.circular(4), gradient: const LinearGradient( colors: [Color(0xFFC8956E), Color(0xFFFFD700)], ), ), ); // ---- 表单卡片(毛玻璃) ---- Widget _buildFormCard() { return ClipRRect( borderRadius: BorderRadius.circular(24), child: BackdropFilter( filter: _glassFilter, child: Container( width: double.infinity, padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.65), borderRadius: BorderRadius.circular(24), border: Border.all(color: const Color(0xFFC8956E), width: 1.5), ), child: Form( key: _formKey, child: Column( children: [ _buildSegmentTabs(), const SizedBox(height: 20), _inputField( icon: '👤', controller: _usernameController, hint: '请输入用户名', maxLength: 20, ), const SizedBox(height: 14), _inputField( icon: '🔒', controller: _passwordController, hint: '请输入密码', maxLength: 20, obscure: !_showPassword, suffix: IconButton( icon: Text( _showPassword ? '👁️' : '👁️‍🗨️', style: const TextStyle(fontSize: 16), ), onPressed: () => setState(() => _showPassword = !_showPassword), ), ), if (_isRegister) ...[ const SizedBox(height: 14), _inputField( icon: '✨', controller: _nicknameController, hint: '请输入昵称(可选)', maxLength: 20, ), ], if (!_isRegister) ...[ const SizedBox(height: 8), Row( children: [ SizedBox( width: 22, height: 22, child: Checkbox( value: _rememberMe, activeColor: const Color(0xFFC8956E), onChanged: (v) => setState(() => _rememberMe = v ?? false), ), ), const SizedBox(width: 6), const Text( '记住密码', style: TextStyle( fontSize: 13, color: Color(0xFF8D6E63), ), ), ], ), ], if (_isRegister) ...[ const SizedBox(height: 8), Row( children: [ SizedBox( width: 22, height: 22, child: Checkbox( value: _agreeProtocol, activeColor: const Color(0xFFC8956E), onChanged: (v) => setState(() => _agreeProtocol = v ?? false), ), ), const Expanded( child: Text( '我已阅读并同意用户协议', style: TextStyle( fontSize: 13, color: Color(0xFF8D6E63), ), ), ), ], ), ], const SizedBox(height: 24), SizedBox( width: double.infinity, height: 48, child: FilledButton( style: FilledButton.styleFrom( backgroundColor: const Color(0xFFC8956E), foregroundColor: Colors.white, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(24), ), ), onPressed: _loading ? null : _submit, child: _loading ? const SizedBox( width: 22, height: 22, child: CircularProgressIndicator( strokeWidth: 2.5, color: Colors.white, ), ) : Text( _isRegister ? '注 册' : '登 录', style: const TextStyle( fontSize: 17, fontWeight: FontWeight.bold, letterSpacing: 4, ), ), ), ), ], ), ), ), ), ); } static final ImageFilter _glassFilter = ImageFilter.blur( sigmaX: 10, sigmaY: 10, ); // ---- 登录/注册切换 ---- Widget _buildSegmentTabs() { return Container( decoration: BoxDecoration( color: const Color(0x33C8956E), borderRadius: BorderRadius.circular(20), ), padding: const EdgeInsets.all(4), child: Row( children: [ _segment( '登录', !_isRegister, () => setState(() => _isRegister = false), ), _segment('注册', _isRegister, () => setState(() => _isRegister = true)), ], ), ); } Widget _segment(String text, bool active, VoidCallback onTap) { return Expanded( child: GestureDetector( onTap: onTap, child: AnimatedContainer( duration: const Duration(milliseconds: 200), padding: const EdgeInsets.symmetric(vertical: 10), decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), color: active ? const Color(0xFFC8956E) : Colors.transparent, ), child: Center( child: Text( text, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w600, color: active ? Colors.white : const Color(0xFF8D6E63), ), ), ), ), ), ); } Widget _inputField({ required String icon, required TextEditingController controller, required String hint, int maxLength = 50, bool obscure = false, Widget? suffix, }) { return Container( padding: const EdgeInsets.symmetric(horizontal: 14), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.8), borderRadius: BorderRadius.circular(14), border: Border.all(color: const Color(0xFFE0C9A6)), ), child: Row( children: [ Text(icon, style: const TextStyle(fontSize: 18)), const SizedBox(width: 10), Expanded( child: TextField( controller: controller, obscureText: obscure, maxLength: maxLength, style: const TextStyle(fontSize: 15, color: Color(0xFF5D4037)), decoration: InputDecoration( hintText: hint, hintStyle: const TextStyle( color: Color(0xFFBCAAA4), fontSize: 14, ), border: InputBorder.none, counterText: '', isDense: true, contentPadding: const EdgeInsets.symmetric(vertical: 14), ), ), ), ?suffix, ], ), ); } }