Files
xianyan/lib/features/auth/presentation/register_section.dart
2026-06-12 22:30:26 +08:00

639 lines
20 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/// ============================================================
/// 闲言APP — 注册区域组件
/// 创建时间: 2026-05-10
/// 更新时间: 2026-06-08
/// 作用: 登录页面的注册区域,分步式注册流程(主控组件)
/// 上次更新: 邮箱变更检测重置验证码、Timer.periodic倒计时、步骤切换动画
/// ============================================================
import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../../core/router/app_nav_extension.dart';
import '../../../../core/theme/app_theme.dart';
import '../../../../core/theme/app_spacing.dart';
import '../../../../core/theme/app_typography.dart';
import '../../../../core/theme/app_radius.dart';
import '../../../../core/utils/logger.dart';
import '../../../../l10n/translation_resolver.dart';
import '../../../../l10n/types/t.dart';
import '../../../../shared/widgets/feedback/app_toast.dart';
import '../../../../shared/widgets/containers/glass_container.dart';
import '../providers/auth_provider.dart';
import '../services/email_service.dart';
import '../services/user_security_service.dart';
import 'register_step_account.dart';
import 'register_step_verify.dart';
import 'register_step_password.dart';
import 'register_dialogs.dart';
class RegisterSection extends ConsumerStatefulWidget {
const RegisterSection({
super.key,
required this.ext,
required this.onSwitchToLogin,
required this.onRegisterSuccess,
});
final AppThemeExtension ext;
final VoidCallback onSwitchToLogin;
final VoidCallback onRegisterSuccess;
@override
ConsumerState<RegisterSection> createState() => _RegisterSectionState();
}
class _RegisterSectionState extends ConsumerState<RegisterSection>
with TickerProviderStateMixin {
// ============================================================
// 状态变量
// ============================================================
int _regStep = 1;
bool _obscureRegPassword = true;
bool _subscribeEmail = false;
bool _agreedToTerms = false;
bool _emsSending = false;
int _emsCountdown = 0;
bool _showSecQuestion = false;
bool _showSecQuestionTip = false;
int? _selectedSecQuestion;
String _selectedSecQuestionText = '';
List<SecQuestionItem> _secQuestions = [];
/// 记录发送验证码时的邮箱,用于检测邮箱变更
String _emailWhenCodeSent = '';
/// 倒计时定时器
Timer? _countdownTimer;
/// 步骤切换动画控制器
late final AnimationController _stepAnimController;
late final Animation<double> _stepFadeAnimation;
late final Animation<Offset> _stepSlideAnimation;
// ============================================================
// 控制器
// ============================================================
final _usernameController = TextEditingController();
final _emailController = TextEditingController();
final _regPasswordController = TextEditingController();
final _regConfirmPasswordController = TextEditingController();
final _regCodeController = TextEditingController();
final _secAnswerController = TextEditingController();
// ============================================================
// 生命周期
// ============================================================
@override
void initState() {
super.initState();
_usernameController.addListener(() => setState(() {}));
_emailController.addListener(() => setState(() {}));
_regPasswordController.addListener(() => setState(() {}));
_regConfirmPasswordController.addListener(() => setState(() {}));
_regCodeController.addListener(() => setState(() {}));
_secAnswerController.addListener(() => setState(() {}));
_loadSecQuestions();
// 步骤切换动画初始化
_stepAnimController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 350),
);
_stepFadeAnimation = CurvedAnimation(
parent: _stepAnimController,
curve: Curves.easeOut,
);
_stepSlideAnimation =
Tween<Offset>(begin: const Offset(0.15, 0), end: Offset.zero).animate(
CurvedAnimation(
parent: _stepAnimController,
curve: Curves.easeOutCubic,
),
);
_stepAnimController.value = 1.0;
}
@override
void dispose() {
_countdownTimer?.cancel();
_stepAnimController.dispose();
_usernameController.dispose();
_emailController.dispose();
_regPasswordController.dispose();
_regConfirmPasswordController.dispose();
_regCodeController.dispose();
_secAnswerController.dispose();
super.dispose();
}
AppThemeExtension get ext => widget.ext;
// ============================================================
// 步骤切换(含动画)
// ============================================================
void _goToStep(int step) {
if (step == _regStep) return;
_stepAnimController.reverse(from: 1.0).then((_) {
if (mounted) {
setState(() => _regStep = step);
_stepAnimController.forward(from: 0.0);
}
});
}
// ============================================================
// 构建方法
// ============================================================
@override
Widget build(BuildContext context) {
final authState = ref.watch(authProvider);
final t = ref.watch(translationsProvider);
final auth = t.auth;
final common = t.common;
return Column(
children: [
_buildHeader(auth),
_buildStepIndicator(),
const SizedBox(height: AppSpacing.md),
GlassContainer(
depth: GlassDepth.elevated,
padding: const EdgeInsets.all(AppSpacing.lg),
child: FadeTransition(
opacity: _stepFadeAnimation,
child: SlideTransition(
position: _stepSlideAnimation,
child: _buildRegStepContent(authState, auth, common),
),
),
),
const SizedBox(height: AppSpacing.md),
CupertinoButton(
onPressed: () {
widget.onSwitchToLogin();
setState(() => _regStep = 1);
},
child: Text(
auth.hasAccountLogin,
style: AppTypography.subhead.copyWith(color: ext.accent),
),
),
],
);
}
// ============================================================
// 头部区域
// ============================================================
Widget _buildHeader(TAuth auth) {
return Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.xl),
child: Row(
children: [
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [ext.accent, ext.accentLight],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: AppRadius.lgBorder,
),
child: Icon(
CupertinoIcons.person_add,
size: 28,
color: ext.textInverse,
),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
auth.createAccount,
style: AppTypography.title2.copyWith(
color: ext.textPrimary,
fontWeight: FontWeight.w700,
),
),
const SizedBox(height: 2),
Text(
auth.registerNewAccount,
style: AppTypography.subhead.copyWith(
color: ext.textSecondary,
),
),
],
),
),
GestureDetector(
onTap: () =>
RegisterDialogs.showRegisterTips(context, ext: ext, auth: auth),
child: Icon(
CupertinoIcons.info_circle,
size: 22,
color: ext.textHint,
),
),
],
),
);
}
// ============================================================
// 步骤指示器
// ============================================================
Widget _buildStepIndicator() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: List.generate(3, (i) {
final step = i + 1;
final isActive = step == _regStep;
final isDone = step < _regStep;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: isActive ? 24 : 8,
height: 8,
decoration: BoxDecoration(
color: isDone
? ext.accent.withValues(alpha: 0.5)
: isActive
? ext.accent
: ext.textHint.withValues(alpha: 0.3),
borderRadius: AppRadius.smBorder,
),
),
);
}),
);
}
// ============================================================
// 步骤内容分发
// ============================================================
Widget _buildRegStepContent(AuthState authState, TAuth auth, TCommon common) {
switch (_regStep) {
case 1:
return RegisterStepAccount(
usernameController: _usernameController,
emailController: _emailController,
onNext: () {
_emailWhenCodeSent = _emailController.text.trim();
_goToStep(2);
_sendCode(_emailController.text.trim());
},
agreedToTerms: _agreedToTerms,
onToggleAgreement: () =>
setState(() => _agreedToTerms = !_agreedToTerms),
onShowAgreement: _showAgreement,
ext: ext,
auth: auth,
);
case 2:
return RegisterStepVerify(
email: _emailController.text.trim(),
codeController: _regCodeController,
emSending: _emsSending,
emsCountdown: _emsCountdown,
showSecQuestionTip: _showSecQuestionTip,
onSendCode: () => _sendCode(_emailController.text.trim()),
onPrev: () {
// 返回步骤1时检测邮箱是否变更若变更则重置验证码状态
_resetCodeStateIfEmailChanged();
_goToStep(1);
},
onNext: () {
setState(() {
_regStep = 3;
_showSecQuestionTip = false;
});
_stepAnimController.forward(from: 0.0);
},
onSkipToSecQuestion: () {
setState(() {
_regStep = 3;
_showSecQuestion = true;
_showSecQuestionTip = false;
});
_stepAnimController.forward(from: 0.0);
},
ext: ext,
auth: auth,
);
case 3:
return RegisterStepPassword(
username: _usernameController.text.trim(),
email: _emailController.text.trim(),
passwordController: _regPasswordController,
confirmPasswordController: _regConfirmPasswordController,
secAnswerController: _secAnswerController,
obscurePassword: _obscureRegPassword,
onToggleObscure: () =>
setState(() => _obscureRegPassword = !_obscureRegPassword),
showSecQuestion: _showSecQuestion,
onToggleSecQuestion: () =>
setState(() => _showSecQuestion = !_showSecQuestion),
secQuestions: _secQuestions,
selectedSecQuestion: _selectedSecQuestion,
selectedSecQuestionText: _selectedSecQuestionText,
onSelectSecQuestion: () => RegisterDialogs.showSecQuestionPicker(
context,
ext: ext,
auth: auth,
common: common,
secQuestions: _secQuestions,
selectedSecQuestion: _selectedSecQuestion,
onSelected: (index) {
if (index < _secQuestions.length) {
setState(() {
_selectedSecQuestion = _secQuestions[index].id;
_selectedSecQuestionText = _secQuestions[index].question;
});
}
},
),
subscribeEmail: _subscribeEmail,
onToggleSubscribe: () =>
setState(() => _subscribeEmail = !_subscribeEmail),
onPrev: () => _goToStep(2),
onBack: () => _goToStep(1),
onRegister: _handleRegister,
isLoading: authState.isLoading,
canRegister:
_agreedToTerms &&
_regPasswordController.text.isNotEmpty &&
_regConfirmPasswordController.text.isNotEmpty &&
!authState.isLoading,
ext: ext,
auth: auth,
);
default:
return const SizedBox.shrink();
}
}
// ============================================================
// 业务逻辑:邮箱变更检测,重置验证码状态
// ============================================================
void _resetCodeStateIfEmailChanged() {
final currentEmail = _emailController.text.trim();
if (currentEmail != _emailWhenCodeSent) {
// 邮箱已变更,重置验证码相关状态
_countdownTimer?.cancel();
_regCodeController.clear();
setState(() {
_emsSending = false;
_emsCountdown = 0;
_showSecQuestionTip = false;
});
Log.d('邮箱已变更,验证码状态已重置');
}
}
// ============================================================
// 业务逻辑:加载密保问题
// ============================================================
Future<void> _loadSecQuestions() async {
try {
final questions = await UserSecurityService.secQuestions();
if (mounted) {
setState(() => _secQuestions = questions);
}
} catch (e) {
Log.w('加载密保问题列表失败: $e');
}
}
// ============================================================
// 业务逻辑:协议跳转
// ============================================================
void _showAgreement(bool isUserAgreement) {
if (isUserAgreement) {
context.appPush('/agreement/user-service-agreement');
} else {
context.appPush('/agreement/privacy-policy');
}
}
// ============================================================
// 业务逻辑:注册处理
// ============================================================
Future<void> _handleRegister() async {
final t = ref.read(translationsProvider);
final username = _usernameController.text.trim();
final password = _regPasswordController.text;
final confirmPassword = _regConfirmPasswordController.text;
final email = _emailController.text.trim();
final code = _regCodeController.text.trim();
// 表单校验
if (username.isEmpty || password.isEmpty || email.isEmpty) {
AppToast.showWarning(t.auth.pleaseFillRequired);
return;
}
if (password != confirmPassword) {
AppToast.showError(t.auth.passwordMismatch);
return;
}
if (password.length < 6) {
AppToast.showWarning(t.auth.passwordTooShort);
return;
}
// 邮箱验证码校验:如果用户填写了验证码则验证,未填写则跳过(使用密保替代)
if (code.isNotEmpty && !EmailService.verifyCode(email: email, code: code)) {
AppToast.showError(t.auth.codeError);
return;
}
try {
final success = await ref
.read(authProvider.notifier)
.register(
username: username,
password: password,
email: email,
secQuestion: _selectedSecQuestion,
secAnswer: _secAnswerController.text.trim().isNotEmpty
? _secAnswerController.text.trim()
: null,
);
if (success && mounted) {
AppToast.showSuccess(t.auth.registerSuccess);
widget.onRegisterSuccess();
}
} catch (e) {
_handleRegisterError(e, email, username, password);
}
}
/// 处理注册错误
void _handleRegisterError(
Object e,
String email,
String username,
String password,
) {
final errorMsg = e.toString();
if (errorMsg.contains('已注册') ||
errorMsg.contains('already') ||
errorMsg.contains('已被') ||
errorMsg.contains('exists')) {
_showEmailRegisteredDialog(email);
} else if (errorMsg.contains('内部服务器错误') ||
errorMsg.contains('Internal') ||
errorMsg.contains('服务器错误')) {
_handleServerErrorAfterRegister(username, password, email);
} else {
AppToast.showError(errorMsg);
}
}
// ============================================================
// 业务逻辑:发送验证码
// ============================================================
Future<void> _sendCode(String email) async {
final t = ref.read(translationsProvider);
if (_emsSending || _emsCountdown > 0 || email.isEmpty) return;
if (!email.contains('@')) {
AppToast.showWarning(t.auth.pleaseEnterValidEmail);
return;
}
setState(() {
_emsSending = true;
_emsCountdown = 60;
});
_startCountdown();
try {
final code = EmailService.generateCode();
final ok = await EmailService.sendVerificationCode(
toEmail: email,
code: code,
);
if (!ok && mounted) {
AppToast.showError(t.auth.codeSendFailed);
_cancelCountdown();
} else if (mounted) {
AppToast.showSuccess(t.auth.codeSent);
setState(() => _emsSending = false);
}
} catch (e) {
Log.e('发送验证码失败', e);
if (mounted) {
final errorMsg = e.toString();
if (errorMsg.contains('已注册') ||
errorMsg.contains('already') ||
errorMsg.contains('exists')) {
_showEmailRegisteredDialog(email);
} else {
AppToast.showError(t.auth.codeSendFailedShort);
}
_cancelCountdown();
}
}
}
// ============================================================
// 业务逻辑倒计时Timer.periodic
// ============================================================
void _startCountdown() {
_countdownTimer?.cancel();
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (!mounted) {
timer.cancel();
return;
}
setState(() {
_emsCountdown--;
if (_emsCountdown <= 0) {
_emsCountdown = 0;
_showSecQuestionTip = true;
_emsSending = false;
timer.cancel();
}
});
});
}
/// 取消倒计时并重置状态
void _cancelCountdown() {
_countdownTimer?.cancel();
if (mounted) {
setState(() {
_emsSending = false;
_emsCountdown = 0;
});
}
}
// ============================================================
// 业务逻辑:邮箱已注册弹窗
// ============================================================
void _showEmailRegisteredDialog(String email) {
final t = ref.read(translationsProvider);
RegisterDialogs.showEmailRegistered(
context,
email: email,
auth: t.auth,
common: t.common,
onGoLogin: widget.onSwitchToLogin,
);
}
// ============================================================
// 业务逻辑:服务器错误后尝试登录确认
// ============================================================
Future<void> _handleServerErrorAfterRegister(
String username,
String password,
String email,
) async {
final t = ref.read(translationsProvider);
try {
final success = await ref
.read(authProvider.notifier)
.login(account: username, password: password);
if (success && mounted) {
AppToast.showSuccess(t.auth.registerSuccess);
widget.onRegisterSuccess();
}
} catch (_) {
if (mounted) {
_showEmailRegisteredDialog(email);
}
}
}
}