519 lines
19 KiB
Dart
519 lines
19 KiB
Dart
|
|
import 'package:flutter/material.dart';
|
|||
|
|
import 'package:flutter/services.dart';
|
|||
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
|
|
import '../../../core/theme/app_colors.dart';
|
|||
|
|
import '../../../core/theme/app_text_styles.dart';
|
|||
|
|
import '../../../core/theme/app_dimensions.dart';
|
|||
|
|
import '../../../core/widgets/custom_button.dart';
|
|||
|
|
import '../../../core/widgets/custom_text_field.dart';
|
|||
|
|
import '../../../core/routes/app_routes.dart';
|
|||
|
|
import '../providers/auth_provider.dart';
|
|||
|
|
|
|||
|
|
/// 注册页面
|
|||
|
|
class RegisterScreen extends ConsumerStatefulWidget {
|
|||
|
|
const RegisterScreen({super.key});
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
ConsumerState<RegisterScreen> createState() => _RegisterScreenState();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
class _RegisterScreenState extends ConsumerState<RegisterScreen> {
|
|||
|
|
final _formKey = GlobalKey<FormState>();
|
|||
|
|
final _usernameController = TextEditingController();
|
|||
|
|
final _nicknameController = TextEditingController();
|
|||
|
|
final _emailController = TextEditingController();
|
|||
|
|
final _passwordController = TextEditingController();
|
|||
|
|
final _confirmPasswordController = TextEditingController();
|
|||
|
|
|
|||
|
|
bool _obscurePassword = true;
|
|||
|
|
bool _obscureConfirmPassword = true;
|
|||
|
|
bool _agreeToTerms = false;
|
|||
|
|
bool _isLoading = false;
|
|||
|
|
|
|||
|
|
// 密码强度提示
|
|||
|
|
bool _hasMinLength = false;
|
|||
|
|
bool _hasNumber = false;
|
|||
|
|
bool _hasLowerCase = false;
|
|||
|
|
bool _hasUpperCase = false;
|
|||
|
|
bool _hasSpecialChar = false;
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
void initState() {
|
|||
|
|
super.initState();
|
|||
|
|
// 监听密码输入,实时验证
|
|||
|
|
_passwordController.addListener(_validatePasswordStrength);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 验证密码强度
|
|||
|
|
void _validatePasswordStrength() {
|
|||
|
|
final password = _passwordController.text;
|
|||
|
|
setState(() {
|
|||
|
|
_hasMinLength = password.length >= 8;
|
|||
|
|
_hasNumber = RegExp(r'[0-9]').hasMatch(password);
|
|||
|
|
_hasLowerCase = RegExp(r'[a-z]').hasMatch(password);
|
|||
|
|
_hasUpperCase = RegExp(r'[A-Z]').hasMatch(password);
|
|||
|
|
_hasSpecialChar = RegExp(r'[!@#\$%^&*()_+\-=\[\]{};:"\\|,.<>\/?]').hasMatch(password);
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
void dispose() {
|
|||
|
|
_usernameController.dispose();
|
|||
|
|
_nicknameController.dispose();
|
|||
|
|
_emailController.dispose();
|
|||
|
|
_passwordController.dispose();
|
|||
|
|
_confirmPasswordController.dispose();
|
|||
|
|
super.dispose();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
@override
|
|||
|
|
Widget build(BuildContext context) {
|
|||
|
|
return Scaffold(
|
|||
|
|
backgroundColor: AppColors.background,
|
|||
|
|
appBar: AppBar(
|
|||
|
|
backgroundColor: Colors.transparent,
|
|||
|
|
elevation: 0,
|
|||
|
|
leading: IconButton(
|
|||
|
|
icon: const Icon(Icons.arrow_back_ios, color: AppColors.onSurface),
|
|||
|
|
onPressed: () => Navigator.of(context).pop(),
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
body: SafeArea(
|
|||
|
|
child: SingleChildScrollView(
|
|||
|
|
padding: const EdgeInsets.all(AppDimensions.pagePadding),
|
|||
|
|
child: Form(
|
|||
|
|
key: _formKey,
|
|||
|
|
child: Column(
|
|||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|||
|
|
children: [
|
|||
|
|
// 标题
|
|||
|
|
Text(
|
|||
|
|
'创建账户',
|
|||
|
|
style: AppTextStyles.headlineLarge.copyWith(
|
|||
|
|
color: AppColors.onSurface,
|
|||
|
|
fontWeight: FontWeight.bold,
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
const SizedBox(height: AppDimensions.spacingSm),
|
|||
|
|
Text(
|
|||
|
|
'加入AI英语学习平台,开启智能学习之旅',
|
|||
|
|
style: AppTextStyles.bodyMedium.copyWith(
|
|||
|
|
color: AppColors.onSurfaceVariant,
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
const SizedBox(height: AppDimensions.spacingXl),
|
|||
|
|
|
|||
|
|
// 用户名输入框
|
|||
|
|
CustomTextField(
|
|||
|
|
controller: _usernameController,
|
|||
|
|
labelText: '用户名',
|
|||
|
|
hintText: '请输入用户名',
|
|||
|
|
prefixIcon: Icons.person_outline,
|
|||
|
|
validator: (value) {
|
|||
|
|
if (value == null || value.isEmpty) {
|
|||
|
|
return '请输入用户名';
|
|||
|
|
}
|
|||
|
|
if (value.length < 3) {
|
|||
|
|
return '用户名至少3个字符';
|
|||
|
|
}
|
|||
|
|
if (value.length > 20) {
|
|||
|
|
return '用户名不能超过20个字符';
|
|||
|
|
}
|
|||
|
|
if (!RegExp(r'^[a-zA-Z0-9_]+$').hasMatch(value)) {
|
|||
|
|
return '用户名只能包含字母、数字和下划线';
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
},
|
|||
|
|
),
|
|||
|
|
const SizedBox(height: AppDimensions.spacingMd),
|
|||
|
|
|
|||
|
|
// 昵称输入框
|
|||
|
|
CustomTextField(
|
|||
|
|
controller: _nicknameController,
|
|||
|
|
labelText: '昵称',
|
|||
|
|
hintText: '请输入昵称',
|
|||
|
|
prefixIcon: Icons.badge_outlined,
|
|||
|
|
validator: (value) {
|
|||
|
|
if (value == null || value.isEmpty) {
|
|||
|
|
return '请输入昵称';
|
|||
|
|
}
|
|||
|
|
if (value.length < 2) {
|
|||
|
|
return '昵称至少2个字符';
|
|||
|
|
}
|
|||
|
|
if (value.length > 20) {
|
|||
|
|
return '昵称不能超过20个字符';
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
},
|
|||
|
|
),
|
|||
|
|
const SizedBox(height: AppDimensions.spacingMd),
|
|||
|
|
|
|||
|
|
// 邮箱输入框
|
|||
|
|
CustomTextField(
|
|||
|
|
controller: _emailController,
|
|||
|
|
labelText: '邮箱',
|
|||
|
|
hintText: '请输入邮箱地址',
|
|||
|
|
prefixIcon: Icons.email_outlined,
|
|||
|
|
keyboardType: TextInputType.emailAddress,
|
|||
|
|
validator: (value) {
|
|||
|
|
if (value == null || value.isEmpty) {
|
|||
|
|
return '请输入邮箱地址';
|
|||
|
|
}
|
|||
|
|
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
|
|||
|
|
return '请输入有效的邮箱地址';
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
},
|
|||
|
|
),
|
|||
|
|
const SizedBox(height: AppDimensions.spacingMd),
|
|||
|
|
|
|||
|
|
// 密码输入框
|
|||
|
|
CustomTextField(
|
|||
|
|
controller: _passwordController,
|
|||
|
|
labelText: '密码',
|
|||
|
|
hintText: '请输入密码',
|
|||
|
|
prefixIcon: Icons.lock_outline,
|
|||
|
|
obscureText: _obscurePassword,
|
|||
|
|
suffixIcon: IconButton(
|
|||
|
|
icon: Icon(
|
|||
|
|
_obscurePassword ? Icons.visibility_off : Icons.visibility,
|
|||
|
|
color: AppColors.onSurfaceVariant,
|
|||
|
|
),
|
|||
|
|
onPressed: () {
|
|||
|
|
setState(() {
|
|||
|
|
_obscurePassword = !_obscurePassword;
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
),
|
|||
|
|
validator: (value) {
|
|||
|
|
if (value == null || value.isEmpty) {
|
|||
|
|
return '请输入密码';
|
|||
|
|
}
|
|||
|
|
if (value.length < 8) {
|
|||
|
|
return '密码至少8个字符';
|
|||
|
|
}
|
|||
|
|
// 至少包含一个数字
|
|||
|
|
if (!RegExp(r'[0-9]').hasMatch(value)) {
|
|||
|
|
return '密码必须包含数字';
|
|||
|
|
}
|
|||
|
|
// 至少包含一个小写字母
|
|||
|
|
if (!RegExp(r'[a-z]').hasMatch(value)) {
|
|||
|
|
return '密码必须包含小写字母';
|
|||
|
|
}
|
|||
|
|
// 至少包含一个大写字母
|
|||
|
|
if (!RegExp(r'[A-Z]').hasMatch(value)) {
|
|||
|
|
return '密码必须包含大写字母';
|
|||
|
|
}
|
|||
|
|
// 至少包含一个特殊字符
|
|||
|
|
if (!RegExp(r'[!@#\$%^&*()_+\-=\[\]{};:"\\|,.<>\/?]').hasMatch(value)) {
|
|||
|
|
return '密码必须包含特殊字符 (!@#\$%^&*等)';
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
},
|
|||
|
|
),
|
|||
|
|
const SizedBox(height: 8),
|
|||
|
|
// 密码强度提示
|
|||
|
|
Container(
|
|||
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|||
|
|
decoration: BoxDecoration(
|
|||
|
|
color: Colors.grey[50],
|
|||
|
|
borderRadius: BorderRadius.circular(8),
|
|||
|
|
border: Border.all(color: Colors.grey[200]!),
|
|||
|
|
),
|
|||
|
|
child: Column(
|
|||
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|||
|
|
children: [
|
|||
|
|
Text(
|
|||
|
|
'密码必须包含:',
|
|||
|
|
style: AppTextStyles.bodySmall.copyWith(
|
|||
|
|
fontWeight: FontWeight.w600,
|
|||
|
|
color: Colors.grey[700],
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
const SizedBox(height: 8),
|
|||
|
|
_buildPasswordRequirement('至少8个字符', _hasMinLength),
|
|||
|
|
_buildPasswordRequirement('至少一个数字', _hasNumber),
|
|||
|
|
_buildPasswordRequirement('至少一个小写字母', _hasLowerCase),
|
|||
|
|
_buildPasswordRequirement('至少一个大写字母', _hasUpperCase),
|
|||
|
|
_buildPasswordRequirement('至少一个特殊字符 (!@#\$%^&*等)', _hasSpecialChar),
|
|||
|
|
],
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
const SizedBox(height: AppDimensions.spacingMd),
|
|||
|
|
|
|||
|
|
// 确认密码输入框
|
|||
|
|
CustomTextField(
|
|||
|
|
controller: _confirmPasswordController,
|
|||
|
|
labelText: '确认密码',
|
|||
|
|
hintText: '请再次输入密码',
|
|||
|
|
prefixIcon: Icons.lock_outline,
|
|||
|
|
obscureText: _obscureConfirmPassword,
|
|||
|
|
suffixIcon: IconButton(
|
|||
|
|
icon: Icon(
|
|||
|
|
_obscureConfirmPassword ? Icons.visibility_off : Icons.visibility,
|
|||
|
|
color: AppColors.onSurfaceVariant,
|
|||
|
|
),
|
|||
|
|
onPressed: () {
|
|||
|
|
setState(() {
|
|||
|
|
_obscureConfirmPassword = !_obscureConfirmPassword;
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
),
|
|||
|
|
validator: (value) {
|
|||
|
|
if (value == null || value.isEmpty) {
|
|||
|
|
return '请确认密码';
|
|||
|
|
}
|
|||
|
|
if (value != _passwordController.text) {
|
|||
|
|
return '两次输入的密码不一致';
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
},
|
|||
|
|
),
|
|||
|
|
const SizedBox(height: AppDimensions.spacingMd),
|
|||
|
|
|
|||
|
|
// 同意条款
|
|||
|
|
Row(
|
|||
|
|
children: [
|
|||
|
|
Checkbox(
|
|||
|
|
value: _agreeToTerms,
|
|||
|
|
onChanged: (value) {
|
|||
|
|
setState(() {
|
|||
|
|
_agreeToTerms = value ?? false;
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
activeColor: AppColors.primary,
|
|||
|
|
),
|
|||
|
|
Expanded(
|
|||
|
|
child: GestureDetector(
|
|||
|
|
onTap: () {
|
|||
|
|
setState(() {
|
|||
|
|
_agreeToTerms = !_agreeToTerms;
|
|||
|
|
});
|
|||
|
|
},
|
|||
|
|
child: RichText(
|
|||
|
|
text: TextSpan(
|
|||
|
|
style: AppTextStyles.bodySmall.copyWith(
|
|||
|
|
color: AppColors.onSurfaceVariant,
|
|||
|
|
),
|
|||
|
|
children: [
|
|||
|
|
const TextSpan(text: '我已阅读并同意'),
|
|||
|
|
TextSpan(
|
|||
|
|
text: '《用户协议》',
|
|||
|
|
style: TextStyle(
|
|||
|
|
color: AppColors.primary,
|
|||
|
|
decoration: TextDecoration.underline,
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
const TextSpan(text: '和'),
|
|||
|
|
TextSpan(
|
|||
|
|
text: '《隐私政策》',
|
|||
|
|
style: TextStyle(
|
|||
|
|
color: AppColors.primary,
|
|||
|
|
decoration: TextDecoration.underline,
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
],
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
],
|
|||
|
|
),
|
|||
|
|
const SizedBox(height: AppDimensions.spacingLg),
|
|||
|
|
|
|||
|
|
// 注册按钮
|
|||
|
|
CustomButton(
|
|||
|
|
text: '注册',
|
|||
|
|
onPressed: _agreeToTerms ? _handleRegister : null,
|
|||
|
|
isLoading: _isLoading,
|
|||
|
|
width: double.infinity,
|
|||
|
|
),
|
|||
|
|
const SizedBox(height: AppDimensions.spacingMd),
|
|||
|
|
|
|||
|
|
// 分割线
|
|||
|
|
Row(
|
|||
|
|
children: [
|
|||
|
|
const Expanded(child: Divider(color: AppColors.divider)),
|
|||
|
|
Padding(
|
|||
|
|
padding: const EdgeInsets.symmetric(horizontal: AppDimensions.buttonPadding),
|
|||
|
|
child: Text(
|
|||
|
|
'或',
|
|||
|
|
style: AppTextStyles.bodySmall.copyWith(
|
|||
|
|
color: AppColors.onSurfaceVariant,
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
const Expanded(child: Divider(color: AppColors.divider)),
|
|||
|
|
],
|
|||
|
|
),
|
|||
|
|
const SizedBox(height: AppDimensions.spacingMd),
|
|||
|
|
|
|||
|
|
// 第三方注册
|
|||
|
|
Row(
|
|||
|
|
children: [
|
|||
|
|
Expanded(
|
|||
|
|
child: CustomButton(
|
|||
|
|
text: '微信注册',
|
|||
|
|
onPressed: () => _handleSocialRegister('wechat'),
|
|||
|
|
backgroundColor: const Color(0xFF07C160),
|
|||
|
|
textColor: Colors.white,
|
|||
|
|
icon: Icons.wechat,
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
const SizedBox(width: AppDimensions.spacingMd),
|
|||
|
|
Expanded(
|
|||
|
|
child: CustomButton(
|
|||
|
|
text: 'QQ注册',
|
|||
|
|
onPressed: () => _handleSocialRegister('qq'),
|
|||
|
|
backgroundColor: const Color(0xFF12B7F5),
|
|||
|
|
textColor: Colors.white,
|
|||
|
|
icon: Icons.chat,
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
],
|
|||
|
|
),
|
|||
|
|
const SizedBox(height: AppDimensions.spacingLg),
|
|||
|
|
|
|||
|
|
// 登录链接
|
|||
|
|
Center(
|
|||
|
|
child: GestureDetector(
|
|||
|
|
onTap: () {
|
|||
|
|
Navigator.of(context).pop();
|
|||
|
|
},
|
|||
|
|
child: RichText(
|
|||
|
|
text: TextSpan(
|
|||
|
|
style: AppTextStyles.bodyMedium.copyWith(
|
|||
|
|
color: AppColors.onSurfaceVariant,
|
|||
|
|
),
|
|||
|
|
children: [
|
|||
|
|
const TextSpan(text: '已有账户?'),
|
|||
|
|
TextSpan(
|
|||
|
|
text: '立即登录',
|
|||
|
|
style: TextStyle(
|
|||
|
|
color: AppColors.primary,
|
|||
|
|
fontWeight: FontWeight.w600,
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
],
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
],
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 构建密码要求项
|
|||
|
|
Widget _buildPasswordRequirement(String text, bool isMet) {
|
|||
|
|
return Padding(
|
|||
|
|
padding: const EdgeInsets.only(bottom: 4),
|
|||
|
|
child: Row(
|
|||
|
|
children: [
|
|||
|
|
Icon(
|
|||
|
|
isMet ? Icons.check_circle : Icons.circle_outlined,
|
|||
|
|
size: 16,
|
|||
|
|
color: isMet ? AppColors.success : Colors.grey[400],
|
|||
|
|
),
|
|||
|
|
const SizedBox(width: 8),
|
|||
|
|
Text(
|
|||
|
|
text,
|
|||
|
|
style: AppTextStyles.bodySmall.copyWith(
|
|||
|
|
color: isMet ? AppColors.success : Colors.grey[600],
|
|||
|
|
fontSize: 13,
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
],
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 处理注册
|
|||
|
|
Future<void> _handleRegister() async {
|
|||
|
|
if (!_formKey.currentState!.validate()) {
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (!_agreeToTerms) {
|
|||
|
|
_showSnackBar('请先同意用户协议和隐私政策');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
setState(() {
|
|||
|
|
_isLoading = true;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
await ref.read(authProvider.notifier).register(
|
|||
|
|
username: _usernameController.text.trim(),
|
|||
|
|
email: _emailController.text.trim(),
|
|||
|
|
password: _passwordController.text,
|
|||
|
|
nickname: _nicknameController.text.trim(),
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (mounted) {
|
|||
|
|
_showSnackBar('注册成功,正在自动登录...', isSuccess: true);
|
|||
|
|
|
|||
|
|
// 注册成功后自动登录
|
|||
|
|
try {
|
|||
|
|
await ref.read(authProvider.notifier).login(
|
|||
|
|
account: _usernameController.text.trim(),
|
|||
|
|
password: _passwordController.text,
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (mounted) {
|
|||
|
|
// 登录成功,跳转到首页
|
|||
|
|
Navigator.of(context).pushReplacementNamed(Routes.home);
|
|||
|
|
}
|
|||
|
|
} catch (loginError) {
|
|||
|
|
if (mounted) {
|
|||
|
|
// 自动登录失败,跳转到登录页
|
|||
|
|
_showSnackBar('注册成功,请登录', isSuccess: true);
|
|||
|
|
Future.delayed(const Duration(milliseconds: 1500), () {
|
|||
|
|
if (mounted) {
|
|||
|
|
Navigator.of(context).pushReplacementNamed(Routes.login);
|
|||
|
|
}
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
if (mounted) {
|
|||
|
|
_showSnackBar(e.toString());
|
|||
|
|
}
|
|||
|
|
} finally {
|
|||
|
|
if (mounted) {
|
|||
|
|
setState(() {
|
|||
|
|
_isLoading = false;
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 处理第三方注册
|
|||
|
|
Future<void> _handleSocialRegister(String provider) async {
|
|||
|
|
try {
|
|||
|
|
// TODO: 实现第三方注册逻辑
|
|||
|
|
_showSnackBar('第三方注册功能即将上线');
|
|||
|
|
} catch (e) {
|
|||
|
|
_showSnackBar(e.toString());
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// 显示提示信息
|
|||
|
|
void _showSnackBar(String message, {bool isSuccess = false}) {
|
|||
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|||
|
|
SnackBar(
|
|||
|
|
content: Text(message),
|
|||
|
|
backgroundColor: isSuccess ? AppColors.success : AppColors.error,
|
|||
|
|
behavior: SnackBarBehavior.floating,
|
|||
|
|
shape: RoundedRectangleBorder(
|
|||
|
|
borderRadius: BorderRadius.circular(8.0),
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|