blob: 19f13caa2184eebd06b4682d60c2a18ce4d498a4 [file] [log] [blame]
ZBD4b0e05a2025-06-08 18:11:26 +08001import React, { useState } from 'react';
2import {
3 Box, Container, Typography, Paper, TextField, Button, CircularProgress, Alert
4} from '@mui/material';
5// import { useNavigate } from 'react-router-dom'; // 如果需要重定向,请取消注释
6
7function LoginPage() {
8 const [username, setUsername] = useState('');
9 const [password, setPassword] = useState('');
10 const [loading, setLoading] = useState(false);
11 const [message, setMessage] = useState(null); // 用于显示成功或错误消息
12 const [messageSeverity, setMessageSeverity] = useState('info'); // 'success', 'error', 'info'
13
14 // const navigate = useNavigate(); // 如果需要重定向,请取消注释
15
16 const handleSubmit = async (event) => {
17 event.preventDefault(); // 阻止表单默认提交行为
18
19 setMessage(null); // 清除之前的消息
20 setLoading(true); // 显示加载指示器
21
22 try {
23 const response = await fetch('http://localhost:8080/api/auth/login', { // 确保这里的URL与您的后端接口一致
24 method: 'POST',
25 headers: {
26 'Content-Type': 'application/json'
27 },
28 body: JSON.stringify({ username, password })
29 });
30
31 const data = await response.json();
32
33 if (response.ok) {
34 // 登录成功
35 setMessage('登录成功!欢迎 ' + data.username);
36 setMessageSeverity('success');
37 console.log('登录成功:', data);
38 // 将 JWT Token 保存到 localStorage
39 localStorage.setItem('jwt_token', data.token);
40 // 可选:登录成功后重定向到其他页面
41 // navigate('/dashboard');
42 } else {
43 // 登录失败
44 const errorMessage = data.error || '未知错误';
45 setMessage('登录失败: ' + errorMessage);
46 setMessageSeverity('error');
47 console.error('登录失败:', data);
48 }
49 } catch (error) {
50 // 网络错误或请求发送失败
51 setMessage('网络错误,请稍后再试。');
52 setMessageSeverity('error');
53 console.error('请求发送失败:', error);
54 } finally {
55 setLoading(false); // 隐藏加载指示器
56 }
57 };
58
59 return (
60 <Box sx={{
61 minHeight: '100vh',
62 display: 'flex',
63 justifyContent: 'center',
64 alignItems: 'center',
65 background: 'linear-gradient(135deg, #2c3e50, #4ca1af)', // 与 Home 组件相同的背景
66 color: 'white',
67 py: 4 // 垂直内边距
68 }}>
69 <Container maxWidth="xs" sx={{ position: 'relative', zIndex: 10 }}>
70 <Typography variant="h4" align="center" sx={{ mb: 4, fontWeight: 'bold' }}>
71 🔐 用户登录
72 </Typography>
73
74 <Paper sx={{
75 p: 4, // 内边距
76 backgroundColor: 'rgba(30, 30, 30, 0.9)', // 半透明深色背景
77 borderRadius: '12px', // 圆角
78 boxShadow: '0 8px 16px rgba(0,0,0,0.3)' // 阴影
79 }}>
80 {message && (
81 <Alert severity={messageSeverity} sx={{ mb: 2, borderRadius: '8px' }}>
82 {message}
83 </Alert>
84 )}
85
86 <form onSubmit={handleSubmit}>
87 <TextField
88 label="用户名"
89 variant="outlined"
90 fullWidth
91 margin="normal"
92 value={username}
93 onChange={(e) => setUsername(e.target.value)}
94 required
95 sx={{
96 mb: 2, // 底部外边距
97 '& .MuiOutlinedInput-root': {
98 borderRadius: '8px',
99 color: 'white', // 输入文字颜色
100 '& fieldset': { borderColor: 'rgba(255, 255, 255, 0.3)' }, // 边框颜色
101 '&:hover fieldset': { borderColor: 'rgba(255, 255, 255, 0.5)' },
102 '&.Mui-focused fieldset': { borderColor: '#8b5cf6' }, // 聚焦时边框颜色
103 },
104 '& .MuiInputLabel-root': { color: '#bbb' }, // 标签颜色
105 '& .MuiInputLabel-root.Mui-focused': { color: '#8b5cf6' }, // 聚焦时标签颜色
106 }}
107 />
108 <TextField
109 label="密码"
110 type="password"
111 variant="outlined"
112 fullWidth
113 margin="normal"
114 value={password}
115 onChange={(e) => setPassword(e.target.value)}
116 required
117 sx={{
118 mb: 3, // 底部外边距
119 '& .MuiOutlinedInput-root': {
120 borderRadius: '8px',
121 color: 'white', // 输入文字颜色
122 '& fieldset': { borderColor: 'rgba(255, 255, 255, 0.3)' },
123 '&:hover fieldset': { borderColor: 'rgba(255, 255, 255, 0.5)' },
124 '&.Mui-focused fieldset': { borderColor: '#8b5cf6' },
125 },
126 '& .MuiInputLabel-root': { color: '#bbb' },
127 '& .MuiInputLabel-root.Mui-focused': { color: '#8b5cf6' },
128 }}
129 />
130 <Button
131 type="submit"
132 variant="contained"
133 fullWidth
134 disabled={loading}
135 sx={{
136 py: '0.75rem', // 垂直内边距
137 backgroundImage: 'linear-gradient(to right, #6366f1, #8b5cf6)', // 渐变背景
138 color: 'white',
139 borderRadius: '0.375rem', // 圆角
140 fontWeight: 600,
141 position: 'relative', // 用于加载指示器定位
142 '&:hover': {
143 backgroundImage: 'linear-gradient(to right, #4f46e5, #7c3aed)',
144 },
145 }}
146 >
147 {loading ? <CircularProgress size={24} color="inherit" sx={{ position: 'absolute' }} /> : '登录'}
148 </Button>
149 </form>
150 </Paper>
151 </Container>
152
153 {/* 背景泡泡动画 - 复用 Home 组件中的样式 */}
154 <Box className="bubbles" sx={{
155 pointerEvents: 'none',
156 position: 'fixed',
157 top: 0,
158 left: 0,
159 width: '100%',
160 height: '100%',
161 overflow: 'hidden',
162 zIndex: 1,
163 }}>
164 {[...Array(40)].map((_, i) => (
165 <Box
166 key={i}
167 className="bubble"
168 sx={{
169 position: 'absolute',
170 bottom: '-150px', // 确保从屏幕外开始
171 background: `hsla(${Math.random() * 360}, 70%, 80%, 0.15)`, // 增加颜色多样性
172 borderRadius: '50%',
173 animation: 'rise 20s infinite ease-in',
174 width: `${Math.random() * 25 + 10}px`, // 调整大小范围
175 height: `${Math.random() * 25 + 10}px`,
176 left: `${Math.random() * 100}%`,
177 animationDuration: `${15 + Math.random() * 20}s`, // 调整动画时长
178 animationDelay: `${Math.random() * 10}s`, // 调整动画延迟
179 opacity: 0, // 初始透明
180 }}
181 />
182 ))}
183 </Box>
184 {/* 定义动画和其他全局可能需要的样式 */}
185 <style>
186 {`
187 body { /* 基本重置 */
188 margin: 0;
189 font-family: 'Roboto', sans-serif; /* 确保字体一致 */
190 }
191
192 @keyframes rise {
193 0% {
194 transform: translateY(0) scale(0.8);
195 opacity: 0; /* 从透明开始 */
196 }
197 10% {
198 opacity: 1; /* 渐显 */
199 }
200 90% {
201 opacity: 1; /* 保持可见 */
202 }
203 100% {
204 transform: translateY(-130vh) scale(0.3); /* 飘得更高更小 */
205 opacity: 0; /* 渐隐 */
206 }
207 }
208 `}
209 </style>
210 </Box>
211 );
212}
213
214export default LoginPage;