diff --git a/frontend/src/components/ProtectedRoute.tsx b/frontend/src/components/ProtectedRoute.tsx new file mode 100644 index 0000000..d0fdf87 --- /dev/null +++ b/frontend/src/components/ProtectedRoute.tsx @@ -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 = ({ 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 ; + } + + return children ? <>{children} : ; +}; + +export default ProtectedRoute; diff --git a/frontend/src/pages/Layout/Sidebar.tsx b/frontend/src/pages/Layout/Sidebar.tsx index a739f55..f93ceee 100644 --- a/frontend/src/pages/Layout/Sidebar.tsx +++ b/frontend/src/pages/Layout/Sidebar.tsx @@ -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 (
{ > 设置 +
) : (
@@ -175,6 +185,7 @@ const AsiderAndHeaderLayout = () => { > +
)} diff --git a/frontend/src/pages/Login/LoginPage.tsx b/frontend/src/pages/Login/LoginPage.tsx new file mode 100644 index 0000000..c9462a3 --- /dev/null +++ b/frontend/src/pages/Login/LoginPage.tsx @@ -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 ( +
+ {contextHolder} + + {/* Background Effects */} +
+
+ {/* Simple grid pattern if possible, or just gradient */} +
+ +
+
+ +
+
+ {/* Decorative line */} +
+ +
+
+ + + +
+ + DataBuilder + + + 一站式数据工作平台 + +
+ +
+ + } + 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" + /> + + + } + 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" + /> + + + + + + +
+ + 企业级数据处理平台 · 安全接入 + +
+
+
+
+
+ ); +}; + +export default LoginPage; diff --git a/frontend/src/routes/routes.ts b/frontend/src/routes/routes.ts index 2b67b4a..97f15c9 100644 --- a/frontend/src/routes/routes.ts +++ b/frontend/src/routes/routes.ts @@ -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; \ No newline at end of file diff --git a/frontend/src/store/slices/authSlice.ts b/frontend/src/store/slices/authSlice.ts index e159eb7..b772a52 100644 --- a/frontend/src/store/slices/authSlice.ts +++ b/frontend/src/store/slices/authSlice.ts @@ -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; \ No newline at end of file