二次开发源码提交
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import 'package:cloudreve4_flutter/presentation/widgets/desktop_constrained.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import '../../../core/validators/string_validator.dart';
|
||||
import '../../../services/auth_service.dart';
|
||||
import '../../widgets/toast_helper.dart';
|
||||
|
||||
class ForgotPasswordPage extends StatefulWidget {
|
||||
const ForgotPasswordPage({super.key});
|
||||
|
||||
@override
|
||||
State<ForgotPasswordPage> createState() => _ForgotPasswordPageState();
|
||||
}
|
||||
|
||||
class _ForgotPasswordPageState extends State<ForgotPasswordPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _emailController = TextEditingController();
|
||||
bool _isLoading = false;
|
||||
String? _errorMessage;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _sendResetEmail() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
try {
|
||||
await AuthService.instance.sendResetPasswordEmail(
|
||||
email: _emailController.text.trim(),
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
ToastHelper.success('重置密码邮件已发送,请查收邮箱');
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() => _errorMessage = e.toString());
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('忘记密码')),
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: DesktopConstrained(
|
||||
maxContentWidth: 480,
|
||||
child: Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
'请输入您的邮箱地址,我们将向您发送重置密码的邮件。',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: theme.hintColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: StringValidator.validateEmail,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '邮箱',
|
||||
hintText: '请输入邮箱地址',
|
||||
prefixIcon: Icon(LucideIcons.mail),
|
||||
),
|
||||
),
|
||||
if (_errorMessage != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
_errorMessage!,
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onErrorContainer,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
FilledButton(
|
||||
onPressed: _isLoading ? null : _sendResetEmail,
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('发送重置邮件'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
import 'package:cloudreve4_flutter/presentation/widgets/desktop_constrained.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../../../core/exceptions/app_exception.dart';
|
||||
import '../../../core/validators/string_validator.dart';
|
||||
import '../../providers/auth_provider.dart';
|
||||
import '../../../services/server_service.dart';
|
||||
import '../../../router/app_router.dart';
|
||||
import 'forgot_password_page.dart';
|
||||
import 'register_page.dart';
|
||||
import '../../widgets/toast_helper.dart';
|
||||
|
||||
class LoginPage extends StatefulWidget {
|
||||
const LoginPage({super.key});
|
||||
|
||||
@override
|
||||
State<LoginPage> createState() => _LoginPageState();
|
||||
}
|
||||
|
||||
class _LoginPageState extends State<LoginPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final _focusNode = FocusNode();
|
||||
bool _obscurePassword = true;
|
||||
bool _rememberMe = false;
|
||||
bool _isLoading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadRememberedInfo();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadRememberedInfo() async {
|
||||
final server = ServerService.instance.currentServer;
|
||||
if (server != null) {
|
||||
setState(() {
|
||||
if (server.email != null) {
|
||||
_emailController.text = server.email!;
|
||||
}
|
||||
if (server.password != null && server.rememberMe) {
|
||||
_passwordController.text = server.password!;
|
||||
}
|
||||
_rememberMe = server.rememberMe;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _login() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
final navigator = Navigator.of(context);
|
||||
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
|
||||
try {
|
||||
final success = await authProvider.passwordLogin(
|
||||
email: _emailController.text.trim(),
|
||||
password: _passwordController.text,
|
||||
rememberMe: _rememberMe,
|
||||
).timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => throw Exception('请求超时'),
|
||||
);
|
||||
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
|
||||
if (success && mounted) {
|
||||
_focusNode.unfocus();
|
||||
ToastHelper.success('登录成功');
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
if (mounted) navigator.pushReplacementNamed(RouteNames.home);
|
||||
} else if (mounted) {
|
||||
final errorMessage = authProvider.errorMessage;
|
||||
if (errorMessage != null && errorMessage.isNotEmpty) {
|
||||
final errorMsg = _parseErrorMessage(errorMessage);
|
||||
ToastHelper.failure(errorMsg);
|
||||
} else {
|
||||
ToastHelper.failure('登录失败');
|
||||
}
|
||||
}
|
||||
} on TwoFactorRequiredException catch (e) {
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
_showTwoFactorDialog(e.sessionId);
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
final errorMsg = _parseErrorMessage(e.toString());
|
||||
ToastHelper.failure(errorMsg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showTwoFactorDialog(String sessionId) async {
|
||||
final success = await showDialog<bool>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => _TwoFactorDialog(
|
||||
sessionId: sessionId,
|
||||
email: _emailController.text.trim(),
|
||||
password: _passwordController.text,
|
||||
rememberMe: _rememberMe,
|
||||
),
|
||||
);
|
||||
|
||||
if (success != true || !mounted) return;
|
||||
|
||||
_focusNode.unfocus();
|
||||
ToastHelper.success('登录成功');
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
if (mounted) Navigator.of(context).pushReplacementNamed(RouteNames.home);
|
||||
}
|
||||
|
||||
String _parseErrorMessage(String error) {
|
||||
if (error.startsWith('Exception(') || error.startsWith('AppException(')) {
|
||||
final startIdx = error.indexOf('(');
|
||||
final endIdx = error.lastIndexOf(')');
|
||||
if (startIdx != -1 && endIdx != -1 && endIdx > startIdx) {
|
||||
return error.substring(startIdx + 1, endIdx).trim();
|
||||
}
|
||||
}
|
||||
if (error.contains(':')) {
|
||||
final parts = error.split(':');
|
||||
if (parts.length > 1) {
|
||||
final msg = parts.sublist(1).join(':').trim();
|
||||
if (msg.isNotEmpty) return '登录失败: $msg';
|
||||
}
|
||||
}
|
||||
if (error.contains('"') && error.split('"').length >= 2) {
|
||||
final parts = error.split('"');
|
||||
if (parts.length >= 2) {
|
||||
final msg = parts[1].trim();
|
||||
if (msg.isNotEmpty && msg != 'login') return '登录失败: $msg';
|
||||
}
|
||||
}
|
||||
return error.isEmpty ? '登录失败: 未知原因' : '登录失败: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: DesktopConstrained(
|
||||
maxContentWidth: 480,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Center(child: _buildLogo()),
|
||||
const SizedBox(height: 32),
|
||||
Center(
|
||||
child: Text(
|
||||
'公云存储',
|
||||
style: theme.textTheme.headlineMedium,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// 邮箱
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
textInputAction: TextInputAction.next,
|
||||
validator: StringValidator.validateEmail,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '邮箱',
|
||||
hintText: '请输入邮箱地址',
|
||||
prefixIcon: Icon(LucideIcons.mail),
|
||||
),
|
||||
onFieldSubmitted: (_) => _focusNode.requestFocus(),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 密码
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
focusNode: _focusNode,
|
||||
obscureText: _obscurePassword,
|
||||
validator: StringValidator.validatePassword,
|
||||
decoration: InputDecoration(
|
||||
labelText: '密码',
|
||||
hintText: '请输入密码',
|
||||
prefixIcon: const Icon(LucideIcons.lock),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscurePassword
|
||||
? LucideIcons.eye
|
||||
: LucideIcons.eyeOff,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () {
|
||||
setState(() => _obscurePassword = !_obscurePassword);
|
||||
},
|
||||
),
|
||||
),
|
||||
onFieldSubmitted: (_) => _login(),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 记住我
|
||||
InkWell(
|
||||
onTap: () => setState(() => _rememberMe = !_rememberMe),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: Checkbox(
|
||||
value: _rememberMe,
|
||||
onChanged: (v) => setState(() => _rememberMe = v ?? false),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const Text('记住我'),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// 链接按钮
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const ForgotPasswordPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('忘记密码?'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const RegisterPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('注册账号'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 登录按钮
|
||||
FilledButton(
|
||||
onPressed: _isLoading ? null : _login,
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('登录'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLogo() {
|
||||
return ClipOval(
|
||||
child: Image.asset(
|
||||
'assets/images/app_logo.png',
|
||||
width: 96,
|
||||
height: 96,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 两步验证输入对话框
|
||||
class _TwoFactorDialog extends StatefulWidget {
|
||||
final String sessionId;
|
||||
final String email;
|
||||
final String password;
|
||||
final bool rememberMe;
|
||||
|
||||
const _TwoFactorDialog({
|
||||
required this.sessionId,
|
||||
required this.email,
|
||||
required this.password,
|
||||
required this.rememberMe,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_TwoFactorDialog> createState() => _TwoFactorDialogState();
|
||||
}
|
||||
|
||||
class _TwoFactorDialogState extends State<_TwoFactorDialog>
|
||||
with SingleTickerProviderStateMixin {
|
||||
final _controller = TextEditingController();
|
||||
final _focusNode = FocusNode();
|
||||
bool _isSubmitting = false;
|
||||
late final AnimationController _shakeController;
|
||||
late final Animation<double> _shakeAnimation;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_shakeController = AnimationController(
|
||||
duration: const Duration(milliseconds: 400),
|
||||
vsync: this,
|
||||
);
|
||||
_shakeAnimation = TweenSequence<double>([
|
||||
TweenSequenceItem(tween: Tween(begin: 0, end: 10), weight: 1),
|
||||
TweenSequenceItem(tween: Tween(begin: 10, end: -10), weight: 1),
|
||||
TweenSequenceItem(tween: Tween(begin: -10, end: 8), weight: 1),
|
||||
TweenSequenceItem(tween: Tween(begin: 8, end: -8), weight: 1),
|
||||
TweenSequenceItem(tween: Tween(begin: -8, end: 4), weight: 1),
|
||||
TweenSequenceItem(tween: Tween(begin: 4, end: 0), weight: 1),
|
||||
]).animate(CurvedAnimation(
|
||||
parent: _shakeController,
|
||||
curve: Curves.easeInOut,
|
||||
));
|
||||
_controller.addListener(_onTextChanged);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_focusNode.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.removeListener(_onTextChanged);
|
||||
_controller.dispose();
|
||||
_focusNode.dispose();
|
||||
_shakeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onTextChanged() {
|
||||
if (_controller.text.length == 6 && !_isSubmitting) {
|
||||
_submit();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _submit() async {
|
||||
final code = _controller.text.trim();
|
||||
if (code.length != 6 || int.tryParse(code) == null) return;
|
||||
|
||||
setState(() => _isSubmitting = true);
|
||||
|
||||
final authProvider = Provider.of<AuthProvider>(context, listen: false);
|
||||
try {
|
||||
final success = await authProvider.twoFactorLogin(
|
||||
otp: code,
|
||||
sessionId: widget.sessionId,
|
||||
email: widget.email,
|
||||
password: widget.password,
|
||||
rememberMe: widget.rememberMe,
|
||||
).timeout(
|
||||
const Duration(seconds: 5),
|
||||
onTimeout: () => throw Exception('请求超时'),
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (success) {
|
||||
Navigator.of(context).pop(true);
|
||||
} else {
|
||||
_onVerifyFailed(authProvider.errorMessage ?? '验证码错误');
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) _onVerifyFailed(_parse2FAError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
void _onVerifyFailed(String message) {
|
||||
setState(() => _isSubmitting = false);
|
||||
_controller.clear();
|
||||
_focusNode.requestFocus();
|
||||
_shakeController.forward(from: 0);
|
||||
ToastHelper.failure(message);
|
||||
}
|
||||
|
||||
String _parse2FAError(String error) {
|
||||
if (error.startsWith('Exception(') || error.startsWith('AppException(')) {
|
||||
final startIdx = error.indexOf('(');
|
||||
final endIdx = error.lastIndexOf(')');
|
||||
if (startIdx != -1 && endIdx != -1 && endIdx > startIdx) {
|
||||
return error.substring(startIdx + 1, endIdx).trim();
|
||||
}
|
||||
}
|
||||
return error.isEmpty ? '验证码错误' : error;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return AlertDialog(
|
||||
title: Row(
|
||||
children: [
|
||||
Icon(LucideIcons.shieldCheck, color: theme.colorScheme.primary),
|
||||
const SizedBox(width: 8),
|
||||
const Text('两步验证'),
|
||||
],
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'请输入身份验证器中的6位数字验证码',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
AnimatedBuilder(
|
||||
animation: _shakeAnimation,
|
||||
builder: (context, child) {
|
||||
return Transform.translate(
|
||||
offset: Offset(_shakeAnimation.value, 0),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: TextField(
|
||||
controller: _controller,
|
||||
focusNode: _focusNode,
|
||||
keyboardType: TextInputType.number,
|
||||
maxLength: 6,
|
||||
textAlign: TextAlign.center,
|
||||
enabled: !_isSubmitting,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 8,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
counterText: '',
|
||||
hintText: '------',
|
||||
hintStyle: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 8,
|
||||
color: theme.colorScheme.outline.withValues(alpha: 0.4),
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
],
|
||||
onSubmitted: (_) => _submit(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: _isSubmitting ? null : () => Navigator.of(context).pop(false),
|
||||
child: const Text('取消'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: _isSubmitting ? null : _submit,
|
||||
child: _isSubmitting
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('验证'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import 'package:cloudreve4_flutter/presentation/widgets/desktop_constrained.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import '../../../core/validators/string_validator.dart';
|
||||
import '../../../services/auth_service.dart';
|
||||
import '../../widgets/toast_helper.dart';
|
||||
|
||||
class RegisterPage extends StatefulWidget {
|
||||
const RegisterPage({super.key});
|
||||
|
||||
@override
|
||||
State<RegisterPage> createState() => _RegisterPageState();
|
||||
}
|
||||
|
||||
class _RegisterPageState extends State<RegisterPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _emailController = TextEditingController();
|
||||
final _passwordController = TextEditingController();
|
||||
final _confirmPasswordController = TextEditingController();
|
||||
bool _obscurePassword = true;
|
||||
bool _obscureConfirmPassword = true;
|
||||
bool _isLoading = false;
|
||||
String? _errorMessage;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_emailController.dispose();
|
||||
_passwordController.dispose();
|
||||
_confirmPasswordController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _register() async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
|
||||
if (_passwordController.text != _confirmPasswordController.text) {
|
||||
setState(() => _errorMessage = '两次输入的密码不一致');
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final response = await AuthService.instance.signUp(
|
||||
email: _emailController.text.trim(),
|
||||
password: _passwordController.text,
|
||||
language: 'zh-CN',
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
ToastHelper.success(
|
||||
response.requiresEmailActivation ? '注册成功,请查收邮箱进行验证' : '注册成功',
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() => _errorMessage = e.toString());
|
||||
}
|
||||
} finally {
|
||||
if (mounted) setState(() => _isLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('注册')),
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: DesktopConstrained(
|
||||
maxContentWidth: 480,
|
||||
child: Card(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextFormField(
|
||||
controller: _emailController,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: StringValidator.validateEmail,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '邮箱',
|
||||
hintText: '请输入邮箱地址',
|
||||
prefixIcon: Icon(LucideIcons.mail),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _passwordController,
|
||||
obscureText: _obscurePassword,
|
||||
validator: StringValidator.validatePassword,
|
||||
decoration: InputDecoration(
|
||||
labelText: '密码',
|
||||
hintText: '请输入密码(至少6位)',
|
||||
prefixIcon: const Icon(LucideIcons.lock),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscurePassword ? LucideIcons.eye : LucideIcons.eyeOff,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () => setState(() => _obscurePassword = !_obscurePassword),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
controller: _confirmPasswordController,
|
||||
obscureText: _obscureConfirmPassword,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) return '请确认密码';
|
||||
if (value != _passwordController.text) return '两次输入的密码不一致';
|
||||
return null;
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: '确认密码',
|
||||
hintText: '请再次输入密码',
|
||||
prefixIcon: const Icon(LucideIcons.lock),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
_obscureConfirmPassword ? LucideIcons.eye : LucideIcons.eyeOff,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () => setState(() => _obscureConfirmPassword = !_obscureConfirmPassword),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_errorMessage != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
_errorMessage!,
|
||||
style: TextStyle(
|
||||
color: theme.colorScheme.onErrorContainer,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
FilledButton(
|
||||
onPressed: _isLoading ? null : _register,
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Text('注册'),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('已有账号?去登录'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user