Final push for project
Change-Id: I9103078156eca93df2482b9fe3854d9301bb98b3
diff --git a/frontend/my-app/src/pages/Login.jsx b/frontend/my-app/src/pages/Login.jsx
new file mode 100644
index 0000000..19f13ca
--- /dev/null
+++ b/frontend/my-app/src/pages/Login.jsx
@@ -0,0 +1,214 @@
+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;
\ No newline at end of file