- 引导页协议多语言支持(languageId传递) - 登录页双书名号修复 + 注册页协议勾选 - 个人中心页面多语言(18个翻译键) - 网络断开提示增加关闭/刷新按钮 - 了解我们:新增秋叶qy开发者 + ayk签名修改 + 贡献者精简 + 微风暴微信搜索 - iOS快捷按钮重复修复(删除Info.plist静态定义) - 测试账号123456警告提示 - 扫码登录自动跳转(HTTP轮询+WebSocket双通道) - 登录页老用户按钮改次要色 - Syncfusion图表崩溃修复(DeferredBuilder+animationDuration:0) - macOS标题栏跟随软件夜间模式 - 平台兼容分发渠道弹窗 - 软件著作权图片+交叉水印 - 桌面小部件平台兼容说明默认收起 - iOS/macOS图标更新+名称确认为闲言 - 12个语言文件补全roleNative+7个分发渠道翻译字段
1015 lines
35 KiB
Dart
1015 lines
35 KiB
Dart
/// ============================================================
|
|
/// 闲言APP — 注册区域组件
|
|
/// 创建时间: 2026-05-10
|
|
/// 更新时间: 2026-06-01
|
|
/// 作用: 登录页面的注册区域,分步式注册流程
|
|
/// 上次更新: 接入多语言翻译系统,替换硬编码中文文本
|
|
/// ============================================================
|
|
|
|
import 'package:flutter/cupertino.dart';
|
|
import 'package:flutter/material.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 'login_form_sections.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> {
|
|
int _regStep = 1;
|
|
bool _obscureRegPassword = true;
|
|
bool _subscribeEmail = false;
|
|
bool _agreedToTerms = false;
|
|
bool _emsSending = false;
|
|
int _emsCountdown = 0;
|
|
bool _showSecQuestion = false;
|
|
int? _selectedSecQuestion;
|
|
String _selectedSecQuestionText = '';
|
|
List<SecQuestionItem> _secQuestions = [];
|
|
|
|
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();
|
|
}
|
|
|
|
Future<void> _loadSecQuestions() async {
|
|
try {
|
|
final questions = await UserSecurityService.secQuestions();
|
|
if (mounted) {
|
|
setState(() => _secQuestions = questions);
|
|
}
|
|
} catch (e) {
|
|
Log.w('加载密保问题列表失败: $e');
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_usernameController.dispose();
|
|
_emailController.dispose();
|
|
_regPasswordController.dispose();
|
|
_regConfirmPasswordController.dispose();
|
|
_regCodeController.dispose();
|
|
_secAnswerController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
AppThemeExtension get ext => widget.ext;
|
|
|
|
@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: _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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
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 Column(
|
|
children: [
|
|
LabeledInputField(
|
|
label: auth.username,
|
|
labelIcon: CupertinoIcons.person_add,
|
|
controller: _usernameController,
|
|
placeholder: auth.usernameHint,
|
|
icon: CupertinoIcons.person_add,
|
|
ext: ext,
|
|
),
|
|
const SizedBox(height: AppSpacing.md),
|
|
LabeledInputField(
|
|
label: auth.emailRequired,
|
|
labelIcon: CupertinoIcons.mail,
|
|
controller: _emailController,
|
|
placeholder: auth.emailHint,
|
|
icon: CupertinoIcons.mail,
|
|
ext: ext,
|
|
keyboardType: TextInputType.emailAddress,
|
|
),
|
|
const SizedBox(height: AppSpacing.lg),
|
|
SizedBox(
|
|
width: double.infinity,
|
|
height: 50,
|
|
child: CupertinoButton(
|
|
padding: EdgeInsets.zero,
|
|
minimumSize: Size.zero,
|
|
color: ext.accent,
|
|
borderRadius: AppRadius.mdBorder,
|
|
onPressed:
|
|
_usernameController.text.trim().isNotEmpty &&
|
|
_emailController.text.trim().isNotEmpty
|
|
? () => setState(() => _regStep = 2)
|
|
: null,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(
|
|
CupertinoIcons.arrow_right,
|
|
size: 16,
|
|
color: CupertinoColors.white,
|
|
),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
auth.nextStep,
|
|
style: AppTypography.callout.copyWith(
|
|
color: CupertinoColors.white,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
|
|
case 2:
|
|
return Column(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: AppSpacing.md),
|
|
child: Column(
|
|
children: [
|
|
Icon(CupertinoIcons.mail, size: 36, color: ext.accent),
|
|
const SizedBox(height: AppSpacing.xs),
|
|
Text(
|
|
'${auth.codeSentTo} ${_emailController.text.trim()}',
|
|
style: AppTypography.footnote.copyWith(
|
|
color: ext.textSecondary,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
LabeledInputField(
|
|
label: auth.verifyCode,
|
|
labelIcon: CupertinoIcons.number,
|
|
controller: _regCodeController,
|
|
placeholder: auth.enterCodeHint,
|
|
icon: CupertinoIcons.number,
|
|
ext: ext,
|
|
keyboardType: TextInputType.number,
|
|
suffix: SizedBox(
|
|
width: 90,
|
|
child: CupertinoButton(
|
|
color: ext.accent.withValues(alpha: 0.15),
|
|
borderRadius: AppRadius.mdBorder,
|
|
padding: EdgeInsets.zero,
|
|
onPressed: (_emsSending || _emsCountdown > 0)
|
|
? null
|
|
: () => _sendCode(_emailController.text.trim()),
|
|
child: _emsSending
|
|
? CupertinoActivityIndicator(radius: 6, color: ext.accent)
|
|
: Text(
|
|
_emsCountdown > 0 ? '${_emsCountdown}s' : auth.resend,
|
|
style: AppTypography.caption1.copyWith(
|
|
color: ext.accent,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: AppSpacing.sm),
|
|
Container(
|
|
padding: const EdgeInsets.all(AppSpacing.sm),
|
|
decoration: BoxDecoration(
|
|
color: ext.accent.withValues(alpha: 0.08),
|
|
borderRadius: AppRadius.mdBorder,
|
|
border: Border.all(
|
|
color: ext.accent.withValues(alpha: 0.2),
|
|
width: 0.5,
|
|
),
|
|
),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Icon(CupertinoIcons.info_circle, size: 16, color: ext.accent),
|
|
const SizedBox(width: AppSpacing.sm),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Tips',
|
|
style: AppTypography.caption1.copyWith(
|
|
color: ext.accent,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
const SizedBox(height: 2),
|
|
Text(
|
|
auth.codeNotReceivedTip,
|
|
style: AppTypography.caption2.copyWith(
|
|
color: ext.textSecondary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: AppSpacing.lg),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: SizedBox(
|
|
height: 50,
|
|
child: CupertinoButton(
|
|
padding: EdgeInsets.zero,
|
|
minimumSize: Size.zero,
|
|
color: ext.bgSecondary,
|
|
borderRadius: AppRadius.mdBorder,
|
|
onPressed: () => setState(() => _regStep = 1),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(
|
|
CupertinoIcons.arrow_left,
|
|
size: 16,
|
|
color: CupertinoColors.white,
|
|
),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
auth.prevStep,
|
|
style: AppTypography.callout.copyWith(
|
|
color: ext.textPrimary,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: AppSpacing.sm),
|
|
Expanded(
|
|
child: SizedBox(
|
|
height: 50,
|
|
child: CupertinoButton(
|
|
padding: EdgeInsets.zero,
|
|
minimumSize: Size.zero,
|
|
color: ext.accent,
|
|
borderRadius: AppRadius.mdBorder,
|
|
onPressed: _regCodeController.text.trim().isNotEmpty
|
|
? () => setState(() => _regStep = 3)
|
|
: null,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(
|
|
CupertinoIcons.checkmark_shield,
|
|
size: 16,
|
|
color: CupertinoColors.white,
|
|
),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
auth.nextStep,
|
|
style: AppTypography.callout.copyWith(
|
|
color: CupertinoColors.white,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
|
|
case 3:
|
|
return Column(
|
|
children: [
|
|
LabeledInputField(
|
|
label: auth.setPassword,
|
|
labelIcon: CupertinoIcons.lock,
|
|
controller: _regPasswordController,
|
|
placeholder: auth.passwordHint,
|
|
icon: CupertinoIcons.lock,
|
|
ext: ext,
|
|
obscureText: _obscureRegPassword,
|
|
suffix: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () =>
|
|
setState(() => _obscureRegPassword = !_obscureRegPassword),
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(
|
|
minWidth: 44,
|
|
minHeight: 44,
|
|
),
|
|
child: Center(
|
|
child: Icon(
|
|
_obscureRegPassword
|
|
? CupertinoIcons.eye_slash
|
|
: CupertinoIcons.eye,
|
|
size: 18,
|
|
color: ext.textHint,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(height: AppSpacing.md),
|
|
LabeledInputField(
|
|
label: auth.confirmPassword,
|
|
labelIcon: CupertinoIcons.lock_shield,
|
|
controller: _regConfirmPasswordController,
|
|
placeholder: auth.confirmPasswordHint,
|
|
icon: CupertinoIcons.lock_shield,
|
|
ext: ext,
|
|
obscureText: true,
|
|
),
|
|
const SizedBox(height: AppSpacing.md),
|
|
GestureDetector(
|
|
onTap: () => setState(() => _showSecQuestion = !_showSecQuestion),
|
|
behavior: HitTestBehavior.opaque,
|
|
child: Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: AppSpacing.md,
|
|
vertical: AppSpacing.sm,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: ext.bgSecondary,
|
|
borderRadius: AppRadius.mdBorder,
|
|
border: Border.all(
|
|
color: _showSecQuestion
|
|
? ext.accent.withValues(alpha: 0.4)
|
|
: ext.textHint.withValues(alpha: 0.15),
|
|
),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
CupertinoIcons.shield,
|
|
size: 16,
|
|
color: _showSecQuestion ? ext.accent : ext.textHint,
|
|
),
|
|
const SizedBox(width: AppSpacing.sm),
|
|
Text(
|
|
auth.secQuestionOptional,
|
|
style: AppTypography.subhead.copyWith(
|
|
color: _showSecQuestion
|
|
? ext.accent
|
|
: ext.textSecondary,
|
|
),
|
|
),
|
|
const Spacer(),
|
|
Text(
|
|
_selectedSecQuestionText.isNotEmpty
|
|
? auth.selected
|
|
: auth.enhanceSecurity,
|
|
style: AppTypography.caption1.copyWith(
|
|
color: ext.textHint,
|
|
),
|
|
),
|
|
const SizedBox(width: 4),
|
|
AnimatedRotation(
|
|
duration: const Duration(milliseconds: 200),
|
|
turns: _showSecQuestion ? 0.5 : 0,
|
|
child: Icon(
|
|
CupertinoIcons.chevron_down,
|
|
size: 14,
|
|
color: ext.textHint,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
if (_showSecQuestion) ...[
|
|
const SizedBox(height: AppSpacing.sm),
|
|
CupertinoButton(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: AppSpacing.md,
|
|
vertical: AppSpacing.sm,
|
|
),
|
|
color: ext.bgSecondary,
|
|
borderRadius: AppRadius.mdBorder,
|
|
onPressed: _secQuestions.isEmpty
|
|
? null
|
|
: () => _showSecQuestionPicker(context, auth, common),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
CupertinoIcons.question_circle,
|
|
size: 16,
|
|
color: ext.textSecondary,
|
|
),
|
|
const SizedBox(width: AppSpacing.sm),
|
|
Expanded(
|
|
child: Text(
|
|
_selectedSecQuestionText.isNotEmpty
|
|
? _selectedSecQuestionText
|
|
: auth.selectSecQuestion,
|
|
style: AppTypography.subhead.copyWith(
|
|
color: _selectedSecQuestionText.isNotEmpty
|
|
? ext.textPrimary
|
|
: ext.textHint,
|
|
),
|
|
),
|
|
),
|
|
Icon(
|
|
CupertinoIcons.chevron_right,
|
|
size: 14,
|
|
color: ext.textHint,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
if (_selectedSecQuestion != null) ...[
|
|
const SizedBox(height: AppSpacing.sm),
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: ext.bgSecondary,
|
|
borderRadius: AppRadius.mdBorder,
|
|
),
|
|
child: CupertinoTextField(
|
|
controller: _secAnswerController,
|
|
placeholder: auth.enterSecAnswerHint,
|
|
placeholderStyle: AppTypography.body.copyWith(
|
|
color: ext.textHint,
|
|
),
|
|
style: AppTypography.body.copyWith(color: ext.textPrimary),
|
|
decoration: null,
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: AppSpacing.md,
|
|
vertical: AppSpacing.sm + 2,
|
|
),
|
|
maxLength: 50,
|
|
),
|
|
),
|
|
],
|
|
],
|
|
const SizedBox(height: AppSpacing.md),
|
|
GestureDetector(
|
|
onTap: () => setState(() => _subscribeEmail = !_subscribeEmail),
|
|
behavior: HitTestBehavior.opaque,
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 20,
|
|
height: 20,
|
|
decoration: BoxDecoration(
|
|
color: _subscribeEmail
|
|
? ext.accent
|
|
: CupertinoColors.transparent,
|
|
borderRadius: BorderRadius.circular(5),
|
|
border: Border.all(
|
|
color: _subscribeEmail ? ext.accent : ext.textHint,
|
|
width: 1.5,
|
|
),
|
|
),
|
|
child: _subscribeEmail
|
|
? Icon(
|
|
CupertinoIcons.checkmark,
|
|
size: 14,
|
|
color: ext.textInverse,
|
|
)
|
|
: null,
|
|
),
|
|
const SizedBox(width: AppSpacing.sm),
|
|
Text(
|
|
auth.subscribeEmail,
|
|
style: AppTypography.footnote.copyWith(
|
|
color: ext.textSecondary,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: AppSpacing.lg),
|
|
_buildAgreementRow(ext, auth),
|
|
const SizedBox(height: AppSpacing.md),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: SizedBox(
|
|
height: 50,
|
|
child: CupertinoButton(
|
|
padding: EdgeInsets.zero,
|
|
minimumSize: Size.zero,
|
|
color: ext.bgSecondary,
|
|
borderRadius: AppRadius.mdBorder,
|
|
onPressed: () => setState(() => _regStep = 2),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(
|
|
CupertinoIcons.arrow_left,
|
|
size: 16,
|
|
color: CupertinoColors.white,
|
|
),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
auth.prevStep,
|
|
style: AppTypography.callout.copyWith(
|
|
color: ext.textPrimary,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: AppSpacing.sm),
|
|
Expanded(
|
|
child: SizedBox(
|
|
height: 50,
|
|
child: CupertinoButton(
|
|
padding: EdgeInsets.zero,
|
|
minimumSize: Size.zero,
|
|
color: ext.accent,
|
|
borderRadius: AppRadius.mdBorder,
|
|
onPressed:
|
|
(_agreedToTerms &&
|
|
_regPasswordController.text.isNotEmpty &&
|
|
_regConfirmPasswordController.text.isNotEmpty &&
|
|
!authState.isLoading)
|
|
? _handleRegister
|
|
: null,
|
|
child: authState.isLoading
|
|
? const CupertinoActivityIndicator(
|
|
color: CupertinoColors.white,
|
|
)
|
|
: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Icon(
|
|
CupertinoIcons.checkmark_circle,
|
|
size: 16,
|
|
color: CupertinoColors.white,
|
|
),
|
|
const SizedBox(width: 6),
|
|
Text(
|
|
auth.completeRegister,
|
|
style: AppTypography.callout.copyWith(
|
|
color: CupertinoColors.white,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
|
|
default:
|
|
return const SizedBox.shrink();
|
|
}
|
|
}
|
|
|
|
Widget _buildAgreementRow(AppThemeExtension ext, TAuth auth) {
|
|
return Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: () => setState(() => _agreedToTerms = !_agreedToTerms),
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(minWidth: 44, minHeight: 44),
|
|
child: Center(
|
|
child: Container(
|
|
width: 20,
|
|
height: 20,
|
|
margin: const EdgeInsets.only(top: 1),
|
|
decoration: BoxDecoration(
|
|
color: _agreedToTerms ? ext.accent : Colors.transparent,
|
|
borderRadius: BorderRadius.circular(5),
|
|
border: Border.all(
|
|
color: _agreedToTerms ? ext.accent : ext.textHint,
|
|
width: 1.5,
|
|
),
|
|
),
|
|
child: _agreedToTerms
|
|
? Icon(
|
|
CupertinoIcons.checkmark,
|
|
size: 13,
|
|
color: ext.textOnAccent,
|
|
)
|
|
: null,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: AppSpacing.sm),
|
|
Expanded(
|
|
child: Text.rich(
|
|
TextSpan(
|
|
text: auth.registerAgreePrefix,
|
|
style: AppTypography.footnote.copyWith(
|
|
color: ext.textSecondary,
|
|
),
|
|
children: [
|
|
WidgetSpan(
|
|
child: GestureDetector(
|
|
onTap: () => _showAgreement(true),
|
|
child: Text(
|
|
auth.userAgreement.startsWith('《')
|
|
? auth.userAgreement
|
|
: '《${auth.userAgreement}》',
|
|
style: AppTypography.footnote.copyWith(
|
|
color: ext.accent,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
TextSpan(
|
|
text: auth.and,
|
|
style: AppTypography.footnote.copyWith(
|
|
color: ext.textSecondary,
|
|
),
|
|
),
|
|
WidgetSpan(
|
|
child: GestureDetector(
|
|
onTap: () => _showAgreement(false),
|
|
child: Text(
|
|
auth.privacyPolicy.startsWith('《')
|
|
? auth.privacyPolicy
|
|
: '《${auth.privacyPolicy}》',
|
|
style: AppTypography.footnote.copyWith(
|
|
color: ext.accent,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
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);
|
|
setState(() {
|
|
_emsSending = false;
|
|
_emsCountdown = 0;
|
|
});
|
|
} 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);
|
|
}
|
|
setState(() {
|
|
_emsSending = false;
|
|
_emsCountdown = 0;
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
void _startCountdown() {
|
|
Future.doWhile(() async {
|
|
await Future<void>.delayed(const Duration(seconds: 1));
|
|
if (!mounted) return false;
|
|
setState(() => _emsCountdown--);
|
|
return _emsCountdown > 0;
|
|
});
|
|
}
|
|
|
|
void _showEmailRegisteredDialog(String email) {
|
|
final t = ref.read(translationsProvider);
|
|
showCupertinoDialog<void>(
|
|
context: context,
|
|
builder: (ctx) => CupertinoAlertDialog(
|
|
title: Text(t.auth.emailRegistered),
|
|
content: Text(t.auth.emailRegisteredHint.replaceAll('{email}', email)),
|
|
actions: [
|
|
CupertinoDialogAction(
|
|
child: Text(t.common.cancel),
|
|
onPressed: () => Navigator.pop(ctx),
|
|
),
|
|
CupertinoDialogAction(
|
|
isDefaultAction: true,
|
|
child: Text(t.auth.goLogin),
|
|
onPressed: () {
|
|
Navigator.pop(ctx);
|
|
widget.onSwitchToLogin();
|
|
},
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _showSecQuestionPicker(
|
|
BuildContext context,
|
|
TAuth auth,
|
|
TCommon common,
|
|
) {
|
|
showCupertinoModalPopup<void>(
|
|
context: context,
|
|
builder: (ctx) => Container(
|
|
height: 260,
|
|
color: ext.bgElevated.withValues(alpha: 0.95),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
CupertinoButton(
|
|
child: Text(
|
|
common.cancel,
|
|
style: AppTypography.subhead.copyWith(
|
|
color: ext.textSecondary,
|
|
),
|
|
),
|
|
onPressed: () => Navigator.pop(ctx),
|
|
),
|
|
Text(
|
|
auth.selectSecQuestion,
|
|
style: AppTypography.headline.copyWith(
|
|
color: ext.textPrimary,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
CupertinoButton(
|
|
child: Text(
|
|
common.confirm,
|
|
style: AppTypography.subhead.copyWith(
|
|
color: ext.accent,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
onPressed: () => Navigator.pop(ctx),
|
|
),
|
|
],
|
|
),
|
|
Expanded(
|
|
child: CupertinoPicker(
|
|
itemExtent: 36,
|
|
scrollController: FixedExtentScrollController(
|
|
initialItem: _selectedSecQuestion != null
|
|
? _secQuestions.indexWhere(
|
|
(q) => q.id == _selectedSecQuestion,
|
|
)
|
|
: 0,
|
|
),
|
|
onSelectedItemChanged: (index) {
|
|
if (index < _secQuestions.length) {
|
|
setState(() {
|
|
_selectedSecQuestion = _secQuestions[index].id;
|
|
_selectedSecQuestionText = _secQuestions[index].question;
|
|
});
|
|
}
|
|
},
|
|
children: _secQuestions.map((q) {
|
|
return Center(
|
|
child: Text(
|
|
q.question,
|
|
style: AppTypography.subhead.copyWith(
|
|
color: ext.textPrimary,
|
|
),
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|