| import React, { useState } from 'react'; |
| import { |
| Box, Container, Typography, Paper, TextField, Button, CircularProgress, Alert |
| } from '@mui/material'; |
| // import { useNavigate } from 'react-router-dom'; // 如果需要重定向,请取消注释 |
| |
| function LoginPage() { |
| const [username, setUsername] = useState(''); |
| const [password, setPassword] = useState(''); |
| const [loading, setLoading] = useState(false); |
| const [message, setMessage] = useState(null); // 用于显示成功或错误消息 |
| const [messageSeverity, setMessageSeverity] = useState('info'); // 'success', 'error', 'info' |
| |
| // const navigate = useNavigate(); // 如果需要重定向,请取消注释 |
| |
| const handleSubmit = async (event) => { |
| event.preventDefault(); // 阻止表单默认提交行为 |
| |
| setMessage(null); // 清除之前的消息 |
| setLoading(true); // 显示加载指示器 |
| |
| try { |
| const response = await fetch('http://localhost:8080/api/auth/login', { // 确保这里的URL与您的后端接口一致 |
| method: 'POST', |
| headers: { |
| 'Content-Type': 'application/json' |
| }, |
| body: JSON.stringify({ username, password }) |
| }); |
| |
| const data = await response.json(); |
| |
| if (response.ok) { |
| // 登录成功 |
| setMessage('登录成功!欢迎 ' + data.username); |
| setMessageSeverity('success'); |
| console.log('登录成功:', data); |
| // 将 JWT Token 保存到 localStorage |
| localStorage.setItem('jwt_token', data.token); |
| // 可选:登录成功后重定向到其他页面 |
| // navigate('/dashboard'); |
| } else { |
| // 登录失败 |
| const errorMessage = data.error || '未知错误'; |
| setMessage('登录失败: ' + errorMessage); |
| setMessageSeverity('error'); |
| console.error('登录失败:', data); |
| } |
| } catch (error) { |
| // 网络错误或请求发送失败 |
| setMessage('网络错误,请稍后再试。'); |
| setMessageSeverity('error'); |
| console.error('请求发送失败:', error); |
| } finally { |
| setLoading(false); // 隐藏加载指示器 |
| } |
| }; |
| |
| return ( |
| <Box sx={{ |
| minHeight: '100vh', |
| display: 'flex', |
| justifyContent: 'center', |
| alignItems: 'center', |
| background: 'linear-gradient(135deg, #2c3e50, #4ca1af)', // 与 Home 组件相同的背景 |
| color: 'white', |
| py: 4 // 垂直内边距 |
| }}> |
| <Container maxWidth="xs" sx={{ position: 'relative', zIndex: 10 }}> |
| <Typography variant="h4" align="center" sx={{ mb: 4, fontWeight: 'bold' }}> |
| 🔐 用户登录 |
| </Typography> |
| |
| <Paper sx={{ |
| p: 4, // 内边距 |
| backgroundColor: 'rgba(30, 30, 30, 0.9)', // 半透明深色背景 |
| borderRadius: '12px', // 圆角 |
| boxShadow: '0 8px 16px rgba(0,0,0,0.3)' // 阴影 |
| }}> |
| {message && ( |
| <Alert severity={messageSeverity} sx={{ mb: 2, borderRadius: '8px' }}> |
| {message} |
| </Alert> |
| )} |
| |
| <form onSubmit={handleSubmit}> |
| <TextField |
| label="用户名" |
| variant="outlined" |
| fullWidth |
| margin="normal" |
| value={username} |
| onChange={(e) => setUsername(e.target.value)} |
| required |
| sx={{ |
| mb: 2, // 底部外边距 |
| '& .MuiOutlinedInput-root': { |
| borderRadius: '8px', |
| color: 'white', // 输入文字颜色 |
| '& fieldset': { borderColor: 'rgba(255, 255, 255, 0.3)' }, // 边框颜色 |
| '&:hover fieldset': { borderColor: 'rgba(255, 255, 255, 0.5)' }, |
| '&.Mui-focused fieldset': { borderColor: '#8b5cf6' }, // 聚焦时边框颜色 |
| }, |
| '& .MuiInputLabel-root': { color: '#bbb' }, // 标签颜色 |
| '& .MuiInputLabel-root.Mui-focused': { color: '#8b5cf6' }, // 聚焦时标签颜色 |
| }} |
| /> |
| <TextField |
| label="密码" |
| type="password" |
| variant="outlined" |
| fullWidth |
| margin="normal" |
| value={password} |
| onChange={(e) => setPassword(e.target.value)} |
| required |
| sx={{ |
| mb: 3, // 底部外边距 |
| '& .MuiOutlinedInput-root': { |
| borderRadius: '8px', |
| color: 'white', // 输入文字颜色 |
| '& fieldset': { borderColor: 'rgba(255, 255, 255, 0.3)' }, |
| '&:hover fieldset': { borderColor: 'rgba(255, 255, 255, 0.5)' }, |
| '&.Mui-focused fieldset': { borderColor: '#8b5cf6' }, |
| }, |
| '& .MuiInputLabel-root': { color: '#bbb' }, |
| '& .MuiInputLabel-root.Mui-focused': { color: '#8b5cf6' }, |
| }} |
| /> |
| <Button |
| type="submit" |
| variant="contained" |
| fullWidth |
| disabled={loading} |
| sx={{ |
| py: '0.75rem', // 垂直内边距 |
| backgroundImage: 'linear-gradient(to right, #6366f1, #8b5cf6)', // 渐变背景 |
| color: 'white', |
| borderRadius: '0.375rem', // 圆角 |
| fontWeight: 600, |
| position: 'relative', // 用于加载指示器定位 |
| '&:hover': { |
| backgroundImage: 'linear-gradient(to right, #4f46e5, #7c3aed)', |
| }, |
| }} |
| > |
| {loading ? <CircularProgress size={24} color="inherit" sx={{ position: 'absolute' }} /> : '登录'} |
| </Button> |
| </form> |
| </Paper> |
| </Container> |
| |
| {/* 背景泡泡动画 - 复用 Home 组件中的样式 */} |
| <Box className="bubbles" sx={{ |
| pointerEvents: 'none', |
| position: 'fixed', |
| top: 0, |
| left: 0, |
| width: '100%', |
| height: '100%', |
| overflow: 'hidden', |
| zIndex: 1, |
| }}> |
| {[...Array(40)].map((_, i) => ( |
| <Box |
| key={i} |
| className="bubble" |
| sx={{ |
| position: 'absolute', |
| bottom: '-150px', // 确保从屏幕外开始 |
| background: `hsla(${Math.random() * 360}, 70%, 80%, 0.15)`, // 增加颜色多样性 |
| borderRadius: '50%', |
| animation: 'rise 20s infinite ease-in', |
| width: `${Math.random() * 25 + 10}px`, // 调整大小范围 |
| height: `${Math.random() * 25 + 10}px`, |
| left: `${Math.random() * 100}%`, |
| animationDuration: `${15 + Math.random() * 20}s`, // 调整动画时长 |
| animationDelay: `${Math.random() * 10}s`, // 调整动画延迟 |
| opacity: 0, // 初始透明 |
| }} |
| /> |
| ))} |
| </Box> |
| {/* 定义动画和其他全局可能需要的样式 */} |
| <style> |
| {` |
| body { /* 基本重置 */ |
| margin: 0; |
| font-family: 'Roboto', sans-serif; /* 确保字体一致 */ |
| } |
| |
| @keyframes rise { |
| 0% { |
| transform: translateY(0) scale(0.8); |
| opacity: 0; /* 从透明开始 */ |
| } |
| 10% { |
| opacity: 1; /* 渐显 */ |
| } |
| 90% { |
| opacity: 1; /* 保持可见 */ |
| } |
| 100% { |
| transform: translateY(-130vh) scale(0.3); /* 飘得更高更小 */ |
| opacity: 0; /* 渐隐 */ |
| } |
| } |
| `} |
| </style> |
| </Box> |
| ); |
| } |
| |
| export default LoginPage; |