import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:formz/formz.dart'; import 'package:go_router/go_router.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../core/di/injection.dart'; import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/banner/app_banner.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; import '../../presentation/cubits/login_cubit.dart'; import '../../presentation/bloc/auth_bloc.dart'; import '../../presentation/bloc/auth_event.dart'; import '../../data/auth_repository.dart'; class LoginScreen extends StatelessWidget { const LoginScreen({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => LoginCubit(sl()), child: const LoginView(), ); } } class LoginView extends StatefulWidget { const LoginView({super.key}); @override State createState() => _LoginViewState(); } class _LoginViewState extends State { final _emailController = TextEditingController(); final _passwordController = TextEditingController(); bool _obscurePassword = true; @override void dispose() { _emailController.dispose(); _passwordController.dispose(); super.dispose(); } Future _handleLogin() async { final cubit = context.read(); cubit.emailChanged(_emailController.text); cubit.passwordChanged(_passwordController.text); if (!cubit.state.isValid) return; final response = await cubit.submit(); if (response != null && mounted) { context.read().add(AuthLoggedIn(user: response.user)); context.go('/home'); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.background, body: SafeArea( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 24), child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( key: const Key('login_main_content'), child: Center( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ _buildAppIcon(), const SizedBox(height: 24), _buildAppTitle(), const SizedBox(height: 32), _buildFormContainer(), ], ), ), ), Container(key: const Key('login_footer'), child: _buildFooter()), const SizedBox(height: 24), ], ), ), ), ); } Widget _buildAppIcon() { return Container( width: 104, height: 104, decoration: BoxDecoration( color: AppColors.appIconRing, borderRadius: BorderRadius.circular(52), border: Border.all(color: AppColors.appIconBorder, width: 1), ), child: Center( child: ClipRRect( borderRadius: BorderRadius.circular(38), child: Image.asset( 'assets/images/logo.png', width: 76, height: 76, fit: BoxFit.cover, ), ), ), ); } Widget _buildAppTitle() { return const Text( 'linksy', style: TextStyle( fontFamily: 'Playfair Display', fontSize: 34, fontWeight: FontWeight.w700, fontStyle: FontStyle.italic, color: AppColors.appTitle, letterSpacing: 0.5, ), ); } Widget _buildFormContainer() { return BlocBuilder( builder: (context, state) { final fieldError = state.email.displayError != null ? state.email.error : state.password.displayError != null ? state.password.error : null; return SizedBox( width: 327, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ _buildInput( label: '邮箱', hint: '请输入邮箱', controller: _emailController, hasError: state.email.displayError != null, ), const SizedBox(height: 12), _buildPasswordInput(state.password.displayError != null), const SizedBox(height: 12), if (state.errorMessage != null) AppBanner(message: state.errorMessage!, type: ToastType.error) else if (fieldError != null) AppBanner(message: fieldError, type: ToastType.warning), const SizedBox(height: 12), AppButton( text: '登录', onPressed: state.status == FormzSubmissionStatus.inProgress ? null : _handleLogin, ), ], ), ); }, ); } Widget _buildInput({ required String label, required String hint, required TextEditingController controller, bool hasError = false, }) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: AppColors.slate600, ), ), const SizedBox(height: 6), TextField( controller: controller, keyboardType: TextInputType.emailAddress, decoration: InputDecoration( hintText: hint, errorText: hasError ? ' ' : null, ), ), ], ); } Widget _buildPasswordInput(bool hasError) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '密码', style: TextStyle( fontSize: 13, fontWeight: FontWeight.w500, color: AppColors.slate600, ), ), const SizedBox(height: 6), TextField( controller: _passwordController, obscureText: _obscurePassword, decoration: InputDecoration( hintText: '请输入密码', errorText: hasError ? ' ' : null, suffixIcon: IconButton( icon: Icon( _obscurePassword ? Icons.visibility_off : Icons.visibility, size: 20, color: AppColors.slate400, ), onPressed: () { setState(() { _obscurePassword = !_obscurePassword; }); }, ), ), ), ], ); } Widget _buildFooter() { return GestureDetector( onTap: () => context.push('/register'), child: const Text( '还没有账号?去注册', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: AppColors.slate500, ), ), ); } }