添加了相关测试文件,引入Tailwindcss

Change-Id: I12054143571bb688590af0357125a0ed26ff2050
diff --git a/src/routes/PermissionRoute.jsx b/src/routes/PermissionRoute.jsx
new file mode 100644
index 0000000..b390011
--- /dev/null
+++ b/src/routes/PermissionRoute.jsx
@@ -0,0 +1,18 @@
+import React from 'react';
+import { Navigate } from 'react-router-dom';
+
+const PermissionRoute = ({ requiredRoles, children }) => {
+  // 从localStorage获取用户信息
+  const user = JSON.parse(localStorage.getItem('user') || '{}');
+  const userRole = user.role || 'guest';
+  
+  // 检查用户是否有所需角色
+  if (requiredRoles.includes(userRole)) {
+    return children;
+  }
+  
+  // 如果没有权限,重定向到未授权页面
+  return <Navigate to="/unauthorized" replace />;
+};
+
+export default PermissionRoute;
\ No newline at end of file
diff --git a/src/routes/PermissionRoute.test.jsx b/src/routes/PermissionRoute.test.jsx
new file mode 100644
index 0000000..fc77107
--- /dev/null
+++ b/src/routes/PermissionRoute.test.jsx
@@ -0,0 +1,51 @@
+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
new file mode 100644
index 0000000..ea53b10
--- /dev/null
+++ b/src/routes/ProtectedRoute.jsx
@@ -0,0 +1,35 @@
+import { Navigate } from 'react-router-dom';
+import { useEffect, useState } from 'react';
+import { Spin } from 'antd';
+
+const ProtectedRoute = ({ children }) => {
+  const [loading, setLoading] = useState(true);
+  const [isAuthenticated, setIsAuthenticated] = useState(false);
+
+  useEffect(() => {
+    // 简单检查是否有token
+    const token = localStorage.getItem('token');
+    if (token) {
+      setIsAuthenticated(true);
+    }
+    setLoading(false);
+  }, []);
+
+  if (loading) {
+    return (
+      <div className="flex justify-center items-center h-screen">
+        <Spin spinning={loading} fullScreen tip="加载中...">
+          {children}
+        </Spin>
+      </div>
+    );
+  }
+
+  if (!isAuthenticated) {
+    return <Navigate to="/login" replace />;
+  }
+
+  return children;
+};
+
+export default ProtectedRoute;
\ No newline at end of file
diff --git a/src/routes/ProtectedRoute.test.jsx b/src/routes/ProtectedRoute.test.jsx
new file mode 100644
index 0000000..f422fd3
--- /dev/null
+++ b/src/routes/ProtectedRoute.test.jsx
@@ -0,0 +1,48 @@
+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
new file mode 100644
index 0000000..66e5838
--- /dev/null
+++ b/src/routes/index.jsx
@@ -0,0 +1,48 @@
+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 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 />,
+  },
+]);
+
+export default router;
\ No newline at end of file