feat(auth): 实现登录注册功能并重构 App 组件
- 新增登录和注册页面组件
- 实现用户认证和权限管理逻辑
- 重构 App 组件,使用 Router 和 AuthProvider
- 添加管理员面板和论坛页面组件
Change-Id: Iaa4502616970e75e3268537f73c75dac8f60e24d
diff --git a/src/routes/PermissionRoute.jsx b/src/routes/PermissionRoute.jsx
index b390011..290c619 100644
--- a/src/routes/PermissionRoute.jsx
+++ b/src/routes/PermissionRoute.jsx
@@ -1,18 +1,48 @@
-import React from 'react';
-import { Navigate } from 'react-router-dom';
+import React from "react";
+import { Navigate, useLocation } from "react-router-dom";
+import { useAuth } from "../features/auth/contexts/AuthContext"; // 更新路径
+import { Spin, Result, Button } from "antd";
-const PermissionRoute = ({ requiredRoles, children }) => {
- // 从localStorage获取用户信息
- const user = JSON.parse(localStorage.getItem('user') || '{}');
- const userRole = user.role || 'guest';
-
- // 检查用户是否有所需角色
- if (requiredRoles.includes(userRole)) {
- return children;
+const PermissionRoute = ({ children, requiredRoles }) => {
+ const { user, isAuthenticated, loading, hasRole } = useAuth();
+ const location = useLocation();
+
+ if (loading) {
+ return (
+ <div className="flex justify-center items-center min-h-screen">
+ <Spin size="large" tip="检查权限中..." />
+ </div>
+ );
}
-
- // 如果没有权限,重定向到未授权页面
- return <Navigate to="/unauthorized" replace />;
+
+ if (!isAuthenticated) {
+ return <Navigate to="/login" state={{ from: location }} replace />;
+ }
+
+ // 角色检查
+ const hasRequiredRoles = requiredRoles
+ ? requiredRoles.some((role) => hasRole(role)) // 满足任一角色即可
+ : true; // 如果没有指定 requiredRoles,则认为通过
+
+ if (hasRequiredRoles) {
+ return children; // 用户有权限,渲染子组件
+ }
+
+ // 用户无权限
+ return (
+ <Result
+ status="403"
+ title="403 - 禁止访问"
+ subTitle="抱歉,您没有权限访问此页面。"
+ extra={
+ <Button type="primary" onClick={() => window.history.back()}>
+ 返回上一页
+ </Button>
+ }
+ />
+ );
+ // 或者重定向到 /unauthorized 页面
+ // return <Navigate to="/unauthorized" state={{ from: location }} replace />;
};
-export default PermissionRoute;
\ No newline at end of file
+export default PermissionRoute;
diff --git a/src/routes/PermissionRoute.test.jsx b/src/routes/PermissionRoute.test.jsx
deleted file mode 100644
index fc77107..0000000
--- a/src/routes/PermissionRoute.test.jsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { render, screen } from '@testing-library/react';
-import { MemoryRouter } from 'react-router-dom';
-import PermissionRoute from './PermissionRoute';
-
-// 模拟 Navigate 组件
-vi.mock('react-router-dom', async () => {
- const actual = await vi.importActual('react-router-dom');
- return {
- ...actual,
- Navigate: () => <div data-testid="navigate-mock">重定向到未授权页面</div>,
- };
-});
-
-describe('PermissionRoute 组件', () => {
- beforeEach(() => {
- window.localStorage.clear();
- });
-
- it('用户没有所需角色时应该重定向', () => {
- // 模拟普通用户
- window.localStorage.setItem('user', JSON.stringify({ role: 'user' }));
-
- render(
- <MemoryRouter>
- <PermissionRoute requiredRoles={['admin']}>
- <div>管理员内容</div>
- </PermissionRoute>
- </MemoryRouter>
- );
-
- // 应该看到重定向组件
- expect(screen.getByTestId('navigate-mock')).toBeInTheDocument();
- });
-
- it('用户有所需角色时应该显示子组件', () => {
- // 模拟管理员
- window.localStorage.setItem('user', JSON.stringify({ role: 'admin' }));
-
- render(
- <MemoryRouter>
- <PermissionRoute requiredRoles={['admin']}>
- <div>管理员内容</div>
- </PermissionRoute>
- </MemoryRouter>
- );
-
- // 应该看到管理员内容
- expect(screen.getByText('管理员内容')).toBeInTheDocument();
- });
-});
\ No newline at end of file
diff --git a/src/routes/ProtectedRoute.jsx b/src/routes/ProtectedRoute.jsx
index ea53b10..4caa16c 100644
--- a/src/routes/ProtectedRoute.jsx
+++ b/src/routes/ProtectedRoute.jsx
@@ -1,35 +1,32 @@
-import { Navigate } from 'react-router-dom';
-import { useEffect, useState } from 'react';
-import { Spin } from 'antd';
+import React, { useEffect } from "react";
+import { Navigate, useLocation } from "react-router-dom";
+import { useAuth } from "../features/auth/contexts/AuthContext"; // 更新路径
+import { Spin } from "antd"; // 用于加载状态
const ProtectedRoute = ({ children }) => {
- const [loading, setLoading] = useState(true);
- const [isAuthenticated, setIsAuthenticated] = useState(false);
+ const { isAuthenticated, loading } = useAuth();
+ const location = useLocation();
+ // 可以添加一个调试日志,查看认证状态变化
useEffect(() => {
- // 简单检查是否有token
- const token = localStorage.getItem('token');
- if (token) {
- setIsAuthenticated(true);
- }
- setLoading(false);
- }, []);
+ console.log("ProtectedRoute - isAuthenticated:", isAuthenticated);
+ }, [isAuthenticated]);
if (loading) {
+ // 当 AuthContext 正在加载认证状态时,显示加载指示器
return (
- <div className="flex justify-center items-center h-screen">
- <Spin spinning={loading} fullScreen tip="加载中...">
- {children}
- </Spin>
+ <div className="flex justify-center items-center min-h-screen">
+ <Spin size="large" />
</div>
);
}
if (!isAuthenticated) {
- return <Navigate to="/login" replace />;
+ // 用户未认证,重定向到登录页,并保存当前位置以便登录后返回
+ return <Navigate to="/login" state={{ from: location }} replace />;
}
- return children;
+ return children; // 用户已认证,渲染子组件
};
-export default ProtectedRoute;
\ No newline at end of file
+export default ProtectedRoute;
diff --git a/src/routes/ProtectedRoute.test.jsx b/src/routes/ProtectedRoute.test.jsx
deleted file mode 100644
index f422fd3..0000000
--- a/src/routes/ProtectedRoute.test.jsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import { describe, it, expect, vi, beforeEach } from 'vitest';
-import { render, screen } from '@testing-library/react';
-import { MemoryRouter, Routes, Route } from 'react-router-dom';
-import ProtectedRoute from './ProtectedRoute';
-
-// 模拟 Navigate 组件
-vi.mock('react-router-dom', async () => {
- const actual = await vi.importActual('react-router-dom');
- return {
- ...actual,
- Navigate: () => <div data-testid="navigate-mock">重定向到登录页</div>,
- };
-});
-
-describe('ProtectedRoute 组件', () => {
- beforeEach(() => {
- window.localStorage.clear();
- });
-
- it('未登录时应该重定向到登录页', () => {
- render(
- <MemoryRouter>
- <ProtectedRoute>
- <div>受保护的内容</div>
- </ProtectedRoute>
- </MemoryRouter>
- );
-
- // 应该看到重定向组件
- expect(screen.getByTestId('navigate-mock')).toBeInTheDocument();
- });
-
- it('已登录时应该显示子组件', () => {
- // 模拟已登录状态
- window.localStorage.setItem('token', 'fake-token');
-
- render(
- <MemoryRouter>
- <ProtectedRoute>
- <div>受保护的内容</div>
- </ProtectedRoute>
- </MemoryRouter>
- );
-
- // 应该看到受保护的内容
- expect(screen.getByText('受保护的内容')).toBeInTheDocument();
- });
-});
\ No newline at end of file
diff --git a/src/routes/index.jsx b/src/routes/index.jsx
index 66e5838..994233b 100644
--- a/src/routes/index.jsx
+++ b/src/routes/index.jsx
@@ -1,48 +1,132 @@
-import { createBrowserRouter } from 'react-router-dom';
-import App from '../App';
-import Login from '../pages/Login';
-import Register from '../pages/Register';
-import NotFound from '../pages/NotFound';
-import Unauthorized from '../pages/Unauthorized';
-import AdminPanel from '../pages/AdminPanel';
-import ProtectedRoute from './protectedroute';
+import React from 'react';
+import { Routes, Route, Navigate } from 'react-router-dom';
+
+// 导入布局
+import MainLayout from '../layouts/MainLayout';
+
+// 导入页面
+import LoginPage from '../features/auth/pages/LoginPage';
+import RegisterPage from '../features/auth/pages/RegisterPage';
+import AdminPanel from '../features/admin/pages/AdminPanel';
+import NotFoundPage from '../pages/NotFoundPage';
+import UnauthorizedPage from '../pages/UnauthorizedPage';
+
+// 导入新创建的页面组件
+import HomePage from '../features/home/pages/HomePage';
+import ForumPage from '../features/forum/pages/ForumPage';
+import PTPage from '../features/pt/pages/PTPage';
+import TorrentListPage from '../features/torrents/pages/TorrentListPage';
+import UploadTorrentPage from '../features/torrents/pages/UploadTorrentPage';
+import ToolsPage from '../features/tools/pages/ToolsPage';
+import ProfilePage from '../features/profile/pages/ProfilePage';
+
+// 导入路由守卫
+import ProtectedRoute from './ProtectedRoute';
import PermissionRoute from './PermissionRoute';
-const router = createBrowserRouter([
- {
- path: '/',
- element: (
- <ProtectedRoute>
- <App />
- </ProtectedRoute>
- ),
- },
- {
- path: '/login',
- element: <Login />,
- },
- {
- path: '/register',
- element: <Register />,
- },
- {
- path: '/unauthorized',
- element: <Unauthorized />,
- },
- {
- path: '/admin',
- element: (
- <ProtectedRoute>
- <PermissionRoute requiredRoles={['admin']}>
- <AdminPanel />
- </PermissionRoute>
- </ProtectedRoute>
- ),
- },
- {
- path: '*',
- element: <NotFound />,
- },
-]);
+const AppRoutes = () => {
+ return (
+ <Routes>
+ {/* 公共路由 */}
+ <Route path="/login" element={<LoginPage />} />
+ <Route path="/register" element={<RegisterPage />} />
+ <Route path="/unauthorized" element={<UnauthorizedPage />} />
-export default router;
\ No newline at end of file
+ {/* 受保护的路由 (需要登录) */}
+ <Route
+ path="/"
+ element={
+ <ProtectedRoute>
+ <MainLayout>
+ <HomePage />
+ </MainLayout>
+ </ProtectedRoute>
+ }
+ />
+
+ <Route
+ path="/forum"
+ element={
+ <ProtectedRoute>
+ <MainLayout>
+ <ForumPage />
+ </MainLayout>
+ </ProtectedRoute>
+ }
+ />
+
+ <Route
+ path="/pt"
+ element={
+ <ProtectedRoute>
+ <MainLayout>
+ <PTPage />
+ </MainLayout>
+ </ProtectedRoute>
+ }
+ />
+
+ <Route
+ path="/torrents"
+ element={
+ <ProtectedRoute>
+ <MainLayout>
+ <TorrentListPage />
+ </MainLayout>
+ </ProtectedRoute>
+ }
+ />
+
+ <Route
+ path="/upload"
+ element={
+ <ProtectedRoute>
+ <MainLayout>
+ <UploadTorrentPage />
+ </MainLayout>
+ </ProtectedRoute>
+ }
+ />
+
+ <Route
+ path="/tools"
+ element={
+ <ProtectedRoute>
+ <MainLayout>
+ <ToolsPage />
+ </MainLayout>
+ </ProtectedRoute>
+ }
+ />
+
+ <Route
+ path="/profile"
+ element={
+ <ProtectedRoute>
+ <MainLayout>
+ <ProfilePage />
+ </MainLayout>
+ </ProtectedRoute>
+ }
+ />
+
+ <Route
+ path="/admin"
+ element={
+ <ProtectedRoute>
+ <PermissionRoute requiredRoles={['admin']}>
+ <MainLayout>
+ <AdminPanel />
+ </MainLayout>
+ </PermissionRoute>
+ </ProtectedRoute>
+ }
+ />
+
+ {/* 404 Not Found 路由 */}
+ <Route path="*" element={<MainLayout><NotFoundPage /></MainLayout>} />
+ </Routes>
+ );
+};
+
+export default AppRoutes;
\ No newline at end of file