You've already forked DataMate
feat(auth): 添加登录功能和路由保护
- 在侧边栏添加退出登录按钮并实现登出逻辑 - 添加 ProtectedRoute 组件用于路由权限控制 - 创建 LoginPage 组件实现登录界面和逻辑 - 集成本地登录验证到 authSlice 状态管理 - 配置路由表添加登录页面和保护路由 - 实现自动跳转到登录页面的重定向逻辑
This commit is contained in:
21
frontend/src/components/ProtectedRoute.tsx
Normal file
21
frontend/src/components/ProtectedRoute.tsx
Normal 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;
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
CloseOutlined,
|
||||
MenuOutlined,
|
||||
SettingOutlined,
|
||||
LogoutOutlined,
|
||||
} from "@ant-design/icons";
|
||||
import { ClipboardList, X } from "lucide-react";
|
||||
import { menuItems } from "@/pages/Layout/menu";
|
||||
@@ -12,6 +13,7 @@ import TaskUpload from "./TaskUpload";
|
||||
import SettingsPage from "../SettingsPage/SettingsPage";
|
||||
import { useAppSelector, useAppDispatch } from "@/store/hooks";
|
||||
import { showSettings, hideSettings } from "@/store/slices/settingsSlice";
|
||||
import { logout } from "@/store/slices/authSlice";
|
||||
|
||||
const isPathMatch = (currentPath: string, targetPath: string) =>
|
||||
currentPath === targetPath || currentPath.startsWith(`${targetPath}/`);
|
||||
@@ -67,6 +69,11 @@ const AsiderAndHeaderLayout = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleLogout = () => {
|
||||
dispatch(logout());
|
||||
navigate("/login");
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${
|
||||
@@ -148,6 +155,9 @@ const AsiderAndHeaderLayout = () => {
|
||||
>
|
||||
设置
|
||||
</Button>
|
||||
<Button block danger onClick={handleLogout}>
|
||||
退出登录
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
@@ -175,6 +185,7 @@ const AsiderAndHeaderLayout = () => {
|
||||
>
|
||||
<SettingOutlined />
|
||||
</Button>
|
||||
<Button block danger onClick={handleLogout} icon={<LogoutOutlined />} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
114
frontend/src/pages/Login/LoginPage.tsx
Normal file
114
frontend/src/pages/Login/LoginPage.tsx
Normal 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;
|
||||
@@ -49,12 +49,21 @@ import EvaluationDetailPage from "@/pages/DataEvaluation/Detail/TaskDetail.tsx";
|
||||
import SynthDataDetail from "@/pages/SynthesisTask/SynthDataDetail.tsx";
|
||||
import Home from "@/pages/Home/Home";
|
||||
import ContentGenerationPage from "@/pages/ContentGeneration/ContentGenerationPage";
|
||||
import LoginPage from "@/pages/Login/LoginPage";
|
||||
import ProtectedRoute from "@/components/ProtectedRoute";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: "/login",
|
||||
Component: LoginPage,
|
||||
},
|
||||
{
|
||||
path: "/",
|
||||
Component: Home,
|
||||
},
|
||||
{
|
||||
Component: ProtectedRoute,
|
||||
children: [
|
||||
{
|
||||
path: "/chat",
|
||||
Component: withErrorBoundary(AgentPage),
|
||||
@@ -286,6 +295,8 @@ const router = createBrowserRouter([
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
export default router;
|
||||
@@ -31,7 +31,7 @@ const authSlice = createSlice({
|
||||
initialState: {
|
||||
user: null,
|
||||
token: localStorage.getItem('token'),
|
||||
isAuthenticated: false,
|
||||
isAuthenticated: !!localStorage.getItem('token'),
|
||||
loading: false,
|
||||
error: null,
|
||||
},
|
||||
@@ -49,6 +49,19 @@ const authSlice = createSlice({
|
||||
state.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) => {
|
||||
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;
|
||||
Reference in New Issue
Block a user