feat(apps): integrate auth cubits with register and login screens

This commit is contained in:
qzl
2026-02-25 15:21:26 +08:00
parent 0a7e1cd2d4
commit 8c1dfa9987
4 changed files with 301 additions and 126 deletions
@@ -35,7 +35,7 @@ class _LoginEmailScreenState extends State<LoginEmailScreen> {
setState(() { setState(() {
_showWarning = false; _showWarning = false;
}); });
context.push('/login/password'); context.push('/login/password', extra: _emailController.text);
} }
@override @override
@@ -1,25 +1,78 @@
import 'package:flutter/material.dart'; 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 'package:go_router/go_router.dart';
import '../../../../core/theme/design_tokens.dart'; import '../../../../core/theme/design_tokens.dart';
import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/app_button.dart';
import '../../../../shared/widgets/warning_banner.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 LoginPasswordScreen extends StatefulWidget { class LoginPasswordScreen extends StatelessWidget {
const LoginPasswordScreen({super.key}); final String? email;
const LoginPasswordScreen({super.key, this.email});
@override @override
State<LoginPasswordScreen> createState() => _LoginPasswordScreenState(); Widget build(BuildContext context) {
final emailFromExtra = GoRouterState.of(context).extra as String?;
final initialEmail = email ?? emailFromExtra ?? '';
return BlocProvider(
create: (context) {
final cubit = LoginCubit(context.read<AuthRepository>());
if (initialEmail.isNotEmpty) {
cubit.emailChanged(initialEmail);
}
return cubit;
},
child: LoginPasswordView(initialEmail: initialEmail),
);
}
} }
class _LoginPasswordScreenState extends State<LoginPasswordScreen> { class LoginPasswordView extends StatefulWidget {
final _passwordController = TextEditingController(); final String initialEmail;
const LoginPasswordView({super.key, required this.initialEmail});
@override
State<LoginPasswordView> createState() => _LoginPasswordViewState();
}
class _LoginPasswordViewState extends State<LoginPasswordView> {
late final TextEditingController _passwordController;
bool _obscureText = true; bool _obscureText = true;
@override
void initState() {
super.initState();
_passwordController = TextEditingController();
}
@override @override
void dispose() { void dispose() {
_passwordController.dispose(); _passwordController.dispose();
super.dispose(); super.dispose();
} }
Future<void> _handleLogin() async {
final cubit = context.read<LoginCubit>();
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -92,54 +145,62 @@ class _LoginPasswordScreenState extends State<LoginPasswordScreen> {
} }
Widget _buildFormContainer() { Widget _buildFormContainer() {
return SizedBox( return BlocBuilder<LoginCubit, LoginState>(
width: 327, builder: (context, state) {
child: Column( return SizedBox(
crossAxisAlignment: CrossAxisAlignment.stretch, width: 327,
children: [ child: Column(
Column( crossAxisAlignment: CrossAxisAlignment.stretch,
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text( Column(
'密码', crossAxisAlignment: CrossAxisAlignment.start,
style: TextStyle( children: [
fontSize: 13, const Text(
fontWeight: FontWeight.w500, '密码',
color: Color(0xFF475569), style: TextStyle(
), fontSize: 13,
), fontWeight: FontWeight.w500,
const SizedBox(height: 6), color: Color(0xFF475569),
TextField(
controller: _passwordController,
obscureText: _obscureText,
decoration: InputDecoration(
hintText: '请输入密码',
suffixIcon: IconButton(
icon: Icon(
_obscureText ? Icons.visibility_off : Icons.visibility,
size: 20,
color: AppColors.slate400,
), ),
onPressed: () {
setState(() {
_obscureText = !_obscureText;
});
},
), ),
), const SizedBox(height: 6),
TextField(
controller: _passwordController,
obscureText: _obscureText,
decoration: InputDecoration(
hintText: '请输入密码',
suffixIcon: IconButton(
icon: Icon(
_obscureText
? Icons.visibility_off
: Icons.visibility,
size: 20,
color: AppColors.slate400,
),
onPressed: () {
setState(() {
_obscureText = !_obscureText;
});
},
),
),
),
],
),
const SizedBox(height: 12),
if (state.errorMessage != null)
WarningBanner(message: state.errorMessage!, visible: true),
const SizedBox(height: 12),
AppButton(
text: '登录',
onPressed: state.status == FormzSubmissionStatus.inProgress
? null
: _handleLogin,
), ),
], ],
), ),
const SizedBox(height: 12), );
AppButton(text: '登录', onPressed: () => context.go('/home')), },
const SizedBox(height: 12),
AppButton(
text: '使用验证码登录',
isOutlined: true,
onPressed: () => context.push('/login/code'),
),
],
),
); );
} }
@@ -1,26 +1,62 @@
import 'package:flutter/material.dart'; 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 'package:go_router/go_router.dart';
import '../../../../core/theme/design_tokens.dart'; import '../../../../core/theme/design_tokens.dart';
import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/app_button.dart';
import '../../../../shared/widgets/warning_banner.dart';
import '../../presentation/cubits/register_cubit.dart';
import '../../data/auth_repository.dart';
class RegisterScreen extends StatefulWidget { class RegisterScreen extends StatelessWidget {
const RegisterScreen({super.key}); const RegisterScreen({super.key});
@override @override
State<RegisterScreen> createState() => _RegisterScreenState(); Widget build(BuildContext context) {
return BlocProvider(
create: (context) => RegisterCubit(context.read<AuthRepository>()),
child: const RegisterView(),
);
}
} }
class _RegisterScreenState extends State<RegisterScreen> { class RegisterView extends StatefulWidget {
const RegisterView({super.key});
@override
State<RegisterView> createState() => _RegisterViewState();
}
class _RegisterViewState extends State<RegisterView> {
final _nicknameController = TextEditingController(); final _nicknameController = TextEditingController();
final _emailController = TextEditingController(); final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscureText = true;
@override @override
void dispose() { void dispose() {
_nicknameController.dispose(); _nicknameController.dispose();
_emailController.dispose(); _emailController.dispose();
_passwordController.dispose();
super.dispose(); super.dispose();
} }
Future<void> _handleNext() async {
final cubit = context.read<RegisterCubit>();
cubit.usernameChanged(_nicknameController.text);
cubit.emailChanged(_emailController.text);
cubit.passwordChanged(_passwordController.text);
if (!cubit.state.isStep1Valid) {
return;
}
final success = await cubit.submitStep1();
if (success && mounted) {
context.push('/register/step2', extra: cubit);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -93,23 +129,39 @@ class _RegisterScreenState extends State<RegisterScreen> {
} }
Widget _buildFormContainer() { Widget _buildFormContainer() {
return SizedBox( return BlocBuilder<RegisterCubit, RegisterState>(
width: 327, builder: (context, state) {
child: Column( return SizedBox(
crossAxisAlignment: CrossAxisAlignment.stretch, width: 327,
children: [ child: Column(
_buildInput('昵称', '请输入昵称', _nicknameController), crossAxisAlignment: CrossAxisAlignment.stretch,
const SizedBox(height: 12), children: [
_buildInput('邮箱', '请输入邮箱', _emailController), _buildInput('昵称', '请输入昵称', _nicknameController),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildStepIndicator(), _buildInput('邮箱', '请输入邮箱', _emailController),
const SizedBox(height: 12), const SizedBox(height: 12),
AppButton( _buildPasswordInput(),
text: '下一步', const SizedBox(height: 12),
onPressed: () => context.push('/register/step2'), _buildStepIndicator(),
if (state.errorMessage != null)
Padding(
padding: const EdgeInsets.only(top: 8),
child: WarningBanner(
message: state.errorMessage!,
visible: true,
),
),
const SizedBox(height: 12),
AppButton(
text: '下一步',
onPressed: state.status == FormzSubmissionStatus.inProgress
? null
: _handleNext,
),
],
), ),
], );
), },
); );
} }
@@ -138,6 +190,42 @@ class _RegisterScreenState extends State<RegisterScreen> {
); );
} }
Widget _buildPasswordInput() {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'密码',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Color(0xFF475569),
),
),
const SizedBox(height: 6),
TextField(
controller: _passwordController,
obscureText: _obscureText,
decoration: InputDecoration(
hintText: '请输入至少 6 位密码',
suffixIcon: IconButton(
icon: Icon(
_obscureText ? Icons.visibility_off : Icons.visibility,
size: 20,
color: AppColors.slate400,
),
onPressed: () {
setState(() {
_obscureText = !_obscureText;
});
},
),
),
),
],
);
}
Widget _buildStepIndicator() { Widget _buildStepIndicator() {
return Row( return Row(
children: [ children: [
@@ -1,29 +1,74 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:formz/formz.dart';
import '../../../../core/theme/design_tokens.dart'; import '../../../../core/theme/design_tokens.dart';
import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/app_button.dart';
import '../../../../shared/widgets/warning_banner.dart';
import '../../presentation/cubits/register_cubit.dart';
import '../../presentation/bloc/auth_bloc.dart';
import '../../presentation/bloc/auth_event.dart';
class RegisterStep2Screen extends StatefulWidget { class RegisterStep2Screen extends StatelessWidget {
const RegisterStep2Screen({super.key}); final RegisterCubit? cubit;
const RegisterStep2Screen({super.key, this.cubit});
@override @override
State<RegisterStep2Screen> createState() => _RegisterStep2ScreenState(); Widget build(BuildContext context) {
final registerCubit =
cubit ?? (GoRouterState.of(context).extra as RegisterCubit?);
if (registerCubit == null) {
return Scaffold(
body: Center(child: Text('Error: RegisterCubit not found')),
);
}
return BlocProvider.value(
value: registerCubit,
child: const RegisterStep2View(),
);
}
} }
class _RegisterStep2ScreenState extends State<RegisterStep2Screen> { class RegisterStep2View extends StatefulWidget {
final _passwordController = TextEditingController(); const RegisterStep2View({super.key});
@override
State<RegisterStep2View> createState() => _RegisterStep2ViewState();
}
class _RegisterStep2ViewState extends State<RegisterStep2View> {
final _codeController = TextEditingController(); final _codeController = TextEditingController();
final _inviteController = TextEditingController(); final _inviteController = TextEditingController();
bool _obscureText = true;
@override @override
void dispose() { void dispose() {
_passwordController.dispose();
_codeController.dispose(); _codeController.dispose();
_inviteController.dispose(); _inviteController.dispose();
super.dispose(); super.dispose();
} }
Future<void> _handleComplete() async {
final cubit = context.read<RegisterCubit>();
cubit.verificationCodeChanged(_codeController.text);
if (!cubit.state.isStep2Valid) {
return;
}
final response = await cubit.submitStep2();
if (response != null && mounted) {
context.read<AuthBloc>().add(AuthLoggedIn(user: response.user));
context.go('/home');
}
}
Future<void> _handleResendCode() async {
await context.read<RegisterCubit>().resendCode();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@@ -96,62 +141,41 @@ class _RegisterStep2ScreenState extends State<RegisterStep2Screen> {
} }
Widget _buildFormContainer() { Widget _buildFormContainer() {
return SizedBox( return BlocBuilder<RegisterCubit, RegisterState>(
width: 327, builder: (context, state) {
child: Column( return SizedBox(
crossAxisAlignment: CrossAxisAlignment.stretch, width: 327,
children: [ child: Column(
_buildPasswordInput(), crossAxisAlignment: CrossAxisAlignment.stretch,
const SizedBox(height: 12), children: [
_buildCodeInput(), _buildCodeInput(state),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildInviteInput(), _buildInviteInput(),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildStepIndicator(), _buildStepIndicator(),
const SizedBox(height: 12), if (state.errorMessage != null)
AppButton(text: '完成注册', onPressed: () {}), Padding(
], padding: const EdgeInsets.only(top: 8),
), child: WarningBanner(
); message: state.errorMessage!,
} visible: true,
),
Widget _buildPasswordInput() { ),
return Column( const SizedBox(height: 12),
crossAxisAlignment: CrossAxisAlignment.start, AppButton(
children: [ text: '完成注册',
const Text( onPressed: state.status == FormzSubmissionStatus.inProgress
'密码', ? null
style: TextStyle( : _handleComplete,
fontSize: 13,
fontWeight: FontWeight.w500,
color: Color(0xFF475569),
),
),
const SizedBox(height: 6),
TextField(
controller: _passwordController,
obscureText: _obscureText,
decoration: InputDecoration(
hintText: '请输入至少 8 位密码',
suffixIcon: IconButton(
icon: Icon(
_obscureText ? Icons.visibility_off : Icons.visibility,
size: 20,
color: AppColors.slate400,
), ),
onPressed: () { ],
setState(() {
_obscureText = !_obscureText;
});
},
),
), ),
), );
], },
); );
} }
Widget _buildCodeInput() { Widget _buildCodeInput(RegisterState state) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -187,7 +211,9 @@ class _RegisterStep2ScreenState extends State<RegisterStep2Screen> {
width: 112, width: 112,
height: 40, height: 40,
child: OutlinedButton( child: OutlinedButton(
onPressed: () {}, onPressed: state.status == FormzSubmissionStatus.inProgress
? null
: _handleResendCode,
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
backgroundColor: AppColors.background, backgroundColor: AppColors.background,
side: const BorderSide(color: AppColors.input), side: const BorderSide(color: AppColors.input),