二次开发源码提交

This commit is contained in:
2026-05-15 08:52:48 +08:00
parent 4131f7321a
commit eb35a1d067
191 changed files with 34566 additions and 0 deletions
+522
View File
@@ -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('验证'),
),
],
);
}
}