个人中心全部,模糊乱序搜索,类型筛选

Change-Id: Id635654fccccaea80bfbf4d1480abd55f7d12046
diff --git a/package-lock.json b/package-lock.json
index 2865ace..556ed37 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,7 @@
         "react": "^19.1.0",
         "react-datepicker": "^8.3.0",
         "react-dom": "^19.1.0",
+        "react-icons": "^5.5.0",
         "react-router-dom": "^6.20.1",
         "react-scripts": "5.0.1",
         "stream-browserify": "^3.0.0",
@@ -15231,6 +15232,15 @@
       "integrity": "sha512-SN/U6Ytxf1QGkw/9ve5Y+NxBbZM6Ht95tuXNMKs8EJyFa/Vy/+Co3stop3KBHARfn/giv+Lj1uUnTfOJ3moFEQ==",
       "license": "MIT"
     },
+    "node_modules/react-icons": {
+      "version": "5.5.0",
+      "resolved": "https://mirrors.huaweicloud.com/repository/npm/react-icons/-/react-icons-5.5.0.tgz",
+      "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
+      "license": "MIT",
+      "peerDependencies": {
+        "react": "*"
+      }
+    },
     "node_modules/react-is": {
       "version": "17.0.2",
       "resolved": "https://mirrors.huaweicloud.com/repository/npm/react-is/-/react-is-17.0.2.tgz",
diff --git a/package.json b/package.json
index 2078e22..866a01f 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
     "react": "^19.1.0",
     "react-datepicker": "^8.3.0",
     "react-dom": "^19.1.0",
+    "react-icons": "^5.5.0",
     "react-router-dom": "^6.20.1",
     "react-scripts": "5.0.1",
     "stream-browserify": "^3.0.0",
@@ -28,6 +29,7 @@
     "test": "jest",
     "test:watch": "jest --watch",
     "test:coverage": "jest --coverage",
+    "test:fail": "jest --silent --verbose | findstr /C:\"FAIL\"",
     "test:update": "jest --updateSnapshot",
     "eject": "react-scripts eject"
   },
diff --git a/src/App.jsx b/src/App.jsx
index dba8f84..54113ea 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -14,7 +14,7 @@
 import TorrentDetail from './components/TorrentDetail'; // 确保路径正确
 import RequestDetail from './components/RequestDetail';
 import HelpDetail from './components/HelpDetail';
-import Favorite from './components/Personal/Favorite';
+import Exchange from './components/Personal/Exchange';
 import Upload from './components/Personal/Upload';
 import Notice from './components/Personal/Notice';
 import Setting from './components/Personal/Setting';
@@ -80,8 +80,8 @@
         <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/exchange" element={
+          isAuthenticated ? <Exchange onLogout={handleLogout} /> : <Navigate to="/login" replace />
         } />
         <Route path="/personal/upload" element={
           isAuthenticated ? <Upload onLogout={handleLogout} /> : <Navigate to="/login" replace />
diff --git a/src/api/auth.js b/src/api/auth.js
index 4b569ef..c094a44 100644
--- a/src/api/auth.js
+++ b/src/api/auth.js
@@ -32,21 +32,25 @@
   });

 };

 

-

-export const getUserInfo = (token) => {

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

+export const getUserInfo = async () => {

+  try {

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

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

+      return response.data.data; 

+    }

+    throw new Error(response.data.message || '获取用户信息失败');

+  } catch (error) {

+    console.error('获取用户信息失败:', error);

+    throw error;

+  }

 };

 

-// // 修改你的 API 请求

-// export const getUserInfo = () => {

-//   const token = localStorage.getItem('token');

-//   if (!token) {

-//     throw new Error("Token 不存在");

-//   }

-  

-//   return api.get('/user/info', {

-//     headers: {

-//       'Authorization': `Bearer ${token}`  // 必须带 Bearer 前缀

-//     }

-//   });

-// };

+export const isAdmin = async () => {

+  try {

+    const userInfo = await getUserInfo();

+    return userInfo.authority === 'ADMIN';

+  } catch (error) {

+    console.error('检查管理员权限失败:', error);

+    return false;

+  }

+};

diff --git a/src/api/auth.test.js b/src/api/auth.test.js
index c9ace4d..9de5691 100644
--- a/src/api/auth.test.js
+++ b/src/api/auth.test.js
@@ -1,11 +1,10 @@
 import MockAdapter from 'axios-mock-adapter';

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

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

 

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

   let mockAxios;

 

   beforeEach(() => {

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

     mockAxios = new MockAdapter(api);

     localStorage.clear();

   });

@@ -16,28 +15,30 @@
 

   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 mockResponse = {

+        code: 200,

+        data: { 

+          token: 'mock-token',

+          userInfo: { username: 'testuser' }

+        },

+        message: '登录成功'

+      };

+      

+      mockAxios.onPost('/user/login', undefined, {

+        params: { username: 'testuser', password: 'testpass' }

+      }).reply(200, mockResponse);

     

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

-        

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

-        // 检查token是否存入localStorage

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

-      });

-    

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

+      

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

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

+    });

 

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

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

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

+        code: 401,

+        message: '登录失败'

+      });

       

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

     });

@@ -50,7 +51,9 @@
         message: '注册成功'

       };

       

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

+      mockAxios.onPost('/user/regist', undefined, {

+        params: { username: 'newuser', password: 'newpass', code: 'invite123' }

+      }).reply(200, mockResponse);

 

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

       

@@ -58,30 +61,66 @@
     });

 

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

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

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

+        code: 400,

+        message: '注册失败'

+      });

       

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

     });

   });

 

   describe('getUserInfo', () => {

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

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

       const mockResponse = {

         code: 200,

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

+        data: { username: 'testuser', authority: 'USER' }

       };

       

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

-

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

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

+  

+      const response = await getUserInfo();

       

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

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

+    });

+  

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

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

+      

+      await expect(getUserInfo()).rejects.toThrow('Request failed with status code 401');

+    });

+  });

+  

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

+    it('should return true when user is admin', async () => {

+      const mockResponse = {

+        code: 200,

+        data: { username: 'admin', authority: 'ADMIN' }

+      };

+      

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

+

+      const result = await isAdmin();

+      expect(result).toBe(true);

     });

 

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

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

+    it('should return false when user is not admin', async () => {

+      const mockResponse = {

+        code: 200,

+        data: { username: 'user', authority: 'USER' }

+      };

       

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

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

+

+      const result = await isAdmin();

+      expect(result).toBe(false);

+    });

+

+    it('should return false when request fails', async () => {

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

+      

+      const result = await isAdmin();

+      expect(result).toBe(false);

     });

   });

 

diff --git a/src/api/helpPost.js b/src/api/helpPost.js
index 4813aa3..92d6402 100644
--- a/src/api/helpPost.js
+++ b/src/api/helpPost.js
@@ -50,4 +50,11 @@
   return api.delete(`/help/posts/${postId}`, {

     params: { authorId }

   });

+};

+

+

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

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

+    params: { keyword, page, size }

+  });

 };
\ No newline at end of file
diff --git a/src/api/personal.js b/src/api/personal.js
new file mode 100644
index 0000000..949afb4
--- /dev/null
+++ b/src/api/personal.js
@@ -0,0 +1,181 @@
+// src/api/personal.js

+import { api } from './auth';

+

+/**

+ * 获取用户信息

+ * @returns {Promise<Object>} 用户信息对象

+ */

+export const getUserInfo = async () => {

+  try {

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

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

+        const userData = response.data.data;

+        return {

+            username: userData.username,

+            level: userData.level,

+            registTime: formatDate(userData.registTime),

+            magicPoints: userData.magicPoints,

+            upload: userData.upload, 

+            download: userData.download, 

+            shareRate: userData.shareRate.toFixed(2)

+        };

+    }

+    throw new Error(response.data.message || '获取用户信息失败');

+  } catch (error) {

+    console.error('获取用户信息失败:', error);

+    throw error;

+  }

+};

+

+export const formatFileSize = (bytes) => {

+    if (bytes < 1024) {

+      return bytes + ' B';

+    }

+    const kb = bytes / 1024;

+    if (kb < 1024) {

+      return kb.toFixed(2) + ' KB';

+    }

+    const mb = kb / 1024;

+    if (mb < 1024) {

+      return mb.toFixed(2) + ' MB';

+    }

+    const gb = mb / 1024;

+    return gb.toFixed(2) + ' GB';

+  };

+  

+  

+  export const getDownloadQuota = async () => {

+    try {

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

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

+        const data = response.data.data;

+        return {

+          total: data.total,  // 已经是字节

+          used: data.used,

+          remaining: data.remaining

+        };

+      }

+      throw new Error(response.data.message || '获取下载额度失败');

+    } catch (error) {

+      console.error('获取下载额度失败:', error);

+      throw error;

+    }

+  };

+

+// 修正后的时间格式化(正确处理时区)

+const formatDate = (dateString) => {

+    const date = new Date(dateString);

+    const year = date.getFullYear();

+    const month = String(date.getMonth() + 1).padStart(2, '0');

+    const day = String(date.getDate()).padStart(2, '0');

+    return `${year}-${month}-${day}`;

+  };

+

+

+  export const getDownloadProgress = async () => {

+    try {

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

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

+        return response.data.data.progresses;

+      }

+      throw new Error(response.data.message || '获取下载进度失败');

+    } catch (error) {

+      console.error('获取下载进度失败:', error);

+      throw error;

+    }

+  };

+

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

+    try {

+      const response = await api.get('/torrent/get/torrentMyself', {

+        params: { page, size }

+      });

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

+        const records = response.data.data.records.map(item => ({

+          ...item.torrent,

+          downloadCount: item.downloadCount,

+          formattedSize: item.formattedSize

+        }));

+        return {

+          records: records,

+          total: response.data.data.total

+        };

+      }

+      throw new Error(response.data.message || '获取上传记录失败');

+    } catch (error) {

+      console.error('获取上传记录失败:', error);

+      throw error;

+    }

+  };

+  

+

+  export const deleteTorrent = async (id) => {

+    try {

+      const response = await api.delete(`/torrent/deleteTorrent/${id}`);

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

+        return response.data;

+      }

+      throw new Error(response.data.message || '删除种子失败');

+    } catch (error) {

+      console.error('删除种子失败:', error);

+      throw error;

+    }

+  };

+

+

+  export const generateInviteCode = async () => {

+    try {

+      const response = await api.post('/invitecode/generate');

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

+        return response.data.data.inviteCode;

+      }

+      throw new Error(response.data.message || '生成邀请码失败');

+    } catch (error) {

+      console.error('生成邀请码失败:', error);

+      throw error;

+    }

+  };

+

+  export const getUserInviteCodes = async () => {

+    try {

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

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

+        return response.data.data.inviteCode;

+      }

+      throw new Error(response.data.message || '获取邀请码列表失败');

+    } catch (error) {

+      console.error('获取邀请码列表失败:', error);

+      throw error;

+    }

+  };

+

+  export const exchangeUpload = async (magicPoints) => {

+    try {

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

+        magicPoint: magicPoints

+      });

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

+        return response.data;

+      }

+      throw new Error(response.data.message || '兑换上传量失败');

+    } catch (error) {

+      console.error('兑换上传量失败:', error);

+      throw error;

+    }

+  };

+

+  export const updatePassword = async (oldPassword, newPassword) => {

+    try {

+      const response = await api.put('/user/password', {

+        oldPassword,

+        newPassword

+      });

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

+        return response.data;

+      }

+      throw new Error(response.data.message || '修改密码失败');

+    } catch (error) {

+      console.error('修改密码失败:', error);

+      throw error;

+    }

+  };

diff --git a/src/api/torrent.js b/src/api/torrent.js
index 6c56fce..8a00e54 100644
--- a/src/api/torrent.js
+++ b/src/api/torrent.js
@@ -36,7 +36,7 @@
         data: {

           ...response.data,

           data: {

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

+            torrent: response.data.data.torrent, // 直接使用后端返回的格式化数据

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

           }

         }

@@ -52,4 +52,10 @@
 

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

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

+};

+

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

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

+    params: { keyword, page, size }

+  });

 };
\ No newline at end of file
diff --git a/src/api/torrent.test.js b/src/api/torrent.test.js
index ca755c3..6515bdc 100644
--- a/src/api/torrent.test.js
+++ b/src/api/torrent.test.js
@@ -1,41 +1,134 @@
 import MockAdapter from 'axios-mock-adapter';

-import { api } from './auth'; // Import api from auth

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

+import { api } from './auth';

+import {

+  createTorrent,

+  getTorrents,

+  getTorrentDetail,

+  likeTorrent,

+  addTorrentComment,

+  searchTorrents

+} from './torrent';

 

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

   let mockAxios;

 

   beforeEach(() => {

     mockAxios = new MockAdapter(api);

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

   });

 

   afterEach(() => {

     mockAxios.restore();

+    localStorage.clear();

   });

 

-  // Test for getting torrent detail

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

+    it('应该正确发送包含文件和数据的表单请求', async () => {

+      const mockFile = new File(['test'], 'test.torrent');

+      const torrentData = { name: '测试种子', description: '测试描述' };

+      const mockResponse = { code: 200, message: '创建成功' };

+  

+      mockAxios.onPost('/torrent').reply((config) => {

+        expect(config.headers['Content-Type']).toBe('multipart/form-data');

+        expect(config.headers['Authorization']).toBe('Bearer test-token'); // 修改为包含Bearer

+        return [200, mockResponse];

+      });

+  

+      const response = await createTorrent(torrentData, mockFile);

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

+    });

+  });

+

+  describe('getTorrents - 获取种子列表', () => {

+    it('应该发送带分页参数的请求', async () => {

+      const mockResponse = {

+        code: 200,

+        data: {

+          list: [{ id: 1, name: '种子1' }, { id: 2, name: '种子2' }],

+          total: 2

+        }

+      };

+

+      mockAxios.onGet('/torrent', { params: { page: 2, size: 10 } })

+        .reply(200, mockResponse);

+

+      const response = await getTorrents(2, 10);

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

+    });

+  });

+

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

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

-      const mockData = {

+      const mockResponse = {

+        code: 200,

         data: {

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

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

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

         }

       };

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

+

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

 

       const response = await getTorrentDetail('123');

-      expect(response.data.data.torrent.name).toBe('测试种子'); // Corrected key to `post.name`

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

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

     });

+

+    it('应该处理没有评论的情况', async () => {

+      const mockResponse = {

+        code: 200,

+        data: {

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

+          comments: null

+        }

+      };

+

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

+

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

+      expect(response.data.data.comments).toEqual([]);

+    });

   });

 

-  // Test for liking a torrent

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

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

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

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

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

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

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

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

+

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

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

     });

   });

-});

+

+  describe('addTorrentComment - 添加种子评论', () => {

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

+      const commentData = { content: '测试评论' };

+      const mockResponse = { code: 200, message: '评论成功' };

+

+      mockAxios.onPost('/torrent/123/comments', commentData)

+        .reply(200, mockResponse);

+

+      const response = await addTorrentComment('123', commentData);

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

+    });

+  });

+

+  describe('searchTorrents - 搜索种子', () => {

+    it('应该发送带搜索关键词和分页的请求', async () => {

+      const mockResponse = {

+        code: 200,

+        data: {

+          list: [{ id: 1, name: '匹配的种子' }],

+          total: 1

+        }

+      };

+

+      mockAxios.onGet('/torrent/search', { 

+        params: { keyword: '测试', page: 1, size: 5 } 

+      }).reply(200, mockResponse);

+

+      const response = await searchTorrents('测试');

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

+    });

+  });

+});
\ No newline at end of file
diff --git a/src/components/Dashboard.css b/src/components/Dashboard.css
index 09ab88f..a6b3a5a 100644
--- a/src/components/Dashboard.css
+++ b/src/components/Dashboard.css
@@ -715,4 +715,58 @@
 

 .upload-btn:hover {

   background-color: #218838;

+}

+

+/* 平台名称样式 */

+.platform-name {

+  flex: 1;

+  display: flex;

+  align-items: center;

+  padding-left: 20px;

+}

+

+.platform-name h2 {

+  margin: 0;

+  color: #333;

+  font-size: 24px;

+  font-weight: bold;

+}

+

+/* 分区搜索框样式 */

+.section-search-container {

+  padding: 10px 20px;

+  margin-bottom: 20px;

+}

+

+.section-search-input {

+  width: 100%;

+  padding: 8px 15px;

+  border: 1px solid #ddd;

+  border-radius: 20px;

+  font-size: 14px;

+  outline: none;

+}

+

+.section-search-input:focus {

+  border-color: #1890ff;

+}

+

+.no-results {

+  text-align: center;

+  padding: 20px;

+  color: #888;

+  font-size: 16px;

+}

+

+.reset-button {

+  padding: 8px 15px;

+  background-color: #f0f0f0;

+  border: 1px solid #ddd;

+  border-radius: 4px;

+  cursor: pointer;

+  transition: background-color 0.3s;

+}

+

+.reset-button:hover {

+  background-color: #e0e0e0;

 }
\ No newline at end of file
diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx
index 836daa7..f8f5dd3 100644
--- a/src/components/Dashboard.jsx
+++ b/src/components/Dashboard.jsx
@@ -1,9 +1,10 @@
 import React, {useEffect, useState} from 'react';

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

-// import { getUserInfo } from '../api/auth';  

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

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

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

 import './Dashboard.css';

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

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

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

+import { api } from '../api/auth'; 

 

 

 const Dashboard = ({onLogout}) => {

@@ -45,6 +46,12 @@
     const [filteredResources, setFilteredResources] = useState(torrentPosts);

     const [isAdmin, setIsAdmin] = useState(false);

 

+    // 新增搜索状态

+    const [announcementSearch, setAnnouncementSearch] = useState('');

+    const [shareSearch, setShareSearch] = useState('');

+    const [requestSearch, setRequestSearch] = useState('');

+    const [helpSearch, setHelpSearch] = useState('');

+

 

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

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

@@ -118,6 +125,90 @@
         // 其他公告...

     ]);

 

+    // 公告区搜索处理

+    const handleSearchAnnouncement = (e) => {

+        setAnnouncementSearch(e.target.value);

+    };

+

+    // 修改后的搜索函数

+    const handleSearchShare = async () => {

+        try {

+            setTorrentLoading(true);

+            const response = await searchTorrents(shareSearch, 1);

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

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

+                const total = response.data.data.total;

+                setTotalPages(Math.ceil(total / 5));

+                setCurrentPage(1);

+            } else {

+                setTorrentError(response.data.message || '搜索失败');

+            }

+        } catch (err) {

+            setTorrentError(err.message || '搜索失败');

+        } finally {

+            setTorrentLoading(false);

+        }

+    };

+

+    const handleResetShareSearch = async () => {

+        setShareSearch('');

+        setSelectedFilters(

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

+                acc[category] = 'all';

+                return acc;

+            }, {})

+        );

+        await fetchTorrentPosts(1, true);

+    };

+

+    // 求种区搜索处理

+    const handleSearchRequest = (e) => {

+        setRequestSearch(e.target.value);

+    };

+

+    // 添加搜索函数

+    const handleSearchHelp = async () => {

+        try {

+        setHelpLoading(true);

+        const response = await searchPosts(helpSearch, currentPage);

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

+            const postsWithCounts = await Promise.all(

+            response.data.data.records.map(async (post) => {

+                try {

+                const detailResponse = await getPostDetail(post.id);

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

+                    return {

+                    ...post,

+                    replyCount: detailResponse.data.data.post.replyCount || 0,

+                    isLiked: false

+                    };

+                }

+                return post;

+                } catch (err) {

+                console.error(`获取帖子${post.id}详情失败:`, err);

+                return post;

+                }

+            })

+            );

+            setHelpPosts(postsWithCounts);

+            setTotalPages(Math.ceil(response.data.data.total / 5));

+        } else {

+            setHelpError(response.data.message || '搜索失败');

+        }

+        } catch (err) {

+        setHelpError(err.message || '搜索失败');

+        } finally {

+        setHelpLoading(false);

+        }

+    };

+    

+    // 添加重置搜索函数

+    const handleResetHelpSearch = async () => {

+        setHelpSearch('');

+        await fetchHelpPosts(1); // 重置到第一页

+    };

+

+

 

     const handleAnnouncementClick = (announcement, e) => {

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

@@ -205,20 +296,41 @@
         }

       };

 

-    // 获取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);

+

+const fetchTorrentPosts = async (page = 1, isReset = false) => {

+    setTorrentLoading(true);

+    try {

+        const params = {

+            page,

+            size: 5

+        };

+        

+        // 如果有筛选条件且不是重置操作

+        if (!isReset && Object.values(selectedFilters).some(v => v !== 'all')) {

+            if (selectedFilters.type !== 'all') params.category = selectedFilters.type;

+            if (selectedFilters.subtitle !== 'all') params.subtitle = selectedFilters.subtitle;

+            if (selectedFilters.region !== 'all') params.region = selectedFilters.region;

+            if (selectedFilters.resolution !== 'all') params.resolution = selectedFilters.resolution;

         }

-    };

+        

+        const response = (shareSearch && !isReset) 

+            ? await searchTorrents(shareSearch, page)

+            : await api.get('http://localhost:8088/torrent', { params });

+            

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

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

+            const total = response.data.data.total;

+            setTotalPages(Math.ceil(total / 5));

+            setCurrentPage(page);

+        } else {

+            setTorrentError(response.data.message);

+        }

+    } catch (err) {

+        setTorrentError(err.message);

+    } finally {

+        setTorrentLoading(false);

+    }

+};

 

     // 在useEffect中调用

     useEffect(() => {

@@ -276,7 +388,7 @@
     }, [activeTab, currentPage]); // 添加 currentPage 作为依赖

 

 

-    // 分类维度配置

+        // 分类维度配置

     const filterCategories = {

         type: {

             label: '类型',

@@ -285,28 +397,50 @@
                 '电影': '电影',

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

                 '动漫': '动漫',

-                '综艺': '综艺'

+                '综艺': '综艺',

+                '音乐': '音乐',

+                '其他': '其他'

             }

         },

         subtitle: {

             label: '字幕',

             options: {

                 'all': '全部',

-                'yes': '有字幕',

-                'no': '无字幕'

+                '无需字幕': '无需字幕',

+                '暂无字幕': '暂无字幕',

+                '自带中文字幕': '自带中文字幕',

+                '自带双语字幕(含中文)': '自带双语字幕(含中文)',

+                '附件中文字幕': '附件中文字幕',

+                '附件双语字幕': '附件双语字幕'

             }

         },

         region: {

             label: '地区',

             options: {

                 'all': '全部',

-                'cn': '大陆',

-                'us': '欧美',

-                'jp': '日本'

+                '中国': '中国',

+                '英国': '英国',

+                '美国': '美国',

+                '日本': '日本',

+                '韩国': '韩国',

+                '其他': '其他'

+            }

+        },

+        resolution: {

+            label: '分辨率',

+            options: {

+                'all': '全部',

+                '4K': '4K',

+                '2K': '2K',

+                '1080P': '1080P',

+                '720P': '720P',

+                'SD': 'SD',

+                '无损音源': '无损音源',

+                '杜比全景声': '杜比全景声',

+                '其他': '其他'

             }

         }

     };

-

     const [selectedFilters, setSelectedFilters] = useState(

         location.state?.savedFilters ||

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

@@ -315,27 +449,54 @@
         }, {})

     );

 

-

-// 处理筛选条件变更

+    // 处理筛选条件变更

     const handleFilterSelect = (category, value) => {

         setSelectedFilters(prev => ({

             ...prev,

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

+            [category]: prev[category] === value ? 'all' : 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);

+    // 应用筛选条件

+    const applyFilters = async () => {

+        try {

+            setTorrentLoading(true);

+            

+            // 构建查询参数

+            const params = {

+                page: 1, // 从第一页开始

+                size: 5 

+            };

+            

+            // 添加筛选条件

+            if (selectedFilters.type !== 'all') {

+                params.category = selectedFilters.type;

+            }

+            if (selectedFilters.subtitle !== 'all') {

+                params.subtitle = selectedFilters.subtitle;

+            }

+            if (selectedFilters.region !== 'all') {

+                params.region = selectedFilters.region;

+            }

+            if (selectedFilters.resolution !== 'all') {

+                params.resolution = selectedFilters.resolution;

+            }

+            

+            // 调用API获取筛选结果

+            const response = await api.get('http://localhost:8088/torrent', { params });

+            

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

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

+                setTotalPages(Math.ceil(response.data.data.total / 5));

+                setCurrentPage(1);

+            } else {

+                setTorrentError(response.data.message || '筛选失败');

+            }

+        } catch (err) {

+            setTorrentError(err.message || '筛选失败');

+        } finally {

+            setTorrentLoading(false);

+        }

     };

 

 

@@ -346,43 +507,37 @@
         }

     }, [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') || '演示用户', // 确保这里读取的是最新值

+    // 在Dashboard.jsx中修改useEffect

+useEffect(() => {

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

+    if (!token) {

+        navigate('/login');

+        return;

+    }

+    

+    const fetchUserInfo = async () => {

+        try {

+          setLoading(true);

+          const backendData = await getUserInfo(); // 调用修改后的方法

+          console.log('后端返回的用户数据:', backendData); // 调试用

+          

+          setUserInfo({

+            name: backendData.username || '演示用户',

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

-            isAdmin: true

-        });

-        setLoading(false);

-    }, [navigate]);

+            isAdmin: backendData.authority === 'ADMIN' // 检查 authority 是否为 "ADMIN"

+          });

+        } catch (error) {

+          console.error('获取用户信息失败:', error);

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

+        } finally {

+          setLoading(false);

+        }

+      };

+    

+    fetchUserInfo();

+}, [navigate]);

 

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

+

     useEffect(() => {

         if (activeTab === 'announcement') {

             const timer = setInterval(() => {

@@ -403,6 +558,22 @@
             case 'announcement':

                 return (

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

+                        <div className="section-search-container">

+                            <input

+                                type="text"

+                                placeholder="搜索公告..."

+                                value={announcementSearch}

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

+                                className="section-search-input"

+                                onKeyPress={(e) => e.key === 'Enter' && handleSearchAnnouncement()}

+                            />

+                            <button 

+                                className="search-button"

+                                onClick={handleSearchAnnouncement}

+                            >

+                                搜索

+                            </button>

+                        </div>

                         {/* 轮播图区域 */}

                         <div className="carousel-container">

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

@@ -443,6 +614,31 @@
             case 'share':

                 return (

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

+                        {/* 分享区搜索框 */}

+                        <div className="section-search-container">

+                            <input

+                                type="text"

+                                placeholder="搜索资源..."

+                                value={shareSearch}

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

+                                className="section-search-input"

+                                onKeyPress={(e) => e.key === 'Enter' && handleSearchShare()}

+                            />

+                            <button 

+                                className="search-button"

+                                onClick={handleSearchShare}

+                            >

+                                搜索

+                            </button>

+                            <button 

+                                className="reset-button"

+                                onClick={handleResetShareSearch}  // 使用新的重置函数

+                                style={{marginLeft: '10px'}}

+                            >

+                                重置

+                            </button>

+                        </div>

+

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

                         <div className="upload-header">

                             <button

@@ -517,19 +713,26 @@
                                                 <option value="动漫">动漫</option>

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

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

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

                                             </select>

                                         </div>

 

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

+                                        {/* 修改后的地区下拉框 */}

                                         <div className="form-group">

                                             <label>地区</label>

-                                            <input

-                                                type="text"

+                                            <select

                                                 value={uploadData.region || ''}

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

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

                                                 required

-                                            />

+                                            >

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

+                                                <option value="中国">中国</option>

+                                                <option value="英国">英国</option>

+                                                <option value="美国">美国</option>

+                                                <option value="日本">日本</option>

+                                                <option value="韩国">韩国</option>

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

+                                            </select>

                                         </div>

 

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

@@ -637,7 +840,7 @@
                                         <p className="resource-subtitle">字幕: {torrent.subtitle}</p>

                                     </div>

                                     <div className="resource-stats">

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

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

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

                                     </div>

                                     <button

@@ -653,7 +856,7 @@
                             ))}

                         </div>

 

-                        {/* 分页控件 */}

+                    {totalPages > 1 && (

                         <div className="pagination">

                             <button

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

@@ -679,12 +882,30 @@
                                 下一页

                             </button>

                         </div>

+                        )}

                     </div>

                 );

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

             case 'request':

                 return (

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

+                        {/* 求种区搜索框 */}

+                        <div className="section-search-container">

+                            <input

+                                type="text"

+                                placeholder="搜索求种..."

+                                value={requestSearch}

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

+                                className="section-search-input"

+                                onKeyPress={(e) => e.key === 'Enter' && handleSearchRequest()}

+                            />

+                            <button 

+                                className="search-button"

+                                onClick={handleSearchRequest}

+                            >

+                                搜索

+                            </button>

+                        </div>

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

                         <div className="request-list">

                             {[

@@ -734,6 +955,31 @@
             case 'help':

                 return (

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

+                        {/* 求助区搜索框 */}

+                        <div className="section-search-container">

+                            <input

+                                type="text"

+                                placeholder="搜索求助..."

+                                value={helpSearch}

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

+                                className="section-search-input"

+                                onKeyPress={(e) => e.key === 'Enter' && handleSearchHelp()}

+                            />

+                            <button 

+                                className="search-button"

+                                onClick={handleSearchHelp}

+                            >

+                                搜索

+                            </button>

+                            <button 

+                                className="reset-button"

+                                onClick={handleResetHelpSearch}

+                                style={{marginLeft: '10px'}}

+                            >

+                                重置

+                            </button>

+                        </div>

+

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

                         <div className="post-header">

                             <button

@@ -891,14 +1137,9 @@
         <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 className="platform-name">

+                    <h2>PT资源站</h2>

                 </div>

 

                 <div className="user-actions">

diff --git a/src/components/Personal/Exchange.jsx b/src/components/Personal/Exchange.jsx
new file mode 100644
index 0000000..1e5b2e8
--- /dev/null
+++ b/src/components/Personal/Exchange.jsx
@@ -0,0 +1,175 @@
+import React, { useState, useEffect } from 'react';

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

+import { generateInviteCode, getUserInviteCodes, exchangeUpload, getUserInfo } from '../../api/personal';

+import './personalSubpage.css';

+

+const Exchange = () => {

+  const navigate = useNavigate();

+  const location = useLocation();

+  const [inviteCodes, setInviteCodes] = useState([]);

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

+  const [magicPoints, setMagicPoints] = useState('0'); 

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

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

+

+  // 获取用户信息和邀请码列表

+  useEffect(() => {

+    const fetchData = async () => {

+      try {

+        setLoading(true);

+        const [userData, codes] = await Promise.all([

+          getUserInfo(),

+          getUserInviteCodes()

+        ]);

+        setUserInfo(userData);

+        setInviteCodes(codes);

+      } catch (err) {

+        setError(err.message);

+      } finally {

+        setLoading(false);

+      }

+    };

+    fetchData();

+  }, []);

+

+  // 生成邀请码

+  const handleGenerateInviteCode = async () => {

+    try {

+      setLoading(true);

+      const newCode = await generateInviteCode();

+      setInviteCodes([...inviteCodes, newCode]);

+      // 刷新用户信息

+      const updatedUser = await getUserInfo();

+      setUserInfo(updatedUser);

+    } catch (err) {

+      setError(err.message);

+    } finally {

+      setLoading(false);

+    }

+  };

+

+  const handleExchangeUpload = async () => {

+    const points = Number(magicPoints);

+    if (!points || points <= 0) {

+      setError('请输入有效的魔力值');

+      return;

+    }

+    try {

+      setLoading(true);

+      await exchangeUpload(points);

+      // 刷新用户信息

+      const updatedUser = await getUserInfo();

+      setUserInfo(updatedUser);

+      setMagicPoints('0');

+      setError(null);

+    } catch (err) {

+      setError(err.message);

+    } finally {

+      setLoading(false);

+    }

+  };

+

+  const handleBack = () => {

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

+    navigate('/personal', { 

+      state: { 

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

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

+      },

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

+    });

+  };

+

+  if (loading) {

+    return <div className="subpage-container">加载中...</div>;

+  }

+

+  if (error) {

+    return (

+      <div className="subpage-container">

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

+          ← 返回个人中心

+        </button>

+        <div className="error">错误: {error}</div>

+      </div>

+    );

+  }

+

+  return (

+    <div className="subpage-container">

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

+        ← 返回个人中心

+      </button>

+

+      <h2 className="page-title">兑换区</h2>

+      

+      <div className="exchange-section">

+        <h3>当前魔力值: {userInfo?.magicPoints || 0}</h3>

+        

+        <div className="exchange-card">

+          <h4>兑换邀请码</h4>

+          <p>消耗10魔力值兑换一个邀请码</p>

+          <button 

+            className="exchange-btn"

+            onClick={handleGenerateInviteCode}

+            disabled={!userInfo || userInfo.magicPoints < 10}

+          >

+            兑换邀请码

+          </button>

+        </div>

+

+        <div className="exchange-card">

+          <h4>兑换上传量</h4>

+          <p>1魔力值 = 1GB上传量</p>

+          <div className="exchange-input-group">

+          <input

+            type="number"

+            value={magicPoints}

+            onChange={(e) => {

+              // 允许空字符串或有效数字

+              const value = e.target.value;

+              if (value === '' || !isNaN(value)) {

+                setMagicPoints(value);

+              }

+            }}

+            min="1"

+            max={userInfo?.magicPoints || 0}

+            placeholder="输入要兑换的魔力值"

+          />

+

+          <button 

+            className="exchange-btn"

+            onClick={handleExchangeUpload}

+            disabled={

+              !magicPoints || 

+              Number(magicPoints) <= 0 || 

+              !userInfo || 

+              Number(magicPoints) > userInfo.magicPoints

+            }

+          >

+            兑换上传量

+          </button>

+          </div>

+        </div>

+

+        {inviteCodes.length > 0 && (

+          <div className="invite-code-list">

+            <h4>我的邀请码</h4>

+            <ul>

+              {inviteCodes.map((code, index) => (

+                <li key={index}>

+                  <span className="code">{code.code}</span>

+                  <span className={`status ${code.isUsed ? 'used' : 'available'}`}>

+                    {code.isUsed ? '已使用' : '可用'}

+                  </span>

+                </li>

+              ))}

+            </ul>

+          </div>

+        )}

+      </div>

+    </div>

+  );

+};

+

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

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

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

+import Exchange from './Exchange';

+import { 

+  generateInviteCode, 

+  getUserInviteCodes, 

+  exchangeUpload, 

+  getUserInfo 

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

+

+// Mock API 调用

+jest.mock('../../api/personal', () => ({

+  generateInviteCode: jest.fn(),

+  getUserInviteCodes: jest.fn(),

+  exchangeUpload: jest.fn(),

+  getUserInfo: jest.fn()

+}));

+

+// Mock react-router-dom hooks

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

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

+  useNavigate: jest.fn(),

+  useLocation: jest.fn()

+}));

+

+describe('Exchange Component', () => {

+  const mockNavigate = jest.fn();

+  const mockLocation = {

+    pathname: '/personal/exchange',

+    state: { dashboardTab: 'exchange' }

+  };

+

+  const mockUserInfo = {

+    magicPoints: 100,

+    username: 'testuser'

+  };

+

+  const mockInviteCodes = [

+    { code: 'ABCD-1234', isUsed: false },

+    { code: 'EFGH-5678', isUsed: true }

+  ];

+

+  beforeEach(() => {

+    useNavigate.mockReturnValue(mockNavigate);

+    useLocation.mockReturnValue(mockLocation);

+    jest.clearAllMocks();

+    

+    // 设置默认 mock 返回值

+    getUserInfo.mockResolvedValue(mockUserInfo);

+    getUserInviteCodes.mockResolvedValue(mockInviteCodes);

+    generateInviteCode.mockResolvedValue({ code: 'NEW-CODE', isUsed: false });

+    exchangeUpload.mockResolvedValue({ success: true });

+  });

+

+  it('应该正确加载并显示用户信息和邀请码', async () => {

+    render(

+      <MemoryRouter>

+        <Exchange />

+      </MemoryRouter>

+    );

+

+    // 初始加载状态

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

+

+    // 等待数据加载完成

+    await waitFor(() => {

+      expect(screen.getByText('兑换区')).toBeInTheDocument();

+      expect(screen.getByText('当前魔力值: 100')).toBeInTheDocument();

+      expect(screen.getByText('ABCD-1234')).toBeInTheDocument();

+      expect(screen.getByText('EFGH-5678')).toBeInTheDocument();

+      expect(screen.getByText('可用')).toBeInTheDocument();

+      expect(screen.getByText('已使用')).toBeInTheDocument();

+    });

+  });

+

+  it('应该处理生成邀请码操作', async () => {

+    render(

+      <MemoryRouter>

+        <Exchange />

+      </MemoryRouter>

+    );

+

+    await waitFor(() => {

+      // 使用更精确的选择器定位按钮

+      const generateButtons = screen.getAllByRole('button', { name: '兑换邀请码' });

+      // 选择第一个按钮(或根据实际情况选择正确的按钮)

+      fireEvent.click(generateButtons[0]);

+    });

+

+    expect(generateInviteCode).toHaveBeenCalled();

+    await waitFor(() => {

+      expect(getUserInfo).toHaveBeenCalledTimes(2); // 初始加载 + 生成后刷新

+    });

+  });

+

+  it('应该处理兑换上传量操作', async () => {

+    render(

+      <MemoryRouter>

+        <Exchange />

+      </MemoryRouter>

+    );

+

+    await waitFor(() => {

+      const input = screen.getByPlaceholderText('输入要兑换的魔力值');

+      const exchangeButton = screen.getByRole('button', { name: '兑换上传量' });

+      

+      // 输入有效值

+      fireEvent.change(input, { target: { value: '50' } });

+      fireEvent.click(exchangeButton);

+    });

+

+    expect(exchangeUpload).toHaveBeenCalledWith(50);

+    await waitFor(() => {

+      expect(getUserInfo).toHaveBeenCalledTimes(2); // 初始加载 + 兑换后刷新

+    });

+  });

+  

+

+  it('应该处理返回按钮点击', async () => {

+    render(

+      <MemoryRouter>

+        <Exchange />

+      </MemoryRouter>

+    );

+

+    await waitFor(() => {

+      const backButton = screen.getByText(/← 返回个人中心/);

+      fireEvent.click(backButton);

+      

+      expect(mockNavigate).toHaveBeenCalledWith('/personal', {

+        state: {

+          fromSubpage: true,

+          dashboardTab: 'exchange'

+        },

+        replace: true

+      });

+    });

+  });

+

+  it('应该显示错误信息当API调用失败', async () => {

+    getUserInfo.mockRejectedValueOnce(new Error('获取用户信息失败'));

+

+    render(

+      <MemoryRouter>

+        <Exchange />

+      </MemoryRouter>

+    );

+

+    await waitFor(() => {

+      expect(screen.getByText('错误: 获取用户信息失败')).toBeInTheDocument();

+    });

+  });

+

+  it('应该禁用兑换按钮当魔力值不足', async () => {

+    getUserInfo.mockResolvedValueOnce({ magicPoints: 5 }); // 设置魔力值不足

+

+    render(

+      <MemoryRouter>

+        <Exchange />

+      </MemoryRouter>

+    );

+

+    await waitFor(() => {

+      const inviteButtons = screen.getAllByRole('button', { name: '兑换邀请码' });

+      expect(inviteButtons[0]).toBeDisabled();

+    });

+  });

+

+  it('应该正确处理空邀请码列表', async () => {

+    getUserInviteCodes.mockResolvedValueOnce([]);

+

+    render(

+      <MemoryRouter>

+        <Exchange />

+      </MemoryRouter>

+    );

+

+    await waitFor(() => {

+      expect(screen.queryByText('我的邀请码')).not.toBeInTheDocument();

+    });

+  });

+

+  it('应该显示加载状态', async () => {

+    // 延迟API响应以测试加载状态

+    getUserInfo.mockImplementation(() => new Promise(() => {}));

+

+    render(

+      <MemoryRouter>

+        <Exchange />

+      </MemoryRouter>

+    );

+

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

+  });

+});
\ No newline at end of file
diff --git a/src/components/Personal/Favorite.jsx b/src/components/Personal/Favorite.jsx
deleted file mode 100644
index 97aa1e6..0000000
--- a/src/components/Personal/Favorite.jsx
+++ /dev/null
@@ -1,47 +0,0 @@
-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/Personal.css b/src/components/Personal/Personal.css
index c087ac6..28a1adb 100644
--- a/src/components/Personal/Personal.css
+++ b/src/components/Personal/Personal.css
@@ -159,4 +159,63 @@
     margin-top: 20px;

     border-top: 1px solid #f0f0f0;

     padding-top: 20px;

-  }
\ No newline at end of file
+  }

+

+

+  /* Personal.css */

+/* ... 其他已有样式 ... */

+

+/* 下载进度卡片样式 */

+.progress-card {

+  background: #fff;

+  border-radius: 8px;

+  padding: 20px;

+  margin-bottom: 20px;

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

+}

+

+.download-task {

+  margin-bottom: 15px;

+}

+

+.task-info {

+  display: flex;

+  justify-content: space-between;

+  margin-bottom: 5px;

+}

+

+.task-id {

+  font-size: 14px;

+  color: #666;

+}

+

+.task-progress {

+  font-size: 14px;

+  font-weight: bold;

+  color: #1890ff;

+}

+

+.progress-bar {

+  height: 8px;

+  background: #f0f0f0;

+  border-radius: 4px;

+  overflow: hidden;

+}

+

+.progress-fill {

+  height: 100%;

+  background: #1890ff;

+  border-radius: 4px;

+  transition: width 0.3s ease;

+}

+

+

+.user-meta span {

+  margin-right: 15px;

+  color: #666;

+}

+

+.user-meta span:last-child {

+  color: #ff9800;

+  font-weight: bold;

+}

diff --git a/src/components/Personal/Personal.jsx b/src/components/Personal/Personal.jsx
index 033952d..8c60baf 100644
--- a/src/components/Personal/Personal.jsx
+++ b/src/components/Personal/Personal.jsx
@@ -1,34 +1,91 @@
-import React from 'react';

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

+// Personal.jsx

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

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

+import { getUserInfo, getDownloadQuota, getDownloadProgress } from '../../api/personal';

 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 location = useLocation();

+  const [userData, setUserData] = useState(null);

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

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

+  const [downloadProgress, setDownloadProgress] = useState({});

+

+  useEffect(() => {

+    const fetchData = async () => {

+      try {

+        // 并行获取用户信息和下载额度

+        const [userInfo, downloadQuota] = await Promise.all([

+          getUserInfo(),

+          getDownloadQuota()

+        ]);

+        

+        setUserData({

+          username: userInfo.username,

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

+          joinDate: userInfo.registTime,

+          level: userInfo.level, // 可以根据userInfo.level设置不同等级

+          points: userInfo.magicPoints,

+          upload: userInfo.upload,

+          download: userInfo.download,

+          ratio: userInfo.shareRate,

+          downloadQuota: {

+            total: downloadQuota.total,

+            used: downloadQuota.used,

+            remaining: downloadQuota.remaining

+          }

+        });

+      } catch (err) {

+        setError(err.message);

+      } finally {

+        setLoading(false);

+      }

+    };

+

+    fetchData();

+  }, []);

+

+  // 获取下载进度数据

+  const fetchDownloadProgress = async () => {

+    try {

+      const progressData = await getDownloadProgress();

+      setDownloadProgress(progressData);

+    } catch (err) {

+      console.error('获取下载进度失败:', err);

     }

   };

 

+  useEffect(() => {

+    // 初始获取下载进度

+    fetchDownloadProgress();

+    

+    // 设置定时器,每10秒获取一次下载进度

+    const intervalId = setInterval(fetchDownloadProgress, 10000);

+    

+    // 组件卸载时清除定时器

+    return () => clearInterval(intervalId);

+  }, []);

+

+

+  if (loading) {

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

+  }

+

+  if (error) {

+    return <div className="error">错误: {error}</div>;

+  }

+

+  if (!userData) {

+    return <div className="error">未获取到用户数据</div>;

+  }

+

   const features = [

     { 

-      title: '我的收藏', 

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

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

+      title: '兑换区', 

+      description: '下载量/邀请码',

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

     },

     { 

       title: '上传记录', 

@@ -63,6 +120,23 @@
     }

   };

 

+  const formatSize = (bytes) => {

+    if (bytes < 1024) return `${bytes} B`;

+    const kb = bytes / 1024;

+    if (kb < 1024) return `${kb.toFixed(2)} KB`;

+    const mb = kb / 1024;

+    if (mb < 1024) return `${mb.toFixed(2)} MB`;

+    const gb = mb / 1024;

+    return `${gb.toFixed(2)} GB`;

+  };

+

+  // 添加进度条颜色函数

+  const getProgressColor = (percentage) => {

+    if (percentage < 0.3) return '#4CAF50'; // 绿色

+    if (percentage < 0.7) return '#FFC107'; // 黄色

+    return '#F44336'; // 红色

+  };

+

   return (

     <div className="personal-container">

       {/* 返回按钮 */}

@@ -82,7 +156,7 @@
             <h2 className="username">{userData.username}</h2>

             <div className="user-meta">

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

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

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

             </div>

           </div>

         </div>

@@ -90,16 +164,16 @@
         {/* 用户数据统计 */}

         <div className="stats-grid">

           <div className="stat-item">

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

+            <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 className="stat-value">{formatSize(userData.upload)}</div>

           </div>

           <div className="stat-item">

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

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

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

           </div>

           <div className="stat-item">

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

@@ -112,17 +186,46 @@
       <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>

+          <span className="quota-used">

+            {formatSize(userData.downloadQuota.used)} 已使用

+          </span>

+          <span className="quota-remaining">

+            {formatSize(userData.downloadQuota.remaining)} 剩余

+          </span>

         </div>

         <div className="progress-bar">

           <div 

             className="progress-fill"

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

+            style={{ 

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

+              backgroundColor: getProgressColor(userData.downloadQuota.used / userData.downloadQuota.total)

+            }}

           ></div>

         </div>

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

+        <div className="quota-total">

+          总额度: {formatSize(userData.downloadQuota.total)}

+        </div>

       </div>

+

+      {Object.keys(downloadProgress).length > 0 && (

+        <div className="progress-card">

+          <h3>当前下载进度</h3>

+          {Object.entries(downloadProgress).map(([taskId, progress]) => (

+            <div key={taskId} className="download-task">

+              <div className="task-info">

+                <span className="task-id">任务: {taskId.substring(0, 8)}...</span>

+                <span className="task-progress">{Math.round(progress * 100)}%</span>

+              </div>

+              <div className="progress-bar">

+                <div 

+                  className="progress-fill"

+                  style={{ width: `${progress * 100}%` }}

+                ></div>

+              </div>

+            </div>

+          ))}

+        </div>

+      )}

       

       {/* 功能卡片区 */}

       <div className="action-cards">

diff --git a/src/components/Personal/Personal.test.jsx b/src/components/Personal/Personal.test.jsx
new file mode 100644
index 0000000..ef6bad8
--- /dev/null
+++ b/src/components/Personal/Personal.test.jsx
@@ -0,0 +1,209 @@
+// Personal.test.jsx

+import React from 'react';

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

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

+import Personal from './Personal';

+import { getUserInfo, getDownloadQuota, getDownloadProgress } from '../../api/personal';

+

+// Mock API 调用

+jest.mock('../../api/personal', () => ({

+  getUserInfo: jest.fn(),

+  getDownloadQuota: jest.fn(),

+  getDownloadProgress: jest.fn()

+}));

+

+// Mock react-router-dom hooks

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

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

+  useNavigate: jest.fn(),

+  useLocation: jest.fn()

+}));

+

+describe('Personal Component', () => {

+  const mockNavigate = jest.fn();

+  const mockLocation = {

+    pathname: '/personal',

+    state: null

+  };

+

+  beforeEach(() => {

+    useNavigate.mockReturnValue(mockNavigate);

+    useLocation.mockReturnValue(mockLocation);

+    

+    // 重置所有 mock

+    jest.clearAllMocks();

+    

+    // 设置默认 mock 返回值

+    getUserInfo.mockResolvedValue({

+      username: 'testuser',

+      registTime: '2023-01-01',

+      level: 2,

+      magicPoints: 1000,

+      upload: 1024 * 1024 * 5, // 5MB

+      download: 1024 * 1024 * 2, // 2MB

+      shareRate: 2.5

+    });

+    

+    getDownloadQuota.mockResolvedValue({

+      total: 1024 * 1024 * 10, // 10MB

+      used: 1024 * 1024 * 3,   // 3MB

+      remaining: 1024 * 1024 * 7 // 7MB

+    });

+    

+    getDownloadProgress.mockResolvedValue({

+      'task1': 0.25,

+      'task2': 0.75

+    });

+  });

+

+  it('应该正确加载并显示用户数据', async () => {

+    render(

+      <MemoryRouter>

+        <Personal />

+      </MemoryRouter>

+    );

+

+    // 初始加载状态

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

+

+    // 等待数据加载完成

+    await waitFor(() => {

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

+      expect(screen.getByText(/加入时间: 2023-01-01/)).toBeInTheDocument();

+      expect(screen.getByText(/会员等级: Lv.2/)).toBeInTheDocument();

+      expect(screen.getByText('1000')).toBeInTheDocument(); // 保种积分

+      expect(screen.getByText('5.00 MB')).toBeInTheDocument(); // 上传量

+      expect(screen.getByText('2.00 MB')).toBeInTheDocument(); // 下载量

+      expect(screen.getByText('2.5')).toBeInTheDocument(); // 分享率

+    });

+  });

+

+  it('应该显示下载额度信息', async () => {

+    render(

+      <MemoryRouter>

+        <Personal />

+      </MemoryRouter>

+    );

+

+    await waitFor(() => {

+      expect(screen.getByText(/3.00 MB 已使用/)).toBeInTheDocument();

+      expect(screen.getByText(/7.00 MB 剩余/)).toBeInTheDocument();

+      expect(screen.getByText(/总额度: 10.00 MB/)).toBeInTheDocument();

+    });

+  });

+

+  it('应该显示下载进度', async () => {

+    render(

+      <MemoryRouter>

+        <Personal />

+      </MemoryRouter>

+    );

+

+    await waitFor(() => {

+      expect(screen.getByText('当前下载进度')).toBeInTheDocument();

+      expect(screen.getByText(/任务: task1/)).toBeInTheDocument();

+      expect(screen.getByText('25%')).toBeInTheDocument();

+      expect(screen.getByText(/任务: task2/)).toBeInTheDocument();

+      expect(screen.getByText('75%')).toBeInTheDocument();

+    });

+  });

+

+  it('应该显示功能卡片并处理点击', async () => {

+    render(

+      <MemoryRouter>

+        <Personal />

+      </MemoryRouter>

+    );

+

+    await waitFor(() => {

+      const exchangeCard = screen.getByText('兑换区');

+      expect(exchangeCard).toBeInTheDocument();

+      

+      // 模拟点击功能卡片

+      act(() => {

+        exchangeCard.closest('.action-card').click();

+      });

+      

+      expect(mockNavigate).toHaveBeenCalledWith('/personal/Exchange');

+    });

+  });

+

+  it('应该处理返回按钮点击', async () => {

+    render(

+      <MemoryRouter>

+        <Personal />

+      </MemoryRouter>

+    );

+

+    await waitFor(() => {

+      const backButton = screen.getByText(/返回/);

+      act(() => {

+        backButton.click();

+      });

+      

+      expect(mockNavigate).toHaveBeenCalledWith(-1);

+    });

+  });

+

+  it('应该处理从子页面返回的情况', async () => {

+    useLocation.mockReturnValue({

+      pathname: '/personal',

+      state: { fromSubpage: true, dashboardTab: 'uploads' }

+    });

+

+    render(

+      <MemoryRouter>

+        <Personal />

+      </MemoryRouter>

+    );

+

+    await waitFor(() => {

+      const backButton = screen.getByText(/返回/);

+      act(() => {

+        backButton.click();

+      });

+      

+      expect(mockNavigate).toHaveBeenCalledWith('/dashboard/uploads', { replace: true });

+    });

+  });

+

+  it('应该显示错误信息当API调用失败', async () => {

+    getUserInfo.mockRejectedValue(new Error('获取用户信息失败'));

+

+    render(

+      <MemoryRouter>

+        <Personal />

+      </MemoryRouter>

+    );

+

+    await waitFor(() => {

+      expect(screen.getByText(/错误: 获取用户信息失败/)).toBeInTheDocument();

+    });

+  });

+

+

+  it('应该定期更新下载进度', async () => {

+    jest.useFakeTimers();

+    

+    render(

+      <MemoryRouter>

+        <Personal />

+      </MemoryRouter>

+    );

+

+    await waitFor(() => {

+      expect(getDownloadProgress).toHaveBeenCalledTimes(1);

+    });

+

+    // 快进时间

+    act(() => {

+      jest.advanceTimersByTime(10000);

+    });

+

+    await waitFor(() => {

+      expect(getDownloadProgress).toHaveBeenCalledTimes(2);

+    });

+

+    jest.useRealTimers();

+  });

+});
\ No newline at end of file
diff --git a/src/components/Personal/Setting.jsx b/src/components/Personal/Setting.jsx
index 967be6c..9ba07e8 100644
--- a/src/components/Personal/Setting.jsx
+++ b/src/components/Personal/Setting.jsx
@@ -1,97 +1,161 @@
-import React, { useState } from 'react';

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

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

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

+import { getUserInfo, updatePassword } from '../../api/personal';

 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 [userInfo, setUserInfo] = useState(null);

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

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

+  const [success, setSuccess] = useState(null);

+  const [passwordForm, setPasswordForm] = useState({

+    oldPassword: '',

+    newPassword: '',

+    confirmPassword: ''

   });

 

-  const handleChange = (e) => {

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

-    setFormData(prev => ({

+  // 获取用户信息

+  useEffect(() => {

+    const fetchUserInfo = async () => {

+      try {

+        const info = await getUserInfo();

+        setUserInfo(info);

+      } catch (err) {

+        console.error('获取用户信息失败:', err);

+      }

+    };

+    fetchUserInfo();

+  }, []);

+

+  const handleBack = () => {

+    navigate('/personal', { 

+      state: { 

+        fromSubpage: true,

+        dashboardTab: location.state?.dashboardTab

+      },

+      replace: true

+    });

+  };

+

+  const handlePasswordChange = (e) => {

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

+    setPasswordForm(prev => ({

       ...prev,

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

+      [name]: value

     }));

   };

 

-  const handleSubmit = (e) => {

+  const handlePasswordSubmit = async (e) => {

     e.preventDefault();

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

-  };

+    setError(null);

+    setSuccess(null);

 

-  const handleBack = () => {

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

-    navigate('/personal', { 

-      state: { 

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

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

-      },

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

-    });

+    // 验证表单

+    if (!passwordForm.oldPassword || !passwordForm.newPassword || !passwordForm.confirmPassword) {

+      setError('请填写所有密码字段');

+      return;

+    }

+

+    if (passwordForm.newPassword !== passwordForm.confirmPassword) {

+      setError('新密码与确认密码不一致');

+      return;

+    }

+

+    if (passwordForm.newPassword.length < 6) {

+      setError('新密码长度至少为6位');

+      return;

+    }

+

+    try {

+      setLoading(true);

+      await updatePassword(passwordForm.oldPassword, passwordForm.newPassword);

+      setSuccess('密码修改成功');

+      setPasswordForm({

+        oldPassword: '',

+        newPassword: '',

+        confirmPassword: ''

+      });

+    } catch (err) {

+      setError(err.message || '修改密码失败');

+    } finally {

+      setLoading(false);

+    }

   };

 

   return (

     <div className="subpage-container">

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

+      <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"

-          />

+      <h2 className="page-title">个人设置</h2>

+

+      <div className="setting-section">

+        <div className="user-info-card">

+          <h3>账户信息</h3>

+          <div className="info-item">

+            <label>用户名:</label>

+            <span>{userInfo?.username || '加载中...'}</span>

+          </div>

+          <p className="info-note">用户名不可更改</p>

         </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="password-form-card">

+          <h3>修改密码</h3>

+          <form onSubmit={handlePasswordSubmit}>

+            <div className="form-group">

+              <label htmlFor="oldPassword">原密码:</label>

+              <input

+                type="password"

+                id="oldPassword"

+                name="oldPassword"

+                value={passwordForm.oldPassword}

+                onChange={handlePasswordChange}

+                required

+              />

+            </div>

 

-        <div className="form-group">

-          <label className="form-label">

-            <input

-              type="checkbox"

-              name="notification"

-              checked={formData.notification}

-              onChange={handleChange}

-            />

-            接收邮件通知

-          </label>

-        </div>

+            <div className="form-group">

+              <label htmlFor="newPassword">新密码:</label>

+              <input

+                type="password"

+                id="newPassword"

+                name="newPassword"

+                value={passwordForm.newPassword}

+                onChange={handlePasswordChange}

+                required

+                minLength="6"

+              />

+            </div>

 

-        <div className="form-actions">

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

-            保存设置

-          </button>

-          <button 

-            type="button" 

-            className="action-btn danger-btn"

-            onClick={onLogout}

-          >

-            退出登录

-          </button>

+            <div className="form-group">

+              <label htmlFor="confirmPassword">确认新密码:</label>

+              <input

+                type="password"

+                id="confirmPassword"

+                name="confirmPassword"

+                value={passwordForm.confirmPassword}

+                onChange={handlePasswordChange}

+                required

+                minLength="6"

+              />

+            </div>

+

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

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

+

+            <button 

+              type="submit" 

+              className="submit-button"

+              disabled={loading}

+            >

+              {loading ? '处理中...' : '修改密码'}

+            </button>

+          </form>

         </div>

-      </form>

+      </div>

     </div>

   );

 };

diff --git a/src/components/Personal/Setting.test.jsx b/src/components/Personal/Setting.test.jsx
new file mode 100644
index 0000000..e8b1cc0
--- /dev/null
+++ b/src/components/Personal/Setting.test.jsx
@@ -0,0 +1,195 @@
+// Setting.test.jsx

+import React from 'react';

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

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

+import Setting from './Setting';

+import { getUserInfo, updatePassword } from '../../api/personal';

+

+// Mock API 调用

+jest.mock('../../api/personal', () => ({

+  getUserInfo: jest.fn(),

+  updatePassword: jest.fn()

+}));

+

+// Mock react-router-dom hooks

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

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

+  useNavigate: jest.fn(),

+  useLocation: jest.fn()

+}));

+

+describe('Setting Component', () => {

+  const mockNavigate = jest.fn();

+  const mockLocation = {

+    pathname: '/personal/setting',

+    state: { dashboardTab: 'settings' }

+  };

+

+  beforeEach(() => {

+    useNavigate.mockReturnValue(mockNavigate);

+    useLocation.mockReturnValue(mockLocation);

+    

+    // 重置所有 mock

+    jest.clearAllMocks();

+    

+    // 设置默认 mock 返回值

+    getUserInfo.mockResolvedValue({

+      username: 'testuser',

+      email: 'test@example.com'

+    });

+    

+    updatePassword.mockResolvedValue({ success: true });

+  });

+

+  it('应该正确加载并显示用户信息', async () => {

+    render(

+      <MemoryRouter>

+        <Setting />

+      </MemoryRouter>

+    );

+

+    await waitFor(() => {

+      expect(screen.getByText('个人设置')).toBeInTheDocument();

+      expect(screen.getByText('账户信息')).toBeInTheDocument();

+      expect(screen.getByText('用户名:')).toBeInTheDocument();

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

+      expect(screen.getByRole('heading', { name: '修改密码' })).toBeInTheDocument();

+    });

+  });

+

+  it('应该处理密码修改表单提交', async () => {

+    render(

+      <MemoryRouter>

+        <Setting />

+      </MemoryRouter>

+    );

+

+    // 填写表单

+    fireEvent.change(screen.getByLabelText('原密码:'), { 

+      target: { value: 'oldpassword123' } 

+    });

+    fireEvent.change(screen.getByLabelText('新密码:'), { 

+      target: { value: 'newpassword123' } 

+    });

+    fireEvent.change(screen.getByLabelText('确认新密码:'), { 

+      target: { value: 'newpassword123' } 

+    });

+    

+    // 提交表单

+    fireEvent.click(screen.getByRole('button', { name: '修改密码' }));

+

+    await waitFor(() => {

+      expect(updatePassword).toHaveBeenCalledWith('oldpassword123', 'newpassword123');

+      expect(screen.getByText('密码修改成功')).toBeInTheDocument();

+    });

+  });

+

+

+  it('应该处理API错误', async () => {

+    updatePassword.mockRejectedValue(new Error('原密码不正确'));

+

+    render(

+      <MemoryRouter>

+        <Setting />

+      </MemoryRouter>

+    );

+

+    // 填写表单

+    fireEvent.change(screen.getByLabelText('原密码:'), { 

+      target: { value: 'wrongpassword' } 

+    });

+    fireEvent.change(screen.getByLabelText('新密码:'), { 

+      target: { value: 'newpassword123' } 

+    });

+    fireEvent.change(screen.getByLabelText('确认新密码:'), { 

+      target: { value: 'newpassword123' } 

+    });

+    

+    // 提交表单

+    fireEvent.click(screen.getByRole('button', { name: '修改密码' }));

+

+    await waitFor(() => {

+      expect(screen.getByText('原密码不正确')).toBeInTheDocument();

+    });

+  });

+

+  it('应该显示加载状态', async () => {

+    // 延迟API响应以测试加载状态

+    updatePassword.mockImplementation(() => new Promise(() => {}));

+

+    render(

+      <MemoryRouter>

+        <Setting />

+      </MemoryRouter>

+    );

+

+    // 填写表单

+    fireEvent.change(screen.getByLabelText('原密码:'), { 

+      target: { value: 'oldpassword123' } 

+    });

+    fireEvent.change(screen.getByLabelText('新密码:'), { 

+      target: { value: 'newpassword123' } 

+    });

+    fireEvent.change(screen.getByLabelText('确认新密码:'), { 

+      target: { value: 'newpassword123' } 

+    });

+    

+    // 提交表单

+    fireEvent.click(screen.getByRole('button', { name: '修改密码' }));

+    

+    // 检查加载状态

+    expect(screen.getByText('处理中...')).toBeInTheDocument();

+    expect(screen.getByRole('button', { name: '处理中...' })).toBeDisabled();

+  });

+

+  it('应该处理返回按钮点击', async () => {

+    render(

+      <MemoryRouter>

+        <Setting />

+      </MemoryRouter>

+    );

+

+    const backButton = screen.getByText(/← 返回个人中心/);

+    fireEvent.click(backButton);

+    

+    expect(mockNavigate).toHaveBeenCalledWith('/personal', {

+      state: {

+        fromSubpage: true,

+        dashboardTab: 'settings'

+      },

+      replace: true

+    });

+  });

+

+  it('应该清空表单并显示成功消息', async () => {

+    render(

+      <MemoryRouter>

+        <Setting />

+      </MemoryRouter>

+    );

+

+    // 填写表单

+    fireEvent.change(screen.getByLabelText('原密码:'), { 

+      target: { value: 'oldpassword123' } 

+    });

+    fireEvent.change(screen.getByLabelText('新密码:'), { 

+      target: { value: 'newpassword123' } 

+    });

+    fireEvent.change(screen.getByLabelText('确认新密码:'), { 

+      target: { value: 'newpassword123' } 

+    });

+    

+    // 提交表单

+    fireEvent.click(screen.getByRole('button', { name: '修改密码' }));

+

+    await waitFor(() => {

+      // 检查表单是否清空

+      expect(screen.getByLabelText('原密码:')).toHaveValue('');

+      expect(screen.getByLabelText('新密码:')).toHaveValue('');

+      expect(screen.getByLabelText('确认新密码:')).toHaveValue('');

+      

+      // 检查成功消息

+      expect(screen.getByText('密码修改成功')).toBeInTheDocument();

+    });

+  });

+});
\ No newline at end of file
diff --git a/src/components/Personal/Upload.jsx b/src/components/Personal/Upload.jsx
index 4d6e934..2d91b30 100644
--- a/src/components/Personal/Upload.jsx
+++ b/src/components/Personal/Upload.jsx
@@ -1,28 +1,117 @@
-import React from 'react';

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

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

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

+import { getUserTorrents, deleteTorrent } from '../../api/personal';

 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 [torrents, setTorrents] = useState([]);

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

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

+  const [pagination, setPagination] = useState({

+    page: 1,

+    size: 5,

+    total: 0

+  });

+

+

+

+  // 格式化日期

+  const formatDate = (dateString) => {

+    const date = new Date(dateString);

+    return date.toLocaleString();

+  };

+

+  // 获取上传记录

+  const fetchTorrents = async () => {

+    try {

+      setLoading(true);

+      const { records, total } = await getUserTorrents(pagination.page, pagination.size);

+      setTorrents(records);

+      setPagination(prev => ({ ...prev, total }));

+    } catch (err) {

+      setError(err.message);

+    } finally {

+      setLoading(false);

+    }

+  };

+

+  

+

+  // 删除种子

+  const handleDelete = async (id) => {

+    if (window.confirm('确定要删除这个种子吗?此操作不可撤销!')) {

+      try {

+        await deleteTorrent(id);

+        // 删除成功后刷新列表

+        fetchTorrents();

+      } catch (err) {

+        alert('删除失败: ' + err.message);

+      }

+    }

+  };

+

+  // 计算总页数

+  const totalPages = Math.ceil(pagination.total / pagination.size);

+

+  // 生成页码数组

+  const getPageNumbers = () => {

+    const pages = [];

+    const maxVisiblePages = 5; // 最多显示5个页码

+    

+    let startPage = Math.max(1, pagination.page - Math.floor(maxVisiblePages / 2));

+    let endPage = Math.min(totalPages, startPage + maxVisiblePages - 1);

+    

+    // 调整起始页码,确保始终显示 maxVisiblePages 个页码(如果总页数足够)

+    if (endPage - startPage + 1 < maxVisiblePages) {

+      startPage = Math.max(1, endPage - maxVisiblePages + 1);

+    }

+    

+    for (let i = startPage; i <= endPage; i++) {

+      pages.push(i);

+    }

+    

+    return pages;

+  };

+

+  // 直接跳转到指定页码

+  const jumpToPage = (page) => {

+    if (page >= 1 && page <= totalPages && page !== pagination.page) {

+      setPagination(prev => ({ ...prev, page }));

+    }

+  };

+

+  // 分页改变

+  const handlePageChange = (newPage) => {

+    setPagination(prev => ({ ...prev, page: newPage }));

+  };

+

+  useEffect(() => {

+    fetchTorrents();

+  }, [pagination.page]);

 

   const handleBack = () => {

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

     navigate('/personal', { 

       state: { 

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

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

+        fromSubpage: true,

+        dashboardTab: location.state?.dashboardTab

       },

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

+      replace: true

     });

   };

+

+  if (loading) {

+    return <div className="subpage-container">加载中...</div>;

+  }

+

+  if (error) {

+    return <div className="subpage-container">错误: {error}</div>;

+  }

+

   return (

     <div className="subpage-container">

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

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

         ← 返回个人中心

       </button>

 

@@ -33,31 +122,99 @@
           <tr>

             <th>资源名称</th>

             <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>

+          {torrents.map(torrent => (

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

+              <td>{torrent.torrentName}</td>

+              <td>{torrent.formattedSize}</td>

+              <td>{formatDate(torrent.createTime)}</td>

+              <td>{torrent.downloadCount || 0}</td>

               <td>

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

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

-                }`}>

-                  {item.status}

-                </span>

-              </td>

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

-              <td>

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

+                <button 

+                  className="action-btn delete-btn"

+                  onClick={() => handleDelete(torrent.id)}

+                >

+                  删除

+                </button>

               </td>

             </tr>

           ))}

         </tbody>

       </table>

+

+     {/* 修改后的分页控件 */}

+     <div className="pagination">

+        <button 

+          disabled={pagination.page <= 1}

+          onClick={() => jumpToPage(1)}

+          className="page-nav"

+        >

+          首页

+        </button>

+        <button 

+          disabled={pagination.page <= 1}

+          onClick={() => jumpToPage(pagination.page - 1)}

+          className="page-nav"

+        >

+          上一页

+        </button>

+        

+        {/* 显示页码 */}

+        {getPageNumbers().map(number => (

+          <button

+            key={number}

+            onClick={() => jumpToPage(number)}

+            className={`page-number ${pagination.page === number ? 'active' : ''}`}

+          >

+            {number}

+          </button>

+        ))}

+        

+        {/* 显示省略号(如果有更多页) */}

+        {totalPages > getPageNumbers()[getPageNumbers().length - 1] && (

+          <span className="ellipsis">...</span>

+        )}

+        

+        <button 

+          disabled={pagination.page >= totalPages}

+          onClick={() => jumpToPage(pagination.page + 1)}

+          className="page-nav"

+        >

+          下一页

+        </button>

+        <button 

+          disabled={pagination.page >= totalPages}

+          onClick={() => jumpToPage(totalPages)}

+          className="page-nav"

+        >

+          末页

+        </button>

+        

+        <div className="page-info">

+          <span>共 {totalPages} 页</span>

+          <span>跳至</span>

+          <input

+            type="number"

+            min="1"

+            max={totalPages}

+            onKeyDown={(e) => {

+              if (e.key === 'Enter') {

+                const page = parseInt(e.target.value);

+                if (!isNaN(page)) {

+                  jumpToPage(Math.max(1, Math.min(page, totalPages)));

+                  e.target.value = '';

+                }

+              }

+            }}

+          />

+          <span>页</span>

+        </div>

+      </div>

     </div>

   );

 };

diff --git a/src/components/Personal/Upload.test.jsx b/src/components/Personal/Upload.test.jsx
new file mode 100644
index 0000000..9c72182
--- /dev/null
+++ b/src/components/Personal/Upload.test.jsx
@@ -0,0 +1,206 @@
+// Upload.test.jsx

+import React from 'react';

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

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

+import Upload from './Upload';

+import { getUserTorrents, deleteTorrent } from '../../api/personal';

+

+// Mock API 调用

+jest.mock('../../api/personal', () => ({

+  getUserTorrents: jest.fn(),

+  deleteTorrent: jest.fn()

+}));

+

+// Mock react-router-dom hooks

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

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

+  useNavigate: jest.fn(),

+  useLocation: jest.fn()

+}));

+

+// Mock window.confirm

+global.confirm = jest.fn(() => true);

+

+describe('Upload Component', () => {

+  const mockNavigate = jest.fn();

+  const mockLocation = {

+    pathname: '/personal/upload',

+    state: { dashboardTab: 'uploads' }

+  };

+

+  const mockTorrents = [

+    {

+      id: 1,

+      torrentName: 'Test Torrent 1',

+      formattedSize: '1.2 GB',

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

+      downloadCount: 10

+    },

+    {

+      id: 2,

+      torrentName: 'Test Torrent 2',

+      formattedSize: '2.5 GB',

+      createTime: '2023-01-02T12:00:00Z',

+      downloadCount: 5

+    }

+  ];

+

+  beforeEach(() => {

+    useNavigate.mockReturnValue(mockNavigate);

+    useLocation.mockReturnValue(mockLocation);

+    jest.clearAllMocks();

+    getUserTorrents.mockResolvedValue({

+      records: mockTorrents,

+      total: 10

+    });

+    deleteTorrent.mockResolvedValue({ success: true });

+  });

+

+  it('应该正确加载并显示上传记录', async () => {

+    render(

+      <MemoryRouter>

+        <Upload />

+      </MemoryRouter>

+    );

+

+    // 初始加载状态

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

+

+    // 等待数据加载完成

+    await waitFor(() => {

+      expect(screen.getByText('上传记录')).toBeInTheDocument();

+      expect(screen.getByText('Test Torrent 1')).toBeInTheDocument();

+      expect(screen.getByText('Test Torrent 2')).toBeInTheDocument();

+      expect(screen.getByText('1.2 GB')).toBeInTheDocument();

+      expect(screen.getByText('2.5 GB')).toBeInTheDocument();

+      expect(screen.getAllByText('删除')).toHaveLength(2);

+    });

+  });

+

+  it('应该处理删除操作', async () => {

+    render(

+      <MemoryRouter>

+        <Upload />

+      </MemoryRouter>

+    );

+

+    await waitFor(() => {

+      const deleteButtons = screen.getAllByText('删除');

+      fireEvent.click(deleteButtons[0]);

+    });

+

+    expect(global.confirm).toHaveBeenCalledWith('确定要删除这个种子吗?此操作不可撤销!');

+    await waitFor(() => {

+      expect(deleteTorrent).toHaveBeenCalledWith(1);

+      expect(getUserTorrents).toHaveBeenCalledTimes(2); // 初始加载 + 删除后刷新

+    });

+  });

+

+  it('应该处理分页变化', async () => {

+    render(

+      <MemoryRouter>

+        <Upload />

+      </MemoryRouter>

+    );

+

+    await waitFor(() => {

+      const nextPageButton = screen.getByText('下一页');

+      fireEvent.click(nextPageButton);

+    });

+

+    await waitFor(() => {

+      expect(getUserTorrents).toHaveBeenLastCalledWith(2, 5);

+    });

+  });

+

+  it('应该处理直接跳转页码', async () => {

+    render(

+      <MemoryRouter>

+        <Upload />

+      </MemoryRouter>

+    );

+

+    await waitFor(() => {

+      const pageInput = screen.getByRole('spinbutton');

+      fireEvent.change(pageInput, { target: { value: '2' } });

+      fireEvent.keyDown(pageInput, { key: 'Enter' });

+    });

+

+    await waitFor(() => {

+      expect(getUserTorrents).toHaveBeenLastCalledWith(2, 5);

+    }, { timeout: 1000 });

+  });

+

+  it('应该处理返回按钮点击', async () => {

+    render(

+      <MemoryRouter>

+        <Upload />

+      </MemoryRouter>

+    );

+

+    await waitFor(() => {

+      const backButton = screen.getByText((content) => 

+        content.includes('返回个人中心')

+      );

+      fireEvent.click(backButton);

+      

+      expect(mockNavigate).toHaveBeenCalledWith('/personal', {

+        state: {

+          fromSubpage: true,

+          dashboardTab: 'uploads'

+        },

+        replace: true

+      });

+    });

+  });

+

+  it('应该显示错误信息当API调用失败', async () => {

+    getUserTorrents.mockRejectedValue(new Error('获取上传记录失败'));

+

+    render(

+      <MemoryRouter>

+        <Upload />

+      </MemoryRouter>

+    );

+

+    await waitFor(() => {

+      expect(screen.getByText('错误: 获取上传记录失败')).toBeInTheDocument();

+    });

+  });

+

+

+  it('应该禁用分页按钮当在第一页或最后一页', async () => {

+    render(

+      <MemoryRouter>

+        <Upload />

+      </MemoryRouter>

+    );

+

+    await waitFor(() => {

+      const prevButton = screen.getByText('上一页');

+      const firstPageButton = screen.getByText('首页');

+      

+      expect(prevButton).toBeDisabled();

+      expect(firstPageButton).toBeDisabled();

+    });

+  });

+

+  it('应该显示正确的页码导航', async () => {

+    // 模拟有更多页的情况

+    getUserTorrents.mockResolvedValue({

+      records: mockTorrents,

+      total: 50

+    });

+

+    render(

+      <MemoryRouter>

+        <Upload />

+      </MemoryRouter>

+    );

+

+    await waitFor(() => {

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

+      expect(screen.getByText('共 10 页')).toBeInTheDocument();

+    });

+  });

+});
\ No newline at end of file
diff --git a/src/components/Personal/personalSubpage.css b/src/components/Personal/personalSubpage.css
index a8e5638..2ba8687 100644
--- a/src/components/Personal/personalSubpage.css
+++ b/src/components/Personal/personalSubpage.css
@@ -1,161 +1,336 @@
-/* 基础布局 */

-.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;

-  }

+/* 上传记录表格样式 */

+.uploads-table {

+  width: 100%;

+  border-collapse: collapse;

+  margin-top: 20px;

+}

 

-  /* 收藏列表 */

-.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
+.uploads-table th, .uploads-table td {

+  padding: 12px 15px;

+  text-align: left;

+  border-bottom: 1px solid #e0e0e0;

+}

+

+.uploads-table th {

+  background-color: #f5f5f5;

+  font-weight: 600;

+}

+

+.list-item:hover {

+  background-color: #f9f9f9;

+}

+

+/* 操作按钮样式 */

+.action-btn {

+  padding: 6px 12px;

+  border: none;

+  border-radius: 4px;

+  cursor: pointer;

+  font-size: 14px;

+  transition: background-color 0.2s;

+}

+

+.delete-btn {

+  background-color: #ff4d4f;

+  color: white;

+}

+

+.delete-btn:hover {

+  background-color: #ff7875;

+}

+

+/* 分页控件样式 */

+.pagination {

+  margin-top: 20px;

+  display: flex;

+  justify-content: center;

+  align-items: center;

+  gap: 15px;

+}

+

+.pagination button {

+  padding: 6px 12px;

+  border: 1px solid #d9d9d9;

+  background-color: #fff;

+  border-radius: 4px;

+  cursor: pointer;

+}

+

+.pagination button:disabled {

+  color: #d9d9d9;

+  cursor: not-allowed;

+}

+

+.pagination button:not(:disabled):hover {

+  border-color: #1890ff;

+  color: #1890ff;

+}

+

+/* 分页控件样式 */

+.pagination {

+  margin-top: 20px;

+  display: flex;

+  justify-content: center;

+  align-items: center;

+  gap: 8px;

+  flex-wrap: wrap;

+}

+

+.page-nav, .page-number {

+  padding: 6px 12px;

+  border: 1px solid #d9d9d9;

+  background-color: #fff;

+  border-radius: 4px;

+  cursor: pointer;

+  min-width: 32px;

+  text-align: center;

+}

+

+.page-nav:disabled, .page-number:disabled {

+  color: #d9d9d9;

+  cursor: not-allowed;

+}

+

+.page-nav:not(:disabled):hover, 

+.page-number:not(:disabled):hover {

+  border-color: #1890ff;

+  color: #1890ff;

+}

+

+.page-number.active {

+  background-color: #1890ff;

+  color: white;

+  border-color: #1890ff;

+}

+

+.ellipsis {

+  padding: 0 8px;

+}

+

+.page-info {

+  display: flex;

+  align-items: center;

+  gap: 8px;

+  margin-left: 15px;

+}

+

+.page-info input {

+  width: 50px;

+  padding: 4px;

+  border: 1px solid #d9d9d9;

+  border-radius: 4px;

+  text-align: center;

+}

+

+.page-info input:focus {

+  outline: none;

+  border-color: #1890ff;

+}

+

+/* 调整表格列宽 */

+.uploads-table th:nth-child(1),

+.uploads-table td:nth-child(1) {

+  width: 30%;

+}

+

+.uploads-table th:nth-child(2),

+.uploads-table td:nth-child(2) {

+  width: 15%;

+}

+

+.uploads-table th:nth-child(3),

+.uploads-table td:nth-child(3) {

+  width: 20%;

+}

+

+.uploads-table th:nth-child(4),

+.uploads-table td:nth-child(4) {

+  width: 15%;

+  text-align: center;

+}

+

+.uploads-table th:nth-child(5),

+.uploads-table td:nth-child(5) {

+  width: 20%;

+  text-align: center;

+}

+

+/* 兑换区样式 */

+.exchange-section {

+  margin-top: 20px;

+  padding: 20px;

+  background-color: #f9f9f9;

+  border-radius: 8px;

+}

+

+.exchange-card {

+  margin-bottom: 20px;

+  padding: 15px;

+  background-color: white;

+  border-radius: 6px;

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

+}

+

+.exchange-card h4 {

+  margin-top: 0;

+  color: #333;

+}

+

+.exchange-card p {

+  color: #666;

+  margin-bottom: 15px;

+}

+

+.exchange-btn {

+  padding: 8px 16px;

+  background-color: #1890ff;

+  color: white;

+  border: none;

+  border-radius: 4px;

+  cursor: pointer;

+  transition: background-color 0.3s;

+}

+

+.exchange-btn:hover {

+  background-color: #40a9ff;

+}

+

+.exchange-btn:disabled {

+  background-color: #d9d9d9;

+  cursor: not-allowed;

+}

+

+.exchange-input-group {

+  display: flex;

+  gap: 10px;

+  margin-top: 10px;

+}

+

+.exchange-input-group input {

+  flex: 1;

+  padding: 8px;

+  border: 1px solid #d9d9d9;

+  border-radius: 4px;

+}

+

+.invite-code-list {

+  margin-top: 20px;

+}

+

+.invite-code-list ul {

+  list-style: none;

+  padding: 0;

+}

+

+.invite-code-list li {

+  display: flex;

+  justify-content: space-between;

+  padding: 10px;

+  border-bottom: 1px solid #eee;

+}

+

+.invite-code-list .code {

+  font-family: monospace;

+}

+

+.invite-code-list .status {

+  padding: 2px 6px;

+  border-radius: 3px;

+  font-size: 12px;

+}

+

+.invite-code-list .status.available {

+  background-color: #f6ffed;

+  color: #52c41a;

+  border: 1px solid #b7eb8f;

+}

+

+.invite-code-list .status.used {

+  background-color: #fff2f0;

+  color: #ff4d4f;

+  border: 1px solid #ffccc7;

+}

+

+

+/* personalSubpage.css 中添加以下样式 */

+

+.setting-section {

+  max-width: 600px;

+  margin: 0 auto;

+  padding: 20px;

+}

+

+.user-info-card, .password-form-card {

+  background: #fff;

+  border-radius: 8px;

+  padding: 20px;

+  margin-bottom: 20px;

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

+}

+

+.info-item {

+  display: flex;

+  margin-bottom: 10px;

+}

+

+.info-item label {

+  font-weight: bold;

+  width: 100px;

+}

+

+.info-item span {

+  flex: 1;

+}

+

+.info-note {

+  color: #666;

+  font-size: 0.9em;

+  margin-top: 10px;

+}

+

+.form-group {

+  margin-bottom: 15px;

+}

+

+.form-group label {

+  display: block;

+  margin-bottom: 5px;

+  font-weight: bold;

+}

+

+.form-group input {

+  width: 100%;

+  padding: 8px;

+  border: 1px solid #ddd;

+  border-radius: 4px;

+  box-sizing: border-box;

+}

+

+.submit-button {

+  background-color: #4CAF50;

+  color: white;

+  padding: 10px 15px;

+  border: none;

+  border-radius: 4px;

+  cursor: pointer;

+  font-size: 16px;

+}

+

+.submit-button:hover {

+  background-color: #45a049;

+}

+

+.submit-button:disabled {

+  background-color: #cccccc;

+  cursor: not-allowed;

+}

+

+.error-message {

+  color: #f44336;

+  margin-bottom: 15px;

+}

+

+.success-message {

+  color: #4CAF50;

+  margin-bottom: 15px;

+}
\ No newline at end of file