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

Change-Id: Id635654fccccaea80bfbf4d1480abd55f7d12046
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