import 'dart:async'; 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/di/injection.dart'; import '../../../../core/theme/design_tokens.dart'; import '../../../../shared/widgets/app_button.dart'; import '../../../../shared/widgets/banner/app_banner.dart'; import '../../../../shared/widgets/fixed_length_code_input.dart'; import '../../../../shared/widgets/link_button.dart'; import '../../../../shared/widgets/toast/toast.dart'; import '../../../../shared/widgets/toast/toast_type.dart'; import '../../data/auth_repository.dart'; import '../../presentation/cubits/register_cubit.dart'; import '../widgets/auth_field.dart'; import '../widgets/auth_page_scaffold.dart'; import '../widgets/password_field.dart'; class RegisterScreen extends StatelessWidget { const RegisterScreen({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => RegisterCubit(sl()), child: const RegisterView(), ); } } class RegisterView extends StatefulWidget { const RegisterView({super.key}); @override State createState() => _RegisterViewState(); } class _RegisterViewState extends State { static const _inviteCodeLength = 4; static const _inviteAllowedChars = { 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '2', '3', '4', '5', '6', '7', '8', '9', }; final _nicknameController = TextEditingController(); final _emailController = TextEditingController(); final _passwordController = TextEditingController(); final _inviteCodeController = TextEditingController(); @override void dispose() { _nicknameController.dispose(); _emailController.dispose(); _passwordController.dispose(); _inviteCodeController.dispose(); super.dispose(); } Future _handleNext() async { final cubit = context.read(); final inviteCode = _inviteCodeController.text.trim().toUpperCase(); final normalizedInviteCode = inviteCode.length == _inviteCodeLength ? inviteCode : ''; cubit.usernameChanged(_nicknameController.text); cubit.emailChanged(_emailController.text); cubit.passwordChanged(_passwordController.text); cubit.inviteCodeChanged(normalizedInviteCode); if (!cubit.state.isStep1Valid || cubit.state.isSending) { String? errorMsg; if (!cubit.state.username.isValid) { errorMsg = '请输入有效的昵称(3-30个字符)'; } else if (!cubit.state.email.isValid) { errorMsg = '请输入有效的邮箱地址'; } else if (!cubit.state.password.isValid) { errorMsg = '密码至少需要6个字符'; } if (errorMsg != null && mounted) { Toast.show(context, errorMsg, type: ToastType.warning); } return; } if (inviteCode.isNotEmpty && normalizedInviteCode.isEmpty && mounted) { Toast.show( context, '邀请码需为 4 位,且仅支持 A-H/J-N/P-Z 与 2-9;已按无邀请码继续注册', type: ToastType.warning, ); } if (mounted) { context.push('/register/verification', extra: cubit); } unawaited(cubit.sendCodeSilently()); } @override Widget build(BuildContext context) { return AuthPageScaffold( mainContent: Padding( padding: const EdgeInsets.symmetric(horizontal: AppSpacing.sm), child: Center( child: ConstrainedBox( constraints: const BoxConstraints(maxWidth: 388), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ const AuthHeroHeader(showBrand: true), SizedBox(height: AppSpacing.xxl), BlocBuilder( builder: (context, state) { return AuthSurfaceCard( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ const Text( '创建账号', style: TextStyle( fontSize: 20, fontWeight: FontWeight.w700, color: AppColors.slate900, ), ), SizedBox(height: AppSpacing.lg), AuthSection( title: '基础信息', child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ AuthField( label: '昵称', hint: '请输入昵称(3-30字符)', controller: _nicknameController, ), SizedBox(height: AppSpacing.lg), AuthField( label: '邮箱', hint: 'name@example.com', controller: _emailController, keyboardType: TextInputType.emailAddress, ), SizedBox(height: AppSpacing.lg), PasswordField( controller: _passwordController, label: '密码', hint: '请输入至少 6 位密码', ), ], ), ), SizedBox(height: AppSpacing.md), AuthSection( title: '邀请码(选填)', child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ FixedLengthCodeInput( controller: _inviteCodeController, length: _inviteCodeLength, semanticLabel: '邀请码输入框', uppercase: true, allowedCharacters: _inviteAllowedChars, onChanged: (value) { context .read() .inviteCodeChanged(value); }, ), SizedBox(height: AppSpacing.md), AppBanner( title: '邀请码', message: '4 位,支持 A-H/J-N/P-Z 与 2-9。', type: ToastType.info, ), ], ), ), if (state.errorMessage != null) ...[ SizedBox(height: AppSpacing.lg), AppBanner( title: '注册失败', message: state.errorMessage!, type: ToastType.error, ), ], SizedBox(height: AppSpacing.lg), Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Container( height: 6, decoration: BoxDecoration( color: AppColors.authPrimaryButton, borderRadius: BorderRadius.circular( AppRadius.full, ), ), ), ), SizedBox(width: AppSpacing.sm), Expanded( child: Container( height: 6, decoration: BoxDecoration( color: AppColors.authSectionBorder, borderRadius: BorderRadius.circular( AppRadius.full, ), ), ), ), ], ), SizedBox(height: AppSpacing.sm), const Text( '第 1 步:完善基础信息', textAlign: TextAlign.center, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, color: AppColors.authLinkMuted, ), ), SizedBox(height: AppSpacing.lg), AppButton( text: '下一步', onPressed: state.status == FormzSubmissionStatus.inProgress || state.isSending ? null : _handleNext, isLoading: state.isSending, ), ], ), ); }, ), ], ), ), ), ), footer: Row( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ const Text( '已有账号?', style: TextStyle(fontSize: 14, color: AppColors.authLinkMuted), ), LinkButton(text: '去登录', onTap: () => context.pop()), ], ), ); } }