// 登录页面 — 用户名密码登录 + 注册切换 // // 设计要点: // - 温暖治愈风格,使用珊瑚色主色调 // - 表单验证友好提示(面向小学生,语言简单) // - 密码可切换可见性 // - 登录/注册模式平滑切换 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_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; 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) { 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.spacing48), _buildForm(context, theme, colorScheme), const SizedBox(height: DesignTokens.spacing24), _buildSubmitButton(context, colorScheme), const SizedBox(height: DesignTokens.spacing16), _buildModeToggle(context, colorScheme), const SizedBox(height: DesignTokens.spacing32), BlocBuilder( builder: (context, state) { if (state is AuthError) { return _buildErrorMessage(state.message, colorScheme); } return const SizedBox.shrink(); }, ), ], ), ), ), ), ), ), ); } Widget _buildHeader(BuildContext context, ColorScheme colorScheme) { return Column( children: [ Container( width: 80, height: 80, decoration: BoxDecoration( color: colorScheme.primaryContainer, borderRadius: AppRadius.lgBorder, ), child: Icon( Icons.edit_note_rounded, size: 44, color: colorScheme.primary, ), ), const SizedBox(height: DesignTokens.spacing16), Text( '暖记', style: Theme.of(context).textTheme.headlineLarge?.copyWith( fontWeight: FontWeight.bold, color: colorScheme.primary, ), ), const SizedBox(height: DesignTokens.spacing4), Text( '记录温暖,书写成长', style: Theme.of(context).textTheme.bodyMedium?.copyWith( color: colorScheme.onSurface.withValues(alpha: 0.6), ), ), ], ); } 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: InputDecoration( labelText: '昵称', hintText: '你想被叫什么名字?', prefixIcon: const Icon(Icons.face_rounded), border: OutlineInputBorder( borderRadius: AppRadius.mdBorder, ), ), 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), border: OutlineInputBorder( borderRadius: AppRadius.mdBorder, ), ), 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; }); }, ), border: OutlineInputBorder( borderRadius: AppRadius.mdBorder, ), ), 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.mdBorder, ), ), 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), ), ), ], ), ); } }