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),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
} |