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