2026-02-25 10:57:43 +08:00
|
|
|
import 'package:flutter/material.dart';
|
2026-02-25 15:21:26 +08:00
|
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
|
|
|
import 'package:formz/formz.dart';
|
2026-02-25 10:57:43 +08:00
|
|
|
import 'package:go_router/go_router.dart';
|
2026-03-13 14:10:13 +08:00
|
|
|
|
2026-02-25 18:00:02 +08:00
|
|
|
import '../../../../core/di/injection.dart';
|
2026-03-19 00:51:58 +08:00
|
|
|
import '../../../../core/router/app_routes.dart';
|
2026-03-13 14:10:13 +08:00
|
|
|
import '../../../../core/theme/design_tokens.dart';
|
2026-02-25 10:57:43 +08:00
|
|
|
import '../../../../shared/widgets/app_button.dart';
|
2026-02-25 18:00:02 +08:00
|
|
|
import '../../../../shared/widgets/banner/app_banner.dart';
|
2026-03-12 16:41:45 +08:00
|
|
|
import '../../../../shared/widgets/link_button.dart';
|
2026-02-25 18:00:02 +08:00
|
|
|
import '../../../../shared/widgets/toast/toast_type.dart';
|
2026-03-13 14:10:13 +08:00
|
|
|
import '../../data/auth_repository.dart';
|
2026-02-25 15:21:26 +08:00
|
|
|
import '../../presentation/bloc/auth_bloc.dart';
|
|
|
|
|
import '../../presentation/bloc/auth_event.dart';
|
2026-03-13 14:10:13 +08:00
|
|
|
import '../../presentation/cubits/login_cubit.dart';
|
|
|
|
|
import '../widgets/auth_field.dart';
|
|
|
|
|
import '../widgets/auth_page_scaffold.dart';
|
|
|
|
|
import '../widgets/password_field.dart';
|
2026-02-25 10:57:43 +08:00
|
|
|
|
2026-02-25 18:00:02 +08:00
|
|
|
class LoginScreen extends StatelessWidget {
|
|
|
|
|
const LoginScreen({super.key});
|
2026-02-25 10:57:43 +08:00
|
|
|
|
|
|
|
|
@override
|
2026-02-25 15:21:26 +08:00
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return BlocProvider(
|
2026-02-25 18:00:02 +08:00
|
|
|
create: (context) => LoginCubit(sl<AuthRepository>()),
|
|
|
|
|
child: const LoginView(),
|
2026-02-25 15:21:26 +08:00
|
|
|
);
|
|
|
|
|
}
|
2026-02-25 10:57:43 +08:00
|
|
|
}
|
|
|
|
|
|
2026-02-25 18:00:02 +08:00
|
|
|
class LoginView extends StatefulWidget {
|
|
|
|
|
const LoginView({super.key});
|
2026-02-25 15:21:26 +08:00
|
|
|
|
|
|
|
|
@override
|
2026-02-25 18:00:02 +08:00
|
|
|
State<LoginView> createState() => _LoginViewState();
|
2026-02-25 15:21:26 +08:00
|
|
|
}
|
|
|
|
|
|
2026-02-25 18:00:02 +08:00
|
|
|
class _LoginViewState extends State<LoginView> {
|
|
|
|
|
final _emailController = TextEditingController();
|
|
|
|
|
final _passwordController = TextEditingController();
|
2026-02-25 15:21:26 +08:00
|
|
|
|
2026-02-25 10:57:43 +08:00
|
|
|
@override
|
|
|
|
|
void dispose() {
|
2026-02-25 18:00:02 +08:00
|
|
|
_emailController.dispose();
|
2026-02-25 10:57:43 +08:00
|
|
|
_passwordController.dispose();
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-25 15:21:26 +08:00
|
|
|
Future<void> _handleLogin() async {
|
|
|
|
|
final cubit = context.read<LoginCubit>();
|
2026-02-25 18:00:02 +08:00
|
|
|
cubit.emailChanged(_emailController.text);
|
2026-02-25 15:21:26 +08:00
|
|
|
cubit.passwordChanged(_passwordController.text);
|
|
|
|
|
|
2026-03-13 14:10:13 +08:00
|
|
|
if (!cubit.state.isValid) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-02-25 15:21:26 +08:00
|
|
|
|
|
|
|
|
final response = await cubit.submit();
|
|
|
|
|
if (response != null && mounted) {
|
|
|
|
|
context.read<AuthBloc>().add(AuthLoggedIn(user: response.user));
|
2026-03-19 00:51:58 +08:00
|
|
|
context.go(AppRoutes.homeMain);
|
2026-02-25 15:21:26 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-25 10:57:43 +08:00
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
2026-03-10 17:42:57 +08:00
|
|
|
return AuthPageScaffold(
|
|
|
|
|
mainContentKey: const Key('login_main_content'),
|
|
|
|
|
footerKey: const Key('login_footer'),
|
2026-03-13 14:10:13 +08:00
|
|
|
mainContent: Padding(
|
|
|
|
|
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.sm),
|
|
|
|
|
child: Center(
|
|
|
|
|
child: ConstrainedBox(
|
|
|
|
|
constraints: const BoxConstraints(maxWidth: 380),
|
|
|
|
|
child: Column(
|
|
|
|
|
mainAxisSize: MainAxisSize.min,
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
|
|
children: [
|
|
|
|
|
const AuthHeroHeader(showBrand: true),
|
|
|
|
|
SizedBox(height: AppSpacing.xxl),
|
|
|
|
|
BlocBuilder<LoginCubit, LoginState>(
|
|
|
|
|
builder: (context, state) {
|
|
|
|
|
final fieldError = state.email.displayError != null
|
|
|
|
|
? state.email.error
|
|
|
|
|
: state.password.displayError != null
|
|
|
|
|
? state.password.error
|
|
|
|
|
: null;
|
|
|
|
|
|
|
|
|
|
return AuthSurfaceCard(
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
|
|
|
children: [
|
|
|
|
|
const Text(
|
|
|
|
|
'登录账号',
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
fontSize: 20,
|
|
|
|
|
fontWeight: FontWeight.w700,
|
|
|
|
|
color: AppColors.slate900,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
SizedBox(height: AppSpacing.xs),
|
|
|
|
|
SizedBox(height: AppSpacing.xl),
|
|
|
|
|
AuthSection(
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
|
|
|
children: [
|
|
|
|
|
AuthField(
|
|
|
|
|
label: '邮箱',
|
|
|
|
|
hint: 'name@example.com',
|
|
|
|
|
controller: _emailController,
|
|
|
|
|
keyboardType: TextInputType.emailAddress,
|
|
|
|
|
),
|
|
|
|
|
SizedBox(height: AppSpacing.lg),
|
|
|
|
|
PasswordField(
|
|
|
|
|
controller: _passwordController,
|
|
|
|
|
label: '密码',
|
|
|
|
|
hint: '请输入密码',
|
|
|
|
|
),
|
|
|
|
|
if (state.errorMessage != null ||
|
|
|
|
|
fieldError != null)
|
|
|
|
|
Padding(
|
|
|
|
|
padding: const EdgeInsets.only(
|
|
|
|
|
top: AppSpacing.lg,
|
|
|
|
|
),
|
|
|
|
|
child: AppBanner(
|
|
|
|
|
message:
|
|
|
|
|
state.errorMessage ?? fieldError!,
|
|
|
|
|
type: state.errorMessage != null
|
|
|
|
|
? ToastType.error
|
|
|
|
|
: ToastType.warning,
|
|
|
|
|
title: state.errorMessage != null
|
|
|
|
|
? '登录失败'
|
|
|
|
|
: '请检查输入',
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
SizedBox(height: AppSpacing.xl),
|
|
|
|
|
AppButton(
|
|
|
|
|
text: '登录',
|
|
|
|
|
onPressed:
|
|
|
|
|
state.status == FormzSubmissionStatus.inProgress
|
|
|
|
|
? null
|
|
|
|
|
: _handleLogin,
|
|
|
|
|
isLoading:
|
|
|
|
|
state.status ==
|
|
|
|
|
FormzSubmissionStatus.inProgress,
|
|
|
|
|
),
|
|
|
|
|
SizedBox(height: AppSpacing.sm),
|
|
|
|
|
Align(
|
|
|
|
|
alignment: Alignment.centerRight,
|
|
|
|
|
child: LinkButton(
|
|
|
|
|
text: '忘记密码?',
|
|
|
|
|
onTap: () => context.push('/reset-password'),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
2026-02-25 10:57:43 +08:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
2026-03-13 14:10:13 +08:00
|
|
|
footer: Row(
|
|
|
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
|
|
|
children: [
|
|
|
|
|
const Text(
|
|
|
|
|
'还没有账号?',
|
|
|
|
|
style: TextStyle(fontSize: 14, color: AppColors.authLinkMuted),
|
2026-02-25 18:00:02 +08:00
|
|
|
),
|
2026-03-13 14:10:13 +08:00
|
|
|
LinkButton(text: '去注册', onTap: () => context.push('/register')),
|
|
|
|
|
],
|
|
|
|
|
),
|
2026-02-25 10:57:43 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|