今天要完成前端的“登录页面”,包含登录之后的“登出”功能。
修改了如下文件:
要创建一个登录页面,我们需要:
- 创建登录页面组件
- 添加登录表单
- 处理登录逻辑
- 添加路由保护
1. 创建一个登录页面(Login.tsx)
import React, { useState } from 'react';
import {
Box,
Paper,
TextField,
Button,
Typography,
Container,
Alert
} from '@mui/material';
import { useNavigate } from 'react-router-dom';
const Login: React.FC = () => {
const navigate = useNavigate();
const [formData, setFormData] = useState({
username: '',
password: ''
});
const [error, setError] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({
...prev,
[name]: value
}));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
try {
const response = await fetch('http://localhost:8080/api/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData),
});
const data = await response.json();
if (response.ok) {
// 保存token和用户信息到localStorage
localStorage.setItem('token', data.token);
localStorage.setItem('user', JSON.stringify(data.user));
// 跳转到仪表盘
navigate('/');
} else {
setError(data.message || '登录失败');
}
} catch (err) {
setError('网络错误,请稍后重试');
}
};
return (
<Container component="main" maxWidth="xs">
<Box
sx={{
marginTop: 8,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Paper
elevation={3}
sx={{
padding: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
width: '100%',
}}
>
<Typography component="h1" variant="h5">
仓库管理系统
</Typography>
<Box component="form" onSubmit={handleSubmit} sx={{ mt: 1, width: '100%' }}>
{error && (
<Alert severity="error" sx={{ mb: 2 }}>
{error}
</Alert>
)}
<TextField
margin="normal"
required
fullWidth
id="username"
label="用户名"
name="username"
autoComplete="username"
autoFocus
value={formData.username}
onChange={handleChange}
/>
<TextField
margin="normal"
required
fullWidth
name="password"
label="密码"
type="password"
id="password"
autoComplete="current-password"
value={formData.password}
onChange={handleChange}
/>
<Button
type="submit"
fullWidth
variant="contained"
sx={{ mt: 3, mb: 2 }}
>
登录
</Button>
</Box>
</Paper>
</Box>
</Container>
);
};
export default Login;
2. 创建一个路由保护(ProtectedRoute.tsx),用于保护需要登录才能访问的页面
import React from 'react';
import { Navigate } from 'react-router-dom';
interface ProtectedRouteProps {
children: React.ReactNode;
}
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
const token = localStorage.getItem('token');
if (!token) {
// 如果没有token,重定向到登录页
return <Navigate to="/login" replace />;
}
return <>{children}</>;
};
export default ProtectedRoute;
3. 更新APP.tsx,添加登录路由和保护其他路由
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
import { ThemeProvider, createTheme } from '@mui/material';
import MainLayout from './layouts/MainLayout';
import Dashboard from './pages/Dashboard';
import Login from './pages/Login';
import ProtectedRoute from './components/ProtectedRoute';
// 临时占位组件,后续会替换为实际页面组件
const PlaceholderPage = () => <div>Page under construction</div>;
const theme = createTheme({
palette: {
primary: {
main: '#1976d2',
},
secondary: {
main: '#dc004e',
},
},
});
function App() {
return (
<ThemeProvider theme={theme}>
<Router>
<Routes>
<Route path="/login" element={<Login />} />
<Route
path="/"
element={
<ProtectedRoute>
<MainLayout>
<Dashboard />
</MainLayout>
</ProtectedRoute>
}
/>
<Route
path="/products"
element={
<ProtectedRoute>
<MainLayout>
<PlaceholderPage />
</MainLayout>
</ProtectedRoute>
}
/>
<Route
path="/orders"
element={
<ProtectedRoute>
<MainLayout>
<PlaceholderPage />
</MainLayout>
</ProtectedRoute>
}
/>
<Route
path="/inbound"
element={
<ProtectedRoute>
<MainLayout>
<PlaceholderPage />
</MainLayout>
</ProtectedRoute>
}
/>
<Route
path="/customers"
element={
<ProtectedRoute>
<MainLayout>
<PlaceholderPage />
</MainLayout>
</ProtectedRoute>
}
/>
<Route
path="/delivery"
element={
<ProtectedRoute>
<MainLayout>
<PlaceholderPage />
</MainLayout>
</ProtectedRoute>
}
/>
<Route
path="/users"
element={
<ProtectedRoute>
<MainLayout>
<PlaceholderPage />
</MainLayout>
</ProtectedRoute>
}
/>
<Route
path="/reports"
element={
<ProtectedRoute>
<MainLayout>
<PlaceholderPage />
</MainLayout>
</ProtectedRoute>
}
/>
<Route
path="/settings"
element={
<ProtectedRoute>
<MainLayout>
<PlaceholderPage />
</MainLayout>
</ProtectedRoute>
}
/>
</Routes>
</Router>
</ThemeProvider>
);
}
export default App;
4. 修改MainLayout.tsx,添加“登出”按钮及功能
import React from 'react';
import { Box, Drawer, AppBar, Toolbar, Typography, List, ListItem, ListItemIcon, ListItemText, IconButton, Button } from '@mui/material';
import {
Menu as MenuIcon,
Dashboard,
Inventory,
People,
LocalShipping,
Settings,
ShoppingCart,
AddBox,
Person,
Assignment,
Logout
} from '@mui/icons-material';
import { useNavigate } from 'react-router-dom';
const drawerWidth = 240;
interface MainLayoutProps {
children: React.ReactNode;
}
const MainLayout: React.FC<MainLayoutProps> = ({ children }) => {
const navigate = useNavigate();
const [mobileOpen, setMobileOpen] = React.useState(false);
const menuItems = [
{ text: 'Dashboard', icon: <Dashboard />, path: '/' },
{ text: 'Products', icon: <Inventory />, path: '/products' },
{ text: 'Orders', icon: <ShoppingCart />, path: '/orders' },
{ text: 'Inbound', icon: <AddBox />, path: '/inbound' },
{ text: 'Customers', icon: <Person />, path: '/customers' },
{ text: 'Delivery', icon: <LocalShipping />, path: '/delivery' },
{ text: 'Users', icon: <People />, path: '/users' },
{ text: 'Reports', icon: <Assignment />, path: '/reports' },
{ text: 'Settings', icon: <Settings />, path: '/settings' },
];
const handleLogout = () => {
// 清除本地存储
localStorage.removeItem('token');
localStorage.removeItem('user');
// 跳转到登录页
navigate('/login');
};
const drawer = (
<div>
<Toolbar>
<Typography variant="h6" noWrap component="div">
QuickStore
</Typography>
</Toolbar>
<List>
{menuItems.map((item) => (
<ListItem
key={item.text}
onClick={() => navigate(item.path)}
sx={{ cursor: 'pointer' }}
>
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={item.text} />
</ListItem>
))}
</List>
</div>
);
return (
<Box sx={{ display: 'flex' }}>
<AppBar
position="fixed"
sx={{
width: { sm: `calc(100% - ${drawerWidth}px)` },
ml: { sm: `${drawerWidth}px` },
}}
>
<Toolbar>
<IconButton
color="inherit"
edge="start"
onClick={() => setMobileOpen(!mobileOpen)}
sx={{ mr: 2, display: { sm: 'none' } }}
>
<MenuIcon />
</IconButton>
<Typography variant="h6" noWrap component="div" sx={{ flexGrow: 1 }}>
Aluminum Warehouse Management
</Typography>
<Button
color="inherit"
onClick={handleLogout}
startIcon={<Logout />}
>
登出
</Button>
</Toolbar>
</AppBar>
<Box
component="nav"
sx={{ width: { sm: drawerWidth }, flexShrink: { sm: 0 } }}
>
<Drawer
variant="temporary"
open={mobileOpen}
onClose={() => setMobileOpen(false)}
ModalProps={{
keepMounted: true,
}}
sx={{
display: { xs: 'block', sm: 'none' },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
>
{drawer}
</Drawer>
<Drawer
variant="permanent"
sx={{
display: { xs: 'none', sm: 'block' },
'& .MuiDrawer-paper': { boxSizing: 'border-box', width: drawerWidth },
}}
open
>
{drawer}
</Drawer>
</Box>
<Box
component="main"
sx={{
flexGrow: 1,
p: 3,
width: { sm: `calc(100% - ${drawerWidth}px)` },
mt: '64px',
}}
>
{children}
</Box>
</Box>
);
};
export default MainLayout;
5. 需要修改一段后端代码
原因:由于前端和后端运行在不同的端口(前端在 3000,后端在 8080),我们需要处理跨域问题。
需要确保后端已经配置了 CORS (Cross-Origin Resource Sharing,跨源资源共享。是一个浏览器的安全机制,它决定了是否允许一个网页从不同的源(域名、端口或协议)请求资源),来允许来自 http://localhost:3000 的请求。
具体修改如下:
1. 创建CORS配置类
package com.quickstore.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
// 允许来自前端开发服务器的请求
config.addAllowedOrigin("http://localhost:3000");
// 允许的HTTP方法
config.addAllowedMethod("*");
// 允许的请求头
config.addAllowedHeader("*");
// 允许发送认证信息(cookies等)
config.setAllowCredentials(true);
// 对所有路径应用CORS配置
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
2. 修改SecurityConfig文件,以启动CORS
package com.quickstore.config;
import com.quickstore.security.JwtAuthenticationFilter;
import com.quickstore.security.JwtTokenProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtTokenProvider tokenProvider, UserDetailsService userDetailsService) throws Exception {
http
.cors().configurationSource(corsConfigurationSource())
.and()
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(new JwtAuthenticationFilter(tokenProvider, userDetailsService),
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
修改完成之后,同样可以使用Postman发送请求,来进行测试。
完整的登录页面如下:
登录之后的“登出”按钮如下(右上角):
点击“登出”之后,回到登录页面。
完成!
说点题外话:
写完前端页面,最大的感觉是:AI让人感到有点害怕。
我以为对于我这种不太懂React的人来说,从零开始搭建框架、完成页面,应该要花费很多时间。但是实际上让AI来做,不用一会就搞定了。那在这个过程当中,我学到了些什么呢?细细回想一下,好像其实什么都没学到。
我能写出一个产品,但我还是不会这门技术。
那面试怎么办呢?
或者说,如果面试成功之后,我在工作当中,又要做些什么呢?老板给我一个需求,我再转达给“Cursor”大人?
我们还有“技术”么,我们还需要学习“技术”么?
未来好像是一个“产品”的时代,而不是一个“技术”的时代了?
更宝贵的是各种创意,不停用AI做出各种各样的“产品”?
有点迷茫。。。
不过不管怎样,还是先把前端最后几个页面做完,然后改简历,然后找到一份合适的工作。然后再想这些。
下一篇:完成前端用户注册页面