Files
social-app/apps/lib/features/auth/ui/screens/login_screen.dart
T

253 lines
7.1 KiB
Dart
Raw Normal View History

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<AuthRepository>()),
child: const LoginView(),
);
}
}
class LoginView extends StatefulWidget {
const LoginView({super.key});
@override
State<LoginView> createState() => _LoginViewState();
}
class _LoginViewState extends State<LoginView> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscurePassword = true;
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
Future<void> _handleLogin() async {
final cubit = context.read<LoginCubit>();
cubit.emailChanged(_emailController.text);
cubit.passwordChanged(_passwordController.text);
if (!cubit.state.isValid) return;
final response = await cubit.submit();
if (response != null && mounted) {
context.read<AuthBloc>().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<LoginCubit, LoginState>(
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,
),
),
);
}
}