From 8c1dfa9987ad0e2e2b1b5e057b36e08652e1e1cf Mon Sep 17 00:00:00 2001 From: qzl Date: Wed, 25 Feb 2026 15:21:26 +0800 Subject: [PATCH] feat(apps): integrate auth cubits with register and login screens --- .../auth/ui/screens/login_email_screen.dart | 2 +- .../ui/screens/login_password_screen.dart | 155 ++++++++++++------ .../auth/ui/screens/register_screen.dart | 126 +++++++++++--- .../ui/screens/register_step2_screen.dart | 144 +++++++++------- 4 files changed, 301 insertions(+), 126 deletions(-) diff --git a/apps/lib/features/auth/ui/screens/login_email_screen.dart b/apps/lib/features/auth/ui/screens/login_email_screen.dart index 6dc1907..0ecf6df 100644 --- a/apps/lib/features/auth/ui/screens/login_email_screen.dart +++ b/apps/lib/features/auth/ui/screens/login_email_screen.dart @@ -35,7 +35,7 @@ class _LoginEmailScreenState extends State { setState(() { _showWarning = false; }); - context.push('/login/password'); + context.push('/login/password', extra: _emailController.text); } @override diff --git a/apps/lib/features/auth/ui/screens/login_password_screen.dart b/apps/lib/features/auth/ui/screens/login_password_screen.dart index 5e027d5..1d86adf 100644 --- a/apps/lib/features/auth/ui/screens/login_password_screen.dart +++ b/apps/lib/features/auth/ui/screens/login_password_screen.dart @@ -1,25 +1,78 @@ 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 '../../../../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 { - const LoginPasswordScreen({super.key}); +class LoginPasswordScreen extends StatelessWidget { + final String? email; + + const LoginPasswordScreen({super.key, this.email}); @override - State 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()); + if (initialEmail.isNotEmpty) { + cubit.emailChanged(initialEmail); + } + return cubit; + }, + child: LoginPasswordView(initialEmail: initialEmail), + ); + } } -class _LoginPasswordScreenState extends State { - final _passwordController = TextEditingController(); +class LoginPasswordView extends StatefulWidget { + final String initialEmail; + + const LoginPasswordView({super.key, required this.initialEmail}); + + @override + State createState() => _LoginPasswordViewState(); +} + +class _LoginPasswordViewState extends State { + late final TextEditingController _passwordController; bool _obscureText = true; + @override + void initState() { + super.initState(); + _passwordController = TextEditingController(); + } + @override void dispose() { _passwordController.dispose(); super.dispose(); } + Future _handleLogin() async { + final cubit = context.read(); + 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( @@ -92,54 +145,62 @@ class _LoginPasswordScreenState extends State { } Widget _buildFormContainer() { - return SizedBox( - width: 327, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, + return BlocBuilder( + builder: (context, state) { + return SizedBox( + width: 327, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, 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: '请输入密码', - suffixIcon: IconButton( - icon: Icon( - _obscureText ? Icons.visibility_off : Icons.visibility, - size: 20, - color: AppColors.slate400, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + '密码', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + color: Color(0xFF475569), ), - 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'), - ), - ], - ), + ); + }, ); } diff --git a/apps/lib/features/auth/ui/screens/register_screen.dart b/apps/lib/features/auth/ui/screens/register_screen.dart index 8a2699d..22de94b 100644 --- a/apps/lib/features/auth/ui/screens/register_screen.dart +++ b/apps/lib/features/auth/ui/screens/register_screen.dart @@ -1,26 +1,62 @@ 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 '../../../../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}); @override - State createState() => _RegisterScreenState(); + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => RegisterCubit(context.read()), + child: const RegisterView(), + ); + } } -class _RegisterScreenState extends State { +class RegisterView extends StatefulWidget { + const RegisterView({super.key}); + + @override + State createState() => _RegisterViewState(); +} + +class _RegisterViewState extends State { final _nicknameController = TextEditingController(); final _emailController = TextEditingController(); + final _passwordController = TextEditingController(); + bool _obscureText = true; @override void dispose() { _nicknameController.dispose(); _emailController.dispose(); + _passwordController.dispose(); super.dispose(); } + Future _handleNext() async { + final cubit = context.read(); + 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 Widget build(BuildContext context) { return Scaffold( @@ -93,23 +129,39 @@ class _RegisterScreenState extends State { } Widget _buildFormContainer() { - return SizedBox( - width: 327, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildInput('昵称', '请输入昵称', _nicknameController), - const SizedBox(height: 12), - _buildInput('邮箱', '请输入邮箱', _emailController), - const SizedBox(height: 12), - _buildStepIndicator(), - const SizedBox(height: 12), - AppButton( - text: '下一步', - onPressed: () => context.push('/register/step2'), + return BlocBuilder( + builder: (context, state) { + return SizedBox( + width: 327, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildInput('昵称', '请输入昵称', _nicknameController), + const SizedBox(height: 12), + _buildInput('邮箱', '请输入邮箱', _emailController), + const SizedBox(height: 12), + _buildPasswordInput(), + const SizedBox(height: 12), + _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 { ); } + 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() { return Row( children: [ diff --git a/apps/lib/features/auth/ui/screens/register_step2_screen.dart b/apps/lib/features/auth/ui/screens/register_step2_screen.dart index 0ea3329..1d5f0d2 100644 --- a/apps/lib/features/auth/ui/screens/register_step2_screen.dart +++ b/apps/lib/features/auth/ui/screens/register_step2_screen.dart @@ -1,29 +1,74 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; +import 'package:formz/formz.dart'; import '../../../../core/theme/design_tokens.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 { - const RegisterStep2Screen({super.key}); +class RegisterStep2Screen extends StatelessWidget { + final RegisterCubit? cubit; + + const RegisterStep2Screen({super.key, this.cubit}); @override - State 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 { - final _passwordController = TextEditingController(); +class RegisterStep2View extends StatefulWidget { + const RegisterStep2View({super.key}); + + @override + State createState() => _RegisterStep2ViewState(); +} + +class _RegisterStep2ViewState extends State { final _codeController = TextEditingController(); final _inviteController = TextEditingController(); - bool _obscureText = true; @override void dispose() { - _passwordController.dispose(); _codeController.dispose(); _inviteController.dispose(); super.dispose(); } + Future _handleComplete() async { + final cubit = context.read(); + cubit.verificationCodeChanged(_codeController.text); + + if (!cubit.state.isStep2Valid) { + return; + } + + final response = await cubit.submitStep2(); + if (response != null && mounted) { + context.read().add(AuthLoggedIn(user: response.user)); + context.go('/home'); + } + } + + Future _handleResendCode() async { + await context.read().resendCode(); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -96,62 +141,41 @@ class _RegisterStep2ScreenState extends State { } Widget _buildFormContainer() { - return SizedBox( - width: 327, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildPasswordInput(), - const SizedBox(height: 12), - _buildCodeInput(), - const SizedBox(height: 12), - _buildInviteInput(), - const SizedBox(height: 12), - _buildStepIndicator(), - const SizedBox(height: 12), - AppButton(text: '完成注册', onPressed: () {}), - ], - ), - ); - } - - 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: '请输入至少 8 位密码', - suffixIcon: IconButton( - icon: Icon( - _obscureText ? Icons.visibility_off : Icons.visibility, - size: 20, - color: AppColors.slate400, + return BlocBuilder( + builder: (context, state) { + return SizedBox( + width: 327, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildCodeInput(state), + const SizedBox(height: 12), + _buildInviteInput(), + const SizedBox(height: 12), + _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 + : _handleComplete, ), - onPressed: () { - setState(() { - _obscureText = !_obscureText; - }); - }, - ), + ], ), - ), - ], + ); + }, ); } - Widget _buildCodeInput() { + Widget _buildCodeInput(RegisterState state) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -187,7 +211,9 @@ class _RegisterStep2ScreenState extends State { width: 112, height: 40, child: OutlinedButton( - onPressed: () {}, + onPressed: state.status == FormzSubmissionStatus.inProgress + ? null + : _handleResendCode, style: OutlinedButton.styleFrom( backgroundColor: AppColors.background, side: const BorderSide(color: AppColors.input),