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,
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>

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,243 +49,254 @@ 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,
},
{
path: "/chat",
Component: withErrorBoundary(AgentPage),
},
{
path: "/orchestration",
Component: ProtectedRoute,
children: [
{
path: "",
index: true,
Component: withErrorBoundary(OrchestrationPage),
path: "/chat",
Component: withErrorBoundary(AgentPage),
},
{
path: "create-workflow",
Component: withErrorBoundary(WorkflowEditor),
},
],
},
{
path: "/data",
Component: withErrorBoundary(MainLayout),
children: [
{
path: "collection",
path: "/orchestration",
children: [
{
path: "",
index: true,
Component: DataCollection,
Component: withErrorBoundary(OrchestrationPage),
},
{
path: "create-task",
Component: CollectionTaskCreate,
path: "create-workflow",
Component: withErrorBoundary(WorkflowEditor),
},
],
},
{
path: "management",
path: "/data",
Component: withErrorBoundary(MainLayout),
children: [
{
path: "",
index: true,
Component: DatasetManagement,
path: "collection",
children: [
{
path: "",
index: true,
Component: DataCollection,
},
{
path: "create-task",
Component: CollectionTaskCreate,
},
],
},
{
path: "create/:id?",
Component: DatasetCreate,
path: "management",
children: [
{
path: "",
index: true,
Component: DatasetManagement,
},
{
path: "create/:id?",
Component: DatasetCreate,
},
{
path: "detail/:id",
Component: DatasetDetail,
},
],
},
{
path: "detail/:id",
Component: DatasetDetail,
path: "knowledge-management",
children: [
{
path: "",
index: true,
Component: KnowledgeManagementPage,
},
{
path: "search",
Component: KnowledgeManagementSearch,
},
{
path: "detail/:id",
Component: KnowledgeSetDetail,
},
],
},
{
path: "cleansing",
children: [
{
path: "",
index: true,
Component: DataCleansing,
},
{
path: "create-task",
Component: CleansingTaskCreate,
},
{
path: "task-detail/:id",
Component: CleansingTaskDetail,
},
{
path: "create-template",
Component: CleansingTemplateCreate,
},
{
path: "template-detail/:id",
Component: CleansingTemplateDetail,
},
{
path: "update-template/:id",
Component: CleansingTemplateCreate,
},
],
},
{
path: "annotation",
children: [
{
path: "",
index: true,
Component: DataAnnotation,
},
{
path: "create-task",
Component: AnnotationTaskCreate,
},
{
path: "annotate/:projectId",
Component: LabelStudioTextEditor,
},
],
},
{
path: "content-generation",
Component: ContentGenerationPage,
},
{
path: "synthesis/task",
children: [
{
path: "",
Component: DataSynthesisPage,
},
{
path: "create-template",
Component: InstructionTemplateCreate,
},
{
path: "create",
Component: SynthesisTaskCreate,
},
{
path: ":id",
Component: SynthFileTask
},
{
path: "file/:id/detail",
Component: SynthDataDetail,
}
],
},
{
path: "synthesis/ratio-task",
children: [
{
path: "",
index: true,
Component: RatioTasksPage,
},
{
path: "create",
Component: CreateRatioTask,
},
{
path: "detail/:id",
Component: RatioTaskDetail,
}
],
},
{
path: "evaluation",
children: [
{
path: "",
index: true,
Component: DataEvaluationPage,
},
{
path: "detail/:id",
Component: EvaluationDetailPage,
},
{
path: "task-report/:id",
Component: EvaluationTaskReport,
},
{
path: "manual-evaluate/:id",
Component: ManualEvaluatePage,
},
],
},
{
path: "knowledge-base",
children: [
{
path: "",
index: true,
Component: KnowledgeBasePage,
},
{
path: "search",
Component: KnowledgeBaseSearch,
},
{
path: "detail/:id",
Component: KnowledgeBaseDetailPage,
},
{
path: "file-detail/:id",
Component: KnowledgeBaseFileDetailPage,
},
],
},
{
path: "operator-market",
children: [
{
path: "",
index: true,
Component: OperatorMarketPage,
},
{
path: "create/:id?",
Component: OperatorPluginCreate,
},
{
path: "plugin-detail/:id",
Component: OperatorPluginDetail,
},
],
},
],
},
{
path: "knowledge-management",
children: [
{
path: "",
index: true,
Component: KnowledgeManagementPage,
},
{
path: "search",
Component: KnowledgeManagementSearch,
},
{
path: "detail/:id",
Component: KnowledgeSetDetail,
},
],
},
{
path: "cleansing",
children: [
{
path: "",
index: true,
Component: DataCleansing,
},
{
path: "create-task",
Component: CleansingTaskCreate,
},
{
path: "task-detail/:id",
Component: CleansingTaskDetail,
},
{
path: "create-template",
Component: CleansingTemplateCreate,
},
{
path: "template-detail/:id",
Component: CleansingTemplateDetail,
},
{
path: "update-template/:id",
Component: CleansingTemplateCreate,
},
],
},
{
path: "annotation",
children: [
{
path: "",
index: true,
Component: DataAnnotation,
},
{
path: "create-task",
Component: AnnotationTaskCreate,
},
{
path: "annotate/:projectId",
Component: LabelStudioTextEditor,
},
],
},
{
path: "content-generation",
Component: ContentGenerationPage,
},
{
path: "synthesis/task",
children: [
{
path: "",
Component: DataSynthesisPage,
},
{
path: "create-template",
Component: InstructionTemplateCreate,
},
{
path: "create",
Component: SynthesisTaskCreate,
},
{
path: ":id",
Component: SynthFileTask
},
{
path: "file/:id/detail",
Component: SynthDataDetail,
}
],
},
{
path: "synthesis/ratio-task",
children: [
{
path: "",
index: true,
Component: RatioTasksPage,
},
{
path: "create",
Component: CreateRatioTask,
},
{
path: "detail/:id",
Component: RatioTaskDetail,
}
],
},
{
path: "evaluation",
children: [
{
path: "",
index: true,
Component: DataEvaluationPage,
},
{
path: "detail/:id",
Component: EvaluationDetailPage,
},
{
path: "task-report/:id",
Component: EvaluationTaskReport,
},
{
path: "manual-evaluate/:id",
Component: ManualEvaluatePage,
},
],
},
{
path: "knowledge-base",
children: [
{
path: "",
index: true,
Component: KnowledgeBasePage,
},
{
path: "search",
Component: KnowledgeBaseSearch,
},
{
path: "detail/:id",
Component: KnowledgeBaseDetailPage,
},
{
path: "file-detail/:id",
Component: KnowledgeBaseFileDetailPage,
},
],
},
{
path: "operator-market",
children: [
{
path: "",
index: true,
Component: OperatorMarketPage,
},
{
path: "create/:id?",
Component: OperatorPluginCreate,
},
{
path: "plugin-detail/:id",
Component: OperatorPluginDetail,
},
],
},
],
},
]
}
]);
export default router;
export default router;

View File

@@ -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;