This commit is contained in:
sjk
2025-11-17 13:39:05 +08:00
commit d4cfe2b9de
479 changed files with 109324 additions and 0 deletions

View File

@@ -0,0 +1,273 @@
import 'package:flutter/material.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 '../providers/auth_provider.dart';
/// 忘记密码页面
class ForgotPasswordScreen extends ConsumerStatefulWidget {
const ForgotPasswordScreen({super.key});
@override
ConsumerState<ForgotPasswordScreen> createState() => _ForgotPasswordScreenState();
}
class _ForgotPasswordScreenState extends ConsumerState<ForgotPasswordScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
bool _isLoading = false;
bool _emailSent = false;
@override
void dispose() {
_emailController.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.spacingXl),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题
Text(
_emailSent ? '邮件已发送' : '忘记密码',
style: AppTextStyles.headlineLarge.copyWith(
color: AppColors.onSurface,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: AppDimensions.spacingMd),
Text(
_emailSent
? '我们已向您的邮箱发送了重置密码的链接,请查收邮件并按照说明操作。'
: '请输入您的邮箱地址,我们将向您发送重置密码的链接。',
style: AppTextStyles.bodyLarge.copyWith(
color: AppColors.onSurfaceVariant,
),
),
const SizedBox(height: AppDimensions.spacingXxl),
if (!_emailSent) ...[
// 邮箱输入框
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.spacingXl),
// 发送重置链接按钮
CustomButton(
text: '发送重置链接',
onPressed: _handleSendResetLink,
isLoading: _isLoading,
width: double.infinity,
),
] else ...[
// 成功图标
Center(
child: Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: AppColors.success.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.mark_email_read_outlined,
size: 60,
color: AppColors.success,
),
),
),
const SizedBox(height: AppDimensions.spacingXl),
// 重新发送按钮
CustomButton(
text: '重新发送',
onPressed: _handleResendEmail,
isLoading: _isLoading,
width: double.infinity,
isOutlined: true,
),
const SizedBox(height: AppDimensions.spacingMd),
// 返回登录按钮
CustomButton(
text: '返回登录',
onPressed: () => Navigator.of(context).pop(),
width: double.infinity,
),
],
const SizedBox(height: AppDimensions.spacingXl),
// 帮助信息
Container(
padding: const EdgeInsets.all(AppDimensions.spacingMd),
decoration: BoxDecoration(
color: AppColors.surfaceVariant,
borderRadius: BorderRadius.circular(AppDimensions.radiusMd),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.info_outline,
size: 20,
color: AppColors.primary,
),
const SizedBox(width: AppDimensions.spacingSm),
Text(
'温馨提示',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.primary,
fontWeight: FontWeight.w600,
),
),
],
),
const SizedBox(height: AppDimensions.spacingSm),
Text(
'• 请检查您的垃圾邮件文件夹\n• 重置链接将在24小时后过期\n• 如果仍未收到邮件,请联系客服',
style: AppTextStyles.bodyMedium.copyWith(
color: AppColors.onSurfaceVariant,
height: 1.5,
),
),
],
),
),
const SizedBox(height: AppDimensions.spacingXl),
// 联系客服
Center(
child: GestureDetector(
onTap: _handleContactSupport,
child: Text(
'联系客服',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.primary,
decoration: TextDecoration.underline,
),
),
),
),
],
),
),
),
),
);
}
/// 处理发送重置链接
Future<void> _handleSendResetLink() async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
_isLoading = true;
});
try {
await ref.read(authProvider.notifier).forgotPassword(_emailController.text.trim());
if (mounted) {
setState(() {
_emailSent = true;
});
}
} catch (e) {
if (mounted) {
_showSnackBar(e.toString());
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
/// 处理重新发送邮件
Future<void> _handleResendEmail() async {
setState(() {
_isLoading = true;
});
try {
await ref.read(authProvider.notifier).forgotPassword(_emailController.text.trim());
if (mounted) {
_showSnackBar('重置链接已重新发送', isSuccess: true);
}
} catch (e) {
if (mounted) {
_showSnackBar(e.toString());
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
/// 处理联系客服
void _handleContactSupport() {
// TODO: 实现联系客服功能
_showSnackBar('客服功能即将上线');
}
/// 显示提示信息
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(AppDimensions.radiusSm),
),
),
);
}
}

View File

@@ -0,0 +1,384 @@
import 'package:flutter/material.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/routes/app_routes.dart';
import '../../../core/widgets/custom_button.dart';
import '../../../core/widgets/custom_text_field.dart';
import '../providers/auth_provider.dart';
import '../../../shared/widgets/custom_app_bar.dart';
/// 登录页面
class LoginScreen extends ConsumerStatefulWidget {
const LoginScreen({super.key});
@override
ConsumerState<LoginScreen> createState() => _LoginScreenState();
}
class _LoginScreenState extends ConsumerState<LoginScreen> {
final _formKey = GlobalKey<FormState>();
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _obscurePassword = true;
bool _rememberMe = false;
@override
void dispose() {
_emailController.dispose();
_passwordController.dispose();
super.dispose();
}
Future<void> _handleLogin() async {
if (!_formKey.currentState!.validate()) {
return;
}
try {
await ref.read(authProvider.notifier).login(
account: _emailController.text.trim(), // 可以是邮箱或用户名
password: _passwordController.text,
rememberMe: _rememberMe,
);
if (mounted) {
Navigator.of(context).pushReplacementNamed(Routes.home);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('登录失败: ${e.toString()}'),
backgroundColor: AppColors.error,
),
);
}
}
}
void _navigateToRegister() {
Navigator.of(context).pushNamed(Routes.register);
}
void _navigateToForgotPassword() {
Navigator.of(context).pushNamed(Routes.forgotPassword);
}
void _fillTestUser() {
setState(() {
_emailController.text = 'test@example.com';
_passwordController.text = 'Test@123';
_rememberMe = true;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('已填充测试用户信息 (test@example.com / Test@123)'),
backgroundColor: AppColors.primary,
duration: const Duration(seconds: 2),
),
);
}
@override
Widget build(BuildContext context) {
final authState = ref.watch(authProvider);
final isLoading = authState.isLoading;
return Scaffold(
appBar: CustomAppBar(
title: '登录',
onBackPressed: () {
Navigator.of(context).pushReplacementNamed(Routes.splash);
},
),
backgroundColor: AppColors.surface,
body: SafeArea(
child: SingleChildScrollView(
padding: EdgeInsets.all(AppDimensions.pagePadding),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SizedBox(height: AppDimensions.spacingXxl),
// Logo和标题
Column(
children: [
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: AppColors.primary,
borderRadius: BorderRadius.circular(60),
),
child: Icon(
Icons.school,
size: 60,
color: AppColors.onPrimary,
),
),
SizedBox(height: AppDimensions.spacingLg),
Text(
'AI英语学习',
style: AppTextStyles.headlineLarge.copyWith(
color: AppColors.primary,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: AppDimensions.spacingSm),
Text(
'智能化英语学习平台',
style: AppTextStyles.bodyLarge.copyWith(
color: AppColors.onSurface.withOpacity(0.7),
),
),
],
),
SizedBox(height: AppDimensions.spacingXxl),
// 登录表单
Text(
'登录',
style: AppTextStyles.headlineMedium.copyWith(
color: AppColors.onSurface,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: AppDimensions.spacingLg),
// 邮箱输入框
CustomTextField(
controller: _emailController,
labelText: '邮箱',
hintText: '请输入邮箱地址',
keyboardType: TextInputType.emailAddress,
prefixIcon: Icons.email_outlined,
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入邮箱地址';
}
if (!RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value)) {
return '请输入有效的邮箱地址';
}
return null;
},
),
SizedBox(height: AppDimensions.spacingMd),
// 密码输入框
CustomTextField(
controller: _passwordController,
labelText: '密码',
hintText: '请输入密码',
obscureText: _obscurePassword,
prefixIcon: Icons.lock_outlined,
suffixIcon: IconButton(
icon: Icon(
_obscurePassword ? Icons.visibility_outlined : Icons.visibility_off_outlined,
),
onPressed: () {
setState(() {
_obscurePassword = !_obscurePassword;
});
},
),
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入密码';
}
if (value.length < 6) {
return '密码长度不能少于6位';
}
return null;
},
),
SizedBox(height: AppDimensions.spacingMd),
// 记住我和忘记密码
Row(
children: [
Checkbox(
value: _rememberMe,
onChanged: (value) {
setState(() {
_rememberMe = value ?? false;
});
},
activeColor: AppColors.primary,
),
Text(
'记住我',
style: AppTextStyles.bodyMedium.copyWith(
color: AppColors.onSurface,
),
),
const Spacer(),
TextButton(
onPressed: _navigateToForgotPassword,
child: Text(
'忘记密码?',
style: AppTextStyles.bodyMedium.copyWith(
color: AppColors.primary,
),
),
),
],
),
SizedBox(height: AppDimensions.spacingLg),
// 测试用户快速填充按钮(仅开发模式显示)
if (const bool.fromEnvironment('dart.vm.product') == false || true) ...[
OutlinedButton.icon(
onPressed: _fillTestUser,
icon: Icon(
Icons.bug_report,
color: AppColors.secondary,
size: 20,
),
label: Text(
'填充测试用户',
style: AppTextStyles.bodyMedium.copyWith(
color: AppColors.secondary,
),
),
style: OutlinedButton.styleFrom(
side: BorderSide(color: AppColors.secondary),
padding: EdgeInsets.symmetric(
vertical: AppDimensions.spacingSm,
horizontal: AppDimensions.spacingMd,
),
),
),
SizedBox(height: AppDimensions.spacingMd),
],
// 登录按钮
CustomButton(
text: '登录',
onPressed: isLoading ? null : _handleLogin,
isLoading: isLoading,
),
SizedBox(height: AppDimensions.spacingLg),
// 分割线
Row(
children: [
Expanded(
child: Divider(
color: AppColors.onSurface.withOpacity(0.3),
),
),
Padding(
padding: EdgeInsets.symmetric(horizontal: AppDimensions.spacingMd),
child: Text(
'',
style: AppTextStyles.bodyMedium.copyWith(
color: AppColors.onSurface.withOpacity(0.7),
),
),
),
Expanded(
child: Divider(
color: AppColors.onSurface.withOpacity(0.3),
),
),
],
),
SizedBox(height: AppDimensions.spacingLg),
// 第三方登录按钮
Row(
children: [
Expanded(
child: OutlinedButton.icon(
onPressed: () {
// TODO: 实现微信登录
},
icon: Icon(
Icons.wechat,
color: AppColors.primary,
),
label: Text(
'微信登录',
style: AppTextStyles.bodyMedium.copyWith(
color: AppColors.primary,
),
),
style: OutlinedButton.styleFrom(
side: BorderSide(color: AppColors.primary),
padding: EdgeInsets.symmetric(
vertical: AppDimensions.spacingMd,
),
),
),
),
SizedBox(width: AppDimensions.spacingMd),
Expanded(
child: OutlinedButton.icon(
onPressed: () {
// TODO: 实现QQ登录
},
icon: Icon(
Icons.account_circle,
color: AppColors.primary,
),
label: Text(
'QQ登录',
style: AppTextStyles.bodyMedium.copyWith(
color: AppColors.primary,
),
),
style: OutlinedButton.styleFrom(
side: BorderSide(color: AppColors.primary),
padding: EdgeInsets.symmetric(
vertical: AppDimensions.spacingMd,
),
),
),
),
],
),
SizedBox(height: AppDimensions.spacingXl),
// 注册链接
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'还没有账号?',
style: AppTextStyles.bodyMedium.copyWith(
color: AppColors.onSurface.withOpacity(0.7),
),
),
TextButton(
onPressed: _navigateToRegister,
child: Text(
'立即注册',
style: AppTextStyles.bodyMedium.copyWith(
color: AppColors.primary,
fontWeight: FontWeight.bold,
),
),
),
],
),
],
),
),
),
),
);
}
}

View File

@@ -0,0 +1,768 @@
import 'package:flutter/material.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 '../providers/auth_provider.dart';
import '../../../core/models/user_model.dart';
/// 个人信息管理页面
class ProfileScreen extends ConsumerStatefulWidget {
const ProfileScreen({super.key});
@override
ConsumerState<ProfileScreen> createState() => _ProfileScreenState();
}
class _ProfileScreenState extends ConsumerState<ProfileScreen> with SingleTickerProviderStateMixin {
late TabController _tabController;
final _formKey = GlobalKey<FormState>();
// 个人信息控制器
final _usernameController = TextEditingController();
final _emailController = TextEditingController();
final _phoneController = TextEditingController();
final _bioController = TextEditingController();
// 密码修改控制器
final _currentPasswordController = TextEditingController();
final _newPasswordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
bool _isLoading = false;
bool _isEditing = false;
bool _obscureCurrentPassword = true;
bool _obscureNewPassword = true;
bool _obscureConfirmPassword = true;
@override
void initState() {
super.initState();
_tabController = TabController(length: 3, vsync: this);
_loadUserData();
}
@override
void dispose() {
_tabController.dispose();
_usernameController.dispose();
_emailController.dispose();
_phoneController.dispose();
_bioController.dispose();
_currentPasswordController.dispose();
_newPasswordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
/// 加载用户数据
void _loadUserData() async {
try {
final authNotifier = await ref.read(authProvider.future);
final user = authNotifier.state.user;
if (user != null) {
_usernameController.text = user.username;
_emailController.text = user.email;
_phoneController.text = user.profile?.phone ?? '';
_bioController.text = user.profile?.bio ?? '';
}
} catch (e) {
// 处理错误
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.background,
appBar: AppBar(
backgroundColor: AppColors.surface,
elevation: 0,
title: Text(
'个人信息',
style: AppTextStyles.headlineSmall.copyWith(
color: AppColors.onSurface,
fontWeight: FontWeight.w600,
),
),
actions: [
if (_tabController.index == 0)
TextButton(
onPressed: () {
setState(() {
_isEditing = !_isEditing;
});
},
child: Text(
_isEditing ? '取消' : '编辑',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.primary,
),
),
),
],
bottom: TabBar(
controller: _tabController,
labelColor: AppColors.primary,
unselectedLabelColor: AppColors.onSurfaceVariant,
indicatorColor: AppColors.primary,
tabs: const [
Tab(text: '基本信息'),
Tab(text: '安全设置'),
Tab(text: '学习偏好'),
],
),
),
body: TabBarView(
controller: _tabController,
children: [
_buildBasicInfoTab(),
_buildSecurityTab(),
_buildPreferencesTab(),
],
),
);
}
/// 基本信息标签页
Widget _buildBasicInfoTab() {
return Consumer(builder: (context, ref, child) {
final authNotifierAsync = ref.watch(authProvider);
return authNotifierAsync.when(
data: (authNotifier) {
final user = authNotifier.state.user;
return SingleChildScrollView(
padding: const EdgeInsets.all(AppDimensions.spacingXl),
child: Form(
key: _formKey,
child: Column(
children: [
// 头像
Center(
child: Stack(
children: [
CircleAvatar(
radius: 60,
backgroundColor: AppColors.surfaceVariant,
backgroundImage: user?.profile?.avatar != null
? NetworkImage(user!.profile!.avatar!)
: null,
child: user?.profile?.avatar == null
? Icon(
Icons.person,
size: 60,
color: AppColors.onSurfaceVariant,
)
: null,
),
if (_isEditing)
Positioned(
bottom: 0,
right: 0,
child: GestureDetector(
onTap: _handleAvatarChange,
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.primary,
shape: BoxShape.circle,
border: Border.all(
color: AppColors.surface,
width: 2,
),
),
child: Icon(
Icons.camera_alt,
size: 20,
color: AppColors.onPrimary,
),
),
),
),
],
),
),
const SizedBox(height: AppDimensions.spacingXl),
// 用户名
CustomTextField(
controller: _usernameController,
labelText: '用户名',
prefixIcon: Icons.person_outline,
enabled: _isEditing,
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入用户名';
}
if (value.length < 3) {
return '用户名至少3个字符';
}
return null;
},
),
const SizedBox(height: AppDimensions.spacingMd),
// 邮箱
CustomTextField(
controller: _emailController,
labelText: '邮箱',
prefixIcon: Icons.email_outlined,
enabled: false, // 邮箱不允许修改
keyboardType: TextInputType.emailAddress,
),
const SizedBox(height: AppDimensions.spacingMd),
// 手机号
CustomTextField(
controller: _phoneController,
labelText: '手机号',
prefixIcon: Icons.phone_outlined,
enabled: _isEditing,
keyboardType: TextInputType.phone,
validator: (value) {
if (value != null && value.isNotEmpty) {
if (!RegExp(r'^1[3-9]\d{9}$').hasMatch(value)) {
return '请输入有效的手机号';
}
}
return null;
},
),
const SizedBox(height: AppDimensions.spacingMd),
// 个人简介
CustomTextField(
controller: _bioController,
labelText: '个人简介',
prefixIcon: Icons.edit_outlined,
enabled: _isEditing,
maxLines: 3,
hintText: '介绍一下自己吧...',
),
const SizedBox(height: AppDimensions.spacingXl),
// 保存按钮
if (_isEditing)
CustomButton(
text: '保存修改',
onPressed: _handleSaveProfile,
isLoading: _isLoading,
width: double.infinity,
),
],
),
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(
child: Text('加载失败: $error'),
),
);
});
}
/// 安全设置标签页
Widget _buildSecurityTab() {
return SingleChildScrollView(
padding: const EdgeInsets.all(AppDimensions.spacingXl),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 修改密码
Text(
'修改密码',
style: AppTextStyles.titleMedium.copyWith(
color: AppColors.onSurface,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: AppDimensions.spacingMd),
// 当前密码
CustomTextField(
controller: _currentPasswordController,
labelText: '当前密码',
prefixIcon: Icons.lock_outline,
obscureText: _obscureCurrentPassword,
suffixIcon: IconButton(
icon: Icon(
_obscureCurrentPassword ? Icons.visibility_off : Icons.visibility,
color: AppColors.onSurfaceVariant,
),
onPressed: () {
setState(() {
_obscureCurrentPassword = !_obscureCurrentPassword;
});
},
),
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入当前密码';
}
return null;
},
),
const SizedBox(height: AppDimensions.spacingMd),
// 新密码
CustomTextField(
controller: _newPasswordController,
labelText: '新密码',
prefixIcon: Icons.lock_outline,
obscureText: _obscureNewPassword,
suffixIcon: IconButton(
icon: Icon(
_obscureNewPassword ? Icons.visibility_off : Icons.visibility,
color: AppColors.onSurfaceVariant,
),
onPressed: () {
setState(() {
_obscureNewPassword = !_obscureNewPassword;
});
},
),
validator: (value) {
if (value == null || value.isEmpty) {
return '请输入新密码';
}
if (value.length < 8) {
return '密码至少8个字符';
}
if (!RegExp(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)').hasMatch(value)) {
return '密码必须包含大小写字母和数字';
}
return null;
},
),
const SizedBox(height: AppDimensions.spacingMd),
// 确认新密码
CustomTextField(
controller: _confirmPasswordController,
labelText: '确认新密码',
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 != _newPasswordController.text) {
return '两次输入的密码不一致';
}
return null;
},
),
const SizedBox(height: AppDimensions.spacingXl),
// 修改密码按钮
CustomButton(
text: '修改密码',
onPressed: _handleChangePassword,
isLoading: _isLoading,
width: double.infinity,
),
const SizedBox(height: AppDimensions.spacingXl),
// 其他安全选项
_buildSecurityOption(
icon: Icons.security,
title: '两步验证',
subtitle: '为您的账户添加额外的安全保护',
onTap: () => _showSnackBar('两步验证功能即将上线'),
),
_buildSecurityOption(
icon: Icons.devices,
title: '设备管理',
subtitle: '查看和管理已登录的设备',
onTap: () => _showSnackBar('设备管理功能即将上线'),
),
_buildSecurityOption(
icon: Icons.history,
title: '登录历史',
subtitle: '查看最近的登录记录',
onTap: () => _showSnackBar('登录历史功能即将上线'),
),
],
),
);
}
/// 学习偏好标签页
Widget _buildPreferencesTab() {
return Consumer(builder: (context, ref, child) {
final authNotifierAsync = ref.watch(authProvider);
return authNotifierAsync.when(
data: (authNotifier) {
final user = authNotifier.state.user;
final settings = user?.profile?.settings;
return SingleChildScrollView(
padding: const EdgeInsets.all(AppDimensions.spacingXl),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'学习设置',
style: AppTextStyles.titleMedium.copyWith(
color: AppColors.onSurface,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: AppDimensions.spacingMd),
_buildPreferenceOption(
icon: Icons.notifications,
title: '学习提醒',
subtitle: '每日学习提醒通知',
value: settings?.notificationsEnabled ?? true,
onChanged: (value) => _updateSetting('notificationsEnabled', value),
),
_buildPreferenceOption(
icon: Icons.volume_up,
title: '音效',
subtitle: '学习过程中的音效反馈',
value: settings?.soundEnabled ?? true,
onChanged: (value) => _updateSetting('soundEnabled', value),
),
_buildPreferenceOption(
icon: Icons.vibration,
title: '震动反馈',
subtitle: '操作时的震动反馈',
value: settings?.vibrationEnabled ?? true,
onChanged: (value) => _updateSetting('vibrationEnabled', value),
),
const SizedBox(height: AppDimensions.spacingXl),
Text(
'学习目标',
style: AppTextStyles.titleMedium.copyWith(
color: AppColors.onSurface,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: AppDimensions.spacingMd),
_buildGoalOption(
title: '每日单词目标',
value: '${settings?.dailyWordGoal ?? 20}',
onTap: () => _showGoalDialog('dailyWordGoal', settings?.dailyWordGoal ?? 20),
),
_buildGoalOption(
title: '每日学习时长',
value: '${settings?.dailyStudyMinutes ?? 30} 分钟',
onTap: () => _showGoalDialog('dailyStudyMinutes', settings?.dailyStudyMinutes ?? 30),
),
const SizedBox(height: AppDimensions.spacingXl),
Text(
'英语水平',
style: AppTextStyles.titleMedium.copyWith(
color: AppColors.onSurface,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: AppDimensions.spacingMd),
_buildLevelOption(
title: '当前水平',
value: _getLevelText(user?.profile?.englishLevel ?? EnglishLevel.beginner),
onTap: () => _showLevelDialog(),
),
],
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(
child: Text('加载失败: $error'),
),
);
});
}
/// 构建安全选项
Widget _buildSecurityOption({
required IconData icon,
required String title,
required String subtitle,
required VoidCallback onTap,
}) {
return ListTile(
leading: Icon(icon, color: AppColors.primary),
title: Text(
title,
style: AppTextStyles.bodyLarge.copyWith(
color: AppColors.onSurface,
),
),
subtitle: Text(
subtitle,
style: AppTextStyles.bodyMedium.copyWith(
color: AppColors.onSurfaceVariant,
),
),
trailing: Icon(
Icons.arrow_forward_ios,
size: 16,
color: AppColors.onSurfaceVariant,
),
onTap: onTap,
);
}
/// 构建偏好选项
Widget _buildPreferenceOption({
required IconData icon,
required String title,
required String subtitle,
required bool value,
required ValueChanged<bool> onChanged,
}) {
return ListTile(
leading: Icon(icon, color: AppColors.primary),
title: Text(
title,
style: AppTextStyles.bodyLarge.copyWith(
color: AppColors.onSurface,
),
),
subtitle: Text(
subtitle,
style: AppTextStyles.bodyMedium.copyWith(
color: AppColors.onSurfaceVariant,
),
),
trailing: Switch(
value: value,
onChanged: onChanged,
activeColor: AppColors.primary,
),
);
}
/// 构建目标选项
Widget _buildGoalOption({
required String title,
required String value,
required VoidCallback onTap,
}) {
return ListTile(
title: Text(
title,
style: AppTextStyles.bodyLarge.copyWith(
color: AppColors.onSurface,
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
value,
style: AppTextStyles.bodyMedium.copyWith(
color: AppColors.primary,
),
),
const SizedBox(width: 8),
Icon(
Icons.arrow_forward_ios,
size: 16,
color: AppColors.onSurfaceVariant,
),
],
),
onTap: onTap,
);
}
/// 构建水平选项
Widget _buildLevelOption({
required String title,
required String value,
required VoidCallback onTap,
}) {
return ListTile(
title: Text(
title,
style: AppTextStyles.bodyLarge.copyWith(
color: AppColors.onSurface,
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
value,
style: AppTextStyles.bodyMedium.copyWith(
color: AppColors.primary,
),
),
const SizedBox(width: 8),
Icon(
Icons.arrow_forward_ios,
size: 16,
color: AppColors.onSurfaceVariant,
),
],
),
onTap: onTap,
);
}
/// 处理头像更改
void _handleAvatarChange() {
// TODO: 实现头像更改功能
_showSnackBar('头像更改功能即将上线');
}
/// 处理保存个人信息
Future<void> _handleSaveProfile() async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
_isLoading = true;
});
try {
final authNotifier = await ref.read(authProvider.future);
await authNotifier.updateProfile(
username: _usernameController.text.trim(),
phone: _phoneController.text.trim(),
);
if (mounted) {
setState(() {
_isEditing = false;
});
_showSnackBar('个人信息更新成功', isSuccess: true);
}
} catch (e) {
if (mounted) {
_showSnackBar(e.toString());
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
/// 处理修改密码
Future<void> _handleChangePassword() async {
if (_currentPasswordController.text.isEmpty ||
_newPasswordController.text.isEmpty ||
_confirmPasswordController.text.isEmpty) {
_showSnackBar('请填写所有密码字段');
return;
}
if (_newPasswordController.text != _confirmPasswordController.text) {
_showSnackBar('两次输入的新密码不一致');
return;
}
setState(() {
_isLoading = true;
});
try {
final authNotifier = await ref.read(authProvider.future);
await authNotifier.changePassword(
currentPassword: _currentPasswordController.text,
newPassword: _newPasswordController.text,
);
if (mounted) {
_currentPasswordController.clear();
_newPasswordController.clear();
_confirmPasswordController.clear();
_showSnackBar('密码修改成功', isSuccess: true);
}
} catch (e) {
if (mounted) {
_showSnackBar(e.toString());
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
/// 更新设置
void _updateSetting(String key, bool value) {
// TODO: 实现设置更新
_showSnackBar('设置已更新', isSuccess: true);
}
/// 显示目标设置对话框
void _showGoalDialog(String type, int currentValue) {
// TODO: 实现目标设置对话框
_showSnackBar('目标设置功能即将上线');
}
/// 显示水平选择对话框
void _showLevelDialog() {
// TODO: 实现水平选择对话框
_showSnackBar('水平设置功能即将上线');
}
/// 获取水平文本
String _getLevelText(EnglishLevel level) {
switch (level) {
case EnglishLevel.beginner:
return '初级';
case EnglishLevel.elementary:
return '基础';
case EnglishLevel.intermediate:
return '中级';
case EnglishLevel.upperIntermediate:
return '中高级';
case EnglishLevel.advanced:
return '高级';
case EnglishLevel.proficient:
return '精通';
case EnglishLevel.expert:
return '专家';
}
}
/// 显示提示信息
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(AppDimensions.radiusSm),
),
),
);
}
}

View File

@@ -0,0 +1,519 @@
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),
),
),
);
}
}

View File

@@ -0,0 +1,419 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.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 '../providers/auth_provider.dart';
/// 重置密码页面
class ResetPasswordScreen extends StatefulWidget {
final String token;
final String email;
const ResetPasswordScreen({
super.key,
required this.token,
required this.email,
});
@override
State<ResetPasswordScreen> createState() => _ResetPasswordScreenState();
}
class _ResetPasswordScreenState extends State<ResetPasswordScreen> {
final _formKey = GlobalKey<FormState>();
final _passwordController = TextEditingController();
final _confirmPasswordController = TextEditingController();
bool _isLoading = false;
bool _obscurePassword = true;
bool _obscureConfirmPassword = true;
bool _resetSuccess = false;
@override
void dispose() {
_passwordController.dispose();
_confirmPasswordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.background,
appBar: AppBar(
backgroundColor: AppColors.surface,
elevation: 0,
title: Text(
'重置密码',
style: AppTextStyles.headlineSmall.copyWith(
color: AppColors.onSurface,
fontWeight: FontWeight.w600,
),
),
),
body: SafeArea(
child: _resetSuccess ? _buildSuccessView() : _buildResetForm(),
),
);
}
/// 构建重置密码表单
Widget _buildResetForm() {
return SingleChildScrollView(
padding: const EdgeInsets.all(AppDimensions.spacingXl),
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 标题和说明
Text(
'设置新密码',
style: AppTextStyles.headlineMedium.copyWith(
color: AppColors.onSurface,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: AppDimensions.spacingSm),
Text(
'为您的账户 ${widget.email} 设置一个新的安全密码',
style: AppTextStyles.bodyLarge.copyWith(
color: AppColors.onSurfaceVariant,
),
),
const SizedBox(height: AppDimensions.spacingXl),
// 新密码输入框
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'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)').hasMatch(value)) {
return '密码必须包含大小写字母和数字';
}
return null;
},
),
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),
// 密码强度提示
_buildPasswordStrengthIndicator(),
const SizedBox(height: AppDimensions.spacingXl),
// 重置密码按钮
CustomButton(
text: '重置密码',
onPressed: _handleResetPassword,
isLoading: _isLoading,
width: double.infinity,
),
const SizedBox(height: AppDimensions.spacingMd),
// 返回登录
Center(
child: TextButton(
onPressed: () {
Navigator.of(context).pushNamedAndRemoveUntil(
'/login',
(route) => false,
);
},
child: Text(
'返回登录',
style: AppTextStyles.labelLarge.copyWith(
color: AppColors.primary,
),
),
),
),
],
),
),
);
}
/// 构建成功视图
Widget _buildSuccessView() {
return Padding(
padding: const EdgeInsets.all(AppDimensions.spacingXl),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 成功图标
Container(
width: 120,
height: 120,
decoration: BoxDecoration(
color: AppColors.success.withOpacity(0.1),
shape: BoxShape.circle,
),
child: Icon(
Icons.check_circle,
size: 80,
color: AppColors.success,
),
),
const SizedBox(height: AppDimensions.spacingXl),
// 成功标题
Text(
'密码重置成功',
style: AppTextStyles.headlineMedium.copyWith(
color: AppColors.onSurface,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
const SizedBox(height: AppDimensions.spacingMd),
// 成功描述
Text(
'您的密码已成功重置,现在可以使用新密码登录您的账户了。',
style: AppTextStyles.bodyLarge.copyWith(
color: AppColors.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
const SizedBox(height: AppDimensions.spacingXl),
// 前往登录按钮
CustomButton(
text: '前往登录',
onPressed: () {
Navigator.of(context).pushNamedAndRemoveUntil(
'/login',
(route) => false,
);
},
width: double.infinity,
),
],
),
);
}
/// 构建密码强度指示器
Widget _buildPasswordStrengthIndicator() {
final password = _passwordController.text;
final strength = _calculatePasswordStrength(password);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'密码强度',
style: AppTextStyles.labelMedium.copyWith(
color: AppColors.onSurfaceVariant,
),
),
const SizedBox(height: AppDimensions.spacingXs),
// 强度条
Row(
children: List.generate(4, (index) {
Color color;
if (index < strength) {
switch (strength) {
case 1:
color = AppColors.error;
break;
case 2:
color = Colors.orange;
break;
case 3:
color = Colors.yellow;
break;
case 4:
color = AppColors.success;
break;
default:
color = AppColors.surfaceVariant;
}
} else {
color = AppColors.surfaceVariant;
}
return Expanded(
child: Container(
height: 4,
margin: EdgeInsets.only(
right: index < 3 ? AppDimensions.spacingXs : 0,
),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(2),
),
),
);
}),
),
const SizedBox(height: AppDimensions.spacingXs),
// 强度文本
Text(
_getStrengthText(strength),
style: AppTextStyles.labelSmall.copyWith(
color: _getStrengthColor(strength),
),
),
],
);
}
/// 计算密码强度
int _calculatePasswordStrength(String password) {
if (password.isEmpty) return 0;
int strength = 0;
// 长度检查
if (password.length >= 8) strength++;
// 包含小写字母
if (password.contains(RegExp(r'[a-z]'))) strength++;
// 包含大写字母
if (password.contains(RegExp(r'[A-Z]'))) strength++;
// 包含数字或特殊字符
if (password.contains(RegExp(r'[0-9!@#$%^&*(),.?":{}|<>]'))) strength++;
return strength;
}
/// 获取强度文本
String _getStrengthText(int strength) {
switch (strength) {
case 0:
case 1:
return '';
case 2:
return '一般';
case 3:
return '良好';
case 4:
return '';
default:
return '';
}
}
/// 获取强度颜色
Color _getStrengthColor(int strength) {
switch (strength) {
case 0:
case 1:
return AppColors.error;
case 2:
return Colors.orange;
case 3:
return Colors.yellow;
case 4:
return AppColors.success;
default:
return AppColors.onSurfaceVariant;
}
}
/// 处理重置密码
Future<void> _handleResetPassword() async {
if (!_formKey.currentState!.validate()) {
return;
}
setState(() {
_isLoading = true;
});
try {
final authProvider = Provider.of<AuthNotifier>(context, listen: false);
await authProvider.resetPassword(
token: widget.token,
newPassword: _passwordController.text,
);
if (mounted) {
setState(() {
_resetSuccess = true;
});
}
} catch (e) {
if (mounted) {
_showSnackBar(e.toString());
}
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
/// 显示提示信息
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),
),
),
);
}
}

View File

@@ -0,0 +1,149 @@
import 'package:flutter/material.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_text_styles.dart';
import '../../../core/theme/app_dimensions.dart';
/// 启动页面
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@override
State<SplashScreen> createState() => _SplashScreenState();
}
class _SplashScreenState extends State<SplashScreen>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _fadeAnimation;
late Animation<double> _scaleAnimation;
@override
void initState() {
super.initState();
_initAnimations();
_navigateToNext();
}
void _initAnimations() {
_animationController = AnimationController(
duration: const Duration(milliseconds: 2000),
vsync: this,
);
_fadeAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.0, 0.6, curve: Curves.easeIn),
));
_scaleAnimation = Tween<double>(
begin: 0.8,
end: 1.0,
).animate(CurvedAnimation(
parent: _animationController,
curve: const Interval(0.2, 0.8, curve: Curves.elasticOut),
));
_animationController.forward();
}
Future<void> _navigateToNext() async {
await Future.delayed(const Duration(seconds: 3));
if (mounted) {
// TODO: 检查用户登录状态,决定跳转到登录页还是主页
Navigator.of(context).pushReplacementNamed('/login');
}
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.primary,
body: Center(
child: AnimatedBuilder(
animation: _animationController,
builder: (context, child) {
return FadeTransition(
opacity: _fadeAnimation,
child: ScaleTransition(
scale: _scaleAnimation,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// 应用图标
Container(
width: AppDimensions.iconXxl * 2,
height: AppDimensions.iconXxl * 2,
decoration: BoxDecoration(
color: AppColors.onPrimary,
borderRadius: BorderRadius.circular(
AppDimensions.radiusXl,
),
boxShadow: [
BoxShadow(
color: AppColors.shadow.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Icon(
Icons.school,
size: AppDimensions.iconXxl,
color: AppColors.primary,
),
),
SizedBox(height: AppDimensions.spacingXl),
// 应用名称
Text(
'AI English Learning',
style: AppTextStyles.headlineLarge.copyWith(
color: AppColors.onPrimary,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: AppDimensions.spacingMd),
// 副标题
Text(
'智能英语学习助手',
style: AppTextStyles.bodyLarge.copyWith(
color: AppColors.onPrimary.withOpacity(0.8),
),
),
SizedBox(height: AppDimensions.spacingXxl),
// 加载指示器
SizedBox(
width: 40,
height: 40,
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
AppColors.onPrimary,
),
strokeWidth: 3,
),
),
],
),
),
);
},
),
),
);
}
}