feat(auth): 添加登录功能和路由保护

- 在侧边栏添加退出登录按钮并实现登出逻辑
- 添加 ProtectedRoute 组件用于路由权限控制
- 创建 LoginPage 组件实现登录界面和逻辑
- 集成本地登录验证到 authSlice 状态管理
- 配置路由表添加登录页面和保护路由
- 实现自动跳转到登录页面的重定向逻辑
This commit is contained in:
2026-02-01 14:11:44 +08:00
parent 906bb39b83
commit 7043a26ab3
5 changed files with 380 additions and 210 deletions

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { Navigate, useLocation, Outlet } from 'react-router';
import { useAppSelector } from '@/store/hooks';
interface ProtectedRouteProps {
children?: React.ReactNode;
}
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
const { isAuthenticated } = useAppSelector((state) => state.auth);
const location = useLocation();
if (!isAuthenticated) {
// Redirect to the login page, but save the current location they were trying to go to
return <Navigate to="/login" state={{ from: location }} replace />;
}
return children ? <>{children}</> : <Outlet />;
};
export default ProtectedRoute;

View File

@@ -4,6 +4,7 @@ import {
CloseOutlined, CloseOutlined,
MenuOutlined, MenuOutlined,
SettingOutlined, SettingOutlined,
LogoutOutlined,
} from "@ant-design/icons"; } from "@ant-design/icons";
import { ClipboardList, X } from "lucide-react"; import { ClipboardList, X } from "lucide-react";
import { menuItems } from "@/pages/Layout/menu"; import { menuItems } from "@/pages/Layout/menu";
@@ -12,6 +13,7 @@ import TaskUpload from "./TaskUpload";
import SettingsPage from "../SettingsPage/SettingsPage"; import SettingsPage from "../SettingsPage/SettingsPage";
import { useAppSelector, useAppDispatch } from "@/store/hooks"; import { useAppSelector, useAppDispatch } from "@/store/hooks";
import { showSettings, hideSettings } from "@/store/slices/settingsSlice"; import { showSettings, hideSettings } from "@/store/slices/settingsSlice";
import { logout } from "@/store/slices/authSlice";
const isPathMatch = (currentPath: string, targetPath: string) => const isPathMatch = (currentPath: string, targetPath: string) =>
currentPath === targetPath || currentPath.startsWith(`${targetPath}/`); currentPath === targetPath || currentPath.startsWith(`${targetPath}/`);
@@ -67,6 +69,11 @@ const AsiderAndHeaderLayout = () => {
}; };
}, []); }, []);
const handleLogout = () => {
dispatch(logout());
navigate("/login");
};
return ( return (
<div <div
className={`${ className={`${
@@ -148,6 +155,9 @@ const AsiderAndHeaderLayout = () => {
> >
</Button> </Button>
<Button block danger onClick={handleLogout}>
退
</Button>
</div> </div>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2">
@@ -175,6 +185,7 @@ const AsiderAndHeaderLayout = () => {
> >
<SettingOutlined /> <SettingOutlined />
</Button> </Button>
<Button block danger onClick={handleLogout} icon={<LogoutOutlined />} />
</div> </div>
)} )}
</div> </div>

View File

@@ -0,0 +1,114 @@
import React, { useState } from 'react';
import { useNavigate, useLocation } from 'react-router';
import { Form, Input, Button, Typography, message, Card } from 'antd';
import { UserOutlined, LockOutlined } from '@ant-design/icons';
import { useAppDispatch, useAppSelector } from '@/store/hooks';
import { loginLocal } from '@/store/slices/authSlice';
const { Title, Text } = Typography;
const LoginPage: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const dispatch = useAppDispatch();
const { loading, error } = useAppSelector((state) => state.auth);
const [messageApi, contextHolder] = message.useMessage();
const from = location.state?.from?.pathname || '/data';
const onFinish = (values: any) => {
dispatch(loginLocal(values));
// The reducer updates state synchronously.
if (values.username === 'admin' && values.password === '123456') {
messageApi.success('登录成功');
navigate(from, { replace: true });
} else {
messageApi.error('账号或密码错误');
}
};
return (
<div className="min-h-screen flex items-center justify-center bg-[#050b14] relative overflow-hidden">
{contextHolder}
{/* Background Effects */}
<div className="absolute inset-0 z-0">
<div className="absolute top-0 left-0 w-full h-full bg-[radial-gradient(ellipse_at_center,_var(--tw-gradient-stops))] from-blue-900/20 via-[#050b14] to-[#050b14]"></div>
{/* Simple grid pattern if possible, or just gradient */}
</div>
<div className="absolute top-1/4 left-1/4 w-72 h-72 bg-blue-500/10 rounded-full blur-3xl animate-pulse"></div>
<div className="absolute bottom-1/4 right-1/4 w-96 h-96 bg-cyan-500/10 rounded-full blur-3xl animate-pulse delay-700"></div>
<div className="z-10 w-full max-w-md p-8 animate-[fadeIn_0.5s_ease-out_forwards]">
<div className="backdrop-blur-xl bg-white/5 border border-white/10 rounded-2xl shadow-2xl p-8 relative overflow-hidden">
{/* Decorative line */}
<div className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-blue-500 to-transparent"></div>
<div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-500/20 mb-4 border border-blue-500/30">
<svg className="w-8 h-8 text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
</div>
<Title level={2} className="!text-white !mb-2 tracking-wide font-bold">
DataBuilder
</Title>
<Text className="text-gray-400 text-sm tracking-wider">
</Text>
</div>
<Form
name="login"
initialValues={{ remember: true, username: 'admin', password: '123456' }}
onFinish={onFinish}
layout="vertical"
size="large"
>
<Form.Item
name="username"
rules={[{ required: true, message: '请输入账号!' }]}
>
<Input
prefix={<UserOutlined className="text-blue-400" />}
placeholder="账号"
className="!bg-white/5 !border-white/10 !text-white placeholder:!text-gray-600 hover:!border-blue-500/50 focus:!border-blue-500 !rounded-lg"
/>
</Form.Item>
<Form.Item
name="password"
rules={[{ required: true, message: '请输入密码!' }]}
>
<Input.Password
prefix={<LockOutlined className="text-blue-400" />}
type="password"
placeholder="密码"
className="!bg-white/5 !border-white/10 !text-white placeholder:!text-gray-600 hover:!border-blue-500/50 focus:!border-blue-500 !rounded-lg"
/>
</Form.Item>
<Form.Item className="mb-2">
<Button
type="primary"
htmlType="submit"
className="w-full bg-gradient-to-r from-blue-600 to-cyan-600 hover:from-blue-500 hover:to-cyan-500 border-none h-12 rounded-lg font-semibold tracking-wide shadow-lg shadow-blue-900/20"
loading={loading}
>
</Button>
</Form.Item>
<div className="text-center mt-4">
<Text className="text-gray-600 text-xs">
·
</Text>
</div>
</Form>
</div>
</div>
</div>
);
};
export default LoginPage;

View File

@@ -49,12 +49,21 @@ import EvaluationDetailPage from "@/pages/DataEvaluation/Detail/TaskDetail.tsx";
import SynthDataDetail from "@/pages/SynthesisTask/SynthDataDetail.tsx"; import SynthDataDetail from "@/pages/SynthesisTask/SynthDataDetail.tsx";
import Home from "@/pages/Home/Home"; import Home from "@/pages/Home/Home";
import ContentGenerationPage from "@/pages/ContentGeneration/ContentGenerationPage"; import ContentGenerationPage from "@/pages/ContentGeneration/ContentGenerationPage";
import LoginPage from "@/pages/Login/LoginPage";
import ProtectedRoute from "@/components/ProtectedRoute";
const router = createBrowserRouter([ const router = createBrowserRouter([
{
path: "/login",
Component: LoginPage,
},
{ {
path: "/", path: "/",
Component: Home, Component: Home,
}, },
{
Component: ProtectedRoute,
children: [
{ {
path: "/chat", path: "/chat",
Component: withErrorBoundary(AgentPage), Component: withErrorBoundary(AgentPage),
@@ -286,6 +295,8 @@ const router = createBrowserRouter([
}, },
], ],
}, },
]
}
]); ]);
export default router; export default router;

View File

@@ -31,7 +31,7 @@ const authSlice = createSlice({
initialState: { initialState: {
user: null, user: null,
token: localStorage.getItem('token'), token: localStorage.getItem('token'),
isAuthenticated: false, isAuthenticated: !!localStorage.getItem('token'),
loading: false, loading: false,
error: null, error: null,
}, },
@@ -49,6 +49,19 @@ const authSlice = createSlice({
state.token = action.payload; state.token = action.payload;
localStorage.setItem('token', action.payload); localStorage.setItem('token', action.payload);
}, },
loginLocal: (state, action) => {
const { username, password } = action.payload;
if (username === 'admin' && password === '123456') {
state.user = { username: 'admin', role: 'admin' };
state.token = 'mock-token-' + Date.now();
state.isAuthenticated = true;
localStorage.setItem('token', state.token);
state.error = null;
} else {
state.error = 'Invalid credentials';
state.isAuthenticated = false;
}
},
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder builder
@@ -71,5 +84,5 @@ const authSlice = createSlice({
}, },
}); });
export const { logout, clearError, setToken } = authSlice.actions; export const { logout, clearError, setToken, loginLocal } = authSlice.actions;
export default authSlice.reducer; export default authSlice.reducer;