init
This commit is contained in:
312
client/lib/features/auth/providers/auth_provider.dart
Normal file
312
client/lib/features/auth/providers/auth_provider.dart
Normal file
@@ -0,0 +1,312 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../../../core/models/user_model.dart';
|
||||
import '../../../core/services/auth_service.dart';
|
||||
import '../../../core/services/storage_service.dart';
|
||||
import '../../../core/network/api_client.dart';
|
||||
import '../../../core/errors/app_exception.dart';
|
||||
|
||||
/// 认证状态
|
||||
class AuthState {
|
||||
final User? user;
|
||||
final bool isAuthenticated;
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
|
||||
const AuthState({
|
||||
this.user,
|
||||
this.isAuthenticated = false,
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
});
|
||||
|
||||
AuthState copyWith({
|
||||
User? user,
|
||||
bool? isAuthenticated,
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
}) {
|
||||
return AuthState(
|
||||
user: user ?? this.user,
|
||||
isAuthenticated: isAuthenticated ?? this.isAuthenticated,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: error,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 认证状态管理器
|
||||
class AuthNotifier extends StateNotifier<AuthState> {
|
||||
final AuthService _authService;
|
||||
final StorageService _storageService;
|
||||
|
||||
AuthNotifier(this._authService, this._storageService) : super(const AuthState()) {
|
||||
_checkAuthStatus();
|
||||
}
|
||||
|
||||
/// 检查认证状态
|
||||
Future<void> _checkAuthStatus() async {
|
||||
try {
|
||||
final token = await _storageService.getToken();
|
||||
if (token != null && token.isNotEmpty) {
|
||||
// 获取真实用户信息
|
||||
try {
|
||||
final user = await _authService.getUserInfo();
|
||||
state = state.copyWith(
|
||||
user: user,
|
||||
isAuthenticated: true,
|
||||
);
|
||||
} catch (e) {
|
||||
// Token可能已过期,清除token
|
||||
await _storageService.clearTokens();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略错误,保持未认证状态
|
||||
}
|
||||
}
|
||||
|
||||
/// 登录
|
||||
Future<void> login({
|
||||
required String account, // 用户名或邮箱
|
||||
required String password,
|
||||
bool rememberMe = false,
|
||||
}) async {
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
|
||||
try {
|
||||
// 调用真实API
|
||||
final authResponse = await _authService.login(
|
||||
account: account,
|
||||
password: password,
|
||||
rememberMe: rememberMe,
|
||||
);
|
||||
|
||||
// 保存token
|
||||
await _storageService.saveToken(authResponse.token);
|
||||
if (rememberMe && authResponse.refreshToken != null) {
|
||||
await _storageService.saveRefreshToken(authResponse.refreshToken!);
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
state = state.copyWith(
|
||||
user: authResponse.user,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
);
|
||||
} catch (e) {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: e.toString(),
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 注册
|
||||
Future<void> register({
|
||||
required String email,
|
||||
required String password,
|
||||
required String username,
|
||||
required String nickname,
|
||||
}) async {
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
|
||||
try {
|
||||
// 调用真实API
|
||||
final authResponse = await _authService.register(
|
||||
email: email,
|
||||
password: password,
|
||||
username: username,
|
||||
nickname: nickname,
|
||||
);
|
||||
|
||||
// 保存token
|
||||
await _storageService.saveToken(authResponse.token);
|
||||
if (authResponse.refreshToken != null) {
|
||||
await _storageService.saveRefreshToken(authResponse.refreshToken!);
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
state = state.copyWith(
|
||||
user: authResponse.user,
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
);
|
||||
} catch (e) {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: e.toString(),
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 登出
|
||||
Future<void> logout() async {
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
|
||||
try {
|
||||
// 模拟网络延迟
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
await _storageService.clearTokens();
|
||||
|
||||
state = const AuthState();
|
||||
} catch (e) {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: e.toString(),
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 忘记密码
|
||||
Future<void> forgotPassword(String email) async {
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
|
||||
try {
|
||||
// 模拟网络延迟
|
||||
await Future.delayed(const Duration(milliseconds: 1000));
|
||||
|
||||
if (email.isEmpty) {
|
||||
throw Exception('邮箱地址不能为空');
|
||||
}
|
||||
|
||||
// 模拟发送重置邮件成功
|
||||
state = state.copyWith(isLoading: false);
|
||||
} catch (e) {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: e.toString(),
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 重置密码
|
||||
Future<void> resetPassword({
|
||||
required String token,
|
||||
required String newPassword,
|
||||
}) async {
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
|
||||
try {
|
||||
// 模拟网络延迟
|
||||
await Future.delayed(const Duration(milliseconds: 1000));
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
throw Exception('密码长度不能少于6位');
|
||||
}
|
||||
|
||||
state = state.copyWith(isLoading: false);
|
||||
} catch (e) {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: e.toString(),
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新用户信息
|
||||
Future<void> updateProfile({
|
||||
String? username,
|
||||
String? email,
|
||||
String? phone,
|
||||
String? avatar,
|
||||
}) async {
|
||||
if (state.user == null) return;
|
||||
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
|
||||
try {
|
||||
// 模拟网络延迟
|
||||
await Future.delayed(const Duration(milliseconds: 1000));
|
||||
|
||||
final updatedUser = state.user!.copyWith(
|
||||
username: username ?? state.user!.username,
|
||||
email: email ?? state.user!.email,
|
||||
phone: phone ?? state.user!.phone,
|
||||
avatar: avatar ?? state.user!.avatar,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
state = state.copyWith(
|
||||
user: updatedUser,
|
||||
isLoading: false,
|
||||
);
|
||||
} catch (e) {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: e.toString(),
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 修改密码
|
||||
Future<void> changePassword({
|
||||
required String currentPassword,
|
||||
required String newPassword,
|
||||
}) async {
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
|
||||
try {
|
||||
// 模拟网络延迟
|
||||
await Future.delayed(const Duration(milliseconds: 1000));
|
||||
|
||||
if (currentPassword.isEmpty || newPassword.isEmpty) {
|
||||
throw Exception('当前密码和新密码不能为空');
|
||||
}
|
||||
|
||||
if (newPassword.length < 6) {
|
||||
throw Exception('新密码长度不能少于6位');
|
||||
}
|
||||
|
||||
state = state.copyWith(isLoading: false);
|
||||
} catch (e) {
|
||||
state = state.copyWith(
|
||||
isLoading: false,
|
||||
error: e.toString(),
|
||||
);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// 清除错误
|
||||
void clearError() {
|
||||
state = state.copyWith(error: null);
|
||||
}
|
||||
}
|
||||
|
||||
/// 认证服务提供者
|
||||
final authServiceProvider = Provider<AuthService>((ref) {
|
||||
return AuthService(ApiClient.instance);
|
||||
});
|
||||
|
||||
/// 存储服务提供者 - 改为同步Provider,因为StorageService已在main中初始化
|
||||
final storageServiceProvider = Provider<StorageService>((ref) {
|
||||
// StorageService已经在main.dart中初始化,直接获取实例
|
||||
// 这里使用一个技巧:通过Future.value包装已初始化的实例
|
||||
throw UnimplementedError('Use storageServiceInstanceProvider instead');
|
||||
});
|
||||
|
||||
/// 认证状态提供者 - 改为StateNotifierProvider以保持状态
|
||||
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
|
||||
final authService = ref.watch(authServiceProvider);
|
||||
// 直接使用已初始化的StorageService实例
|
||||
final storageService = StorageService.instance;
|
||||
|
||||
return AuthNotifier(authService, storageService);
|
||||
});
|
||||
|
||||
/// 是否已认证的提供者
|
||||
final isAuthenticatedProvider = Provider<bool>((ref) {
|
||||
return ref.watch(authProvider).isAuthenticated;
|
||||
});
|
||||
|
||||
/// 当前用户提供者
|
||||
final currentUserProvider = Provider<User?>((ref) {
|
||||
return ref.watch(authProvider).user;
|
||||
});
|
||||
273
client/lib/features/auth/screens/forgot_password_screen.dart
Normal file
273
client/lib/features/auth/screens/forgot_password_screen.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
384
client/lib/features/auth/screens/login_screen.dart
Normal file
384
client/lib/features/auth/screens/login_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
768
client/lib/features/auth/screens/profile_screen.dart
Normal file
768
client/lib/features/auth/screens/profile_screen.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
519
client/lib/features/auth/screens/register_screen.dart
Normal file
519
client/lib/features/auth/screens/register_screen.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
419
client/lib/features/auth/screens/reset_password_screen.dart
Normal file
419
client/lib/features/auth/screens/reset_password_screen.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
149
client/lib/features/auth/screens/splash_screen.dart
Normal file
149
client/lib/features/auth/screens/splash_screen.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user