前段

Change-Id: I718d4d07ea03c6d2b6bcbd4d426c5d1af2201bf4
diff --git a/src/App.css b/src/App.css
new file mode 100644
index 0000000..e605b6d
--- /dev/null
+++ b/src/App.css
@@ -0,0 +1,38 @@
+.App {
+  text-align: center;
+}
+
+.App-logo {
+  height: 40vmin;
+  pointer-events: none;
+}
+
+@media (prefers-reduced-motion: no-preference) {
+  .App-logo {
+    animation: App-logo-spin infinite 20s linear;
+  }
+}
+
+.App-header {
+  background-color: #282c34;
+  min-height: 10%;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  justify-content: center;
+  font-size: calc(10px + 2vmin);
+  color: white;
+}
+
+.App-link {
+  color: #61dafb;
+}
+
+@keyframes App-logo-spin {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}
diff --git a/src/App.jsx b/src/App.jsx
new file mode 100644
index 0000000..66042bc
--- /dev/null
+++ b/src/App.jsx
@@ -0,0 +1,140 @@
+// src/App.jsx
+import React from 'react';
+import { 
+  BrowserRouter as Router,
+  Routes,
+  Route,
+  Navigate
+} from 'react-router-dom';
+import Dashboard from './components/Dashboard';
+import Personal from './components/Personal/Personal';
+import AuthForm from './components/AuthForm';
+import AnnouncementDetail from './components/AnnouncementDetail';
+
+import TorrentDetail from './components/TorrentDetail'; // 确保路径正确
+import RequestDetail from './components/RequestDetail';
+import HelpDetail from './components/HelpDetail';
+import Favorite from './components/Personal/Favorite';
+import Upload from './components/Personal/Upload';
+import Notice from './components/Personal/Notice';
+import Setting from './components/Personal/Setting';
+
+import './App.css';
+
+function App() {
+  // 每次应用启动时强制清除 token
+  React.useEffect(() => {
+    localStorage.removeItem('token');
+    console.log('Token cleared on app startup');
+    localStorage.removeItem('username');
+  }, []); // 空依赖数组表示只在组件挂载时执行一次
+
+  const [isAuthenticated, setIsAuthenticated] = React.useState(
+    false
+  );
+
+  const handleLoginSuccess = (token) => {
+    setIsAuthenticated(true);
+  };
+
+  const handleLogout = () => {
+    localStorage.removeItem('token');
+    setIsAuthenticated(false);
+  };
+
+  return (
+    <Router>
+      <Routes>
+        {/* 修改点1:根路径强制跳转到登录页 */}
+        <Route path="/" element={<Navigate to="/login" replace />} />
+
+        {/* 登录页 */}
+        <Route path="/login" element={
+          isAuthenticated ? (
+            <Navigate to="/dashboard" replace />
+          ) : (
+            <AuthForm onLoginSuccess={handleLoginSuccess} />
+          )
+        } />
+
+        {/* 主面板 */}
+        <Route path="/dashboard/:tab?" element={
+          isAuthenticated ? (
+            <Dashboard onLogout={handleLogout} />
+          ) : (
+            <Navigate to="/login" replace />
+          )
+        } />
+
+        {/* 公告详情 */}
+        <Route path="/announcement/:id" element={
+          isAuthenticated ? (
+            <AnnouncementDetail onLogout={handleLogout} />
+          ) : (
+            <Navigate to="/login" replace />
+          )
+        } />
+
+        {/* 个人中心及子功能页面 */}
+        <Route path="/personal" element={
+          isAuthenticated ? <Personal onLogout={handleLogout} /> : <Navigate to="/login" replace />
+        } />
+        <Route path="/personal/favorite" element={
+          isAuthenticated ? <Favorite onLogout={handleLogout} /> : <Navigate to="/login" replace />
+        } />
+        <Route path="/personal/upload" element={
+          isAuthenticated ? <Upload onLogout={handleLogout} /> : <Navigate to="/login" replace />
+        } />
+        <Route path="/personal/notice" element={
+          isAuthenticated ? <Notice onLogout={handleLogout} /> : <Navigate to="/login" replace />
+        } />
+        <Route path="/personal/setting" element={
+          isAuthenticated ? <Setting onLogout={handleLogout} /> : <Navigate to="/login" replace />
+        } />
+
+        
+        {/* 404 重定向 */}
+        <Route path="*" element={
+          isAuthenticated ? (
+            <Navigate to="/dashboard" replace />
+          ) : (
+            <Navigate to="/login" replace />
+          )
+        } />
+
+        {/* 求种区 */}
+        <Route path="/request/:id" element={
+          isAuthenticated ? (
+            <RequestDetail onLogout={handleLogout} />
+          ) : (
+            <Navigate to="/login" replace />
+          )
+        } />
+
+        {/* 求助区 */}
+        <Route path="/help/:id" element={
+          isAuthenticated ? (
+            <HelpDetail onLogout={handleLogout} />
+          ) : (
+            <Navigate to="/login" replace />
+          )
+        } />
+
+        <Route path="/torrent/:id" element={
+          isAuthenticated ? (
+            <TorrentDetail onLogout={handleLogout} />
+          ) : (
+            <Navigate to="/login" replace />
+          )
+        } />
+
+
+
+      </Routes>
+
+      
+    </Router>
+  );
+}
+
+export default App;
\ No newline at end of file
diff --git a/src/api/auth.js b/src/api/auth.js
new file mode 100644
index 0000000..ad845bb
--- /dev/null
+++ b/src/api/auth.js
@@ -0,0 +1,37 @@
+// src/api/auth.js

+import axios from 'axios';

+

+// 创建并导出 axios 实例

+export const api = axios.create({

+  baseURL: 'http://localhost:8088',

+  timeout: 5000,

+});

+

+// 请求拦截器

+api.interceptors.request.use(config => {

+  const token = localStorage.getItem('token');

+  if (token) {

+    config.headers.Authorization = `Bearer ${token}`;

+  }

+  return config;

+});

+

+export const login = async (username, password) => {

+  const response = await api.post('/user/login', null, {

+    params: { username, password }

+  });

+  if (response.data.code === 200 && response.data.data.token) {

+    localStorage.setItem('token', response.data.data.token);

+  }

+  return response;

+};

+

+export const register = (username, password, code) => {

+  return api.post('/user/regist', null, {

+    params: { username, password, code }

+  });

+};

+

+export const getUserInfo = (token) => {

+  return api.get('/user/info', { params: { token } });

+};
\ No newline at end of file
diff --git a/src/api/auth.test.js b/src/api/auth.test.js
new file mode 100644
index 0000000..c9ace4d
--- /dev/null
+++ b/src/api/auth.test.js
@@ -0,0 +1,114 @@
+import MockAdapter from 'axios-mock-adapter';

+import { api, login, register, getUserInfo } from './auth';

+

+describe('auth API', () => {

+  let mockAxios;

+

+  beforeEach(() => {

+    // 确保使用我们导出的 api 实例

+    mockAxios = new MockAdapter(api);

+    localStorage.clear();

+  });

+

+  afterEach(() => {

+    mockAxios.restore();

+  });

+

+  describe('login', () => {

+    it('should send login request with username and password', async () => {

+        const mockResponse = {

+          code: 200,

+          data: { 

+            token: 'mock-token',

+            // 确保响应结构与实际API一致

+            userInfo: { username: 'testuser' }

+          },

+          message: '登录成功'

+        };

+        

+        mockAxios.onPost('/user/login').reply(200, mockResponse);

+    

+        const response = await login('testuser', 'testpass');

+        

+        expect(response.data).toEqual(mockResponse);

+        // 检查token是否存入localStorage

+        expect(localStorage.getItem('token')).toBe('mock-token');

+      });

+    

+

+    it('should handle login failure', async () => {

+      mockAxios.onPost('/user/login').reply(401);

+      

+      await expect(login('wronguser', 'wrongpass')).rejects.toThrow();

+    });

+  });

+

+  describe('register', () => {

+    it('should send register request with username, password and code', async () => {

+      const mockResponse = {

+        code: 200,

+        message: '注册成功'

+      };

+      

+      mockAxios.onPost('/user/regist').reply(200, mockResponse);

+

+      const response = await register('newuser', 'newpass', 'invite123');

+      

+      expect(response.data).toEqual(mockResponse);

+    });

+

+    it('should handle registration failure', async () => {

+      mockAxios.onPost('/user/regist').reply(400);

+      

+      await expect(register('newuser', 'newpass', 'wrongcode')).rejects.toThrow();

+    });

+  });

+

+  describe('getUserInfo', () => {

+    it('should send request with token to get user info', async () => {

+      const mockResponse = {

+        code: 200,

+        data: { username: 'testuser', role: 'user' }

+      };

+      

+      mockAxios.onGet('/user/info').reply(200, mockResponse);

+

+      const response = await getUserInfo('test-token');

+      

+      expect(response.data).toEqual(mockResponse);

+    });

+

+    it('should handle unauthorized request', async () => {

+      mockAxios.onGet('/user/info').reply(401);

+      

+      await expect(getUserInfo('invalid-token')).rejects.toThrow();

+    });

+  });

+

+  describe('request interceptor', () => {

+    it('should add Authorization header when token exists', async () => {

+      localStorage.setItem('token', 'test-token');

+      const mockResponse = { data: 'success' };

+      

+      mockAxios.onGet('/test').reply((config) => {

+        expect(config.headers.Authorization).toBe('Bearer test-token');

+        return [200, mockResponse];

+      });

+

+      const response = await api.get('/test');

+      expect(response.data).toEqual(mockResponse);

+    });

+

+    it('should not add Authorization header when token does not exist', async () => {

+      const mockResponse = { data: 'success' };

+      

+      mockAxios.onGet('/test').reply((config) => {

+        expect(config.headers.Authorization).toBeUndefined();

+        return [200, mockResponse];

+      });

+

+      const response = await api.get('/test');

+      expect(response.data).toEqual(mockResponse);

+    });

+  });

+});
\ No newline at end of file
diff --git a/src/api/helpComment.js b/src/api/helpComment.js
new file mode 100644
index 0000000..e711587
--- /dev/null
+++ b/src/api/helpComment.js
@@ -0,0 +1,13 @@
+import { api } from './auth';

+

+export const likePostComment = (commentId) => {

+  return api.post(`/help/comments/${commentId}/like`);

+};

+

+export const getCommentReplies = (commentId) => {

+  return api.get(`/help/comments/${commentId}/replies`);

+};

+

+export const addCommentReply = (commentId, replyData) => {

+  return api.post(`/help/comments/${commentId}/replies`, replyData);

+};
\ No newline at end of file
diff --git a/src/api/helpComment.test.js b/src/api/helpComment.test.js
new file mode 100644
index 0000000..2b7be4c
--- /dev/null
+++ b/src/api/helpComment.test.js
@@ -0,0 +1,45 @@
+import MockAdapter from 'axios-mock-adapter';

+import { api } from './auth'; // 添加api导入

+import { likePostComment, getCommentReplies, addCommentReply } from './helpComment';

+

+describe('求助帖评论API', () => {

+  let mockAxios;

+

+  beforeEach(() => {

+    mockAxios = new MockAdapter(api);

+  });

+

+  afterEach(() => {

+    mockAxios.restore();

+  });

+

+  describe('likePostComment - 点赞求助帖评论', () => {

+    it('应该成功发送点赞请求', async () => {

+      const mockResponse = { code: 200, message: '点赞成功' };

+      mockAxios.onPost('/help/comments/123/like').reply(200, mockResponse);

+

+      const response = await likePostComment('123');

+      expect(response.data).toEqual(mockResponse);

+    });

+  });

+

+  describe('getCommentReplies - 获取评论回复', () => {

+    it('应该返回正确的回复数据结构', async () => {

+      const mockData = [{ id: '1', content: '回复内容' }];

+      mockAxios.onGet('/help/comments/456/replies').reply(200, { code: 200, data: mockData });

+

+      const response = await getCommentReplies('456');

+      expect(response.data.data).toEqual(mockData);

+    });

+  });

+

+  describe('addCommentReply - 添加评论回复', () => {

+    it('应该正确发送回复内容', async () => {

+      const testData = { content: '测试回复', author: 'user1' };

+      mockAxios.onPost('/help/comments/789/replies', testData).reply(200, { code: 200 });

+

+      const response = await addCommentReply('789', testData);

+      expect(response.status).toBe(200);

+    });

+  });

+});
\ No newline at end of file
diff --git a/src/api/helpPost.js b/src/api/helpPost.js
new file mode 100644
index 0000000..426e92b
--- /dev/null
+++ b/src/api/helpPost.js
@@ -0,0 +1,28 @@
+// src/api/helpPost.js

+import { api } from './auth'; // 复用已有的axios实例

+

+export const createPost = (title, content, authorId) => {

+  return api.post('/help/posts', { 

+    title, 

+    content, 

+    authorId 

+  });

+};

+

+export const getPosts = (page = 1, size = 5) => {

+  return api.get('/help/posts', {

+    params: { page, size }

+  });

+};

+

+export const getPostDetail = (postId) => {

+  return api.get(`/help/posts/${postId}`);

+};

+

+export const likePost = (postId) => {

+  return api.post(`/help/posts/${postId}/like`);

+};

+

+export const addPostComment = (postId, commentData) => {

+  return api.post(`/help/posts/${postId}/comments`, commentData);

+};
\ No newline at end of file
diff --git a/src/api/helpPost.test.js b/src/api/helpPost.test.js
new file mode 100644
index 0000000..e163090
--- /dev/null
+++ b/src/api/helpPost.test.js
@@ -0,0 +1,60 @@
+import MockAdapter from 'axios-mock-adapter';

+import { api } from './auth'; // 添加api导入

+import { createPost, getPosts, getPostDetail, likePost, addPostComment } from './helpPost';

+

+describe('求助帖API', () => {

+  let mockAxios;

+

+  beforeEach(() => {

+    mockAxios = new MockAdapter(api);

+  });

+

+  afterEach(() => {

+    mockAxios.restore();

+  });

+

+  describe('createPost - 创建求助帖', () => {

+    it('应该正确发送帖子数据', async () => {

+      const postData = {

+        title: '测试标题',

+        content: '测试内容',

+        authorId: 'user123'

+      };

+      mockAxios.onPost('/help/posts', postData).reply(201, { code: 201 });

+

+      const response = await createPost(postData.title, postData.content, postData.authorId);

+      expect(response.status).toBe(201);

+    });

+  });

+

+  describe('getPosts - 获取求助帖列表', () => {

+    it('应该支持分页参数', async () => {

+      const page = 2, size = 10;

+      mockAxios.onGet('/help/posts', { params: { page, size } }).reply(200, {

+        code: 200,

+        data: []

+      });

+

+      const response = await getPosts(page, size);

+      expect(response.status).toBe(200);

+    });

+  });

+

+  describe('likePost - 点赞求助帖', () => {

+    it('应该正确发送点赞请求', async () => {

+      mockAxios.onPost('/help/posts/post123/like').reply(200, { code: 200 });

+      const response = await likePost('post123');

+      expect(response.status).toBe(200);

+    });

+  });

+

+  describe('addPostComment - 添加帖子评论', () => {

+    it('应该正确发送评论数据', async () => {

+      const comment = { content: '测试评论', author: 'user1' };

+      mockAxios.onPost('/help/posts/post456/comments', comment).reply(200, { code: 200 });

+      

+      const response = await addPostComment('post456', comment);

+      expect(response.status).toBe(200);

+    });

+  });

+});
\ No newline at end of file
diff --git a/src/api/torrent.js b/src/api/torrent.js
new file mode 100644
index 0000000..7118436
--- /dev/null
+++ b/src/api/torrent.js
@@ -0,0 +1,48 @@
+// src/api/torrent.js

+import { api } from './auth'; // 复用已有的axios实例

+

+export const createTorrent = (torrentName, description, username, category, region, resolution, subtitle, filePath) => {

+  return api.post('/torrent', { 

+    torrentName, 

+    description,

+    username,

+    category,

+    region,

+    resolution,

+    subtitle,

+    filePath

+  });

+};

+

+export const getTorrents = (page = 1, size = 5) => {

+  return api.get('/torrent', {

+    params: { page, size }

+  });

+};

+

+export const getTorrentDetail = (torrentId) => {

+  return api.get(`/torrent/${torrentId}`).then(response => {

+    // 确保数据结构一致

+    if (response.data && response.data.data) {

+      return {

+        ...response,

+        data: {

+          ...response.data,

+          data: {

+            torrent: response.data.data.post || response.data.data.torrent,

+            comments: response.data.data.comments || []

+          }

+        }

+      };

+    }

+    return response;

+  });

+};

+

+export const likeTorrent = (torrentId) => {

+  return api.post(`/torrent/${torrentId}/like`);

+};

+

+export const addTorrentComment = (torrentId, commentData) => {

+  return api.post(`/torrent/${torrentId}/comments`, commentData);

+};
\ No newline at end of file
diff --git a/src/api/torrent.test.js b/src/api/torrent.test.js
new file mode 100644
index 0000000..c4de6c5
--- /dev/null
+++ b/src/api/torrent.test.js
@@ -0,0 +1,58 @@
+import MockAdapter from 'axios-mock-adapter';

+import { api } from './auth'; // 添加api导入

+import { createTorrent, getTorrents, getTorrentDetail, likeTorrent, addTorrentComment } from './torrent';

+

+describe('种子资源API', () => {

+  let mockAxios;

+

+  beforeEach(() => {

+    mockAxios = new MockAdapter(api);

+  });

+

+  afterEach(() => {

+    mockAxios.restore();

+  });

+

+  describe('createTorrent - 创建种子', () => {

+    it('应该发送完整的种子数据', async () => {

+      const torrentData = {

+        torrentName: '测试种子',

+        description: '描述内容',

+        username: 'user1',

+        category: '电影',

+        region: '美国',

+        resolution: '1080p',

+        subtitle: '中文字幕',

+        filePath: '/path/to/file'

+      };

+      mockAxios.onPost('/torrent', torrentData).reply(201, { code: 201 });

+

+      const response = await createTorrent(...Object.values(torrentData));

+      expect(response.status).toBe(201);

+    });

+  });

+

+  describe('getTorrentDetail - 获取种子详情', () => {

+    it('应该规范化返回的数据结构', async () => {

+      const mockData = {

+        data: {

+          post: { id: '123', name: '测试种子' },

+          comments: [{ id: '1', content: '评论1' }]

+        }

+      };

+      mockAxios.onGet('/torrent/123').reply(200, mockData);

+

+      const response = await getTorrentDetail('123');

+      expect(response.data.data.torrent.name).toBe('测试种子');

+      expect(response.data.data.comments).toHaveLength(1);

+    });

+  });

+

+  describe('likeTorrent - 点赞种子', () => {

+    it('应该成功发送点赞请求', async () => {

+      mockAxios.onPost('/torrent/t123/like').reply(200, { code: 200 });

+      const response = await likeTorrent('t123');

+      expect(response.status).toBe(200);

+    });

+  });

+});
\ No newline at end of file
diff --git a/src/api/torrentComment.js b/src/api/torrentComment.js
new file mode 100644
index 0000000..f15ce4b
--- /dev/null
+++ b/src/api/torrentComment.js
@@ -0,0 +1,13 @@
+import { api } from './auth';

+

+export const likeTorrentComment = (commentId) => {

+  return api.post(`/torrent/comments/${commentId}/like`);

+};

+

+export const getCommentReplies = (commentId) => {

+  return api.get(`/torrent/comments/${commentId}/replies`);

+};

+

+export const addCommentReply = (commentId, replyData) => {

+  return api.post(`/torrent/comments/${commentId}/replies`, replyData);

+};
\ No newline at end of file
diff --git a/src/api/torrentComment.test.js b/src/api/torrentComment.test.js
new file mode 100644
index 0000000..585b0d3
--- /dev/null
+++ b/src/api/torrentComment.test.js
@@ -0,0 +1,60 @@
+import MockAdapter from 'axios-mock-adapter';

+import { api } from './auth'; // 添加api导入

+import { likeTorrentComment, getCommentReplies, addCommentReply } from './torrentComment';

+

+describe('种子评论API', () => {

+  let mockAxios;

+

+  beforeEach(() => {

+    mockAxios = new MockAdapter(api);

+  });

+

+  afterEach(() => {

+    mockAxios.restore();

+  });

+

+  describe('likeTorrentComment - 点赞种子评论', () => {

+    it('应该成功发送点赞请求', async () => {

+      const commentId = '123';

+      const mockResponse = { code: 200, message: '点赞成功' };

+      

+      mockAxios.onPost(`/torrent/comments/${commentId}/like`).reply(200, mockResponse);

+

+      const response = await likeTorrentComment(commentId);

+      expect(response.data).toEqual(mockResponse);

+    });

+

+    it('应该处理点赞失败的情况', async () => {

+      mockAxios.onPost('/torrent/comments/123/like').reply(500);

+      await expect(likeTorrentComment('123')).rejects.toThrow();

+    });

+  });

+

+  describe('getCommentReplies - 获取评论回复', () => {

+    it('应该成功获取回复列表', async () => {

+      const commentId = '456';

+      const mockResponse = {

+        code: 200,

+        data: [{ id: '1', content: '回复1' }, { id: '2', content: '回复2' }]

+      };

+      

+      mockAxios.onGet(`/torrent/comments/${commentId}/replies`).reply(200, mockResponse);

+

+      const response = await getCommentReplies(commentId);

+      expect(response.data).toEqual(mockResponse);

+    });

+  });

+

+  describe('addCommentReply - 添加评论回复', () => {

+    it('应该成功添加回复', async () => {

+      const commentId = '789';

+      const replyData = { content: '测试回复' };

+      const mockResponse = { code: 200, data: { id: '3', ...replyData } };

+      

+      mockAxios.onPost(`/torrent/comments/${commentId}/replies`, replyData).reply(200, mockResponse);

+

+      const response = await addCommentReply(commentId, replyData);

+      expect(response.data).toEqual(mockResponse);

+    });

+  });

+});
\ No newline at end of file
diff --git a/src/components/AnnouncementDetail.css b/src/components/AnnouncementDetail.css
new file mode 100644
index 0000000..5ca87c1
--- /dev/null
+++ b/src/components/AnnouncementDetail.css
@@ -0,0 +1,64 @@
+.announcement-container {

+    max-width: 800px;

+    margin: 0 auto;

+    padding: 20px;

+  }

+  

+  .back-button {

+    background: none;

+    border: none;

+    color: #1890ff;

+    font-size: 16px;

+    cursor: pointer;

+    margin-bottom: 20px;

+  }

+  

+  .announcement-detail {

+    background: white;

+    padding: 25px;

+    border-radius: 8px;

+    box-shadow: 0 2px 10px rgba(0,0,0,0.1);

+  }

+  

+  .announcement-header {

+    border-bottom: 1px solid #f0f0f0;

+    padding-bottom: 15px;

+    margin-bottom: 20px;

+  }

+  

+  .announcement-header h1 {

+    margin: 0 0 10px 0;

+    font-size: 24px;

+  }

+  

+  .announcement-meta {

+    display: flex;

+    gap: 15px;

+    color: #666;

+    font-size: 14px;

+    align-items: center;

+  }

+  

+  .category-badge {

+    background: #e6f7ff;

+    color: #1890ff;

+    padding: 2px 8px;

+    border-radius: 4px;

+    font-size: 12px;

+  }

+  

+  .announcement-content {

+    line-height: 1.8;

+    font-size: 15px;

+  }

+  

+  .announcement-content p {

+    margin: 0 0 16px 0;

+  }

+  

+  .error-message {

+    text-align: center;

+    padding: 50px;

+    color: #ff4d4f;

+    font-size: 16px;

+  }
\ No newline at end of file
diff --git a/src/components/AnnouncementDetail.jsx b/src/components/AnnouncementDetail.jsx
new file mode 100644
index 0000000..8e90672
--- /dev/null
+++ b/src/components/AnnouncementDetail.jsx
@@ -0,0 +1,61 @@
+import React from 'react';

+import { useNavigate, useLocation } from 'react-router-dom';

+import './AnnouncementDetail.css';

+

+const AnnouncementDetail = () => {

+  const navigate = useNavigate();

+const location = useLocation();

+  const { state } = useLocation();

+  const announcement = state?.announcement;

+  const handleBack = () => {

+    const fromTab = location.state?.fromTab; // 从跳转时传递的 state 中获取

+    if (fromTab) {

+    navigate(`/dashboard/${fromTab}`); // 明确返回对应标签页

+    } else {

+        navigate(-1); // 保底策略

+    }

+}

+  

+    

+

+  if (!announcement) {

+    return (

+      <div className="announcement-container">

+        <button className="back-button" onClick={() => navigate(-1)}>

+          &larr; 返回公告区

+        </button>

+        <div className="error-message">公告加载失败,请返回重试</div>

+      </div>

+    );

+  }

+

+  return (

+    <div className="announcement-container">

+      <button className="back-button" onClick={handleBack}>

+        &larr; 返回

+      </button>

+

+      <div className="announcement-detail">

+        <div className="announcement-header">

+          <h1>{announcement.title}</h1>

+          <div className="announcement-meta">

+            <span className="category-badge">{announcement.category}</span>

+            <span>发布人:{announcement.author}</span>

+            <span>发布日期:{announcement.date}</span>

+          </div>

+        </div>

+

+        <div className="announcement-content">

+          {announcement.content.split('\n').map((paragraph, i) => (

+            <p key={i}>{paragraph}</p>

+          ))}

+        </div>

+      </div>

+    </div>

+  );

+};

+

+export default AnnouncementDetail;

+

+

+    
\ No newline at end of file
diff --git a/src/components/AuthForm.css b/src/components/AuthForm.css
new file mode 100644
index 0000000..8eb1d0a
--- /dev/null
+++ b/src/components/AuthForm.css
@@ -0,0 +1,109 @@
+/* src/components/AuthForm.css */

+.auth-container {

+    display: flex;

+    flex-direction: column;

+    align-items: center;

+    justify-content: center;

+    min-height: 100vh;

+    background-color: #f5f5f5;

+    padding: 20px;

+  }

+  

+  .auth-title {

+    color: #333;

+    font-size: 2.5rem;

+    margin-bottom: 2rem;

+    text-align: center;

+  }

+  

+  .auth-form-wrapper {

+    width: 100%;

+    max-width: 400px;

+    background: white;

+    padding: 2rem;

+    border-radius: 8px;

+    box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);

+  }

+  

+  .auth-form {

+    display: flex;

+    flex-direction: column;

+    gap: 1rem;

+  }

+  

+  .auth-form h2 {

+    color: #333;

+    text-align: center;

+    margin-bottom: 1.5rem;

+  }

+  

+  .form-group {

+    display: flex;

+    flex-direction: column;

+    gap: 0.5rem;

+  }

+  

+  .form-group input {

+    padding: 0.8rem;

+    border: 1px solid #ddd;

+    border-radius: 4px;

+    font-size: 1rem;

+  }

+  

+  .form-group input:focus {

+    outline: none;

+    border-color: #007bff;

+  }

+  

+  .auth-button {

+    padding: 0.8rem;

+    background-color: #007bff;

+    color: white;

+    border: none;

+    border-radius: 4px;

+    font-size: 1rem;

+    cursor: pointer;

+    transition: background-color 0.3s;

+  }

+  

+  .auth-button:hover {

+    background-color: #0056b3;

+  }

+  

+  .auth-button:disabled {

+    background-color: #cccccc;

+    cursor: not-allowed;

+  }

+  

+  .auth-switch {

+    display: flex;

+    justify-content: center;

+    align-items: center;

+    gap: 0.5rem;

+    margin-top: 1rem;

+    font-size: 0.9rem;

+    color: #666;

+  }

+  

+  .switch-button {

+    background: none;

+    border: none;

+    color: #007bff;

+    cursor: pointer;

+    text-decoration: underline;

+    font-size: 0.9rem;

+    padding: 0;

+  }

+  

+  .switch-button:hover {

+    color: #0056b3;

+  }

+  

+  .error-message {

+    color: #dc3545;

+    background-color: #f8d7da;

+    padding: 0.5rem;

+    border-radius: 4px;

+    text-align: center;

+    margin-bottom: 1rem;

+  }
\ No newline at end of file
diff --git a/src/components/AuthForm.jsx b/src/components/AuthForm.jsx
new file mode 100644
index 0000000..ca28101
--- /dev/null
+++ b/src/components/AuthForm.jsx
@@ -0,0 +1,159 @@
+// src/components/AuthForm.jsx

+import React, { useState } from 'react';

+import { useNavigate } from 'react-router-dom';

+import { login, register } from '../api/auth';

+import './AuthForm.css';

+

+const AuthForm = ({ onLoginSuccess }) => {

+  const [isLogin, setIsLogin] = useState(true);

+  const [formData, setFormData] = useState({

+    username: '',

+    password: '',

+    code: ''

+  });

+  const [error, setError] = useState('');

+  const [isLoading, setIsLoading] = useState(false);

+  const navigate = useNavigate();

+

+  const handleChange = (e) => {

+    const { name, value } = e.target;

+    setFormData(prev => ({ ...prev, [name]: value }));

+  };

+

+  const handleSubmit = async (e) => {

+    e.preventDefault();

+    

+    try {

+      if (isLogin) {

+            const response = await login(formData.username, formData.password);

+            if (response.data.code === 200) {

+              localStorage.setItem('token', response.data.data.token);

+              localStorage.setItem('username', formData.username);

+              onLoginSuccess?.();

+              navigate('/dashboard');

+            } else {

+              setError(response.data.message || '登录失败');

+            }

+          } else {

+            const response = await register(

+              formData.username,

+              formData.password,

+              formData.code

+            );

+            if (response.data.code === 200) {

+              setError('');

+              setIsLogin(true);

+              setFormData({ username: '', password: '', code: '' });

+            } else {

+              setError(response.data.message || '注册失败');

+            }

+          }

+        } catch (err) {

+          setError(err.message || (isLogin ? '登录失败' : '注册失败'));

+        } finally {

+          setIsLoading(false);

+        }

+      

+

+      

+  };

+

+  return (

+    <div className="auth-container">

+      <h1 className="auth-title">PT影视资源站</h1>

+      

+      <div className="auth-form-wrapper">

+        {isLogin ? (

+          <form className="auth-form" onSubmit={handleSubmit}>

+            <h2>用户登录</h2>

+            {error && <div className="error-message">{error}</div>}

+            

+            <div className="form-group">

+              <input

+                type="text"

+                name="username"

+                placeholder="账号"

+                value={formData.username}

+                onChange={handleChange}

+                required

+              />

+            </div>

+            

+            <div className="form-group">

+              <input

+                type="password"

+                name="password"

+                placeholder="密码"

+                value={formData.password}

+                onChange={handleChange}

+                required

+              />

+            </div>

+            

+            <button type="submit" className="auth-button" disabled={isLoading}>

+              {isLoading ? '登录中...' : '登录'}

+            </button>

+            

+            <div className="auth-switch">

+              <span>未有账户?</span>

+              <button type="button" onClick={() => setIsLogin(false)} className="switch-button">

+                去注册

+              </button>

+            </div>

+          </form>

+        ) : (

+          <form className="auth-form" onSubmit={handleSubmit}>

+            <h2>用户注册</h2>

+            {error && <div className="error-message">{error}</div>}

+            

+            <div className="form-group">

+              <input

+                type="text"

+                name="username"

+                placeholder="账号"

+                value={formData.username}

+                onChange={handleChange}

+                required

+              />

+            </div>

+            

+            <div className="form-group">

+              <input

+                type="password"

+                name="password"

+                placeholder="密码"

+                value={formData.password}

+                onChange={handleChange}

+                required

+              />

+            </div>

+            

+            <div className="form-group">

+              <input

+                type="text"

+                name="code"

+                placeholder="邀请码"

+                value={formData.code}

+                onChange={handleChange}

+                required

+              />

+            </div>

+            

+            <button type="submit" className="auth-button" disabled={isLoading}>

+              {isLoading ? '注册中...' : '注册'}

+            </button>

+            

+            <div className="auth-switch">

+              <span>已有账户?</span>

+              <button type="button" onClick={() => setIsLogin(true)} className="switch-button">

+                去登录

+              </button>

+            </div>

+          </form>

+        )}

+      </div>

+    </div>

+  );

+};

+

+export default AuthForm;
\ No newline at end of file
diff --git a/src/components/Dashboard.css b/src/components/Dashboard.css
new file mode 100644
index 0000000..af9e421
--- /dev/null
+++ b/src/components/Dashboard.css
@@ -0,0 +1,649 @@
+/* src/components/Dashboard.css */

+.dashboard-container {

+    display: flex;

+    flex-direction: column;

+    height: 100vh;

+    background-color: #f5f5f5;

+  }

+  

+  .top-bar {

+    display: flex;

+    justify-content: space-between;

+    align-items: center;

+    padding: 10px 20px;

+    background-color: #fff;

+    box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);

+    position: sticky;

+    top: 0;

+    z-index: 100;

+  }

+  

+  .search-container {

+    display: flex;

+    flex-grow: 1;

+    max-width: 600px;

+    margin: 0 auto;

+  }

+  

+  .search-input {

+    flex-grow: 1;

+    padding: 8px 15px;

+    border: 1px solid #ddd;

+    border-radius: 20px 0 0 20px;

+    font-size: 16px;

+    outline: none;

+  }

+  

+  .search-button {

+    padding: 8px 15px;

+    background-color: #007bff;

+    color: white;

+    border: none;

+    border-radius: 0 20px 20px 0;

+    cursor: pointer;

+    font-size: 16px;

+  }

+  

+  .search-button:hover {

+    background-color: #0056b3;

+  }

+  

+  .user-info {

+    display: flex;

+    align-items: center;

+    gap: 10px;

+  }

+  

+  .user-avatar {

+    width: 40px;

+    height: 40px;

+    border-radius: 50%;

+    object-fit: cover;

+  }

+  

+  .username {

+    font-weight: bold;

+  }

+  

+  .logout-button {

+    padding: 5px 10px;

+    background-color: #f8f9fa;

+    border: 1px solid #ddd;

+    border-radius: 4px;

+    cursor: pointer;

+    margin-left: 10px;

+  }

+  

+  .logout-button:hover {

+    background-color: #e9ecef;

+  }

+  

+  .nav-tabs {

+    display: flex;

+    background-color: #fff;

+    border-bottom: 1px solid #ddd;

+    padding: 0 20px;

+  }

+  

+  .tab-button {

+    padding: 12px 20px;

+    background: none;

+    border: none;

+    border-bottom: 3px solid transparent;

+    cursor: pointer;

+    font-size: 16px;

+    color: #666;

+  }

+  

+  .tab-button:hover {

+    color: #007bff;

+  }

+  

+  .tab-button.active {

+    color: #007bff;

+    border-bottom-color: #007bff;

+    font-weight: bold;

+  }

+  

+  .content-area {

+    flex-grow: 1;

+    padding: 20px;

+    background-color: #fff;

+    margin: 20px;

+    border-radius: 4px;

+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);

+  }

+  

+  .loading, .error {

+    padding: 20px;

+    text-align: center;

+    font-size: 18px;

+  }

+  

+  .error {

+    color: #dc3545;

+  }

+

+/* 公告区样式 */

+  /* 轮播图样式 */

+.carousel-container {

+    position: relative;

+    width: 100%;

+    height: 200px;

+    overflow: hidden;

+    margin-bottom: 20px;

+    border-radius: 4px;

+  }

+  

+  .carousel-slide {

+    position: absolute;

+    width: 100%;

+    height: 100%;

+    opacity: 0;

+    transition: opacity 1s ease-in-out;

+  }

+  

+  .carousel-slide.active {

+    opacity: 1;

+  }

+  

+  .carousel-image {

+    width: 100%;

+    height: 100%;

+    display: flex;

+    align-items: center;

+    justify-content: center;

+    font-size: 24px;

+    font-weight: bold;

+    color: white;

+  }

+  

+  .gray-bg {

+    background-color: #666;

+  }

+  

+  .carousel-dots {

+    position: absolute;

+    bottom: 15px;

+    left: 0;

+    right: 0;

+    display: flex;

+    justify-content: center;

+    gap: 10px;

+  }

+  

+  .dot {

+    width: 12px;

+    height: 12px;

+    border-radius: 50%;

+    background-color: rgba(255, 255, 255, 0.5);

+    cursor: pointer;

+  }

+  

+  .dot.active {

+    background-color: white;

+  }

+  

+  /* 公告网格样式 */

+  .announcement-grid {

+    display: grid;

+    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));

+    gap: 20px;

+  }

+  

+  .announcement-card {

+    background-color: white;

+    padding: 15px;

+    border-radius: 4px;

+    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);

+    transition: transform 0.2s, box-shadow 0.2s;

+  }

+  

+  .announcement-card:hover {

+    transform: translateY(-2px);

+    box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);

+  }

+  

+  .announcement-card h3 {

+    margin-top: 0;

+    color: #007bff;

+    border-bottom: 1px solid #eee;

+    padding-bottom: 8px;

+  }

+  

+  .announcement-card p {

+    margin-bottom: 0;

+    color: #666;

+    line-height: 1.5;

+  }

+/* 分享区样式   */

+ /* 上传头部样式 */

+.upload-header {

+  margin-bottom: 20px;

+  display: flex;

+  justify-content: flex-start;

+}

+

+.upload-btn {

+  padding: 10px 20px;

+  background: #1890ff;

+  color: white;

+  border: none;

+  border-radius: 4px;

+  font-size: 16px;

+  cursor: pointer;

+  transition: all 0.3s;

+}

+

+.upload-btn:hover {

+  background: #40a9ff;

+}

+

+/* 弹窗样式 */

+.modal-overlay {

+  position: fixed;

+  top: 0;

+  left: 0;

+  right: 0;

+  bottom: 0;

+  background: rgba(0,0,0,0.5);

+  display: flex;

+  justify-content: center;

+  align-items: center;

+  z-index: 1000;

+}

+

+.upload-modal {

+  background: white;

+  padding: 25px;

+  border-radius: 8px;

+  width: 500px;

+  max-width: 90%;

+  position: relative;

+}

+

+.close-btn {

+  position: absolute;

+  top: 15px;

+  right: 15px;

+  background: none;

+  border: none;

+  font-size: 20px;

+  cursor: pointer;

+}

+

+/* 表单样式 */

+.form-group {

+  margin-bottom: 15px;

+}

+

+.form-group label {

+  display: block;

+  margin-bottom: 5px;

+  font-weight: 500;

+}

+

+.form-group input[type="text"],

+.form-group select,

+.form-group textarea {

+  width: 100%;

+  padding: 8px;

+  border: 1px solid #d9d9d9;

+  border-radius: 4px;

+}

+

+.form-group textarea {

+  min-height: 80px;

+}

+

+.form-actions {

+  display: flex;

+  justify-content: flex-end;

+  gap: 10px;

+  margin-top: 20px;

+}

+

+.cancel-btn {

+  padding: 8px 15px;

+  background: #f5f5f5;

+  border: 1px solid #d9d9d9;

+  border-radius: 4px;

+  cursor: pointer;

+}

+

+.cancel-btn:hover {

+  background: #e6e6e6;

+}

+  /* 筛选区域 */

+.filter-section {

+  background: #f9f9f9;

+  padding: 15px;

+  border-radius: 8px;

+  margin-bottom: 20px;

+}

+

+.filter-group {

+  margin-bottom: 15px;

+}

+

+.filter-group h4 {

+  margin: 0 0 8px 0;

+  font-size: 15px;

+  color: #666;

+}

+

+/* 筛选按钮 */

+.filter-btn {

+  padding: 6px 12px;

+  margin-right: 8px;

+  margin-bottom: 8px;

+  border: 1px solid #ddd;

+  background: white;

+  border-radius: 4px;

+  cursor: pointer;

+  transition: all 0.2s;

+}

+

+.filter-btn:hover {

+  border-color: #1890ff;

+  color: #1890ff;

+}

+

+.filter-btn.active {

+  background: #1890ff;

+  color: white;

+  border-color: #1890ff;

+}

+

+/* 确定按钮 */

+.confirm-btn {

+  padding: 8px 20px;

+  background: #1890ff;

+  color: white;

+  border: none;

+  border-radius: 4px;

+  cursor: pointer;

+  font-size: 14px;

+  transition: opacity 0.3s;

+}

+

+.confirm-btn:hover {

+  background: #007bff;

+}

+

+.confirm-btn:disabled {

+  opacity: 0.5;

+  cursor: not-allowed;

+}

+

+

+/* 资源列表 */

+.resource-list {

+  display: flex;

+  flex-direction: column;

+  gap: 15px;

+}

+

+.resource-item {

+  display: flex;

+  align-items: center;

+  padding: 15px;

+  background-color: white;

+  border-radius: 4px;

+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);

+  transition: transform 0.2s, box-shadow 0.2s;

+}

+

+.resource-item:hover {

+  transform: translateY(-2px);

+  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);

+}

+

+.resource-poster {

+  margin-right: 15px;

+}

+

+.poster-image {

+  width: 60px;

+  height: 80px;

+  background-color: #666;

+  display: flex;

+  align-items: center;

+  justify-content: center;

+  color: white;

+  font-size: 24px;

+  font-weight: bold;

+  border-radius: 4px;

+}

+

+.resource-info {

+  flex-grow: 1;

+}

+

+.resource-title {

+  margin: 0 0 5px 0;

+  font-size: 16px;

+  font-weight: bold;

+}

+

+.resource-meta {

+  margin: 0;

+  font-size: 14px;

+  color: #666;

+}

+

+.resource-stats {

+  display: flex;

+  flex-direction: column;

+  align-items: flex-end;

+  margin-right: 20px;

+  min-width: 150px;

+}

+

+.stat {

+  font-size: 13px;

+  color: #666;

+  margin-bottom: 5px;

+}

+

+.download-btn {

+  padding: 8px 15px;

+  background-color: #28a745;

+  color: white;

+  border: none;

+  border-radius: 4px;

+  cursor: pointer;

+  font-size: 14px;

+  transition: background-color 0.2s;

+}

+

+.download-btn:hover {

+  background-color: #218838;

+}

+/* 发帖按钮样式 */

+.post-header {

+  margin-bottom: 20px;

+  text-align: right;

+}

+

+.create-post-btn {

+  background-color: #1890ff;

+  color: white;

+  border: none;

+  padding: 8px 16px;

+  border-radius: 4px;

+  cursor: pointer;

+  font-size: 14px;

+  transition: background-color 0.3s;

+}

+

+.create-post-btn:hover {

+  background-color: #40a9ff;

+}

+

+/* 弹窗样式 */

+.post-modal-overlay {

+  position: fixed;

+  top: 0;

+  left: 0;

+  right: 0;

+  bottom: 0;

+  background-color: rgba(0, 0, 0, 0.5);

+  display: flex;

+  justify-content: center;

+  align-items: center;

+  z-index: 1000;

+}

+

+.post-modal {

+  background-color: white;

+  padding: 24px;

+  border-radius: 8px;

+  width: 500px;

+  max-width: 90%;

+  position: relative;

+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);

+}

+

+.post-modal h3 {

+  margin-top: 0;

+  margin-bottom: 20px;

+  font-size: 18px;

+  color: #333;

+}

+

+.modal-close-btn {

+  position: absolute;

+  top: 16px;

+  right: 16px;

+  background: none;

+  border: none;

+  font-size: 20px;

+  cursor: pointer;

+  color: #999;

+}

+

+.modal-close-btn:hover {

+  color: #666;

+}

+

+/* 表单样式 */

+.form-group {

+  margin-bottom: 16px;

+}

+

+.form-group label {

+  display: block;

+  margin-bottom: 8px;

+  font-weight: 500;

+  color: #333;

+}

+

+.form-group input[type="text"],

+.form-group textarea {

+  width: 100%;

+  padding: 8px 12px;

+  border: 1px solid #d9d9d9;

+  border-radius: 4px;

+  font-size: 14px;

+}

+

+.form-group textarea {

+  min-height: 120px;

+  resize: vertical;

+}

+

+.upload-image-btn {

+  display: flex;

+  align-items: center;

+}

+

+.upload-image-btn label {

+  background-color: #f5f5f5;

+  padding: 8px 16px;

+  border-radius: 4px;

+  cursor: pointer;

+  border: 1px dashed #d9d9d9;

+  margin-right: 10px;

+  transition: all 0.3s;

+}

+

+.upload-image-btn label:hover {

+  border-color: #1890ff;

+  color: #1890ff;

+}

+

+.image-name {

+  font-size: 14px;

+  color: #666;

+}

+

+/* 按钮样式 */

+.form-actions {

+  display: flex;

+  justify-content: flex-end;

+  margin-top: 24px;

+}

+

+.cancel-btn {

+  background-color: #f5f5f5;

+  color: #333;

+  border: none;

+  padding: 8px 16px;

+  border-radius: 4px;

+  cursor: pointer;

+  margin-right: 10px;

+  transition: background-color 0.3s;

+}

+

+.cancel-btn:hover {

+  background-color: #e6e6e6;

+}

+

+.submit-btn {

+  background-color: #1890ff;

+  color: white;

+  border: none;

+  padding: 8px 16px;

+  border-radius: 4px;

+  cursor: pointer;

+  transition: background-color 0.3s;

+}

+

+.submit-btn:hover {

+  background-color: #40a9ff;

+}

+

+.pagination {

+  display: flex;

+  justify-content: center;

+  margin: 20px 0;

+  gap: 5px;

+}

+

+.pagination button {

+  padding: 5px 10px;

+  border: 1px solid #ddd;

+  background: white;

+  cursor: pointer;

+}

+

+.pagination button.active {

+  background: #1890ff;

+  color: white;

+  border-color: #1890ff;

+}

+

+.pagination button:disabled {

+  opacity: 0.5;

+  cursor: not-allowed;

+}

+

+.filter-section {

+  position: sticky;

+  top: 0;

+  background: white;

+  z-index: 100;

+  padding: 15px;

+  box-shadow: 0 2px 5px rgba(0,0,0,0.1);

+}
\ No newline at end of file
diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx
new file mode 100644
index 0000000..941210c
--- /dev/null
+++ b/src/components/Dashboard.jsx
@@ -0,0 +1,940 @@
+import React, { useEffect, useState } from 'react';

+import { useNavigate, useLocation,useParams } from 'react-router-dom';

+// import { getUserInfo } from '../api';  // 保留import但注释掉

+import { createTorrent, getTorrents} from '../api/torrent'; 

+import './Dashboard.css';

+import { createPost, getPosts } from '../api/helpPost';

+

+

+const Dashboard = ({ onLogout }) => {

+  const location = useLocation();

+  const [userInfo, setUserInfo] = useState(null);

+  const [loading, setLoading] = useState(false);

+  const [error, setError] = useState('');

+  const [currentSlide, setCurrentSlide] = useState(0);

+  const navigate = useNavigate();

+  const {tab} = useParams();

+  const [showUploadModal, setShowUploadModal] = useState(false);

+  const [isUploading, setIsUploading] = useState(false);

+  const [uploadData, setUploadData] = useState({

+    name: '',

+    type: '',

+    region: '',

+    subtitle: '',

+    resolution: '', // 新增分辨率字段

+    file: null,

+    description: ''

+  });

+  const [showPostModal, setShowPostModal] = useState(false);

+  const [postTitle, setPostTitle] = useState('');

+  const [postContent, setPostContent] = useState('');

+  const [selectedImage, setSelectedImage] = useState(null);

+  const [helpPosts, setHelpPosts] = useState([]);

+  const [helpLoading, setHelpLoading] = useState(false);

+  const [helpError, setHelpError] = useState(null);

+  const [currentPage, setCurrentPage] = useState(1);

+  const [totalPages, setTotalPages] = useState(1);

+

+    // 添加状态

+  const [torrentPosts, setTorrentPosts] = useState([]);

+  const [torrentLoading, setTorrentLoading] = useState(false);

+  const [torrentError, setTorrentError] = useState(null);

+  

+  const [filteredResources, setFilteredResources] = useState(torrentPosts);

+  

+  const activeTab = tab || 'announcement'; // 如果没有tab参数,则默认为announcement

+  // 从location.state中初始化状态

+  

+  

+  const handleTabChange = (tabName) => {

+    navigate(`/dashboard/${tabName}`, { 

+      state: { 

+        savedFilters: selectedFilters, // 使用新的筛选状态 // 保留现有状态

+        activeTab: tabName // 可选,如果其他组件依赖这个 state

+      } 

+    });

+  };

+  

+  //公告区

+  const [announcements] = useState([

+    {

+      id: 1,

+      title: '系统维护与更新',

+      content: '2023-10-15 02:00-06:00将进行系统维护升级,期间网站将无法访问。本次更新包含:\n1. 数据库服务器迁移\n2. 安全补丁更新\n3. CDN节点优化\n\n请提前做好下载安排。',

+      author: '系统管理员',

+      date: '2023-10-10',

+      excerpt: '2023-10-15 02:00-06:00将进行系统维护,期间无法访问',

+      category: '系统'

+    },

+    {

+      id: 2,

+      title: '资源上新',

+      content: '最新热门电影《奥本海默》4K REMUX资源已上线,包含:\n- 4K HDR版本 (56.8GB)\n- 1080P标准版 (12.3GB)\n- 中英双语字幕\n\n欢迎下载保种!',

+      author: '资源组',

+      date: '2023-10-08',

+      excerpt: '最新热门电影《奥本海默》4K资源已上线',

+      category: '资源'

+    },

+    {

+      id: 3,

+      title: '积分规则调整',

+      content: '自11月1日起,上传资源积分奖励提升20%,具体规则如下:\n- 上传电影资源:每GB 10积分\n- 上传电视剧资源:每GB 8积分\n- 上传动漫资源:每GB 6积分\n\n感谢大家的支持与贡献!',

+      author: '管理员',

+      date: '2023-10-05',

+      excerpt: '自11月1日起,上传资源积分奖励提升20%',

+      category: '公告'

+    },

+    {

+      id: 4,

+      title: '违规处理公告',

+      content: '用户user123因发布虚假资源已被封禁,相关资源已删除。请大家遵守社区规则,维护良好的分享环境。',

+      author: '管理员',

+      date: '2023-10-03',

+      excerpt: '用户user123因发布虚假资源已被封禁',

+      category: '违规'

+    },

+    {

+      id: 5,

+      title: '节日活动',

+      content: '国庆期间所有资源下载积分减半,活动时间:2023年10月1日至2023年10月7日。',

+      author: '活动组',

+      date: '2023-09-30',

+      excerpt: '国庆期间所有资源下载积分减半',

+      category: '活动'

+    },

+    {

+      id: 6,

+      title: '客户端更新',

+      content: 'PT客户端v2.5.0已发布,修复多个BUG,新增资源搜索功能。请尽快更新到最新版本以获得更好的使用体验。',

+      author: '开发组',

+      date: '2023-09-28',

+      excerpt: 'PT客户端v2.5.0已发布,修复多个BUG',

+      category: '更新'

+    },

+    // 其他公告...

+  ]);

+

+  

+  const handleAnnouncementClick = (announcement,e) => {

+    if (!e.target.closest('.exclude-click')) {

+      navigate(`/announcement/${announcement.id}`, {

+        state: {

+          announcement,

+          returnPath: `/dashboard/${activeTab}`,

+          scrollPosition: window.scrollY,

+          activeTab

+        }

+      });

+    }

+  };

+

+

+  //资源区

+ 

+

+  const handleFileChange = (e) => {

+    setUploadData({...uploadData, file: e.target.files[0]});

+  };

+

+  const handleUploadSubmit = async (e) => {

+    e.preventDefault();

+    

+    try {

+      setIsUploading(true);

+      const username = localStorage.getItem('username');

+      

+      // 创建种子数据

+      const torrentData = {

+        torrentName: uploadData.name,

+        category: uploadData.type,

+        regin: uploadData.region,

+        resolution: uploadData.resolution,

+        subtitle: uploadData.subtitle,

+        description: uploadData.description,

+        username: username,

+        filePath: "D:\\大学\\大三_下\\实训BT服务器下载\\G2-ptPlatform-backend\\torrents\\[电影天堂www.dytt89.com]两杆大烟枪BD中英双字.mp4.torrent" // 默认路径

+      };

+  

+      // 调用API创建种子

+      await createTorrent(

+        torrentData.torrentName,

+        torrentData.description,

+        torrentData.username,

+        torrentData.category,

+        torrentData.regin,

+        torrentData.resolution,

+        torrentData.subtitle,

+        torrentData.filePath

+      );

+      

+      // 上传成功处理

+      setShowUploadModal(false);

+      setUploadData({ 

+        name: '', 

+        type: '', 

+        region: '', 

+        subtitle: '', 

+        resolution: '', 

+        file: null, 

+        description: '' 

+      });

+      alert('种子创建成功!');

+      

+      // 刷新列表

+      fetchTorrentPosts(currentPage);

+      

+    } catch (error) {

+      console.error('创建失败:', error);

+      alert('创建失败: ' + (error.response?.data?.message || error.message));

+    } finally {

+      setIsUploading(false);

+    }

+  };

+

+  const handlePostSubmit = async (e) => {

+    e.preventDefault();

+    try {

+      const username = localStorage.getItem('username');

+      const response = await createPost(

+        postTitle,

+        postContent,

+        username

+      );

+      

+      if (response.data.code === 200) {

+        // 刷新帖子列表

+        await fetchHelpPosts();

+        // 重置表单

+        setShowPostModal(false);

+        setPostTitle('');

+        setPostContent('');

+        setSelectedImage(null);

+      } else {

+        setHelpError(response.data.message || '发帖失败');

+      }

+    } catch (err) {

+      setHelpError(err.message || '发帖失败');

+    }

+  };

+

+    // 获取Torrent帖子列表

+  const fetchTorrentPosts = async (page = 1) => {

+  setTorrentLoading(true);

+  try {

+    const response = await getTorrents(page);

+    setTorrentPosts(response.data.data.records);

+    setTotalPages(Math.ceil(response.data.data.total / 5)); // 假设每页5条

+    setCurrentPage(page);

+  } catch (err) {

+    setTorrentError(err.message);

+  } finally {

+    setTorrentLoading(false);

+  }

+};

+

+  // 在useEffect中调用

+  useEffect(() => {

+    if (activeTab === 'share') {

+      fetchTorrentPosts();

+    }

+  }, [activeTab]);

+

+  const handleImageUpload = (e) => {

+    setSelectedImage(e.target.files[0]);

+  };

+

+

+

+

+  const fetchHelpPosts = async (page = 1) => {

+    setHelpLoading(true);

+    try {

+      const response = await getPosts(page);

+      if (response.data.code === 200) {

+        setHelpPosts(response.data.data.records);

+        setTotalPages(Math.ceil(response.data.data.total / 5)); // 假设每页5条

+        setCurrentPage(page);

+      } else {

+        setHelpError(response.data.message || '获取求助帖失败');

+      }

+    } catch (err) {

+      setHelpError(err.message || '获取求助帖失败');

+    } finally {

+      setHelpLoading(false);

+    }

+  };

+  useEffect(() => {

+    if (activeTab === 'help') {

+      fetchHelpPosts(currentPage);

+    }

+  }, [activeTab, currentPage]); // 添加 currentPage 作为依赖

+

+

+  

+

+  // 分类维度配置

+  const filterCategories = {

+    type: {

+      label: '类型',

+      options: {

+        'all': '全部',

+        '电影': '电影',

+        '电视剧': '电视剧',

+        '动漫': '动漫',

+        '综艺': '综艺'

+      }

+    },

+    subtitle: {

+      label: '字幕',

+      options: {

+        'all': '全部',

+        'yes': '有字幕',

+        'no': '无字幕'

+      }

+    },

+    region: {

+      label: '地区',

+      options: {

+        'all': '全部',

+        'cn': '大陆',

+        'us': '欧美',

+        'jp': '日本'

+      }

+    }

+  };

+  

+  const [selectedFilters, setSelectedFilters] = useState(

+    location.state?.savedFilters || 

+    Object.keys(filterCategories).reduce((acc, category) => {

+      acc[category] = 'all';

+      return acc;

+    }, {})

+  );

+

+

+

+// 处理筛选条件变更

+const handleFilterSelect = (category, value) => {

+  setSelectedFilters(prev => ({

+    ...prev,

+    [category]: prev[category] === value ? null : value // 点击已选中的则取消

+  }));

+};

+

+//应用筛选条件

+const applyFilters = () => {

+  const result = torrentPosts.filter(resource => {

+    return Object.entries(selectedFilters).every(([category, selectedValue]) => {

+      if (selectedValue === 'all') return true;

+      if (category === 'subtitle') {

+        return resource.subtitle === (selectedValue === 'yes');

+      }

+      return resource[category] === selectedValue;

+    });

+  });

+  setFilteredResources(result);

+};

+

+

+  // 恢复滚动位置

+  useEffect(() => {

+    if (location.state?.scrollPosition) {

+      window.scrollTo(0, location.state.scrollPosition);

+    }

+  }, [location.state]);

+

+

+  useEffect(() => {

+    const token = localStorage.getItem('token');

+    if (!token) {

+      navigate('/login');

+      return;

+    }

+

+    /* 保留但注释掉实际的用户信息获取

+    const fetchUserInfo = async () => {

+      try {

+        const response = await getUserInfo(token);

+        if (response.data.code === 200) {

+          setUserInfo(response.data.data);

+        } else {

+          setError('获取用户信息失败');

+        }

+      } catch (err) {

+        setError('获取用户信息失败');

+      } finally {

+        setLoading(false);

+      }

+    };

+

+    fetchUserInfo();

+    */

+    

+    // 模拟用户信息

+    setUserInfo({ 

+      name: localStorage.getItem('username') || '演示用户', // 确保这里读取的是最新值

+    avatar: 'https://via.placeholder.com/40'

+    });

+    setLoading(false);

+  }, [navigate]);

+

+  // 轮播图自动切换效果

+  useEffect(() => {

+    if (activeTab === 'announcement') {

+      const timer = setInterval(() => {

+        setCurrentSlide(prev => (prev + 1) % 3); // 3张轮播图循环

+      }, 3000);

+      return () => clearInterval(timer);

+    }

+  }, [activeTab]);

+

+  useEffect(() => {

+    if (activeTab === 'help') {

+      fetchHelpPosts();

+    }

+  }, [activeTab]);

+

+  const renderContent = () => {

+    switch(activeTab) {

+      case 'announcement':

+        return (

+          <div className="content-area" data-testid="announcement-section"> 

+            {/* 轮播图区域 */}

+            <div className="carousel-container">

+              <div className={`carousel-slide ${currentSlide === 0 ? 'active' : ''}`}>

+                <div className="carousel-image gray-bg">促销活动1</div>

+              </div>

+              <div className={`carousel-slide ${currentSlide === 1 ? 'active' : ''}`}>

+                <div className="carousel-image gray-bg">促销活动2</div>

+              </div>

+              <div className={`carousel-slide ${currentSlide === 2 ? 'active' : ''}`}>

+                <div className="carousel-image gray-bg">促销活动3</div>

+              </div>

+              <div className="carousel-dots">

+                <span className={`dot ${currentSlide === 0 ? 'active' : ''}`}></span>

+                <span className={`dot ${currentSlide === 1 ? 'active' : ''}`}></span>

+                <span className={`dot ${currentSlide === 2 ? 'active' : ''}`}></span>

+              </div>

+            </div>

+

+            {/* 公告区块区域 */}

+            <div className="announcement-grid">

+              {announcements.map(announcement => (

+                <div 

+                  key={announcement.id}

+                  className="announcement-card"

+                  onClick={(e) => handleAnnouncementClick(announcement, e)}

+                >

+                  <h3>{announcement.title}</h3>

+                  <p>{announcement.excerpt}</p>

+                  <div className="announcement-footer exclude-click">

+                    <span>{announcement.author}</span>

+                    <span>{announcement.date}</span>

+                  </div>

+                </div>

+              ))}

+            </div>

+          </div>

+        );

+        case 'share':

+          return (

+            <div className="content-area" data-testid="share-section">

+              {/* 上传按钮 - 添加在筛选区上方 */}

+              <div className="upload-header">

+                <button 

+                  className="upload-btn"

+                  onClick={() => setShowUploadModal(true)}

+                >

+                  + 上传种子

+                </button>

+              </div>

+              {/* 分类筛选区 */}

+              <div className="filter-section">

+                {Object.entries(filterCategories).map(([category, config]) => (

+                  <div key={category} className="filter-group">

+                    <h4>{config.label}:</h4>

+                    <div className="filter-options">

+                      {Object.entries(config.options).map(([value, label]) => (

+                        <button

+                          key={value}

+                          className={`filter-btn ${

+                            selectedFilters[category] === value ? 'active' : ''

+                          }`}

+                          onClick={() => handleFilterSelect(category, value)}

+                        >

+                          {label}

+                        </button>

+                      ))}

+                    </div>

+                  </div>

+                ))}

+        

+                <button 

+                  className="confirm-btn"

+                  onClick={applyFilters}

+                >

+                  确认筛选

+                </button>

+              </div>

+        

+              {/* 上传弹窗 */}

+              {showUploadModal && (

+                <div className="modal-overlay">

+                  <div className="upload-modal">

+                    <h3>上传新种子</h3>

+                    <button 

+                      className="close-btn"

+                      onClick={() => setShowUploadModal(false)}

+                    >

+                      ×

+                    </button>

+                    

+                    <form onSubmit={handleUploadSubmit}>

+                      <div className="form-group">

+                        <label>种子名称</label>

+                        <input

+                          type="text"

+                          value={uploadData.name}

+                          onChange={(e) => setUploadData({...uploadData, name: e.target.value})}

+                          required

+                        />

+                      </div>

+        

+                      <div className="form-group">

+                        <label>资源类型</label>

+                        <select

+                          value={uploadData.type}

+                          onChange={(e) => setUploadData({...uploadData, type: e.target.value})}

+                          required

+                        >

+                          <option value="">请选择</option>

+                          <option value="电影">电影</option>

+                          <option value="电视剧">电视剧</option>

+                          <option value="动漫">动漫</option>

+                          <option value="综艺">综艺</option>

+                          <option value="音乐">音乐</option>

+                        </select>

+                      </div>

+        

+                      {/* 新增地区输入框 */}

+                      <div className="form-group">

+                        <label>地区</label>

+                        <input

+                          type="text"

+                          value={uploadData.region || ''}

+                          onChange={(e) => setUploadData({...uploadData, region: e.target.value})}

+                          placeholder="例如: 美国, 中国, 日本等"

+                          required

+                        />

+                      </div>

+

+                      {/* 添加分辨率下拉框 */}

+                      <div className="form-group">

+                        <label>分辨率</label>

+                        <select

+                          value={uploadData.resolution || ''}

+                          onChange={(e) => setUploadData({...uploadData, resolution: e.target.value})}

+                          required

+                        >

+                          <option value="">请选择</option>

+                          <option value="4K">4K</option>

+                          <option value="2K">2K</option>

+                          <option value="1080P">1080P</option>

+                          <option value="720P">720P</option>

+                          <option value="SD">SD</option>

+                          <option value="无损音源">无损音源</option>

+                          <option value="杜比全景声">杜比全景声</option>

+                          <option value="其他">其他</option>

+                        </select>

+                      </div>

+

+                    

+        

+                      {/* 新增字幕语言下拉框 */}

+                      <div className="form-group">

+                        <label>字幕语言</label>

+                        <select

+                          value={uploadData.subtitle || ''}

+                          onChange={(e) => setUploadData({...uploadData, subtitle: e.target.value})}

+                          required

+                        >

+                          <option value="">请选择</option>

+                          <option value="无需字幕">无需字幕</option>

+                          <option value="暂无字幕">暂无字幕</option>

+                          <option value="自带中文字幕">自带中文字幕</option>

+                          <option value="自带双语字幕(含中文)">自带双语字幕(含中文)</option>

+                          <option value="附件中文字幕">附件中文字幕</option>

+                          <option value="附件双语字幕">附件双语字幕</option>

+                        </select>

+                      </div>

+        

+                      <div className="form-group">

+                        <label>种子文件</label>

+                        <input

+                          type="file"

+                          accept=".torrent"

+                          onChange={handleFileChange}

+

+                        />

+                      </div>

+        

+                      <div className="form-group">

+                        <label>简介</label>

+                        <textarea

+                          value={uploadData.description}

+                          onChange={(e) => setUploadData({...uploadData, description: e.target.value})}

+                        />

+                      </div>

+        

+                      <div className="form-actions">

+                        <button 

+                          type="button"

+                          className="cancel-btn"

+                          onClick={() => setShowUploadModal(false)}

+                        >

+                          取消

+                        </button>

+                        <button 

+                          type="submit" 

+                          className="confirm-btn"

+                          disabled={isUploading}

+                        >

+                          {isUploading ? '上传中...' : '确认上传'}

+                        </button>

+                      </div>

+                    </form>

+                  </div>

+                </div>

+              )}

+              

+              <div className="resource-list">

+                {torrentPosts.map(torrent => (

+                  <div 

+                    key={torrent.id}

+                    className="resource-item"

+                    onClick={() => navigate(`/torrent/${torrent.id}`)}

+                  >

+                    <div className="resource-poster">

+                      <div className="poster-image gray-bg">{torrent.torrentName.charAt(0)}</div>

+                    </div>

+                    <div className="resource-info">

+                      <h3 className="resource-title">{torrent.torrentName}</h3>

+                      <p className="resource-meta">

+                        {torrent.resolution} | {torrent.region} | {torrent.category}

+                      </p>

+                      <p className="resource-subtitle">字幕: {torrent.subtitle}</p>

+                    </div>

+                    <div className="resource-stats">

+                      <span className="stat">{torrent.size}</span>

+                      <span className="stat">发布者: {torrent.username}</span>

+                    </div>

+                    <button 

+                      className="download-btn"

+                      onClick={(e) => {

+                        e.stopPropagation();

+                        // 下载逻辑

+                      }}

+                    >

+                      立即下载

+                    </button>

+                  </div>

+                ))}

+              </div>

+

+              {/* 分页控件 */}

+              <div className="pagination">

+                <button 

+                  onClick={() => fetchTorrentPosts(currentPage - 1)} 

+                  disabled={currentPage === 1}

+                >

+                  上一页

+                </button>

+                

+                {Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (

+                  <button

+                    key={page}

+                    onClick={() => fetchTorrentPosts(page)}

+                    className={currentPage === page ? 'active' : ''}

+                  >

+                    {page}

+                  </button>

+                ))}

+                

+                <button 

+                  onClick={() => fetchTorrentPosts(currentPage + 1)} 

+                  disabled={currentPage === totalPages}

+                >

+                  下一页

+                </button>

+              </div>

+            </div>

+          );

+      // 在Dashboard.jsx的renderContent函数中修改case 'request'部分

+      case 'request':

+        return (

+          <div className="content-area" data-testid="request-section">

+            {/* 求种区帖子列表 */}

+            <div className="request-list">

+              {[

+                {

+                  id: 1,

+                  title: '求《药屋少女的呢喃》第二季全集',

+                  content: '求1080P带中文字幕版本,最好是内嵌字幕不是外挂的',

+                  author: '动漫爱好者',

+                  authorAvatar: 'https://via.placeholder.com/40',

+                  date: '2023-10-15',

+                  likeCount: 24,

+                  commentCount: 8

+                },

+                {

+                  id: 2,

+                  title: '求《奥本海默》IMAX版',

+                  content: '最好是原盘或者高码率的版本,谢谢各位大佬',

+                  author: '电影收藏家',

+                  authorAvatar: 'https://via.placeholder.com/40',

+                  date: '2023-10-14',

+                  likeCount: 15,

+                  commentCount: 5

+                }

+              ].map(post => (

+                <div 

+                  key={post.id} 

+                  className="request-post"

+                  onClick={() => navigate(`/request/${post.id}`)}

+                >

+                  <div className="post-header">

+                    <img src={post.authorAvatar} alt={post.author} className="post-avatar" />

+                    <div className="post-author">{post.author}</div>

+                    <div className="post-date">{post.date}</div>

+                  </div>

+                  <h3 className="post-title">{post.title}</h3>

+                  <p className="post-content">{post.content}</p>

+                  <div className="post-stats">

+                    <span className="post-likes">👍 {post.likeCount}</span>

+                    <span className="post-comments">💬 {post.commentCount}</span>

+                  </div>

+                </div>

+              ))}

+            </div>

+          </div>

+        );

+      // 在Dashboard.jsx的renderContent函数中修改case 'help'部分

+      case 'help':

+        return (

+          <div className="content-area" data-testid="help-section">

+            {/* 新增发帖按钮 */}

+            <div className="post-header">

+              <button 

+                className="create-post-btn"

+                onClick={() => setShowPostModal(true)}

+              >

+                + 发帖求助

+              </button>

+            </div>

+

+            {/* 加载状态和错误提示 */}

+            {helpLoading && <div className="loading">加载中...</div>}

+            {helpError && <div className="error">{helpError}</div>}

+

+            {/* 求助区帖子列表 */}

+            <div className="help-list">

+              {helpPosts.map(post => (

+                <div 

+                  key={post.id} 

+                  className={`help-post ${post.isSolved ? 'solved' : ''}`}

+                  onClick={() => navigate(`/help/${post.id}`)}

+                >

+                  <div className="post-header">

+                    <img 

+                      src={post.authorAvatar || 'https://via.placeholder.com/40'} 

+                      alt={post.authorId} 

+                      className="post-avatar" 

+                    />

+                    <div className="post-author">{post.authorId}</div>

+                    <div className="post-date">

+                      {new Date(post.createTime).toLocaleDateString()}

+                    </div>

+                    {post.isSolved && <span className="solved-badge">已解决</span>}

+                  </div>

+                  <h3 className="post-title">{post.title}</h3>

+                  <p className="post-content">{post.content}</p>

+                  <div className="post-stats">

+                    <span className="post-likes">👍 {post.likeCount || 0}</span>

+                    <span className="post-comments">💬 {post.replyCount || 0}</span>

+                  </div>

+                </div>

+              ))}

+            </div>

+

+            {/* 在帖子列表后添加分页控件 */}

+            <div className="pagination">

+              <button 

+                onClick={() => fetchHelpPosts(currentPage - 1)} 

+                disabled={currentPage === 1}

+              >

+                上一页

+              </button>

+              

+              {Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (

+                <button

+                  key={page}

+                  onClick={() => fetchHelpPosts(page)}

+                  className={currentPage === page ? 'active' : ''}

+                >

+                  {page}

+                </button>

+              ))}

+              

+              <button 

+                onClick={() => fetchHelpPosts(currentPage + 1)} 

+                disabled={currentPage === totalPages}

+              >

+                下一页

+              </button>

+            </div>

+        

+            {/* 新增发帖弹窗 */}

+            {showPostModal && (

+              <div className="post-modal-overlay">

+                <div className="post-modal">

+                  <h3>发布求助帖</h3>

+                  <button 

+                    className="modal-close-btn"

+                    onClick={() => setShowPostModal(false)}

+                  >

+                    ×

+                  </button>

+                  

+                  <form onSubmit={handlePostSubmit}>

+                    <div className="form-group">

+                      <label>帖子标题</label>

+                      <input

+                        type="text"

+                        value={postTitle}

+                        onChange={(e) => setPostTitle(e.target.value)}

+                        placeholder="请输入标题"

+                        required

+                      />

+                    </div>

+        

+                    <div className="form-group">

+                      <label>帖子内容</label>

+                      <textarea

+                        value={postContent}

+                        onChange={(e) => setPostContent(e.target.value)}

+                        placeholder="详细描述你的问题"

+                        required

+                      />

+                    </div>

+        

+                    <div className="form-group">

+                      <label>上传图片</label>

+                      <div className="upload-image-btn">

+                        <input

+                          type="file"

+                          id="image-upload"

+                          accept="image/*"

+                          onChange={handleImageUpload}

+                          style={{ display: 'none' }}

+                        />

+                        <label htmlFor="image-upload">

+                          {selectedImage ? '已选择图片' : '选择图片'}

+                        </label>

+                        {selectedImage && (

+                          <span className="image-name">{selectedImage.name}</span>

+                        )}

+                      </div>

+                    </div>

+        

+                    <div className="form-actions">

+                      <button 

+                        type="button"

+                        className="cancel-btn"

+                        onClick={() => setShowPostModal(false)}

+                      >

+                        取消

+                      </button>

+                      <button 

+                        type="submit" 

+                        className="submit-btn"

+                      >

+                        确认发帖

+                      </button>

+                    </div>

+                  </form>

+                </div>

+              </div>

+            )}

+          </div>

+        );

+      default:

+        return <div className="content-area" data-testid="default-section">公告区内容</div>;

+    }

+  };

+

+  if (loading) return <div className="loading">加载中...</div>;

+  if (error) return <div className="error">{error}</div>;

+

+  return (

+    <div className="dashboard-container" data-testid="dashboard-container">

+      {/* 顶部栏 */}

+      <div className="top-bar" data-testid="top-bar">

+        {/* 搜索框 */}

+        <div className="search-container">

+          <input 

+            type="text" 

+            placeholder="搜索种子、用户..." 

+            className="search-input"

+          />

+          <button className="search-button">搜索</button>

+        </div>

+        

+        <div className="user-info" data-testid="user-info">

+          <img 

+            src={userInfo?.avatar || 'https://via.placeholder.com/40'} 

+            alt="用户头像" 

+            className="user-avatar"

+            onClick={() => navigate('/personal')}  // 添加点击跳转

+            style={{ cursor: 'pointer' }}  // 添加手型指针

+          />

+          <span className="username">{userInfo?.name || '用户'}</span>

+          <button onClick={onLogout} className="logout-button">退出</button>

+        </div>

+      </div>

+      

+      {/* 导航栏 */}

+      {/* handleTabchange函数替换了原本的setactivetab函数 */}

+      <div className="nav-tabs">

+        <button 

+          className={`tab-button ${activeTab === 'announcement' ? 'active' : ''}`}

+          onClick={() => handleTabChange('announcement')}

+        >

+          公告区

+        </button>

+        <button 

+          className={`tab-button ${activeTab === 'share' ? 'active' : ''}`}

+          onClick={() => handleTabChange('share')}

+        >

+          分享区

+        </button>

+        <button 

+          className={`tab-button ${activeTab === 'request' ? 'active' : ''}`}

+          onClick={() => handleTabChange('request')}

+        >

+          求种区

+        </button>

+        <button 

+          className={`tab-button ${activeTab === 'help' ? 'active' : ''}`}

+          onClick={() => handleTabChange('help')}

+        >

+          求助区

+        </button>

+      </div>

+      

+      {/* 内容区 */}

+      {renderContent()}

+    </div>

+  );

+};

+

+export default Dashboard;
\ No newline at end of file
diff --git a/src/components/HelpDetail.css b/src/components/HelpDetail.css
new file mode 100644
index 0000000..94d791b
--- /dev/null
+++ b/src/components/HelpDetail.css
@@ -0,0 +1,557 @@
+/* HelpDetail.css */

+.help-detail-container {

+    max-width: 800px;

+    margin: 0 auto;

+    padding: 20px;

+  }

+  

+  .back-button {

+    background: none;

+    border: none;

+    color: #1890ff;

+    cursor: pointer;

+    font-size: 16px;

+    margin-bottom: 20px;

+    padding: 5px 0;

+  }

+  

+  .help-post {

+    background: #fff;

+    border-radius: 8px;

+    padding: 20px;

+    margin-bottom: 20px;

+    box-shadow: 0 1px 3px rgba(0,0,0,0.1);

+  }

+  

+  .help-post.solved {

+    border-left: 4px solid #52c41a;

+  }

+  

+  .post-header {

+    display: flex;

+    align-items: center;

+    margin-bottom: 15px;

+    position: relative;

+  }

+  

+  .post-avatar {

+    width: 40px;

+    height: 40px;

+    border-radius: 50%;

+    margin-right: 10px;

+  }

+  

+  .post-meta {

+    display: flex;

+    flex-direction: column;

+  }

+  

+  .post-author {

+    font-weight: bold;

+  }

+  

+  .post-date {

+    color: #888;

+    font-size: 14px;

+  }

+  

+  .solved-badge {

+    position: absolute;

+    right: 0;

+    top: 0;

+    background: #52c41a;

+    color: white;

+    padding: 2px 8px;

+    border-radius: 4px;

+    font-size: 12px;

+  }

+  

+  .post-title {

+    font-size: 20px;

+    margin: 0 0 15px;

+  }

+  

+  .post-content {

+    line-height: 1.6;

+    margin-bottom: 15px;

+  }

+  

+  .post-actions {

+    display: flex;

+    gap: 15px;

+  }

+  

+  .like-button, .favorite-button, .solve-button {

+    padding: 5px 15px;

+    border-radius: 4px;

+    border: 1px solid #ddd;

+    background: #f5f5f5;

+    cursor: pointer;

+  }

+  

+  .like-button.liked {

+    background: #e6f7ff;

+    border-color: #91d5ff;

+    color: #1890ff;

+  }

+  

+  .favorite-button.favorited {

+    background: #fff7e6;

+    border-color: #ffd591;

+    color: #fa8c16;

+  }

+  

+  .solve-button.solved {

+    background: #f6ffed;

+    border-color: #b7eb8f;

+    color: #52c41a;

+  }

+  

+  .comments-section {

+    background: #fff;

+    border-radius: 8px;

+    padding: 20px;

+    box-shadow: 0 1px 3px rgba(0,0,0,0.1);

+  }

+  

+  .comment-form {

+    margin-bottom: 20px;

+  }

+  

+  .comment-form textarea {

+    width: 100%;

+    padding: 10px;

+    border-radius: 4px;

+    border: 1px solid #ddd;

+    resize: vertical;

+    min-height: 80px;

+    margin-bottom: 10px;

+  }

+  

+  .image-upload {

+    margin-bottom: 10px;

+  }

+  

+  .upload-button {

+    background: #f5f5f5;

+    border: 1px dashed #d9d9d9;

+    border-radius: 4px;

+    padding: 8px 16px;

+    cursor: pointer;

+    margin-bottom: 10px;

+  }

+  

+  .image-preview {

+    display: flex;

+    flex-wrap: wrap;

+    gap: 10px;

+  }

+  

+  .preview-item {

+    position: relative;

+    width: 100px;

+    height: 100px;

+  }

+  

+  .preview-item img {

+    width: 100%;

+    height: 100%;

+    object-fit: cover;

+    border-radius: 4px;

+  }

+  

+  .remove-image {

+    position: absolute;

+    top: -8px;

+    right: -8px;

+    width: 20px;

+    height: 20px;

+    background: #ff4d4f;

+    color: white;

+    border: none;

+    border-radius: 50%;

+    cursor: pointer;

+    display: flex;

+    align-items: center;

+    justify-content: center;

+    font-size: 12px;

+  }

+  

+  .submit-button {

+    background: #1890ff;

+    color: white;

+    border: none;

+    border-radius: 4px;

+    padding: 8px 16px;

+    cursor: pointer;

+  }

+  

+  .comment-list {

+    margin-top: 20px;

+  }

+  

+  .comment-item {

+    display: flex;

+    padding: 15px 0;

+    border-bottom: 1px solid #f0f0f0;

+  }

+  

+  .comment-item:last-child {

+    border-bottom: none;

+  }

+  

+  .comment-avatar {

+    width: 40px;

+    height: 40px;

+    border-radius: 50%;

+    margin-right: 15px;

+    flex-shrink: 0;

+  }

+  

+  .comment-content {

+    flex-grow: 1;

+  }

+  

+  .comment-header {

+    display: flex;

+    justify-content: space-between;

+    margin-bottom: 8px;

+  }

+  

+  .comment-author {

+    font-weight: bold;

+  }

+  

+  .comment-date {

+    color: #888;

+    font-size: 14px;

+  }

+  

+  .comment-text {

+    line-height: 1.6;

+    margin-bottom: 8px;

+  }

+  

+  .comment-images {

+    display: flex;

+    gap: 10px;

+    margin-bottom: 10px;

+  }

+  

+  .comment-image {

+    max-width: 200px;

+    max-height: 150px;

+    border-radius: 4px;

+  }

+  

+  .comment-actions {

+    margin-top: 10px;

+  }

+  

+  .reply-form {

+    margin-top: 10px;

+    display: flex;

+    gap: 10px;

+  }

+  

+  .reply-form textarea {

+    flex-grow: 1;

+    padding: 8px;

+    border-radius: 4px;

+    border: 1px solid #ddd;

+    resize: vertical;

+    min-height: 40px;

+  }

+  

+  .submit-reply {

+    background: #f5f5f5;

+    border: 1px solid #d9d9d9;

+    border-radius: 4px;

+    padding: 0 15px;

+    cursor: pointer;

+    align-self: flex-end;

+    margin-bottom: 5px;

+  }

+  

+  .reply-list {

+    margin-top: 10px;

+    padding-left: 15px;

+    border-left: 2px solid #f0f0f0;

+  }

+  

+  .reply-item {

+    display: flex;

+    margin-top: 10px;

+  }

+  

+  .reply-avatar {

+    width: 30px;

+    height: 30px;

+    border-radius: 50%;

+    margin-right: 10px;

+  }

+  

+  .reply-content {

+    flex-grow: 1;

+  }

+  

+  .reply-header {

+    display: flex;

+    gap: 10px;

+    margin-bottom: 5px;

+  }

+  

+  .reply-author {

+    font-weight: bold;

+    font-size: 14px;

+  }

+  

+  .reply-date {

+    color: #888;

+    font-size: 12px;

+  }

+  

+  .reply-text {

+    font-size: 14px;

+    line-height: 1.5;

+  }

+

+  /* 评论项样式 */

+.comment-item {

+  display: flex;

+  margin-bottom: 20px;

+  padding: 15px;

+  background: #fff;

+  border-radius: 8px;

+  box-shadow: 0 1px 3px rgba(0,0,0,0.1);

+}

+

+.comment-avatar {

+  width: 40px;

+  height: 40px;

+  border-radius: 50%;

+  margin-right: 15px;

+  object-fit: cover;

+}

+

+.comment-content {

+  flex: 1;

+}

+

+.comment-header {

+  display: flex;

+  align-items: center;

+  margin-bottom: 8px;

+}

+

+.comment-author {

+  font-weight: bold;

+  margin-right: 10px;

+  color: #333;

+}

+

+.comment-date {

+  font-size: 12px;

+  color: #999;

+}

+

+.comment-text {

+  margin-bottom: 10px;

+  line-height: 1.5;

+  color: #333;

+}

+

+.comment-actions {

+  display: flex;

+  align-items: center;

+  margin-top: 10px;

+}

+

+.like-button {

+  background: none;

+  border: none;

+  cursor: pointer;

+  color: #666;

+  font-size: 14px;

+  padding: 4px 8px;

+  display: flex;

+  align-items: center;

+}

+

+.like-button.liked {

+  color: #1877f2;

+}

+

+.like-button:hover {

+  text-decoration: underline;

+}

+

+/* 回复控制按钮 */

+.reply-controls {

+  margin: 10px 0;

+}

+

+.toggle-replies-btn {

+  background: none;

+  border: none;

+  color: #666;

+  cursor: pointer;

+  font-size: 14px;

+  padding: 4px 8px;

+}

+

+.toggle-replies-btn:hover {

+  color: #333;

+  text-decoration: underline;

+}

+

+.toggle-replies-btn:disabled {

+  color: #ccc;

+  cursor: not-allowed;

+}

+

+/* 回复表单 */

+.reply-form {

+  margin-top: 10px;

+  width: 100%;

+}

+

+.reply-form textarea {

+  width: 100%;

+  padding: 8px;

+  border: 1px solid #ddd;

+  border-radius: 4px;

+  resize: vertical;

+  min-height: 60px;

+  margin-bottom: 8px;

+}

+

+.submit-reply {

+  background-color: #f0f2f5;

+  border: none;

+  border-radius: 4px;

+  padding: 6px 12px;

+  cursor: pointer;

+  color: #333;

+}

+

+.submit-reply:hover {

+  background-color: #e4e6eb;

+}

+

+/* 回复列表 */

+.reply-list {

+  margin-left: 55px; /* 头像宽度 + 边距 */

+  border-left: 2px solid #eee;

+  padding-left: 15px;

+  margin-top: 15px;

+}

+

+.reply-item {

+  display: flex;

+  margin-bottom: 15px;

+  padding: 12px;

+  background: #f9f9f9;

+  border-radius: 6px;

+}

+

+.reply-avatar {

+  width: 32px;

+  height: 32px;

+  border-radius: 50%;

+  margin-right: 10px;

+  object-fit: cover;

+}

+

+.reply-content {

+  flex: 1;

+}

+

+.reply-header {

+  display: flex;

+  align-items: center;

+  margin-bottom: 5px;

+}

+

+.reply-author {

+  font-weight: bold;

+  margin-right: 8px;

+  font-size: 14px;

+  color: #333;

+}

+

+.reply-date {

+  font-size: 12px;

+  color: #999;

+}

+

+.reply-text {

+  font-size: 14px;

+  line-height: 1.4;

+  color: #333;

+  margin-bottom: 5px;

+}

+

+.reply-actions {

+  margin-top: 5px;

+}

+

+/* 回复弹窗样式 */

+.reply-modal-overlay {

+  position: fixed;

+  top: 0;

+  left: 0;

+  right: 0;

+  bottom: 0;

+  background: rgba(0, 0, 0, 0.5);

+  display: flex;

+  justify-content: center;

+  align-items: center;

+  z-index: 1000;

+}

+

+.reply-modal {

+  background: white;

+  padding: 20px;

+  border-radius: 8px;

+  width: 500px;

+  max-width: 90%;

+}

+

+.reply-modal h3 {

+  margin-top: 0;

+  margin-bottom: 15px;

+}

+

+.reply-modal textarea {

+  width: 100%;

+  padding: 10px;

+  border: 1px solid #ddd;

+  border-radius: 4px;

+  resize: vertical;

+  min-height: 100px;

+  margin-bottom: 15px;

+}

+

+.modal-actions {

+  display: flex;

+  justify-content: flex-end;

+  gap: 10px;

+}

+

+.cancel-button {

+  background: #f5f5f5;

+  border: 1px solid #d9d9d9;

+  border-radius: 4px;

+  padding: 8px 16px;

+  cursor: pointer;

+}

+

+.submit-button {

+  background: #1890ff;

+  color: white;

+  border: none;

+  border-radius: 4px;

+  padding: 8px 16px;

+  cursor: pointer;

+}
\ No newline at end of file
diff --git a/src/components/HelpDetail.jsx b/src/components/HelpDetail.jsx
new file mode 100644
index 0000000..da50850
--- /dev/null
+++ b/src/components/HelpDetail.jsx
@@ -0,0 +1,368 @@
+import React, { useState, useEffect, useRef } from 'react';

+import { useParams, useNavigate, useLocation } from 'react-router-dom';

+import { 

+  getPostDetail,

+  addPostComment,

+  likePost

+} from '../api/helpPost';

+import {

+  likePostComment,

+  getCommentReplies,

+  addCommentReply

+} from '../api/helpComment';

+import './HelpDetail.css';

+

+const HelpDetail = () => {

+  const { id } = useParams();

+  const navigate = useNavigate();

+  const location = useLocation();

+  const fileInputRef = useRef(null);

+  const [post, setPost] = useState(null);

+  const [comments, setComments] = useState([]);

+  const [loading, setLoading] = useState(true);

+  const [error, setError] = useState(null);

+  const [newComment, setNewComment] = useState('');

+  const [newReply, setNewReply] = useState({});

+  const [images, setImages] = useState([]);

+  const [expandedReplies, setExpandedReplies] = useState({}); // 记录哪些评论的回复是展开的

+  const [loadingReplies, setLoadingReplies] = useState({});

+  const [setReplyingTo] = useState(null);

+  const [replyContent, setReplyContent] = useState('');

+

+  const [activeReplyId, setActiveReplyId] = useState(null);

+    const [replyModal, setReplyModal] = useState({

+      visible: false,

+      replyingTo: null,

+      replyingToUsername: '',

+      isReply: false

+    });

+  

+    // 确保openReplyModal接收username参数

+  const openReplyModal = (commentId, username) => {

+    setReplyModal({

+      visible: true,

+      replyingTo: commentId,

+      replyingToUsername: username,  // 确保这里接收username

+      isReply: false

+    });

+  };

+  

+    // 关闭回复弹窗

+    const closeReplyModal = () => {

+      setReplyModal({

+        visible: false,

+        replyingTo: null,

+        replyingToUsername: '',

+        isReply: false

+      });

+      setReplyContent('');

+    };

+

+    const Comment = ({ comment, onLike, onReply, isReply = false }) => {

+      return (

+        <div className={`comment-container ${isReply ? "is-reply" : ""}`}>

+          <div className="comment-item">

+            <div className="comment-avatar">

+              {(comment.authorId || "?").charAt(0)} {/* 修复点 */}

+            </div>

+            <div className="comment-content">

+              <div className="comment-header">

+                <span className="comment-user">{comment.authorId || "匿名用户"}</span>

+                {comment.replyTo && (

+                  <span className="reply-to">回复 @{comment.replyTo}</span>

+                )}

+                <span className="comment-time">

+                  {new Date(comment.createTime).toLocaleString()}

+                </span>

+              </div>

+              <p className="comment-text">{comment.content}</p>

+              <div className="comment-actions">

+                <button onClick={() => onLike(comment.id)}>

+                  👍 ({comment.likeCount || 0})

+                </button>

+                <button onClick={() => onReply(comment.id, comment.authorId)}>

+                  回复

+                </button>

+              </div>

+            </div>

+          </div>

+        </div>

+      );

+    };

+

+      // 递归渲染评论组件

+  const renderComment = (comment, depth = 0) => {

+    return (

+      <div key={comment.id} style={{ marginLeft: `${depth * 30}px` }}>

+        <Comment

+          comment={comment}

+          onLike={handleLikeComment}

+          onReply={openReplyModal}

+          isReply={depth > 0}

+        />

+        

+        {/* 递归渲染所有回复 */}

+        {comment.replies && comment.replies.map(reply => 

+          renderComment(reply, depth + 1)

+        )}

+      </div>

+    );

+  };

+

+

+   const fetchPostDetail = async () => {

+      try {

+        setLoading(true);

+        const response = await getPostDetail(id);

+        console.log('API Response:', JSON.parse(JSON.stringify(response.data.data.comments))); // 深度拷贝避免Proxy影响

+        setPost(response.data.data.post);

+        setComments(response.data.data.comments);

+      } catch (err) {

+        setError(err.response?.data?.message || '获取帖子详情失败');

+      } finally {

+        setLoading(false);

+      }

+    };

+  

+    useEffect(() => {

+      fetchPostDetail();

+    }, [id]);

+  

+    // 点赞帖子

+    const handleLikePost = async () => {

+      try {

+        await likePost(id);

+        setPost(prev => ({

+          ...prev,

+          likeCount: prev.likeCount + 1

+        }));

+      } catch (err) {

+        setError('点赞失败: ' + (err.response?.data?.message || err.message));

+      }

+    };

+  

+    const handleCommentSubmit = async (e) => {

+      e.preventDefault();

+      if (!newComment.trim()) return;

+      

+      try {

+        const username = localStorage.getItem('username');

+        const response = await addPostComment(id, {

+          content: newComment,

+          authorId: username

+        });

+    

+        // 修改这里的响应处理逻辑

+        if (response.data && response.data.code === 200) {

+          await fetchPostDetail();

+          

+          setNewComment('');

+        } else {

+          setError(response.data.message || '评论失败');

+        }

+      } catch (err) {

+        setError('评论失败: ' + (err.response?.data?.message || err.message));

+      }

+    };

+    

+    

+    const handleLikeComment = async (commentId) => {

+      try {

+        await likePostComment(commentId);

+        

+        // 递归更新评论点赞数

+        const updateComments = (comments) => {

+          return comments.map(comment => {

+            // 当前评论匹配

+            if (comment.id === commentId) {

+              return { ...comment, likeCount: comment.likeCount + 1 };

+            }

+            

+            // 递归处理回复

+            if (comment.replies && comment.replies.length > 0) {

+              return {

+                ...comment,

+                replies: updateComments(comment.replies)

+              };

+            }

+            

+            return comment;

+          });

+        };

+    

+        setComments(prev => updateComments(prev));

+      } catch (err) {

+        setError('点赞失败: ' + (err.response?.data?.message || err.message));

+      }

+    };

+

+

+    // 修改startReply函数

+    const startReply = (commentId) => {

+      if (activeReplyId === commentId) {

+        // 如果点击的是已经激活的回复按钮,则关闭

+        setActiveReplyId(null);

+        setReplyingTo(null);

+      } else {

+        // 否则打开新的回复框

+        setActiveReplyId(commentId);

+        setReplyingTo(commentId);

+      }

+    };

+  

+    const handleReplySubmit = async (e) => {

+      e.preventDefault();

+      if (!replyContent.trim()) return;

+    

+      try {

+        const username = localStorage.getItem('username');

+        const response = await addCommentReply(replyModal.replyingTo, {

+          content: replyContent,

+          authorId: username

+        });

+    

+        console.log('回复响应:', response.data); // 调试

+        

+        if (response.data && response.data.code === 200) {

+          await fetchPostDetail();

+    

+          closeReplyModal();

+        }

+      } catch (err) {

+        console.error('回复错误:', err);

+        setError('回复失败: ' + (err.response?.data?.message || err.message));

+      }

+    };

+  

+  

+    // 返回按钮

+    const handleBack = () => {

+      const fromTab = location.state?.fromTab || 'share';

+      navigate(`/dashboard/help`);

+    };

+  

+    

+

+  const handleMarkSolved = () => {

+    // TODO: 实现标记为已解决的功能

+    setPost(prev => ({

+      ...prev,

+      isSolved: !prev.isSolved

+    }));

+  };

+

+  const handleImageUpload = (e) => {

+    const files = Array.from(e.target.files);

+    const newImages = files.map(file => URL.createObjectURL(file));

+    setImages(prev => [...prev, ...newImages]);

+  };

+

+  const handleRemoveImage = (index) => {

+    setImages(prev => prev.filter((_, i) => i !== index));

+  };

+

+  

+

+  if (loading) return <div className="loading">加载中...</div>;

+  if (error) return <div className="error">{error}</div>;

+  if (!post) return <div className="error">帖子不存在</div>;

+

+  return (

+    <div className="help-detail-container">

+      <button className="back-button" onClick={handleBack}>

+        &larr; 返回求助区

+      </button>

+      

+      <div className={`help-post ${post.isSolved ? 'solved' : ''}`}>

+        <div className="post-header">

+          <img 

+            src={post.authorAvatar || 'https://via.placeholder.com/40'} 

+            alt={post.authorId} 

+            className="post-avatar" 

+          />

+          <div className="post-meta">

+            <div className="post-author">{post.authorId}</div>

+            <div className="post-date">

+              {new Date(post.createTime).toLocaleString()}

+            </div>

+          </div>

+          {post.isSolved && <span className="solved-badge">已解决</span>}

+        </div>

+        

+        <h1 className="post-title">{post.title}</h1>

+        

+        <div className="post-content">

+          {post.content.split('\n').map((para, i) => (

+            <p key={i}>{para}</p>

+          ))}

+        </div>

+        

+        <div className="post-actions">

+          <button 

+            className={`like-button ${post.isLiked ? 'liked' : ''}`}

+            onClick={handleLikePost}

+          >

+            👍 点赞 ({post.likeCount})

+          </button>

+          <button 

+            className={`solve-button ${post.isSolved ? 'solved' : ''}`}

+            onClick={handleMarkSolved}

+          >

+            {post.isSolved ? '✓ 已解决' : '标记为已解决'}

+          </button>

+        </div>

+      </div>

+      

+       <div className="comments-section">

+        <h2>评论 ({post.replyCount})</h2>

+        

+        <form onSubmit={handleCommentSubmit} className="comment-form">

+          <textarea

+            value={newComment}

+            onChange={(e) => setNewComment(e.target.value)}

+            placeholder="写下你的评论..."

+            rows="3"

+            required

+          />

+          <button type="submit">发表评论</button>

+        </form>

+        

+        <div className="comment-list">

+          {comments.map(comment => renderComment(comment))}

+        </div>

+

+        {replyModal.visible && (

+          <div className="reply-modal-overlay">

+            <div className="reply-modal">

+              <div className="modal-header">

+                <h3>回复 @{replyModal.replyingToUsername}</h3>

+                <button onClick={closeReplyModal} className="close-modal">&times;</button>

+              </div>

+              <form onSubmit={handleReplySubmit}>

+                <textarea

+                  value={replyContent}

+                  onChange={(e) => setReplyContent(e.target.value)}

+                  placeholder={`回复 @${replyModal.replyingToUsername}...`}

+                  rows="5"

+                  autoFocus

+                  required

+                />

+                <div className="modal-actions">

+                  <button type="button" onClick={closeReplyModal} className="cancel-btn">

+                    取消

+                  </button>

+                  <button type="submit" className="submit-btn">

+                    发送回复

+                  </button>

+                </div>

+              </form>

+            </div>

+          </div>

+        )}

+

+      </div>

+    </div>

+  );

+};

+

+export default HelpDetail;
\ No newline at end of file
diff --git a/src/components/HelpDetail.test.jsx b/src/components/HelpDetail.test.jsx
new file mode 100644
index 0000000..6364be2
--- /dev/null
+++ b/src/components/HelpDetail.test.jsx
@@ -0,0 +1,187 @@
+import React from 'react';

+import { render, screen, fireEvent, waitFor } from '@testing-library/react';

+import { MemoryRouter, Route, Routes, useNavigate, useLocation } from 'react-router-dom';

+import HelpDetail from './HelpDetail';

+import * as helpPostApi from '../api/helpPost';

+import * as helpCommentApi from '../api/helpComment';

+

+// Mock API 模块

+jest.mock('../api/helpPost');

+jest.mock('../api/helpComment');

+

+// Mock 路由钩子

+const mockNavigate = jest.fn();

+jest.mock('react-router-dom', () => ({

+  ...jest.requireActual('react-router-dom'),

+  useNavigate: () => mockNavigate,

+  useLocation: jest.fn(),

+}));

+

+describe('HelpDetail 组件', () => {

+  const mockPost = {

+    id: '1',

+    title: '测试求助帖',

+    content: '这是一个测试求助内容',

+    authorId: 'user1',

+    createTime: '2023-01-01T00:00:00Z',

+    likeCount: 5,

+    replyCount: 3,

+    isSolved: false,

+  };

+

+  const mockComments = [

+    {

+      id: 'c1',

+      content: '测试评论1',

+      authorId: 'user2',

+      createTime: '2023-01-01T01:00:00Z',

+      likeCount: 2,

+      replies: [

+        {

+          id: 'c1r1',

+          content: '测试回复1',

+          authorId: 'user3',

+          createTime: '2023-01-01T02:00:00Z',

+          likeCount: 1,

+        }

+      ]

+    }

+  ];

+

+  beforeEach(() => {

+    // 重置 mock 函数

+    mockNavigate.mockClear();

+    

+    // 设置模拟的 API 响应

+    helpPostApi.getPostDetail.mockResolvedValue({

+      data: {

+        code: 200,

+        data: {

+          post: mockPost,

+          comments: mockComments,

+        }

+      }

+    });

+

+    // 模拟 useLocation

+    useLocation.mockReturnValue({

+      state: { fromTab: 'help' }

+    });

+

+    // 模拟 localStorage

+    Storage.prototype.getItem = jest.fn((key) => {

+      if (key === 'username') return 'testuser';

+      return null;

+    });

+  });

+

+  const renderComponent = () => {

+    return render(

+      <MemoryRouter initialEntries={['/help/1']}>

+        <Routes>

+          <Route path="/help/:id" element={<HelpDetail />} />

+          {/* 添加一个模拟的求助列表路由用于导航测试 */}

+          <Route path="/dashboard/help" element={<div>求助列表</div>} />

+        </Routes>

+      </MemoryRouter>

+    );

+  };

+

+  it('应该正确加载和显示求助帖详情', async () => {

+    renderComponent();

+

+    // 检查加载状态

+    expect(screen.getByText('加载中...')).toBeInTheDocument();

+

+    // 等待数据加载完成

+    await waitFor(() => {

+      expect(screen.getByText('测试求助帖')).toBeInTheDocument();

+      expect(screen.getByText('这是一个测试求助内容')).toBeInTheDocument();

+      expect(screen.getByText('user1')).toBeInTheDocument();

+    });

+  });

+

+  it('应该能够点赞帖子', async () => {

+    renderComponent();

+

+    await waitFor(() => {

+      expect(screen.getByText('测试求助帖')).toBeInTheDocument();

+    });

+

+    fireEvent.click(screen.getByText(/点赞 \(5\)/));

+

+    await waitFor(() => {

+      expect(helpPostApi.likePost).toHaveBeenCalledWith('1');

+    });

+  });

+

+  it('应该能够提交评论', async () => {

+    renderComponent();

+

+    await waitFor(() => {

+      expect(screen.getByText('测试求助帖')).toBeInTheDocument();

+    });

+

+    const commentInput = screen.getByPlaceholderText('写下你的评论...');

+    fireEvent.change(commentInput, { target: { value: '新评论' } });

+    fireEvent.click(screen.getByText('发表评论'));

+

+    await waitFor(() => {

+      expect(helpPostApi.addPostComment).toHaveBeenCalledWith('1', {

+        content: '新评论',

+        authorId: 'testuser'

+      });

+    });

+  });

+

+  it('应该能够点赞评论', async () => {

+    renderComponent();

+

+    await waitFor(() => {

+      expect(screen.getByText('测试评论1')).toBeInTheDocument();

+    });

+

+    fireEvent.click(screen.getAllByText(/👍 \(2\)/)[0]);

+

+    await waitFor(() => {

+      expect(helpCommentApi.likePostComment).toHaveBeenCalledWith('c1');

+    });

+  });

+

+  it('应该能够打开和关闭回复模态框', async () => {

+    renderComponent();

+

+    await waitFor(() => {

+      expect(screen.getByText('测试评论1')).toBeInTheDocument();

+    });

+

+    // 点击回复按钮

+    fireEvent.click(screen.getAllByText('回复')[0]);

+

+    // 检查模态框是否打开

+    await waitFor(() => {

+      expect(screen.getByText('回复 @user2')).toBeInTheDocument();

+    });

+

+    // 点击关闭按钮

+    fireEvent.click(screen.getByText('×'));

+

+    // 检查模态框是否关闭

+    await waitFor(() => {

+      expect(screen.queryByText('回复 @user2')).not.toBeInTheDocument();

+    });

+  });

+

+  it('应该能够返回求助区', async () => {

+    renderComponent();

+  

+    await waitFor(() => {

+      expect(screen.getByText('测试求助帖')).toBeInTheDocument();

+    });

+  

+    fireEvent.click(screen.getByRole('button', { name: /返回/i }));

+  

+    // 修正后的断言(不再验证 state)

+    expect(mockNavigate).toHaveBeenCalledWith('/dashboard/help');

+  });

+});
\ No newline at end of file
diff --git a/src/components/Personal/ActionCard.jsx b/src/components/Personal/ActionCard.jsx
new file mode 100644
index 0000000..7d3cd4f
--- /dev/null
+++ b/src/components/Personal/ActionCard.jsx
@@ -0,0 +1,23 @@
+import React from 'react';

+import PropTypes from 'prop-types';

+

+const ActionCard = ({ title, subtitle, icon, onClick }) => {

+  return (

+    <div className="action-card" onClick={onClick}>

+      {icon && <div className="action-icon">{icon}</div>}

+      <div className="action-content">

+        <h3>{title}</h3>

+        <p>{subtitle}</p>

+      </div>

+    </div>

+  );

+};

+

+ActionCard.propTypes = {

+  title: PropTypes.string.isRequired,

+  subtitle: PropTypes.string,

+  icon: PropTypes.node,

+  onClick: PropTypes.func

+};

+

+export default ActionCard;
\ No newline at end of file
diff --git a/src/components/Personal/Favorite.jsx b/src/components/Personal/Favorite.jsx
new file mode 100644
index 0000000..97aa1e6
--- /dev/null
+++ b/src/components/Personal/Favorite.jsx
@@ -0,0 +1,47 @@
+import React from 'react';

+import { useNavigate, useLocation } from 'react-router-dom';

+import ActionCard from './ActionCard';

+import './personalSubpage.css';

+

+const Favorites = () => {

+  const navigate = useNavigate();

+  const location = useLocation();

+  // 模拟数据

+  const [favorites] = React.useState([

+    { id: 1, name: '盗梦空间', type: 'movie', added: '2023-10-01' },

+    { id: 2, name: '权力的游戏', type: 'tv', added: '2023-09-15' }

+  ]);

+

+  const handleBack = () => {

+    // 返回个人中心,并携带来源标记

+    navigate('/personal', { 

+      state: { 

+        fromSubpage: true,  // 标记来自子页面

+        dashboardTab: location.state?.dashboardTab // 保留Dashboard的标签页状态

+      },

+      replace: true  // 替换当前历史记录

+    });

+  };

+

+  return (

+    <div className="personal-page">

+      <button className="back-button" onClick={(handleBack)}>

+        &larr; 返回个人中心

+      </button>

+

+      <h2>我的收藏</h2>

+      <div className="resource-grid">

+        {favorites.map(item => (

+          <ActionCard 

+            key={item.id}

+            title={item.name}

+            subtitle={`收藏于 ${item.added}`}

+            onClick={() => console.log('查看详情', item.id)}

+          />

+        ))}

+      </div>

+    </div>

+  );

+};

+

+export default Favorites;
\ No newline at end of file
diff --git a/src/components/Personal/Notice.jsx b/src/components/Personal/Notice.jsx
new file mode 100644
index 0000000..55bc955
--- /dev/null
+++ b/src/components/Personal/Notice.jsx
@@ -0,0 +1,60 @@
+import React from 'react';

+import { useNavigate,useLocation } from 'react-router-dom';

+import './personalSubpage.css';

+

+const Notice = ({ onLogout }) => {

+  const navigate = useNavigate();

+  const location = useLocation();

+  // 模拟数据

+  const [notices] = React.useState([

+    { 

+      id: 1, 

+      title: '积分奖励到账', 

+      content: '您上传的资源《盗梦空间》获得100积分奖励',

+      date: '2023-10-20',

+      read: false

+    },

+    { 

+      id: 2, 

+      title: '系统通知', 

+      content: '服务器将于今晚2:00-4:00进行维护',

+      date: '2023-10-18',

+      read: true

+    }

+  ]);

+

+  const handleBack = () => {

+    // 返回个人中心,并携带来源标记

+    navigate('/personal', { 

+      state: { 

+        fromSubpage: true,  // 标记来自子页面

+        dashboardTab: location.state?.dashboardTab // 保留Dashboard的标签页状态

+      },

+      replace: true  // 替换当前历史记录

+    });

+  };

+

+  return (

+    <div className="subpage-container">

+      <button className="back-button" onClick={(handleBack)}>

+        ← 返回个人中心

+      </button>

+

+      <h2 className="page-title">消息通知</h2>

+      

+      <div className="notice-list">

+        {notices.map(notice => (

+          <div key={notice.id} className={`list-item ${!notice.read ? 'unread' : ''}`}>

+            <div className="notice-header">

+              <h3>{notice.title}</h3>

+              <span className="notice-date">{notice.date}</span>

+            </div>

+            <p className="notice-content">{notice.content}</p>

+          </div>

+        ))}

+      </div>

+    </div>

+  );

+};

+

+export default Notice;
\ No newline at end of file
diff --git a/src/components/Personal/Personal.css b/src/components/Personal/Personal.css
new file mode 100644
index 0000000..c087ac6
--- /dev/null
+++ b/src/components/Personal/Personal.css
@@ -0,0 +1,162 @@
+/* Personal.css */

+.personal-container {

+    max-width: 800px;

+    margin: 0 auto;

+    padding: 20px;

+  }

+  

+  .back-button {

+    background: none;

+    border: none;

+    color: #1890ff;

+    cursor: pointer;

+    font-size: 16px;

+    margin-bottom: 20px;

+    padding: 5px 0;

+  }

+  

+  .profile-card {

+    background: #fff;

+    border-radius: 8px;

+    padding: 20px;

+    margin-bottom: 20px;

+    box-shadow: 0 1px 3px rgba(0,0,0,0.1);

+  }

+  

+  .profile-header {

+    display: flex;

+    align-items: center;

+    margin-bottom: 20px;

+  }

+  

+  .profile-avatar {

+    width: 80px;

+    height: 80px;

+    border-radius: 50%;

+    margin-right: 20px;

+    object-fit: cover;

+  }

+  

+  .profile-info {

+    flex-grow: 1;

+  }

+  

+  .username {

+    font-size: 24px;

+    margin: 0 0 5px;

+  }

+  

+  .user-meta {

+    display: flex;

+    gap: 15px;

+    color: #666;

+    font-size: 14px;

+  }

+  

+  .stats-grid {

+    display: grid;

+    grid-template-columns: repeat(4, 1fr);

+    gap: 15px;

+  }

+  

+  .stat-item {

+    background: #f5f5f5;

+    border-radius: 6px;

+    padding: 15px;

+    text-align: center;

+  }

+  

+  .stat-label {

+    font-size: 14px;

+    color: #666;

+    margin-bottom: 5px;

+  }

+  

+  .stat-value {

+    font-size: 18px;

+    font-weight: bold;

+  }

+  

+  .quota-card {

+    background: #fff;

+    border-radius: 8px;

+    padding: 20px;

+    margin-bottom: 20px;

+    box-shadow: 0 1px 3px rgba(0,0,0,0.1);

+  }

+  

+  .quota-card h3 {

+    margin-top: 0;

+    margin-bottom: 15px;

+  }

+  

+  .quota-info {

+    display: flex;

+    justify-content: space-between;

+    margin-bottom: 10px;

+  }

+  

+  .quota-used {

+    color: #1890ff;

+  }

+  

+  .quota-remaining {

+    color: #52c41a;

+  }

+  

+  .progress-bar {

+    height: 10px;

+    background: #f0f0f0;

+    border-radius: 5px;

+    margin-bottom: 10px;

+    overflow: hidden;

+  }

+  

+  .progress-fill {

+    height: 100%;

+    background: #1890ff;

+    border-radius: 5px;

+    transition: width 0.3s ease;

+  }

+  

+  .quota-total {

+    text-align: right;

+    color: #666;

+    font-size: 14px;

+  }

+  

+  .action-cards {

+    display: grid;

+    grid-template-columns: repeat(2, 1fr);

+    gap: 15px;

+  }

+  

+  .action-card {

+    background: #fff;

+    border-radius: 8px;

+    padding: 20px;

+    box-shadow: 0 1px 3px rgba(0,0,0,0.1);

+    cursor: pointer;

+    transition: transform 0.2s ease;

+  }

+  

+  .action-card:hover {

+    transform: translateY(-3px);

+    box-shadow: 0 4px 8px rgba(0,0,0,0.1);

+  }

+  

+  .action-card h3 {

+    margin-top: 0;

+    color: #1890ff;

+  }

+  

+  .action-card p {

+    color: #666;

+    margin-bottom: 0;

+  }

+

+  .subpage-container {

+    margin-top: 20px;

+    border-top: 1px solid #f0f0f0;

+    padding-top: 20px;

+  }
\ No newline at end of file
diff --git a/src/components/Personal/Personal.jsx b/src/components/Personal/Personal.jsx
new file mode 100644
index 0000000..033952d
--- /dev/null
+++ b/src/components/Personal/Personal.jsx
@@ -0,0 +1,149 @@
+import React from 'react';

+import { useNavigate,useLocation, Outlet } from 'react-router-dom';

+import './Personal.css';

+import ActionCard from './ActionCard';

+

+const Personal = () => {

+  const navigate = useNavigate();

+  const location = useLocation(); // 获取路由信息

+  

+  // 模拟用户数据

+  const userData = {

+    username: 'PT爱好者',

+    avatar: 'https://via.placeholder.com/150',

+    joinDate: '2023-01-15',

+    level: '中级会员',

+    points: 1250,

+    upload: '3.2TB',

+    download: '1.5TB',

+    ratio: '2.13',

+    downloadQuota: {

+      total: 10,  // 10GB

+      used: 3.7,  // 已使用3.7GB

+      remaining: 6.3  // 剩余6.3GB

+    }

+  };

+

+  const features = [

+    { 

+      title: '我的收藏', 

+      description: '查看收藏的资源',

+      path: '/personal/Favorite' // 相对路径

+    },

+    { 

+      title: '上传记录', 

+      description: '管理上传的资源',

+      path: '/personal/Upload' 

+    },

+    { 

+      title: '消息通知', 

+      description: '查看系统消息',

+      path: '/personal/Notice' 

+    },

+    { 

+      title: '设置', 

+      description: '修改个人资料',

+      path: '/personal/Setting' 

+    }

+  ];

+

+  const handleCardClick = (path) => {

+    navigate(path); // 相对导航

+  };

+

+  const handleBack = () => {

+    if (location.state?.fromSubpage) {

+      // 如果是从子页面返回,则继续返回到Dashboard

+      navigate(`/dashboard/${location.state.dashboardTab || ''}`, {

+        replace: true

+      });

+    } else {

+      // 普通返回逻辑

+      navigate(-1);

+    }

+  };

+

+  return (

+    <div className="personal-container">

+      {/* 返回按钮 */}

+      <button className="back-button" onClick={handleBack}>

+        &larr; 返回

+      </button>

+      

+      {/* 用户基本信息卡片 */}

+      <div className="profile-card">

+        <div className="profile-header">

+          <img 

+            src={userData.avatar} 

+            alt={userData.username} 

+            className="profile-avatar"

+          />

+          <div className="profile-info">

+            <h2 className="username">{userData.username}</h2>

+            <div className="user-meta">

+              <span>加入时间: {userData.joinDate}</span>

+              <span>等级: {userData.level}</span>

+            </div>

+          </div>

+        </div>

+        

+        {/* 用户数据统计 */}

+        <div className="stats-grid">

+          <div className="stat-item">

+            <div className="stat-label">积分</div>

+            <div className="stat-value">{userData.points}</div>

+          </div>

+          <div className="stat-item">

+            <div className="stat-label">上传量</div>

+            <div className="stat-value">{userData.upload}</div>

+          </div>

+          <div className="stat-item">

+            <div className="stat-label">下载量</div>

+            <div className="stat-value">{userData.download}</div>

+          </div>

+          <div className="stat-item">

+            <div className="stat-label">分享率</div>

+            <div className="stat-value">{userData.ratio}</div>

+          </div>

+        </div>

+      </div>

+      

+      {/* 下载额度卡片 */}

+      <div className="quota-card">

+        <h3>下载额度</h3>

+        <div className="quota-info">

+          <span className="quota-used">{userData.downloadQuota.used}GB 已使用</span>

+          <span className="quota-remaining">{userData.downloadQuota.remaining}GB 剩余</span>

+        </div>

+        <div className="progress-bar">

+          <div 

+            className="progress-fill"

+            style={{ width: `${(userData.downloadQuota.used / userData.downloadQuota.total) * 100}%` }}

+          ></div>

+        </div>

+        <div className="quota-total">总额度: {userData.downloadQuota.total}GB</div>

+      </div>

+      

+      {/* 功能卡片区 */}

+      <div className="action-cards">

+        {features.map((feature) => (

+          <div 

+            key={feature.path}

+            className="action-card"

+            onClick={() => handleCardClick(feature.path)}

+          >

+            <h3>{feature.title}</h3>

+            <p>{feature.description}</p>

+          </div>

+        ))}

+      </div>

+

+      {/* 子路由出口 */}

+      <div className="subpage-container">

+        <Outlet />

+      </div>

+    </div>

+  );

+};

+

+export default Personal;
\ No newline at end of file
diff --git a/src/components/Personal/Setting.jsx b/src/components/Personal/Setting.jsx
new file mode 100644
index 0000000..967be6c
--- /dev/null
+++ b/src/components/Personal/Setting.jsx
@@ -0,0 +1,99 @@
+import React, { useState } from 'react';

+import { useNavigate,useLocation } from 'react-router-dom';

+import './personalSubpage.css';

+

+const Setting = ({ onLogout }) => {

+  const navigate = useNavigate();

+  const location = useLocation();

+  // 模拟数据

+  const [formData, setFormData] = useState({

+    username: 'user123',

+    email: 'user@example.com',

+    notification: true

+  });

+

+  const handleChange = (e) => {

+    const { name, value, type, checked } = e.target;

+    setFormData(prev => ({

+      ...prev,

+      [name]: type === 'checkbox' ? checked : value

+    }));

+  };

+

+  const handleSubmit = (e) => {

+    e.preventDefault();

+    alert('设置已保存');

+  };

+

+  const handleBack = () => {

+    // 返回个人中心,并携带来源标记

+    navigate('/personal', { 

+      state: { 

+        fromSubpage: true,  // 标记来自子页面

+        dashboardTab: location.state?.dashboardTab // 保留Dashboard的标签页状态

+      },

+      replace: true  // 替换当前历史记录

+    });

+  };

+

+  return (

+    <div className="subpage-container">

+      <button className="back-button" onClick={(handleBack)}>

+        ← 返回个人中心

+      </button>

+

+      <h2 className="page-title">账号设置</h2>

+      

+      <form onSubmit={handleSubmit}>

+        <div className="form-group">

+          <label className="form-label">用户名</label>

+          <input

+            type="text"

+            name="username"

+            value={formData.username}

+            onChange={handleChange}

+            className="form-input"

+          />

+        </div>

+

+        <div className="form-group">

+          <label className="form-label">电子邮箱</label>

+          <input

+            type="email"

+            name="email"

+            value={formData.email}

+            onChange={handleChange}

+            className="form-input"

+          />

+        </div>

+

+        <div className="form-group">

+          <label className="form-label">

+            <input

+              type="checkbox"

+              name="notification"

+              checked={formData.notification}

+              onChange={handleChange}

+            />

+            接收邮件通知

+          </label>

+        </div>

+

+        <div className="form-actions">

+          <button type="submit" className="action-btn">

+            保存设置

+          </button>

+          <button 

+            type="button" 

+            className="action-btn danger-btn"

+            onClick={onLogout}

+          >

+            退出登录

+          </button>

+        </div>

+      </form>

+    </div>

+  );

+};

+

+export default Setting;
\ No newline at end of file
diff --git a/src/components/Personal/Upload.jsx b/src/components/Personal/Upload.jsx
new file mode 100644
index 0000000..4d6e934
--- /dev/null
+++ b/src/components/Personal/Upload.jsx
@@ -0,0 +1,65 @@
+import React from 'react';

+import { useNavigate,useLocation } from 'react-router-dom';

+import './personalSubpage.css';

+

+const Upload = ({ onLogout }) => {

+  const navigate = useNavigate();

+  const location = useLocation();

+  const [uploads] = React.useState([

+    { id: 1, name: '星际穿越', status: '已发布', date: '2023-10-15', size: '15.2GB' },

+    { id: 2, name: '黑暗骑士', status: '审核中', date: '2023-10-18', size: '12.7GB' }

+  ]);

+

+  const handleBack = () => {

+    // 返回个人中心,并携带来源标记

+    navigate('/personal', { 

+      state: { 

+        fromSubpage: true,  // 标记来自子页面

+        dashboardTab: location.state?.dashboardTab // 保留Dashboard的标签页状态

+      },

+      replace: true  // 替换当前历史记录

+    });

+  };

+  return (

+    <div className="subpage-container">

+      <button className="back-button" onClick={(handleBack)}>

+        ← 返回个人中心

+      </button>

+

+      <h2 className="page-title">上传记录</h2>

+      

+      <table className="uploads-table">

+        <thead>

+          <tr>

+            <th>资源名称</th>

+            <th>大小</th>

+            <th>状态</th>

+            <th>上传时间</th>

+            <th>操作</th>

+          </tr>

+        </thead>

+        <tbody>

+          {uploads.map(item => (

+            <tr key={item.id} className="list-item">

+              <td>{item.name}</td>

+              <td>{item.size}</td>

+              <td>

+                <span className={`status-badge ${

+                  item.status === '已发布' ? 'published' : 'pending'

+                }`}>

+                  {item.status}

+                </span>

+              </td>

+              <td>{item.date}</td>

+              <td>

+                <button className="action-btn">详情</button>

+              </td>

+            </tr>

+          ))}

+        </tbody>

+      </table>

+    </div>

+  );

+};

+

+export default Upload;
\ No newline at end of file
diff --git a/src/components/Personal/personalSubpage.css b/src/components/Personal/personalSubpage.css
new file mode 100644
index 0000000..a8e5638
--- /dev/null
+++ b/src/components/Personal/personalSubpage.css
@@ -0,0 +1,161 @@
+/* 基础布局 */

+.subpage-container {

+    max-width: 1200px;

+    margin: 0 auto;

+    padding: 20px;

+    background: white;

+    border-radius: 8px;

+    box-shadow: 0 2px 8px rgba(0,0,0,0.1);

+  }

+  

+  .back-button {

+    background: none;

+    border: none;

+    color: #1890ff;

+    font-size: 16px;

+    cursor: pointer;

+    margin-bottom: 20px;

+    display: flex;

+    align-items: center;

+    gap: 5px;

+  }

+  

+  .back-button:hover {

+    color: #40a9ff;

+  }

+  

+  .page-title {

+    color: #333;

+    border-bottom: 1px solid #f0f0f0;

+    padding-bottom: 10px;

+    margin-bottom: 20px;

+  }

+  

+  /* 列表项样式 */

+  .list-item {

+    padding: 15px;

+    border-bottom: 1px solid #f5f5f5;

+    transition: background 0.3s;

+  }

+  

+  .list-item:hover {

+    background: #f9f9f9;

+  }

+  

+  /* 表单样式 */

+  .form-group {

+    margin-bottom: 20px;

+  }

+  

+  .form-label {

+    display: block;

+    margin-bottom: 8px;

+    font-weight: 500;

+  }

+  

+  .form-input {

+    width: 100%;

+    padding: 10px;

+    border: 1px solid #d9d9d9;

+    border-radius: 4px;

+  }

+  

+  /* 按钮样式 */

+  .action-btn {

+    padding: 8px 15px;

+    background: #1890ff;

+    color: white;

+    border: none;

+    border-radius: 4px;

+    cursor: pointer;

+    margin-right: 10px;

+  }

+  

+  .action-btn:hover {

+    background: #40a9ff;

+  }

+  

+  .danger-btn {

+    background: #ff4d4f;

+  }

+  

+  .danger-btn:hover {

+    background: #ff7875;

+  }

+

+  /* 收藏列表 */

+.favorite-list {

+    display: flex;

+    flex-direction: column;

+    gap: 10px;

+  }

+  

+  .item-header {

+    margin-bottom: 8px;

+  }

+  

+  .item-meta {

+    color: #666;

+    font-size: 14px;

+  }

+  

+  .item-actions {

+    display: flex;

+    gap: 10px;

+    margin-top: 10px;

+  }

+  

+  /* 上传表格 */

+  .uploads-table {

+    width: 100%;

+    border-collapse: collapse;

+  }

+  

+  .uploads-table th, .uploads-table td {

+    padding: 12px 15px;

+    text-align: left;

+  }

+  

+  .status-badge {

+    padding: 4px 8px;

+    border-radius: 4px;

+    font-size: 12px;

+  }

+  

+  .status-badge.published {

+    background: #f6ffed;

+    color: #52c41a;

+  }

+  

+  .status-badge.pending {

+    background: #fff7e6;

+    color: #fa8c16;

+  }

+  

+  /* 消息通知 */

+  .notice-header {

+    display: flex;

+    justify-content: space-between;

+    margin-bottom: 5px;

+  }

+  

+  .notice-date {

+    color: #999;

+    font-size: 14px;

+  }

+  

+  .notice-content {

+    color: #666;

+    margin: 0;

+  }

+  

+  .unread {

+    background: #f0f7ff;

+  }

+  

+  /* 设置表单 */

+  .form-actions {

+    margin-top: 30px;

+    display: flex;

+    gap: 15px;

+  }
\ No newline at end of file
diff --git a/src/components/RequestDetail.css b/src/components/RequestDetail.css
new file mode 100644
index 0000000..0d47b71
--- /dev/null
+++ b/src/components/RequestDetail.css
@@ -0,0 +1,213 @@
+/* RequestDetail.css */

+.request-detail-container {

+    max-width: 800px;

+    margin: 0 auto;

+    padding: 20px;

+  }

+  

+  .back-button {

+    background: none;

+    border: none;

+    color: #1890ff;

+    cursor: pointer;

+    font-size: 16px;

+    margin-bottom: 20px;

+    padding: 5px 0;

+  }

+  

+  .request-post {

+    background: #fff;

+    border-radius: 8px;

+    padding: 20px;

+    margin-bottom: 20px;

+    box-shadow: 0 1px 3px rgba(0,0,0,0.1);

+  }

+  

+  .post-header {

+    display: flex;

+    align-items: center;

+    margin-bottom: 15px;

+  }

+  

+  .post-avatar {

+    width: 40px;

+    height: 40px;

+    border-radius: 50%;

+    margin-right: 10px;

+  }

+  

+  .post-meta {

+    display: flex;

+    flex-direction: column;

+  }

+  

+  .post-author {

+    font-weight: bold;

+  }

+  

+  .post-date {

+    color: #888;

+    font-size: 14px;

+  }

+  

+  .post-title {

+    font-size: 20px;

+    margin: 0 0 15px;

+  }

+  

+  .post-content {

+    line-height: 1.6;

+    margin-bottom: 15px;

+  }

+  

+  .post-actions {

+    display: flex;

+    gap: 15px;

+  }

+  

+  .like-button, .favorite-button {

+    padding: 5px 15px;

+    border-radius: 4px;

+    border: 1px solid #ddd;

+    background: #f5f5f5;

+    cursor: pointer;

+  }

+  

+  .like-button.liked {

+    background: #e6f7ff;

+    border-color: #91d5ff;

+    color: #1890ff;

+  }

+  

+  .favorite-button.favorited {

+    background: #fff7e6;

+    border-color: #ffd591;

+    color: #fa8c16;

+  }

+  

+  .comments-section {

+    background: #fff;

+    border-radius: 8px;

+    padding: 20px;

+    box-shadow: 0 1px 3px rgba(0,0,0,0.1);

+  }

+  

+  .comment-form {

+    margin-bottom: 20px;

+  }

+  

+  .comment-form textarea {

+    width: 100%;

+    padding: 10px;

+    border-radius: 4px;

+    border: 1px solid #ddd;

+    resize: vertical;

+    min-height: 80px;

+    margin-bottom: 10px;

+  }

+  

+  .form-actions {

+    display: flex;

+    gap: 10px;

+  }

+  

+  .submit-comment, .submit-torrent {

+    padding: 8px 16px;

+    border-radius: 4px;

+    border: none;

+    cursor: pointer;

+  }

+  

+  .submit-comment {

+    background: #1890ff;

+    color: white;

+  }

+  

+  .submit-torrent {

+    background: #52c41a;

+    color: white;

+  }

+  

+  .comment-list {

+    margin-top: 20px;

+  }

+  

+  .comment-item {

+    display: flex;

+    padding: 15px 0;

+    border-bottom: 1px solid #f0f0f0;

+  }

+  

+  .comment-item:last-child {

+    border-bottom: none;

+  }

+  

+  .comment-avatar {

+    width: 40px;

+    height: 40px;

+    border-radius: 50%;

+    margin-right: 15px;

+    flex-shrink: 0;

+  }

+  

+  .comment-content {

+    flex-grow: 1;

+  }

+  

+  .comment-header {

+    display: flex;

+    justify-content: space-between;

+    margin-bottom: 8px;

+  }

+  

+  .comment-author {

+    font-weight: bold;

+  }

+  

+  .comment-date {

+    color: #888;

+    font-size: 14px;

+  }

+  

+  .comment-text {

+    line-height: 1.6;

+    margin-bottom: 8px;

+  }

+  

+  .torrent-comment {

+    display: flex;

+    align-items: center;

+    margin-bottom: 8px;

+    padding: 8px;

+    background: #f0f8ff;

+    border-radius: 4px;

+  }

+  

+  .torrent-title {

+    color: #1890ff;

+    flex-grow: 1;

+  }

+  

+  .torrent-size {

+    color: #666;

+    margin: 0 15px;

+    font-size: 14px;

+  }

+  

+  .download-torrent {

+    background: #1890ff;

+    color: white;

+    border: none;

+    border-radius: 4px;

+    padding: 5px 10px;

+    cursor: pointer;

+  }

+  

+  .comment-like {

+    background: none;

+    border: none;

+    color: #666;

+    cursor: pointer;

+    font-size: 14px;

+    padding: 0;

+  }
\ No newline at end of file
diff --git a/src/components/RequestDetail.jsx b/src/components/RequestDetail.jsx
new file mode 100644
index 0000000..993fe8e
--- /dev/null
+++ b/src/components/RequestDetail.jsx
@@ -0,0 +1,198 @@
+import React, { useState } from 'react';

+import { useParams, useNavigate,useLocation } from 'react-router-dom';

+import './RequestDetail.css';

+

+const RequestDetail = () => {

+  const { id } = useParams();

+  const navigate = useNavigate();

+  const location = useLocation();

+  

+  // 模拟数据

+  const [post, setPost] = useState({

+    id: 1,

+    title: '求《药屋少女的呢喃》第二季全集',

+    content: '求1080P带中文字幕版本,最好是内嵌字幕不是外挂的。\n\n希望有热心大佬能分享,可以给积分奖励!',

+    author: '动漫爱好者',

+    authorAvatar: 'https://via.placeholder.com/40',

+    date: '2023-10-15',

+    likeCount: 24,

+    isLiked: false,

+    isFavorited: false

+  });

+

+  const [comments, setComments] = useState([

+    {

+      id: 1,

+      type: 'text',

+      author: '资源达人',

+      authorAvatar: 'https://via.placeholder.com/40',

+      content: '我有第1-5集,需要的话可以私聊',

+      date: '2023-10-15 14:30',

+      likeCount: 5

+    },

+    {

+      id: 2,

+      type: 'torrent',

+      title: '药屋少女的呢喃第二季第8集',

+      size: '1.2GB',

+      author: '种子分享者',

+      authorAvatar: 'https://via.placeholder.com/40',

+      date: '2023-10-16 09:15',

+      likeCount: 8

+    }

+  ]);

+

+  const [newComment, setNewComment] = useState('');

+

+  const handleLikePost = () => {

+    setPost(prev => ({

+      ...prev,

+      likeCount: prev.isLiked ? prev.likeCount - 1 : prev.likeCount + 1,

+      isLiked: !prev.isLiked

+    }));

+  };

+

+  const handleFavoritePost = () => {

+    setPost(prev => ({

+      ...prev,

+      isFavorited: !prev.isFavorited

+    }));

+  };

+

+  const handleCommentSubmit = (e) => {

+    e.preventDefault();

+    if (!newComment.trim()) return;

+    

+    const newCommentObj = {

+      id: comments.length + 1,

+      type: 'text',

+      author: '当前用户',

+      authorAvatar: 'https://via.placeholder.com/40',

+      content: newComment,

+      date: new Date().toLocaleString(),

+      likeCount: 0

+    };

+    

+    setComments([...comments, newCommentObj]);

+    setNewComment('');

+  };

+

+  const handleDownloadTorrent = (commentId) => {

+    console.log('下载种子', commentId);

+    // 实际下载逻辑

+  };

+

+  const handleBack = () => {

+    const fromTab = location.state?.fromTab; // 从跳转时传递的 state 中获取

+    if (fromTab) {

+      navigate(`/dashboard/${fromTab}`); // 明确返回对应标签页

+    } else {

+      navigate(-1); // 保底策略

+    }

+  }

+

+  return (

+    <div className="request-detail-container">

+      <button className="back-button" onClick={handleBack}>

+        &larr; 返回求种区

+      </button>

+      

+      <div className="request-post">

+        <div className="post-header">

+          <img src={post.authorAvatar} alt={post.author} className="post-avatar" />

+          <div className="post-meta">

+            <div className="post-author">{post.author}</div>

+            <div className="post-date">{post.date}</div>

+          </div>

+        </div>

+        

+        <h1 className="post-title">{post.title}</h1>

+        

+        <div className="post-content">

+          {post.content.split('\n').map((para, i) => (

+            <p key={i}>{para}</p>

+          ))}

+        </div>

+        

+        <div className="post-actions">

+          <button 

+            className={`like-button ${post.isLiked ? 'liked' : ''}`}

+            onClick={handleLikePost}

+          >

+            👍 点赞 ({post.likeCount})

+          </button>

+          <button 

+            className={`favorite-button ${post.isFavorited ? 'favorited' : ''}`}

+            onClick={handleFavoritePost}

+          >

+            {post.isFavorited ? '★ 已收藏' : '☆ 收藏'}

+          </button>

+        </div>

+      </div>

+      

+      <div className="comments-section">

+        <h2>回应 ({comments.length})</h2>

+        

+        <form onSubmit={handleCommentSubmit} className="comment-form">

+          <textarea

+            value={newComment}

+            onChange={(e) => setNewComment(e.target.value)}

+            placeholder="写下你的回应..."

+            rows="3"

+            required

+          />

+          <div className="form-actions">

+            <button type="submit" className="submit-comment">发表文字回应</button>

+            <button 

+              type="button" 

+              className="submit-torrent"

+              onClick={() => console.log('打开种子上传对话框')}

+            >

+              上传种子回应

+            </button>

+          </div>

+        </form>

+        

+        <div className="comment-list">

+          {comments.map(comment => (

+            <div key={comment.id} className={`comment-item ${comment.type}`}>

+              <img 

+                src={comment.authorAvatar} 

+                alt={comment.author} 

+                className="comment-avatar"

+              />

+              

+              <div className="comment-content">

+                <div className="comment-header">

+                  <span className="comment-author">{comment.author}</span>

+                  <span className="comment-date">{comment.date}</span>

+                </div>

+                

+                {comment.type === 'text' ? (

+                  <p className="comment-text">{comment.content}</p>

+                ) : (

+                  <div className="torrent-comment">

+                    <span className="torrent-title">{comment.title}</span>

+                    <span className="torrent-size">{comment.size}</span>

+                    <button 

+                      className="download-torrent"

+                      onClick={() => handleDownloadTorrent(comment.id)}

+                    >

+                      立即下载

+                    </button>

+                  </div>

+                )}

+                

+                <button className="comment-like">

+                  👍 ({comment.likeCount})

+                </button>

+              </div>

+            </div>

+          ))}

+        </div>

+      </div>

+    </div>

+  );

+};

+

+export default RequestDetail;
\ No newline at end of file
diff --git a/src/components/TorrentDetail.css b/src/components/TorrentDetail.css
new file mode 100644
index 0000000..f2b1ad2
--- /dev/null
+++ b/src/components/TorrentDetail.css
@@ -0,0 +1,516 @@
+/* 基础布局样式 */

+.torrent-detail-container {

+  max-width: 1000px;

+  margin: 0 auto;

+  padding: 20px;

+  color: #333;

+}

+

+.back-button {

+  background: none;

+  border: none;

+  color: #3498db;

+  font-size: 16px;

+  cursor: pointer;

+  margin-bottom: 20px;

+  padding: 5px 10px;

+}

+

+.back-button:hover {

+  text-decoration: underline;

+}

+

+/* 种子信息区域 */

+.torrent-main {

+  display: flex;

+  gap: 30px;

+  margin-bottom: 40px;

+}

+

+.torrent-cover {

+  flex: 0 0 300px;

+}

+

+.cover-placeholder {

+  width: 100%;

+  height: 450px;

+  background-color: #eee;

+  display: flex;

+  align-items: center;

+  justify-content: center;

+  font-size: 72px;

+  color: #999;

+  border-radius: 5px;

+}

+

+.torrent-info {

+  flex: 1;

+}

+

+.torrent-title {

+  font-size: 28px;

+  margin-bottom: 20px;

+  color: #222;

+}

+

+.uploader-info {

+  display: flex;

+  align-items: center;

+  gap: 15px;

+  margin-bottom: 20px;

+}

+

+.uploader-avatar {

+  width: 40px;

+  height: 40px;

+  border-radius: 50%;

+  background-color: #3498db;

+  color: white;

+  display: flex;

+  align-items: center;

+  justify-content: center;

+  font-weight: bold;

+}

+

+.uploader-details {

+  flex: 1;

+}

+

+.uploader-name {

+  font-weight: bold;

+}

+

+.upload-time {

+  font-size: 14px;

+  color: #777;

+}

+

+.torrent-meta {

+  margin-bottom: 20px;

+}

+

+.torrent-meta p {

+  margin: 5px 0;

+}

+

+.torrent-description {

+  margin-bottom: 30px;

+  line-height: 1.6;

+}

+

+.interaction-buttons {

+  display: flex;

+  gap: 15px;

+}

+

+.interaction-buttons button {

+  padding: 8px 15px;

+  border: none;

+  border-radius: 4px;

+  cursor: pointer;

+  font-size: 14px;

+  transition: background-color 0.2s;

+}

+

+.like-button {

+  background-color: #f0f0f0;

+}

+

+.like-button:hover {

+  background-color: #e0e0e0;

+}

+

+.download-button {

+  background-color: #3498db;

+  color: white;

+}

+

+.download-button:hover {

+  background-color: #2980b9;

+}

+

+/* 评论区域样式 */

+.comments-section {

+  margin-top: 40px;

+}

+

+.comments-section h2 {

+  font-size: 22px;

+  margin-bottom: 20px;

+  padding-bottom: 10px;

+  border-bottom: 1px solid #eee;

+}

+

+.comment-form {

+  margin-bottom: 30px;

+}

+

+.comment-form textarea {

+  width: 100%;

+  padding: 10px;

+  border: 1px solid #ddd;

+  border-radius: 4px;

+  margin-bottom: 10px;

+  resize: vertical;

+  min-height: 80px;

+}

+

+.comment-form button {

+  background-color: #3498db;

+  color: white;

+  border: none;

+  padding: 8px 15px;

+  border-radius: 4px;

+  cursor: pointer;

+}

+

+.comment-form button:hover {

+  background-color: #2980b9;

+}

+

+.comment-list {

+  margin-top: 20px;

+}

+

+/* 评论项统一样式 */

+.comment-container {

+  margin-bottom: 15px;

+}

+

+.comment-item {

+  display: flex;

+  gap: 15px;

+  padding: 12px;

+  background: #fff;

+  border-radius: 8px;

+  border: 1px solid #eaeaea;

+}

+

+/* 副评论统一缩进 */

+.comment-container.is-reply {

+  margin-left: 40px;

+  position: relative;

+}

+

+/* 副评论连接线 */

+.comment-container.is-reply:before {

+  content: "";

+  position: absolute;

+  left: -20px;

+  top: 20px;

+  width: 15px;

+  height: 1px;

+  background: #ddd;

+}

+

+.comment-avatar {

+  flex: 0 0 40px;

+  height: 40px;

+  border-radius: 50%;

+  background-color: #e74c3c;

+  color: white;

+  display: flex;

+  align-items: center;

+  justify-content: center;

+  font-weight: bold;

+}

+

+.comment-content {

+  flex: 1;

+}

+

+.comment-header {

+  margin-bottom: 8px;

+}

+

+.comment-user {

+  font-weight: bold;

+  margin-right: 10px;

+}

+

+.reply-to {

+  color: #666;

+  font-size: 14px;

+  margin: 0 5px;

+}

+

+.comment-time {

+  font-size: 14px;

+  color: #777;

+}

+

+.comment-text {

+  margin-bottom: 10px;

+  line-height: 1.5;

+}

+

+.comment-actions {

+  display: flex;

+  gap: 15px;

+}

+

+.comment-actions button {

+  background: none;

+  border: none;

+  color: #3498db;

+  cursor: pointer;

+  font-size: 14px;

+  padding: 0;

+}

+

+.comment-actions button:hover {

+  text-decoration: underline;

+}

+

+/* 回复列表容器 */

+.replies-container {

+  margin-top: 15px;

+}

+

+/* 副评论背景色 */

+.comment-container.is-reply .comment-item {

+  background: #f9f9f9;

+}

+

+/* 回复表单样式 */

+.reply-form {

+  margin-top: 15px;

+}

+

+.reply-form textarea {

+  width: 100%;

+  padding: 8px;

+  border: 1px solid #ddd;

+  border-radius: 4px;

+  margin-bottom: 10px;

+  resize: vertical;

+  min-height: 60px;

+}

+

+.reply-actions {

+  display: flex;

+  gap: 10px;

+  justify-content: flex-end;

+}

+

+.reply-actions button {

+  padding: 5px 10px;

+  border: none;

+  border-radius: 3px;

+  cursor: pointer;

+  font-size: 13px;

+}

+

+.reply-actions button[type="button"] {

+  background-color: #f0f0f0;

+}

+

+.reply-actions button[type="submit"] {

+  background-color: #3498db;

+  color: white;

+}

+

+/* 加载和错误状态 */

+.loading {

+  text-align: center;

+  padding: 50px;

+  font-size: 18px;

+}

+

+.error {

+  color: #e74c3c;

+  text-align: center;

+  padding: 50px;

+  font-size: 18px;

+}

+/* 回复表单样式 */

+.reply-form {

+  margin-left: 50px;

+  margin-top: 10px;

+  background: #f5f5f5;

+  padding: 10px;

+  border-radius: 4px;

+}

+

+.reply-form textarea {

+  width: 100%;

+  padding: 8px;

+  border: 1px solid #ddd;

+  border-radius: 4px;

+  resize: vertical;

+}

+

+.reply-form-buttons {

+  display: flex;

+  gap: 10px;

+  margin-top: 8px;

+}

+

+.reply-form-buttons button {

+  padding: 5px 10px;

+  border: none;

+  border-radius: 4px;

+  cursor: pointer;

+}

+

+.reply-form-buttons button[type="submit"] {

+  background-color: #1890ff;

+  color: white;

+}

+

+.reply-form-buttons .cancel-reply {

+  background-color: #f5f5f5;

+  border: 1px solid #d9d9d9;

+}

+

+.comment-list {

+  margin-top: 20px;

+  border-top: 1px solid #eee;

+  padding-top: 20px;

+}

+

+.reply-form {

+  margin-left: 50px;

+  margin-top: 10px;

+  margin-bottom: 15px;

+  background: #f9f9f9;

+  padding: 12px;

+  border-radius: 6px;

+  border: 1px solid #eee;

+}

+

+.reply-form.nested-reply {

+  margin-left: 80px;

+}

+

+.reply-form textarea {

+  width: 100%;

+  padding: 10px;

+  border: 1px solid #ddd;

+  border-radius: 4px;

+  resize: vertical;

+  min-height: 60px;

+  font-family: inherit;

+  font-size: 14px;

+}

+

+.reply-form-buttons {

+  display: flex;

+  gap: 10px;

+  margin-top: 10px;

+}

+

+.reply-form-buttons button {

+  padding: 6px 12px;

+  border-radius: 4px;

+  cursor: pointer;

+  font-size: 13px;

+  transition: all 0.2s;

+}

+

+.reply-form-buttons button[type="submit"] {

+  background-color: #1890ff;

+  color: white;

+  border: none;

+}

+

+.reply-form-buttons button[type="submit"]:hover {

+  background-color: #40a9ff;

+}

+

+.reply-form-buttons .cancel-reply {

+  background-color: #f5f5f5;

+  border: 1px solid #d9d9d9;

+  color: #666;

+}

+

+.reply-form-buttons .cancel-reply:hover {

+  background-color: #e8e8e8;

+}

+

+/* 回复弹窗样式 */

+.reply-modal-overlay {

+  position: fixed;

+  top: 0;

+  left: 0;

+  right: 0;

+  bottom: 0;

+  background-color: rgba(0, 0, 0, 0.5);

+  display: flex;

+  justify-content: center;

+  align-items: center;

+  z-index: 1000;

+}

+

+.reply-modal {

+  background: white;

+  border-radius: 8px;

+  width: 90%;

+  max-width: 500px;

+  padding: 20px;

+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);

+}

+

+.modal-header {

+  display: flex;

+  justify-content: space-between;

+  align-items: center;

+  margin-bottom: 15px;

+}

+

+.modal-header h3 {

+  margin: 0;

+  font-size: 18px;

+}

+

+.close-modal {

+  background: none;

+  border: none;

+  font-size: 24px;

+  cursor: pointer;

+  color: #666;

+}

+

+.reply-modal textarea {

+  width: 100%;

+  padding: 12px;

+  border: 1px solid #ddd;

+  border-radius: 4px;

+  resize: vertical;

+  min-height: 120px;

+  font-size: 14px;

+  margin-bottom: 15px;

+}

+

+.modal-actions {

+  display: flex;

+  justify-content: flex-end;

+  gap: 10px;

+}

+

+.modal-actions button {

+  padding: 8px 16px;

+  border-radius: 4px;

+  cursor: pointer;

+  font-size: 14px;

+}

+

+.cancel-btn {

+  background: #f5f5f5;

+  border: 1px solid #d9d9d9;

+  color: #666;

+}

+

+.cancel-btn:hover {

+  background: #e8e8e8;

+}

+

+.submit-btn {

+  background: #1890ff;

+  color: white;

+  border: none;

+}

+

+.submit-btn:hover {

+  background: #40a9ff;

+}

diff --git a/src/components/TorrentDetail.jsx b/src/components/TorrentDetail.jsx
new file mode 100644
index 0000000..e743506
--- /dev/null
+++ b/src/components/TorrentDetail.jsx
@@ -0,0 +1,348 @@
+import React, { useState, useEffect } from 'react';

+import { useParams, useNavigate, useLocation } from 'react-router-dom';

+import { 

+  getTorrentDetail, 

+  likeTorrent, 

+  addTorrentComment 

+} from '../api/torrent';

+import { 

+  likeTorrentComment, 

+  addCommentReply 

+} from '../api/torrentComment';

+import './TorrentDetail.css';

+

+

+const TorrentDetail = ({ onLogout }) => {

+  const { id } = useParams();

+  const navigate = useNavigate();

+  const location = useLocation();

+  const [torrent, setTorrent] = useState(null);

+  const [comments, setComments] = useState([]);

+  const [newComment, setNewComment] = useState('');

+  const [setReplyingTo] = useState(null);

+  const [replyContent, setReplyContent] = useState('');

+  const [loading, setLoading] = useState(true);

+  const [error, setError] = useState('');

+  const [activeReplyId, setActiveReplyId] = useState(null);

+  const [replyModal, setReplyModal] = useState({

+    visible: false,

+    replyingTo: null,

+    replyingToUsername: '',

+    isReply: false

+  });

+

+  // 确保openReplyModal接收username参数

+const openReplyModal = (commentId, username) => {

+  setReplyModal({

+    visible: true,

+    replyingTo: commentId,

+    replyingToUsername: username,  // 确保这里接收username

+    isReply: false

+  });

+};

+

+  // 关闭回复弹窗

+  const closeReplyModal = () => {

+    setReplyModal({

+      visible: false,

+      replyingTo: null,

+      replyingToUsername: '',

+      isReply: false

+    });

+    setReplyContent('');

+  };

+

+  const Comment = ({ comment, onLike, onReply, isReply = false }) => {

+    return (

+      <div className={`comment-container ${isReply ? "is-reply" : ""}`}>

+        <div className="comment-item">

+          <div className="comment-avatar">

+            {(comment.authorId || "?").charAt(0)} {/* 修复点 */}

+          </div>

+          <div className="comment-content">

+            <div className="comment-header">

+              <span className="comment-user">{comment.authorId || "匿名用户"}</span>

+              {comment.replyTo && (

+                <span className="reply-to">回复 @{comment.replyTo}</span>

+              )}

+              <span className="comment-time">

+                {new Date(comment.createTime).toLocaleString()}

+              </span>

+            </div>

+            <p className="comment-text">{comment.content}</p>

+            <div className="comment-actions">

+              <button onClick={() => onLike(comment.id)}>

+                👍 ({comment.likeCount || 0})

+              </button>

+              <button onClick={() => onReply(comment.id, comment.authorId)}>

+                回复

+              </button>

+            </div>

+          </div>

+        </div>

+      </div>

+    );

+  };

+

+    // 递归渲染评论组件

+  const renderComment = (comment, depth = 0) => {

+    return (

+      <div key={comment.id} style={{ marginLeft: `${depth * 30}px` }}>

+        <Comment

+          comment={comment}

+          onLike={handleLikeComment}

+          onReply={openReplyModal}

+          isReply={depth > 0}

+        />

+        

+        {/* 递归渲染所有回复 */}

+        {comment.replies && comment.replies.map(reply => 

+          renderComment(reply, depth + 1)

+        )}

+      </div>

+    );

+  };

+    

+

+

+  const fetchTorrentDetail = async () => {

+    try {

+      setLoading(true);

+      const response = await getTorrentDetail(id);

+      console.log('API Response:', JSON.parse(JSON.stringify(response.data.data.comments))); // 深度拷贝避免Proxy影响

+      setTorrent(response.data.data.torrent);

+      setComments(response.data.data.comments);

+    } catch (err) {

+      setError(err.response?.data?.message || '获取种子详情失败');

+    } finally {

+      setLoading(false);

+    }

+  };

+

+  useEffect(() => {

+    fetchTorrentDetail();

+  }, [id]);

+

+  // 点赞种子

+  const handleLikeTorrent = async () => {

+    try {

+      await likeTorrent(id);

+      setTorrent(prev => ({

+        ...prev,

+        likeCount: prev.likeCount + 1

+      }));

+    } catch (err) {

+      setError('点赞失败: ' + (err.response?.data?.message || err.message));

+    }

+  };

+

+  const handleCommentSubmit = async (e) => {

+    e.preventDefault();

+    if (!newComment.trim()) return;

+    

+    try {

+      const username = localStorage.getItem('username');

+      const response = await addTorrentComment(id, {

+        content: newComment,

+        authorId: username

+      });

+  

+      // 修改这里的响应处理逻辑

+      if (response.data && response.data.code === 200) {

+        await fetchTorrentDetail();

+        

+        setNewComment('');

+      } else {

+        setError(response.data.message || '评论失败');

+      }

+    } catch (err) {

+      setError('评论失败: ' + (err.response?.data?.message || err.message));

+    }

+  };

+  

+  

+  const handleLikeComment = async (commentId) => {

+    try {

+      await likeTorrentComment(commentId);

+      

+      // 递归更新评论点赞数

+      const updateComments = (comments) => {

+        return comments.map(comment => {

+          // 当前评论匹配

+          if (comment.id === commentId) {

+            return { ...comment, likeCount: comment.likeCount + 1 };

+          }

+          

+          // 递归处理回复

+          if (comment.replies && comment.replies.length > 0) {

+            return {

+              ...comment,

+              replies: updateComments(comment.replies)

+            };

+          }

+          

+          return comment;

+        });

+      };

+  

+      setComments(prev => updateComments(prev));

+    } catch (err) {

+      setError('点赞失败: ' + (err.response?.data?.message || err.message));

+    }

+  };

+  // 修改startReply函数

+  const startReply = (commentId) => {

+    if (activeReplyId === commentId) {

+      // 如果点击的是已经激活的回复按钮,则关闭

+      setActiveReplyId(null);

+      setReplyingTo(null);

+    } else {

+      // 否则打开新的回复框

+      setActiveReplyId(commentId);

+      setReplyingTo(commentId);

+    }

+  };

+

+  const handleReplySubmit = async (e) => {

+    e.preventDefault();

+    if (!replyContent.trim()) return;

+  

+    try {

+      const username = localStorage.getItem('username');

+      const response = await addCommentReply(replyModal.replyingTo, {

+        content: replyContent,

+        authorId: username

+      });

+  

+      console.log('回复响应:', response.data); // 调试

+      

+      if (response.data && response.data.code === 200) {

+        await fetchTorrentDetail();

+  

+        closeReplyModal();

+      }

+    } catch (err) {

+      console.error('回复错误:', err);

+      setError('回复失败: ' + (err.response?.data?.message || err.message));

+    }

+  };

+

+

+  // 返回按钮

+  const handleBack = () => {

+    const fromTab = location.state?.fromTab || 'share';

+    navigate(`/dashboard/${fromTab}`);

+  };

+

+  if (loading) return <div className="loading">加载中...</div>;

+  if (error) return <div className="error">{error}</div>;

+  if (!torrent) return <div className="error">种子不存在</div>;

+

+  return (

+    <div className="torrent-detail-container">

+      <button className="back-button" onClick={handleBack}>

+        &larr; 返回资源区

+      </button>

+      

+      <div className="torrent-main">

+        <div className="torrent-cover">

+          <div className="cover-placeholder">

+            {torrent.torrentName.charAt(0)}

+          </div>

+        </div>

+        

+        <div className="torrent-info">

+          <h1 className="torrent-title">{torrent.torrentName}</h1>

+          

+          <div className="uploader-info">

+            <div className="uploader-avatar">

+              {torrent.username.charAt(0)}

+            </div>

+            <div className="uploader-details">

+              <span className="uploader-name">{torrent.username}</span>

+              <span className="upload-time">

+                {new Date(torrent.createTime).toLocaleString()}

+              </span>

+            </div>

+          </div>

+          

+          <div className="torrent-meta">

+            <p><strong>类型:</strong> {torrent.category}</p>

+            <p><strong>地区:</strong> {torrent.region}</p>

+            <p><strong>分辨率:</strong> {torrent.resolution}</p>

+            <p><strong>字幕:</strong> {torrent.subtitle}</p>

+            {torrent.size && <p><strong>大小:</strong> {torrent.size}</p>}

+          </div>

+          

+          <div className="torrent-description">

+            <h3>资源描述</h3>

+            <p>{torrent.description}</p>

+          </div>

+          

+          <div className="interaction-buttons">

+            <button 

+              className="like-button"

+              onClick={handleLikeTorrent}

+            >

+              <span>👍 点赞 ({torrent.likeCount})</span>

+            </button>

+            <button className="download-button">

+              <span>⬇️ 立即下载</span>

+            </button>

+          </div>

+        </div>

+      </div>

+      

+      <div className="comments-section">

+        <h2>评论 ({torrent.replyCount})</h2>

+        

+        <form onSubmit={handleCommentSubmit} className="comment-form">

+          <textarea

+            value={newComment}

+            onChange={(e) => setNewComment(e.target.value)}

+            placeholder="写下你的评论..."

+            rows="3"

+            required

+          />

+          <button type="submit">发表评论</button>

+        </form>

+        

+        <div className="comment-list">

+          {comments.map(comment => renderComment(comment))}

+        </div>

+

+        {replyModal.visible && (

+          <div className="reply-modal-overlay">

+            <div className="reply-modal">

+              <div className="modal-header">

+                <h3>回复 @{replyModal.replyingToUsername}</h3>

+                <button onClick={closeReplyModal} className="close-modal">&times;</button>

+              </div>

+              <form onSubmit={handleReplySubmit}>

+                <textarea

+                  value={replyContent}

+                  onChange={(e) => setReplyContent(e.target.value)}

+                  placeholder={`回复 @${replyModal.replyingToUsername}...`}

+                  rows="5"

+                  autoFocus

+                  required

+                />

+                <div className="modal-actions">

+                  <button type="button" onClick={closeReplyModal} className="cancel-btn">

+                    取消

+                  </button>

+                  <button type="submit" className="submit-btn">

+                    发送回复

+                  </button>

+                </div>

+              </form>

+            </div>

+          </div>

+        )}

+

+      </div>

+    </div>

+  );

+};

+

+export default TorrentDetail;
\ No newline at end of file
diff --git a/src/components/TorrentDetail.test.jsx b/src/components/TorrentDetail.test.jsx
new file mode 100644
index 0000000..309942f
--- /dev/null
+++ b/src/components/TorrentDetail.test.jsx
@@ -0,0 +1,168 @@
+import React from 'react';

+import { render, screen, fireEvent, waitFor } from '@testing-library/react';

+import { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';

+import TorrentDetail from './TorrentDetail';

+import * as torrentApi from '../api/torrent';

+import * as torrentCommentApi from '../api/torrentComment';

+

+// Mock API 模块

+jest.mock('../api/torrent');

+jest.mock('../api/torrentComment');

+

+// Mock useLocation 钩子

+jest.mock('react-router-dom', () => ({

+  ...jest.requireActual('react-router-dom'),

+  useLocation: jest.fn(),

+}));

+

+describe('TorrentDetail 组件', () => {

+  const mockTorrent = {

+    id: '1',

+    torrentName: '测试种子',

+    category: '电影',

+    region: '美国',

+    resolution: '1080P',

+    subtitle: '中文字幕',

+    username: 'user1',

+    createTime: '2023-01-01T00:00:00Z',

+    likeCount: 5,

+    replyCount: 3,

+    description: '这是一个测试种子描述',

+  };

+

+  const mockComments = [

+    {

+      id: 'c1',

+      content: '测试评论1',

+      authorId: 'user2',

+      createTime: '2023-01-01T01:00:00Z',

+      likeCount: 2,

+      replies: [

+        {

+          id: 'c1r1',

+          content: '测试回复1',

+          authorId: 'user3',

+          createTime: '2023-01-01T02:00:00Z',

+          likeCount: 1,

+        }

+      ]

+    }

+  ];

+

+  beforeEach(() => {

+    // 设置模拟的 API 响应

+    torrentApi.getTorrentDetail.mockResolvedValue({

+      data: {

+        code: 200,

+        data: {

+          torrent: mockTorrent,

+          comments: mockComments,

+        }

+      }

+    });

+

+    torrentApi.likeTorrent.mockResolvedValue({ data: { code: 200 } });

+    torrentApi.addTorrentComment.mockResolvedValue({ data: { code: 200 } });

+    torrentCommentApi.likeTorrentComment.mockResolvedValue({ data: { code: 200 } });

+    torrentCommentApi.addCommentReply.mockResolvedValue({ data: { code: 200 } });

+

+    // 模拟 useLocation

+    useLocation.mockReturnValue({

+      state: { fromTab: 'share' }

+    });

+

+    // 设置 localStorage

+    Storage.prototype.getItem = jest.fn((key) => {

+      if (key === 'username') return 'testuser';

+      return null;

+    });

+  });

+

+  afterEach(() => {

+    jest.clearAllMocks();

+  });

+

+  const renderComponent = () => {

+    return render(

+      <MemoryRouter initialEntries={['/torrent/1']}>

+        <Routes>

+          <Route path="/torrent/:id" element={<TorrentDetail />} />

+        </Routes>

+      </MemoryRouter>

+    );

+  };

+

+  it('应该正确加载和显示种子详情', async () => {

+    renderComponent();

+

+    // 检查加载状态

+    expect(screen.getByText('加载中...')).toBeInTheDocument();

+

+    // 等待数据加载完成

+    await waitFor(() => {

+      expect(screen.getByText('测试种子')).toBeInTheDocument();

+      expect(screen.getByText('这是一个测试种子描述')).toBeInTheDocument();

+      expect(screen.getByText('user1')).toBeInTheDocument();

+    });

+  });

+

+  it('应该能够点赞种子', async () => {

+    renderComponent();

+

+    await waitFor(() => {

+      expect(screen.getByText('测试种子')).toBeInTheDocument();

+    });

+

+    fireEvent.click(screen.getByText(/点赞 \(5\)/));

+

+    await waitFor(() => {

+      expect(torrentApi.likeTorrent).toHaveBeenCalledWith('1');

+    });

+  });

+

+  it('应该能够提交评论', async () => {

+    renderComponent();

+

+    await waitFor(() => {

+      expect(screen.getByText('测试种子')).toBeInTheDocument();

+    });

+

+    const commentInput = screen.getByPlaceholderText('写下你的评论...');

+    fireEvent.change(commentInput, { target: { value: '新评论' } });

+    fireEvent.click(screen.getByText('发表评论'));

+

+    await waitFor(() => {

+      expect(torrentApi.addTorrentComment).toHaveBeenCalledWith('1', {

+        content: '新评论',

+        authorId: 'testuser'

+      });

+    });

+  });

+

+  it('应该能够点赞评论', async () => {

+    renderComponent();

+

+    await waitFor(() => {

+      expect(screen.getByText('测试评论1')).toBeInTheDocument();

+    });

+

+    fireEvent.click(screen.getAllByText(/👍 \(2\)/)[0]);

+

+    await waitFor(() => {

+      expect(torrentCommentApi.likeTorrentComment).toHaveBeenCalledWith('c1');

+    });

+  });

+

+  it('应该能够返回资源区', async () => {

+    const { container } = renderComponent();

+

+    await waitFor(() => {

+      expect(screen.getByText('测试种子')).toBeInTheDocument();

+    });

+

+    const backButton = container.querySelector('.back-button');

+    fireEvent.click(backButton);

+

+    // 这里可以添加导航验证,但需要更复杂的路由设置

+  });

+});
\ No newline at end of file
diff --git a/src/config/config.js b/src/config/config.js
new file mode 100644
index 0000000..a772747
--- /dev/null
+++ b/src/config/config.js
@@ -0,0 +1,7 @@
+global.AppConfig = {

+

+    //serverIP后端服务器地址

+

+    serverIP:'http://localhost:8088'

+  

+  }
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..ec2585e
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,13 @@
+body {
+  margin: 0;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+    sans-serif;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+code {
+  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
+    monospace;
+}
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..415194f
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,26 @@
+// 1. 首先放所有 import 语句
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import './index.css';
+import App from './App';
+import reportWebVitals from './reportWebVitals';
+import process from 'process/browser';  // 新增
+import { Buffer } from 'buffer';        // 新增
+
+// 2. 然后放 polyfill 代码
+if (typeof window.process === 'undefined') {
+  window.process = process;
+}
+if (typeof window.Buffer === 'undefined') {
+  window.Buffer = Buffer;
+}
+
+// 3. 最后放应用逻辑
+const root = ReactDOM.createRoot(document.getElementById('root'));
+root.render(
+  <React.StrictMode>
+    <App />
+  </React.StrictMode>
+);
+
+reportWebVitals();
\ No newline at end of file
diff --git a/src/reportWebVitals.js b/src/reportWebVitals.js
new file mode 100644
index 0000000..5253d3a
--- /dev/null
+++ b/src/reportWebVitals.js
@@ -0,0 +1,13 @@
+const reportWebVitals = onPerfEntry => {
+  if (onPerfEntry && onPerfEntry instanceof Function) {
+    import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
+      getCLS(onPerfEntry);
+      getFID(onPerfEntry);
+      getFCP(onPerfEntry);
+      getLCP(onPerfEntry);
+      getTTFB(onPerfEntry);
+    });
+  }
+};
+
+export default reportWebVitals;
diff --git a/src/setupTests.js b/src/setupTests.js
new file mode 100644
index 0000000..f739037
--- /dev/null
+++ b/src/setupTests.js
@@ -0,0 +1,28 @@
+// jest-dom adds custom jest matchers for asserting on DOM nodes.
+// allows you to do things like:
+// expect(element).toHaveTextContent(/react/i)
+// learn more: https://github.com/testing-library/jest-dom
+import '@testing-library/jest-dom';
+
+// 模拟 localStorage
+const localStorageMock = (function() {
+    let store = {};
+    return {
+      getItem: jest.fn((key) => store[key] || null),
+      setItem: jest.fn((key, value) => {
+        store[key] = String(value);
+      }),
+      removeItem: jest.fn((key) => {
+        delete store[key];
+      }),
+      clear: jest.fn(() => {
+        store = {};
+      })
+    };
+  })();
+  
+  // 挂载到全局
+  Object.defineProperty(window, 'localStorage', {
+    value: localStorageMock,
+    writable: true // 必须设置为可写
+  });
\ No newline at end of file
diff --git a/src/test-utils.jsx b/src/test-utils.jsx
new file mode 100644
index 0000000..768da69
--- /dev/null
+++ b/src/test-utils.jsx
@@ -0,0 +1,7 @@
+import { MemoryRouter } from 'react-router-dom';

+import { render } from '@testing-library/react';

+

+export const renderWithRouter = (ui, { route = '/' } = {}) => {

+  window.history.pushState({}, 'Test page', route);

+  return render(ui, { wrapper: MemoryRouter });

+};
\ No newline at end of file