// 登录页面 — 用户名密码登录 + 注册切换 // // 设计要点: // - 温暖治愈风格,使用珊瑚色主色调 // - 表单验证友好提示(面向小学生,语言简单) // - 密码可切换可见性 // - 登录/注册模式平滑切换 import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import '../../../core/constants/design_tokens.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_radius.dart'; import '../bloc/auth_bloc.dart'; /// 登录/注册页面 class LoginPage extends StatefulWidget { const LoginPage({super.key}); @override State createState() => _LoginPageState(); } class _LoginPageState extends State with SingleTickerProviderStateMixin { final _formKey = GlobalKey(); final _usernameController = TextEditingController(); final _passwordController = TextEditingController(); final _displayNameController = TextEditingController(); bool _isRegister = false; bool _obscurePassword = true; bool _agreedToTerms = false; late final AnimationController _animController; late final Animation _fadeAnim; @override void initState() { super.initState(); _animController = AnimationController( vsync: this, duration: DesignTokens.animNormal, ); _fadeAnim = CurvedAnimation( parent: _animController, curve: DesignTokens.warmCurve, ); _animController.forward(); } @override void dispose() { _usernameController.dispose(); _passwordController.dispose(); _displayNameController.dispose(); _animController.dispose(); super.dispose(); } void _submit() { if (!_formKey.currentState!.validate()) return; if (_isRegister && !_agreedToTerms) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('请先阅读并同意用户协议和隐私政策')), ); return; } if (_isRegister) { context.read().add(RegisterRequested( username: _usernameController.text.trim(), password: _passwordController.text, displayName: _displayNameController.text.trim().isEmpty ? null : _displayNameController.text.trim(), )); } else { context.read().add(LoginRequested( username: _usernameController.text.trim(), password: _passwordController.text, )); } } @override Widget build(BuildContext context) { final theme = Theme.of(context); final colorScheme = theme.colorScheme; return BlocListener( listener: (context, state) { if (state is Authenticated) { if (state.needsRoleSelection) { context.go('/role-selection'); } else if (state.needsClassCode) { context.go('/class-code'); } else { context.go('/home'); } } }, child: Scaffold( body: SafeArea( child: Center( child: SingleChildScrollView( padding: const EdgeInsets.symmetric( horizontal: DesignTokens.spacing32, ), child: FadeTransition( opacity: _fadeAnim, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildHeader(context, colorScheme), const SizedBox(height: DesignTokens.spacing32), _buildForm(context, theme, colorScheme), const SizedBox(height: DesignTokens.spacing24), _buildSubmitButton(context, colorScheme), const SizedBox(height: DesignTokens.spacing16), _buildModeToggle(context, colorScheme), const SizedBox(height: DesignTokens.spacing24), // 协议复选框(注册模式下显示) if (_isRegister) ...[ _buildAgreementRow(context, colorScheme), const SizedBox(height: DesignTokens.spacing16), ], BlocBuilder( builder: (context, state) { if (state is AuthError) { return _buildErrorMessage(state.message, colorScheme); } return const SizedBox.shrink(); }, ), // 社交登录分割线 const SizedBox(height: DesignTokens.spacing24), _buildSocialLoginDivider(context, colorScheme), const SizedBox(height: DesignTokens.spacing16), _buildSocialLoginButtons(context, colorScheme), const SizedBox(height: DesignTokens.spacing32), ], ), ), ), ), ), ), ); } Widget _buildHeader(BuildContext context, ColorScheme colorScheme) { final isDark = Theme.of(context).brightness == Brightness.dark; final bgColor = isDark ? const Color(0xFF1A1614) : const Color(0xFFFFF8F0); final tertiarySoft = isDark ? AppColors.tertiarySoftDark : AppColors.tertiarySoftLight; return Container( width: double.infinity, padding: const EdgeInsets.symmetric(vertical: 40), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [bgColor, tertiarySoft], ), ), child: Stack( clipBehavior: Clip.none, alignment: Alignment.center, children: [ // 装饰圆圈 Positioned(left: 20, top: -10, child: _decorCircle(60, AppColors.accent, 0.15)), Positioned(right: 30, top: 20, child: _decorCircle(40, AppColors.secondary, 0.12)), Positioned(left: 80, bottom: -20, child: _decorCircle(30, AppColors.tertiary, 0.18)), Positioned(right: 60, bottom: 10, child: _decorCircle(20, AppColors.accent, 0.10)), Positioned(left: -10, top: 50, child: _decorCircle(25, AppColors.secondary, 0.13)), Column( mainAxisSize: MainAxisSize.min, children: [ // Logo — 自定义笔记本图标 Container( width: 80, height: 80, decoration: BoxDecoration( border: Border.all(color: AppColors.accent, width: 3), borderRadius: AppRadius.lgBorder, color: colorScheme.surface.withValues(alpha: 0.5), ), child: const Icon( Icons.edit_note_rounded, size: 44, color: AppColors.accent, ), ), const SizedBox(height: DesignTokens.spacing16), // 品牌名 Text( '暖记', style: Theme.of(context).textTheme.headlineLarge?.copyWith( fontWeight: FontWeight.w700, color: AppColors.accent, ), ), const SizedBox(height: DesignTokens.spacing4), // 标语 — Caveat 手写风格 Text( '记录温暖,书写成长', style: const TextStyle( fontSize: 18, fontWeight: FontWeight.w500, color: AppColors.accent, fontFamily: 'Caveat', ), ), ], ), ], ), ); } /// 装饰圆圈 Widget _decorCircle(double size, Color color, double opacity) { return Container( width: size, height: size, decoration: BoxDecoration( color: color.withValues(alpha: opacity), shape: BoxShape.circle, ), ); } Widget _buildForm(BuildContext context, ThemeData theme, ColorScheme colorScheme) { return Form( key: _formKey, child: Column( children: [ AnimatedSize( duration: DesignTokens.animNormal, curve: DesignTokens.warmCurve, child: AnimatedSwitcher( duration: DesignTokens.animNormal, child: _isRegister ? Padding( key: const ValueKey('display-name'), padding: const EdgeInsets.only(bottom: DesignTokens.spacing16), child: TextFormField( controller: _displayNameController, decoration: const InputDecoration( labelText: '昵称', hintText: '你想被叫什么名字?', prefixIcon: Icon(Icons.face_rounded), ), textInputAction: TextInputAction.next, ), ) : const SizedBox.shrink(key: ValueKey('display-name-hide')), ), ), TextFormField( controller: _usernameController, decoration: InputDecoration( labelText: '账号', hintText: _isRegister ? '设置一个账号名' : '输入你的账号', prefixIcon: const Icon(Icons.person_rounded), ), textInputAction: TextInputAction.next, validator: (value) { if (value == null || value.trim().isEmpty) { return '请输入账号'; } if (value.trim().length < 3) { return '账号至少需要 3 个字符'; } return null; }, ), const SizedBox(height: DesignTokens.spacing16), TextFormField( controller: _passwordController, obscureText: _obscurePassword, decoration: InputDecoration( labelText: '密码', hintText: _isRegister ? '设置一个密码' : '输入你的密码', prefixIcon: const Icon(Icons.lock_rounded), suffixIcon: IconButton( icon: Icon( _obscurePassword ? Icons.visibility_off_rounded : Icons.visibility_rounded, ), onPressed: () { setState(() { _obscurePassword = !_obscurePassword; }); }, ), ), textInputAction: TextInputAction.done, onFieldSubmitted: (_) => _submit(), validator: (value) { if (value == null || value.isEmpty) { return '请输入密码'; } if (value.length < 6) { return '密码至少需要 6 个字符'; } return null; }, ), ], ), ); } Widget _buildSubmitButton(BuildContext context, ColorScheme colorScheme) { return BlocBuilder( builder: (context, state) { final isLoading = state is Authenticating; return SizedBox( width: double.infinity, height: 52, child: FilledButton( onPressed: isLoading ? null : _submit, style: FilledButton.styleFrom( shape: RoundedRectangleBorder( borderRadius: AppRadius.pillBorder, ), ), child: isLoading ? const SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2.5, color: Colors.white, ), ) : Text( _isRegister ? '注册' : '登录', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), ), ), ); }, ); } Widget _buildModeToggle(BuildContext context, ColorScheme colorScheme) { return TextButton( onPressed: () { setState(() { _isRegister = !_isRegister; }); _formKey.currentState?.reset(); }, child: Text( _isRegister ? '已有账号?去登录' : '没有账号?去注册', style: TextStyle(color: colorScheme.primary), ), ); } Widget _buildErrorMessage(String message, ColorScheme colorScheme) { return Container( width: double.infinity, padding: const EdgeInsets.all(DesignTokens.spacing12), decoration: BoxDecoration( color: colorScheme.errorContainer, borderRadius: BorderRadius.circular(12), ), child: Row( children: [ Icon(Icons.info_outline_rounded, size: 20, color: colorScheme.onErrorContainer), const SizedBox(width: DesignTokens.spacing8), Expanded( child: Text( message, style: TextStyle(color: colorScheme.onErrorContainer, fontSize: 14), ), ), ], ), ); } /// 社交登录分割线 Widget _buildSocialLoginDivider(BuildContext context, ColorScheme colorScheme) { final dividerColor = colorScheme.onSurface.withValues(alpha: 0.15); return Row( children: [ Expanded(child: Divider(color: dividerColor)), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Text('其他登录方式', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.4), )), ), Expanded(child: Divider(color: dividerColor)), ], ); } /// 社交登录按钮行 Widget _buildSocialLoginButtons(BuildContext context, ColorScheme colorScheme) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ // 微信 _SocialButton( bgColor: const Color(0xFF07C160), icon: Icons.chat_bubble, semanticLabel: '微信登录', onTap: () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('微信登录即将支持')), ); }, ), const SizedBox(width: 24), // Apple _SocialButton( bgColor: const Color(0xFF1D1D1F), icon: Icons.apple, semanticLabel: 'Apple 登录', onTap: () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Apple 登录即将支持')), ); }, ), const SizedBox(width: 24), // Google _SocialButton( bgColor: colorScheme.surface, borderColor: colorScheme.outlineVariant, child: const Text('G', style: TextStyle( fontSize: 24, fontWeight: FontWeight.w700, color: Color(0xFF4285F4), )), semanticLabel: 'Google 登录', onTap: () { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Google 登录即将支持')), ); }, ), ], ); } /// 协议复选框行 Widget _buildAgreementRow(BuildContext context, ColorScheme colorScheme) { return Row( children: [ SizedBox( width: 24, height: 24, child: Checkbox( value: _agreedToTerms, onChanged: (v) => setState(() => _agreedToTerms = v ?? false), activeColor: AppColors.accent, materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ), const SizedBox(width: 8), Expanded( child: Wrap( children: [ Text('我已阅读并同意', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.6), )), GestureDetector( onTap: () { // TODO: 打开用户协议 }, child: Text('《用户协议》', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: AppColors.accent, fontWeight: FontWeight.w500, )), ), Text('和', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.6), )), GestureDetector( onTap: () { // TODO: 打开隐私政策 }, child: Text('《隐私政策》', style: Theme.of(context).textTheme.bodySmall?.copyWith( color: AppColors.accent, fontWeight: FontWeight.w500, )), ), ], ), ), ], ); } } /// 社交登录圆形按钮 class _SocialButton extends StatelessWidget { const _SocialButton({ required this.bgColor, required this.semanticLabel, required this.onTap, this.icon, this.child, this.borderColor, }); final Color bgColor; final Color? borderColor; final IconData? icon; final Widget? child; final String semanticLabel; final VoidCallback onTap; @override Widget build(BuildContext context) { return SizedBox( width: 56, height: 56, child: Material( color: bgColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(28), side: borderColor != null ? BorderSide(color: borderColor!, width: 1.5) : BorderSide.none, ), child: InkWell( onTap: onTap, customBorder: RoundedRectangleBorder( borderRadius: BorderRadius.circular(28), ), child: Center( child: child ?? Icon(icon, size: 28, color: Colors.white), ), ), ), ); } }