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(() {
_showWarning = false;
});
context.push('/login/password');
context.push('/login/password', extra: _emailController.text);
}
@override
@@ -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<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> {
final _passwordController = TextEditingController();
class LoginPasswordView extends StatefulWidget {
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;
@override
void initState() {
super.initState();
_passwordController = TextEditingController();
}
@override
void dispose() {
_passwordController.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
Widget build(BuildContext context) {
return Scaffold(
@@ -92,54 +145,62 @@ class _LoginPasswordScreenState extends State<LoginPasswordScreen> {
}
Widget _buildFormContainer() {
return SizedBox(
width: 327,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
return BlocBuilder<LoginCubit, LoginState>(
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'),
),
],
),
);
},
);
}
@@ -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<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 _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscureText = true;
@override
void dispose() {
_nicknameController.dispose();
_emailController.dispose();
_passwordController.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
Widget build(BuildContext context) {
return Scaffold(
@@ -93,23 +129,39 @@ class _RegisterScreenState extends State<RegisterScreen> {
}
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<RegisterCubit, RegisterState>(
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<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() {
return Row(
children: [
@@ -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<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> {
final _passwordController = TextEditingController();
class RegisterStep2View extends StatefulWidget {
const RegisterStep2View({super.key});
@override
State<RegisterStep2View> createState() => _RegisterStep2ViewState();
}
class _RegisterStep2ViewState extends State<RegisterStep2View> {
final _codeController = TextEditingController();
final _inviteController = TextEditingController();
bool _obscureText = true;
@override
void dispose() {
_passwordController.dispose();
_codeController.dispose();
_inviteController.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
Widget build(BuildContext context) {
return Scaffold(
@@ -96,62 +141,41 @@ class _RegisterStep2ScreenState extends State<RegisterStep2Screen> {
}
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<RegisterCubit, RegisterState>(
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<RegisterStep2Screen> {
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),