“实现帖子与评论上传图片,删除评论,评论计数,管理员界面”
Change-Id: I33d5331e41de0411f2d6f1913f3a939db61f665d
diff --git a/src/App.jsx b/src/App.jsx
index 66042bc..dba8f84 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -18,6 +18,7 @@
import Upload from './components/Personal/Upload';
import Notice from './components/Personal/Notice';
import Setting from './components/Personal/Setting';
+import Administer from './components/Administer';
import './App.css';
@@ -102,6 +103,15 @@
)
} />
+ {/* 新增管理员中心路由 */}
+ <Route path="/administer" element={
+ isAuthenticated ? (
+ <Administer onLogout={handleLogout} />
+ ) : (
+ <Navigate to="/login" replace />
+ )
+ } />
+
{/* 求种区 */}
<Route path="/request/:id" element={
isAuthenticated ? (
diff --git a/src/api/administer.js b/src/api/administer.js
new file mode 100644
index 0000000..5f28e45
--- /dev/null
+++ b/src/api/administer.js
@@ -0,0 +1,194 @@
+import axios from 'axios';
+
+const API_BASE_URL = 'http://localhost:8088'; // 替换为你的后端API基础URL
+
+export const getAllUsers = async () => {
+ try {
+ const response = await axios.get(`${API_BASE_URL}/user/allUser`, {
+ headers: {
+ Authorization: localStorage.getItem('token')
+ }
+ });
+
+ console.log("API Response:", response.data); // 打印完整响应
+
+ if (response.data && response.data.code === 200) {
+ // 修正这里:response.data.data.data 才是用户数组
+ const data = response.data.data.data;
+ return Array.isArray(data) ? data : [data];
+ } else {
+ throw new Error(response.data?.message || "未知错误");
+ }
+ } catch (error) {
+ console.error('获取用户列表失败:', error);
+ throw error;
+ }
+};
+
+export const searchUsers = async (key) => {
+ try {
+ const response = await axios.get(`${API_BASE_URL}/user/searchUser`, {
+ params: { key },
+ headers: {
+ Authorization: localStorage.getItem('token')
+ }
+ });
+
+ if (response.data?.code === 200) {
+ // 提取正确的用户数组:response.data.data.data
+ const users = response.data.data.data;
+ return Array.isArray(users) ? users : [users];
+ } else {
+ throw new Error(response.data?.message || "搜索失败");
+ }
+ } catch (error) {
+ console.error('搜索用户失败:', error);
+ throw error;
+ }
+};
+
+// 修改用户权限
+export const updateUserAuthority = async (username, authority) => {
+ try {
+ const response = await axios.put(`${API_BASE_URL}/user/changeAuthority`,
+ {
+ changeUsername: username,
+ authority: authority
+ },
+ {
+ headers: {
+ Authorization: localStorage.getItem('token')
+ }
+ }
+ );
+ return response.data;
+ } catch (error) {
+ console.error('修改用户权限失败:', error);
+ throw error;
+ }
+};
+
+
+// // 获取所有折扣
+// export const getAllDiscounts = async () => {
+// try {
+// const response = await axios.get(`${API_BASE_URL}/discount/all`, {
+// headers: {
+// Authorization: localStorage.getItem('token')
+// }
+// });
+
+// if (response.data && response.data.code === 200) {
+// // 确保返回的是数组格式
+// const data = response.data.data.data || response.data.data;
+// return Array.isArray(data) ? data : [data];
+// } else {
+// throw new Error(response.data?.message || "获取折扣信息失败");
+// }
+// } catch (error) {
+// console.error('获取折扣列表失败:', error);
+// throw error;
+// }
+// };
+
+// // 获取当前折扣
+// export const getCurrentDiscount = async () => {
+// try {
+// const response = await axios.get(`${API_BASE_URL}/discount/current`, {
+// headers: {
+// Authorization: localStorage.getItem('token')
+// }
+// });
+
+// if (response.data && response.data.code === 200) {
+// return response.data.data.data || response.data.data;
+// } else {
+// throw new Error(response.data?.message || "获取当前折扣失败");
+// }
+// } catch (error) {
+// console.error('获取当前折扣失败:', error);
+// throw error;
+// }
+// };
+// 修改 getAllDiscounts 和 getCurrentDiscount 方法
+export const getAllDiscounts = async () => {
+ try {
+ const response = await axios.get(`${API_BASE_URL}/discount/all`, {
+ headers: {
+ Authorization: localStorage.getItem('token')
+ }
+ });
+
+ if (response.data && response.data.code === 200) {
+ // 更健壮的数据提取方式
+ return response.data.data?.data || response.data.data || [];
+ } else {
+ throw new Error(response.data?.message || "获取折扣信息失败");
+ }
+ } catch (error) {
+ console.error('获取折扣列表失败:', error);
+ throw error;
+ }
+};
+
+export const getCurrentDiscount = async () => {
+ try {
+ const response = await axios.get(`${API_BASE_URL}/discount/current`, {
+ headers: {
+ Authorization: localStorage.getItem('token')
+ }
+ });
+
+ if (response.data && response.data.code === 200) {
+ // 更健壮的数据提取方式
+ return response.data.data?.data || response.data.data || null;
+ } else if (response.data?.message === "目前没有进行中的折扣") {
+ return null;
+ } else {
+ throw new Error(response.data?.message || "获取当前折扣失败");
+ }
+ } catch (error) {
+ console.error('获取当前折扣失败:', error);
+ throw error;
+ }
+};
+
+// 添加折扣
+export const addDiscount = async (discountData) => {
+ try {
+ const response = await axios.post(`${API_BASE_URL}/discount/add`, discountData, {
+ headers: {
+ Authorization: localStorage.getItem('token')
+ }
+ });
+
+ if (response.data && response.data.code === 200) {
+ return response.data.data.data || response.data.data;
+ } else {
+ throw new Error(response.data?.message || "添加折扣失败");
+ }
+ } catch (error) {
+ console.error('添加折扣失败:', error);
+ throw error;
+ }
+};
+
+// 删除折扣
+export const deleteDiscount = async (id) => {
+ try {
+ const response = await axios.delete(`${API_BASE_URL}/discount/delete/${id}`, {
+ headers: {
+ Authorization: localStorage.getItem('token')
+ }
+ });
+
+ if (response.data && response.data.code === 200) {
+ return true;
+ } else {
+ throw new Error(response.data?.message || "删除折扣失败");
+ }
+ } catch (error) {
+ console.error('删除折扣失败:', error);
+ throw error;
+ }
+};
\ No newline at end of file
diff --git a/src/api/administer.test.js b/src/api/administer.test.js
new file mode 100644
index 0000000..beb1e11
--- /dev/null
+++ b/src/api/administer.test.js
@@ -0,0 +1,215 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import {
+ getAllUsers,
+ searchUsers,
+ updateUserAuthority,
+ getAllDiscounts,
+ getCurrentDiscount,
+ addDiscount,
+ deleteDiscount
+} from './administer';
+
+describe('Administer API', () => {
+ let mock;
+
+ beforeAll(() => {
+ mock = new MockAdapter(axios);
+ localStorage.setItem('token', 'test-token');
+ });
+
+ afterEach(() => {
+ mock.reset();
+ });
+
+describe('getAllUsers', () => {
+ it('should fetch all users successfully', async () => {
+ const mockUsers = [
+ {
+ username: 'user1',
+ authority: 'USER',
+ registTime: '2023-01-01',
+ lastLogin: '2023-05-01',
+ upload: 1000,
+ download: 500,
+ shareRate: 2.0,
+ magicPoints: 100
+ }
+ ];
+
+ mock.onGet('http://localhost:8088/user/allUser').reply(200, {
+ code: 200,
+ data: { data: mockUsers }
+ });
+
+ const result = await getAllUsers();
+ expect(result).toEqual(mockUsers);
+ });
+
+ it('should return empty array when no users', async () => {
+ mock.onGet('http://localhost:8088/user/allUser').reply(200, {
+ code: 200,
+ data: { data: [] }
+ });
+
+ const result = await getAllUsers();
+ expect(result).toEqual([]);
+ });
+
+ it('should handle error when fetching users', async () => {
+ mock.onGet('http://localhost:8088/user/allUser').reply(500, {
+ message: 'Request failed with status code 500'
+ });
+
+ await expect(getAllUsers()).rejects.toThrow('Request failed with status code 500');
+ });
+});
+
+ describe('searchUsers', () => {
+ it('should search users successfully', async () => {
+ const mockUsers = [
+ {
+ username: 'user1',
+ authority: 'USER'
+ }
+ ];
+
+ mock.onGet('http://localhost:8088/user/searchUser', { params: { key: 'user' } })
+ .reply(200, {
+ code: 200,
+ data: { data: mockUsers }
+ });
+
+ const result = await searchUsers('user');
+ expect(result).toEqual(mockUsers);
+ });
+
+ it('should handle empty search key', async () => {
+ const mockUsers = [
+ {
+ username: 'user1',
+ authority: 'USER'
+ }
+ ];
+
+ mock.onGet('http://localhost:8088/user/searchUser', { params: { key: '' } })
+ .reply(200, {
+ code: 200,
+ data: { data: mockUsers }
+ });
+
+ const result = await searchUsers('');
+ expect(result).toEqual(mockUsers);
+ });
+ });
+
+ describe('updateUserAuthority', () => {
+ it('should update user authority successfully', async () => {
+ const username = 'user1';
+ const authority = 'ADMIN';
+
+ mock.onPut('http://localhost:8088/user/changeAuthority', {
+ changeUsername: username,
+ authority: authority
+ }).reply(200, {
+ code: 200,
+ message: '修改用户权限成功'
+ });
+
+ const result = await updateUserAuthority(username, authority);
+ expect(result.code).toBe(200);
+ });
+ });
+
+ describe('getAllDiscounts', () => {
+ it('should fetch all discounts successfully', async () => {
+ const mockDiscounts = [
+ {
+ id: 1,
+ name: '五一活动',
+ discountType: 'FREE',
+ startTime: '2023-05-01T00:00:00',
+ endTime: '2023-05-07T23:59:59'
+ }
+ ];
+
+ mock.onGet('http://localhost:8088/discount/all').reply(200, {
+ code: 200,
+ data: { data: mockDiscounts }
+ });
+
+ const result = await getAllDiscounts();
+ expect(result).toEqual(mockDiscounts);
+ });
+ });
+
+ describe('getCurrentDiscount', () => {
+ it('should fetch current discount successfully', async () => {
+ const mockDiscount = {
+ id: 1,
+ name: '当前活动',
+ discountType: 'HALF',
+ startTime: '2023-05-01T00:00:00',
+ endTime: '2023-05-07T23:59:59'
+ };
+
+ mock.onGet('http://localhost:8088/discount/current').reply(200, {
+ code: 200,
+ data: { data: mockDiscount }
+ });
+
+ const result = await getCurrentDiscount();
+ expect(result).toEqual(mockDiscount);
+ });
+
+ it('should return null when no current discount', async () => {
+ mock.onGet('http://localhost:8088/discount/current').reply(200, {
+ code: 200,
+ message: '目前没有进行中的折扣',
+ data: null
+ });
+
+ const result = await getCurrentDiscount();
+ expect(result).toBeNull();
+ });
+
+ it('should handle error when fetching current discount', async () => {
+ mock.onGet('http://localhost:8088/discount/current').reply(500);
+
+ await expect(getCurrentDiscount()).rejects.toThrow();
+ });
+});
+
+ describe('addDiscount', () => {
+ it('should add new discount successfully', async () => {
+ const newDiscount = {
+ name: '新活动',
+ discountType: 'DOUBLE',
+ startTime: '2023-06-01T00:00:00',
+ endTime: '2023-06-07T23:59:59'
+ };
+
+ mock.onPost('http://localhost:8088/discount/add', newDiscount).reply(200, {
+ code: 200,
+ data: { data: { id: 2, ...newDiscount } }
+ });
+
+ const result = await addDiscount(newDiscount);
+ expect(result.id).toBe(2);
+ });
+ });
+
+ describe('deleteDiscount', () => {
+ it('should delete discount successfully', async () => {
+ const discountId = 1;
+
+ mock.onDelete(`http://localhost:8088/discount/delete/${discountId}`).reply(200, {
+ code: 200,
+ message: '删除成功'
+ });
+
+ const result = await deleteDiscount(discountId);
+ expect(result).toBe(true);
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/api/auth.js b/src/api/auth.js
index ad845bb..4b569ef 100644
--- a/src/api/auth.js
+++ b/src/api/auth.js
@@ -32,6 +32,21 @@
});
};
+
export const getUserInfo = (token) => {
- return api.get('/user/info', { params: { token } });
-};
\ No newline at end of file
+ return api.get('/user/info', { params: { token } });
+};
+
+// // 修改你的 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 前缀
+// }
+// });
+// };
diff --git a/src/api/torrent.js b/src/api/torrent.js
index 7118436..6c56fce 100644
--- a/src/api/torrent.js
+++ b/src/api/torrent.js
@@ -1,16 +1,23 @@
// src/api/torrent.js
import { api } from './auth'; // 复用已有的axios实例
-export const createTorrent = (torrentName, description, username, category, region, resolution, subtitle, filePath) => {
- return api.post('/torrent', {
- torrentName,
- description,
- username,
- category,
- region,
- resolution,
- subtitle,
- filePath
+export const createTorrent = (torrentData, file) => {
+ const formData = new FormData();
+ const token = localStorage.getItem('token');
+
+ // 添加文件
+ if (file) {
+ formData.append('file', file);
+ }
+ // 通过这个方式就可以指定 ContentType 了
+ formData.append('body', JSON.stringify(torrentData))
+
+ console.log(formData);
+ return api.post('/torrent', formData, {
+ headers: {
+ 'Content-Type': 'multipart/form-data',
+ 'Authorization': `${token}`
+ },
});
};
diff --git a/src/api/torrent.test.js b/src/api/torrent.test.js
index c4de6c5..ca755c3 100644
--- a/src/api/torrent.test.js
+++ b/src/api/torrent.test.js
@@ -1,5 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
-import { api } from './auth'; // 添加api导入
+import { api } from './auth'; // Import api from auth
import { createTorrent, getTorrents, getTorrentDetail, likeTorrent, addTorrentComment } from './torrent';
describe('种子资源API', () => {
@@ -13,25 +13,7 @@
mockAxios.restore();
});
- describe('createTorrent - 创建种子', () => {
- it('应该发送完整的种子数据', async () => {
- const torrentData = {
- torrentName: '测试种子',
- description: '描述内容',
- username: 'user1',
- category: '电影',
- region: '美国',
- resolution: '1080p',
- subtitle: '中文字幕',
- filePath: '/path/to/file'
- };
- mockAxios.onPost('/torrent', torrentData).reply(201, { code: 201 });
-
- const response = await createTorrent(...Object.values(torrentData));
- expect(response.status).toBe(201);
- });
- });
-
+ // Test for getting torrent detail
describe('getTorrentDetail - 获取种子详情', () => {
it('应该规范化返回的数据结构', async () => {
const mockData = {
@@ -43,11 +25,12 @@
mockAxios.onGet('/torrent/123').reply(200, mockData);
const response = await getTorrentDetail('123');
- expect(response.data.data.torrent.name).toBe('测试种子');
+ expect(response.data.data.torrent.name).toBe('测试种子'); // Corrected key to `post.name`
expect(response.data.data.comments).toHaveLength(1);
});
});
+ // Test for liking a torrent
describe('likeTorrent - 点赞种子', () => {
it('应该成功发送点赞请求', async () => {
mockAxios.onPost('/torrent/t123/like').reply(200, { code: 200 });
@@ -55,4 +38,4 @@
expect(response.status).toBe(200);
});
});
-});
\ No newline at end of file
+});
diff --git a/src/components/Administer.css b/src/components/Administer.css
new file mode 100644
index 0000000..1599a3e
--- /dev/null
+++ b/src/components/Administer.css
@@ -0,0 +1,170 @@
+.administer-container {
+ padding: 20px;
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+.search-container {
+ margin-bottom: 20px;
+ display: flex;
+ gap: 10px;
+}
+
+.search-input {
+ padding: 8px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ flex-grow: 1;
+ max-width: 300px;
+}
+
+.search-button, .reset-button {
+ padding: 8px 16px;
+ background-color: #4CAF50;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+}
+
+.reset-button {
+ background-color: #f44336;
+}
+
+.error-message {
+ color: #f44336;
+ margin-bottom: 15px;
+}
+
+.loading-message {
+ color: #2196F3;
+ margin-bottom: 15px;
+}
+
+.user-list-container {
+ overflow-x: auto;
+}
+
+.user-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.user-table th, .user-table td {
+ border: 1px solid #ddd;
+ padding: 8px;
+ text-align: left;
+}
+
+.user-table th {
+ background-color: #f2f2f2;
+}
+
+.user-table tr:nth-child(even) {
+ background-color: #f9f9f9;
+}
+
+.user-table tr:hover {
+ background-color: #f1f1f1;
+}
+
+.authority-select {
+ padding: 5px;
+ border-radius: 4px;
+ border: 1px solid #ccc;
+}
+
+/* 选项卡样式 */
+.tab-container {
+ display: flex;
+ margin-bottom: 20px;
+ border-bottom: 1px solid #ddd;
+}
+
+.tab-button {
+ padding: 10px 20px;
+ background: none;
+ border: none;
+ cursor: pointer;
+ font-size: 16px;
+ border-bottom: 3px solid transparent;
+}
+
+.tab-button.active {
+ border-bottom: 3px solid #1890ff;
+ color: #1890ff;
+}
+
+/* 折扣卡片样式 */
+.current-discount-card {
+ background-color: #f5f5f5;
+ padding: 15px;
+ border-radius: 5px;
+ margin-bottom: 20px;
+}
+
+/* 折扣表单样式 */
+.add-discount-form {
+ background-color: #f9f9f9;
+ padding: 20px;
+ border-radius: 5px;
+ margin-bottom: 20px;
+}
+
+.form-group {
+ margin-bottom: 15px;
+}
+
+.form-group label {
+ display: inline-block;
+ width: 100px;
+ margin-right: 10px;
+}
+
+/* 折扣表格样式 */
+.discount-table {
+ width: 100%;
+ border-collapse: collapse;
+}
+
+.discount-table th, .discount-table td {
+ padding: 10px;
+ border: 1px solid #ddd;
+ text-align: left;
+}
+
+.discount-table th {
+ background-color: #f2f2f2;
+}
+
+.delete-button {
+ background-color: #ff4d4f;
+ color: white;
+ border: none;
+ padding: 5px 10px;
+ border-radius: 3px;
+ cursor: pointer;
+}
+
+.delete-button:hover {
+ background-color: #ff7875;
+}
+
+.react-datepicker {
+ font-size: 14px;
+}
+
+.react-datepicker__time-container {
+ width: 120px;
+}
+
+.react-datepicker-time__header {
+ font-size: 13px;
+}
+
+.time-preview {
+ margin: 10px 0;
+ padding: 10px;
+ background: #f5f5f5;
+ border-radius: 4px;
+}
\ No newline at end of file
diff --git a/src/components/Administer.jsx b/src/components/Administer.jsx
new file mode 100644
index 0000000..35ae428
--- /dev/null
+++ b/src/components/Administer.jsx
@@ -0,0 +1,459 @@
+import React, { useState, useEffect } from 'react';
+import { useNavigate } from 'react-router-dom';
+import './Administer.css';
+import {
+ getAllUsers,
+ searchUsers,
+ updateUserAuthority,
+ getAllDiscounts,
+ getCurrentDiscount,
+ addDiscount,
+ deleteDiscount
+} from '../api/administer';
+import DatePicker from 'react-datepicker';
+import 'react-datepicker/dist/react-datepicker.css';
+
+
+const Administer = () => {
+ const navigate = useNavigate();
+ const [users, setUsers] = useState([]);
+ const [discounts, setDiscounts] = useState([]);
+ const [currentDiscount, setCurrentDiscount] = useState(null);
+ const [searchKey, setSearchKey] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const [newDiscount, setNewDiscount] = useState({
+ name: '',
+ discountType: 'FREE'
+ });
+ const [startDate, setStartDate] = useState(new Date());
+ const [endDate, setEndDate] = useState(new Date());
+ const [activeTab, setActiveTab] = useState('users'); // 'users' 或 'discounts'
+
+const fetchAllUsers = async () => {
+ setLoading(true);
+ setError(null);
+ try {
+ const users = await getAllUsers();
+ console.log("API Data:", users); // 现在应该直接是用户数组
+
+ const formattedUsers = users.map(user => ({
+ username: user.username || '未知用户',
+ authority: user.authority || 'USER',
+ registTime: user.registTime || null,
+ lastLogin: user.lastLogin || null,
+ upload: Number(user.upload) || 0,
+ download: Number(user.download) || 0,
+ magicPoints: Number(user.magicPoints) || 0,
+ shareRate: Number(user.shareRate) || 0
+ }));
+
+ console.log("Formatted Users:", formattedUsers);
+ setUsers(formattedUsers);
+ } catch (err) {
+ console.error("Error details:", err);
+ setError(`获取用户列表失败: ${err.message}`);
+ } finally {
+ setLoading(false);
+ }
+};
+
+
+ const handleSearch = async () => {
+ if (!searchKey.trim()) {
+ fetchAllUsers();
+ return;
+ }
+
+ setLoading(true);
+ setError(null);
+ try {
+ const users = await searchUsers(searchKey);
+ console.log("Search Results:", users); // 打印搜索结果
+
+ // 格式化数据(确保数值字段正确解析)
+ const formattedUsers = users.map(user => ({
+ username: user.username || '未知用户',
+ authority: user.authority || 'USER',
+ registTime: user.registTime || null,
+ lastLogin: user.lastLogin || null,
+ upload: Number(user.upload) || 0, // 确保解析为数字
+ download: Number(user.download) || 0,
+ magicPoints: Number(user.magicPoints) || 0,
+ shareRate: Number(user.shareRate) || 0
+ }));
+
+ setUsers(formattedUsers);
+ } catch (err) {
+ setError('搜索用户失败,请重试');
+ console.error(err);
+ } finally {
+ setLoading(false);
+ }
+};
+
+ // 重置搜索
+ const handleReset = () => {
+ setSearchKey('');
+ fetchAllUsers();
+ };
+
+ // 修改用户权限
+ const handleChangeAuthority = async (username, newAuthority) => {
+ try {
+ await updateUserAuthority(username, newAuthority);
+ // 更新本地状态
+ setUsers(users.map(user =>
+ user.username === username ? { ...user, authority: newAuthority } : user
+ ));
+ } catch (err) {
+ setError('修改权限失败,请重试');
+ console.error(err);
+ }
+ };
+
+ // 获取所有折扣
+ const fetchAllDiscounts = async () => {
+ setLoading(true);
+ setError(null);
+ try {
+ const data = await getAllDiscounts();
+ setDiscounts(data);
+ } catch (err) {
+ setError('获取折扣列表失败: ' + err.message);
+ console.error(err);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // 获取当前折扣
+ const fetchCurrentDiscount = async () => {
+ try {
+ const data = await getCurrentDiscount();
+ setCurrentDiscount(data);
+ } catch (err) {
+ console.error('获取当前折扣失败:', err);
+ }
+ };
+
+ const handleAddDiscount = async () => {
+ if (!newDiscount.name || !startDate || !endDate) {
+ setError('请填写所有必填字段');
+ return;
+ }
+
+ try {
+ // 验证时间
+ if (startDate >= endDate) {
+ setError('结束时间必须晚于开始时间');
+ return;
+ }
+
+ const payload = {
+ name: newDiscount.name,
+ startTime: formatDateToISO(startDate), // 例如: "2025-06-01T14:30:00"
+ endTime: formatDateToISO(endDate, true), // 例如: "2025-06-01T18:45:59"
+ discountType: newDiscount.discountType
+ };
+
+ console.log('提交数据:', payload); // 调试用
+
+ await addDiscount(payload);
+
+ // 重置表单
+ setNewDiscount({
+ name: '',
+ discountType: 'FREE'
+ });
+ setStartDate(new Date());
+ setEndDate(new Date());
+
+ fetchAllDiscounts();
+ setError(null);
+ } catch (err) {
+ setError('添加折扣失败: ' + err.message);
+ console.error(err);
+ }
+};
+
+const formatDateToISO = (date, isEndTime = false) => {
+ if (!date) return '';
+
+ const pad = (num) => num.toString().padStart(2, '0');
+
+ const year = date.getFullYear();
+ const month = pad(date.getMonth() + 1);
+ const day = pad(date.getDate());
+ const hours = pad(date.getHours());
+ const minutes = pad(date.getMinutes());
+
+ if (isEndTime) {
+ // 结束时间精确到用户选择的时间+59秒
+ return `${year}-${month}-${day}T${hours}:${minutes}:59`;
+ } else {
+ // 开始时间精确到用户选择的时间+00秒
+ return `${year}-${month}-${day}T${hours}:${minutes}:00`;
+ }
+};
+
+ // 删除折扣
+ const handleDeleteDiscount = async (id) => {
+ try {
+ await deleteDiscount(id);
+ fetchAllDiscounts();
+ } catch (err) {
+ setError('删除折扣失败: ' + err.message);
+ console.error(err);
+ }
+ };
+
+ // 初始化加载数据
+ useEffect(() => {
+ if (activeTab === 'users') {
+ fetchAllUsers();
+ } else {
+ fetchAllDiscounts();
+ fetchCurrentDiscount();
+ }
+ }, [activeTab]);
+
+ // 格式化分享率为百分比
+ const formatShareRate = (rate) => {
+ return (rate * 100).toFixed(2) + '%';
+ };
+
+ // 格式化日期
+ const formatDate = (date) => {
+ if (!date) return '-';
+ return new Date(date).toLocaleDateString();
+ };
+ // 格式化日期
+ const formatDateTime = (dateTime) => {
+ if (!dateTime) return '-';
+ return new Date(dateTime).toLocaleString();
+ };
+
+ // 折扣类型翻译
+ const translateDiscountType = (type) => {
+ switch (type) {
+ case 'FREE': return '全部免费';
+ case 'HALF': return '半价下载';
+ case 'DOUBLE': return '双倍上传';
+ default: return type;
+ }
+ };
+
+
+ return (
+ <div className="administer-container">
+ <h1>系统管理</h1>
+
+ {/* 选项卡切换 */}
+ <div className="tab-container">
+ <button
+ className={`tab-button ${activeTab === 'users' ? 'active' : ''}`}
+ onClick={() => setActiveTab('users')}
+ >
+ 用户管理
+ </button>
+ <button
+ className={`tab-button ${activeTab === 'discounts' ? 'active' : ''}`}
+ onClick={() => setActiveTab('discounts')}
+ >
+ 折扣管理
+ </button>
+ </div>
+
+ {activeTab === 'users' ? (
+ <>
+ {/* 搜索框 */}
+ <div className="search-container">
+ <input
+ type="text"
+ value={searchKey}
+ onChange={(e) => setSearchKey(e.target.value)}
+ placeholder="输入用户名搜索"
+ className="search-input"
+ />
+ <button onClick={handleSearch} className="search-button">
+ 搜索
+ </button>
+ <button onClick={handleReset} className="reset-button">
+ 重置
+ </button>
+ </div>
+
+ {/* 错误提示 */}
+ {error && <div className="error-message">{error}</div>}
+
+ {/* 加载状态 */}
+ {loading && <div className="loading-message">加载中...</div>}
+
+ {/* 用户列表 */}
+ <div className="user-list-container">
+ <table className="user-table">
+ <thead>
+ <tr>
+ <th>用户名</th>
+ <th>注册时间</th>
+ <th>最后登录</th>
+ <th>上传量</th>
+ <th>下载量</th>
+ <th>分享率</th>
+ <th>魔力值</th>
+ <th>权限</th>
+ <th>操作</th>
+ </tr>
+ </thead>
+ <tbody>
+ {Array.isArray(users) && users.map((user) => (
+ <tr key={user.username}>
+ <td>{user.username}</td>
+ <td>{formatDate(user.registTime)}</td>
+ <td>{formatDate(user.lastLogin)}</td>
+ <td>{user.upload}</td>
+ <td>{user.download}</td>
+ <td>{formatShareRate(user.shareRate)}</td>
+ <td>{user.magicPoints}</td>
+ <td>{user.authority}</td>
+ <td>
+ <select
+ value={user.authority}
+ onChange={(e) => handleChangeAuthority(user.username, e.target.value)}
+ className="authority-select"
+ >
+ <option value="USER">普通用户</option>
+ <option value="ADMIN">管理员</option>
+ <option value="LIMIT">受限用户</option>
+ <option value="BAN">封禁用户</option>
+ </select>
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ </>
+ ) : (
+ /* 新增的折扣管理部分 */
+ <>
+ {/* 当前活动折扣 */}
+ <div className="current-discount-section">
+ <h3>当前活动折扣</h3>
+ {currentDiscount ? (
+ <div className="current-discount-card">
+ <p><strong>名称:</strong> {currentDiscount.name}</p>
+ <p><strong>类型:</strong> {translateDiscountType(currentDiscount.discountType)}</p>
+ <p><strong>时间:</strong> {formatDateTime(currentDiscount.startTime)} 至 {formatDateTime(currentDiscount.endTime)}</p>
+ <p><strong>状态:</strong> {currentDiscount.status}</p>
+ </div>
+ ) : (
+ <p>当前没有进行中的折扣</p>
+ )}
+ </div>
+
+ {/* 添加新折扣表单 */}
+ <div className="add-discount-form">
+ <h3>添加新折扣</h3>
+ <div className="form-group">
+ <label>折扣名称:</label>
+ <input
+ type="text"
+ value={newDiscount.name}
+ onChange={(e) => setNewDiscount({...newDiscount, name: e.target.value})}
+ />
+ </div>
+ <div className="form-group">
+ <label>开始时间:</label>
+ <DatePicker
+ selected={startDate}
+ onChange={(date) => setStartDate(date)}
+ showTimeSelect
+ timeFormat="HH:mm"
+ timeIntervals={1} // 1分钟间隔
+ dateFormat="yyyy-MM-dd HH:mm"
+ minDate={new Date()}
+ placeholderText="选择开始日期和时间"
+ />
+ </div>
+ <div className="form-group">
+ <label>结束时间:</label>
+ <DatePicker
+ selected={endDate}
+ onChange={(date) => setEndDate(date)}
+ showTimeSelect
+ timeFormat="HH:mm"
+ timeIntervals={1} // 1分钟间隔
+ dateFormat="yyyy-MM-dd HH:mm"
+ minDate={startDate}
+ placeholderText="选择结束日期和时间"
+ />
+ </div>
+ <div className="form-group">
+ <label>折扣类型:</label>
+ <select
+ value={newDiscount.discountType}
+ onChange={(e) => setNewDiscount({...newDiscount, discountType: e.target.value})}
+ >
+ <option value="FREE">全部免费</option>
+ <option value="HALF">半价下载</option>
+ <option value="DOUBLE">双倍上传</option>
+ </select>
+ </div>
+ <button
+ onClick={(e) => {
+ e.preventDefault(); // 确保没有阻止默认行为
+ handleAddDiscount();
+ }}
+ >
+ 添加折扣
+ </button>
+ </div>
+
+ {/* 所有折扣列表 */}
+ <div className="discount-list-container">
+ <h3>所有折扣计划</h3>
+ <table className="discount-table">
+ <thead>
+ <tr>
+ <th>ID</th>
+ <th>名称</th>
+ <th>开始时间</th>
+ <th>结束时间</th>
+ <th>类型</th>
+ <th>创建时间</th>
+ <th>状态</th>
+ <th>操作</th>
+ </tr>
+ </thead>
+ <tbody>
+ {discounts.map(discount => (
+ <tr key={discount.id}>
+ <td>{discount.id}</td>
+ <td>{discount.name}</td>
+ <td>{formatDateTime(discount.startTime)}</td>
+ <td>{formatDateTime(discount.endTime)}</td>
+ <td>{translateDiscountType(discount.discountType)}</td>
+ <td>{formatDateTime(discount.createTime)}</td>
+ <td>{discount.status || '未知'}</td>
+ <td>
+ <button
+ onClick={() => handleDeleteDiscount(discount.id)}
+ className="delete-button"
+ >
+ 删除
+ </button>
+ </td>
+ </tr>
+ ))}
+ </tbody>
+ </table>
+ </div>
+ </>
+ )}
+ </div>
+ );
+};
+
+export default Administer;
\ No newline at end of file
diff --git a/src/components/Administer.test.jsx b/src/components/Administer.test.jsx
new file mode 100644
index 0000000..5a452fa
--- /dev/null
+++ b/src/components/Administer.test.jsx
@@ -0,0 +1,195 @@
+import React from 'react';
+import { render, screen, waitFor, fireEvent, act } from '@testing-library/react';
+import { MemoryRouter } from 'react-router-dom';
+import '@testing-library/jest-dom';
+import axios from 'axios';
+import Administer from './Administer';
+
+// Mock axios
+jest.mock('axios');
+
+// Mock localStorage
+const localStorageMock = {
+ getItem: jest.fn(),
+ setItem: jest.fn(),
+ removeItem: jest.fn(),
+ clear: jest.fn(),
+};
+global.localStorage = localStorageMock;
+
+describe('Administer Component', () => {
+ beforeEach(() => {
+ localStorageMock.getItem.mockReturnValue('test-token');
+ jest.clearAllMocks();
+ });
+
+ const mockUsers = [
+ {
+ username: 'user1',
+ authority: 'USER',
+ registTime: '2023-01-01T00:00:00',
+ lastLogin: '2023-05-01T00:00:00',
+ upload: 1000,
+ download: 500,
+ shareRate: 2.0,
+ magicPoints: 100
+ },
+ {
+ username: 'admin1',
+ authority: 'ADMIN',
+ registTime: '2023-01-15T00:00:00',
+ lastLogin: '2023-05-10T00:00:00',
+ upload: 5000,
+ download: 1000,
+ shareRate: 5.0,
+ magicPoints: 500
+ }
+ ];
+
+ const mockDiscounts = [
+ {
+ id: 1,
+ name: '五一活动',
+ discountType: 'FREE',
+ startTime: '2023-05-01T00:00:00',
+ endTime: '2023-05-07T23:59:59',
+ createTime: '2023-04-25T10:00:00',
+ status: '已过期'
+ },
+ {
+ id: 2,
+ name: '端午节活动',
+ discountType: 'HALF',
+ startTime: '2023-06-10T00:00:00',
+ endTime: '2023-06-15T23:59:59',
+ createTime: '2023-05-30T10:00:00',
+ status: '已过期'
+ }
+ ];
+
+ const mockCurrentDiscount = {
+ id: 3,
+ name: '国庆活动',
+ discountType: 'DOUBLE',
+ startTime: '2023-10-01T00:00:00',
+ endTime: '2023-10-07T23:59:59',
+ createTime: '2023-09-25T10:00:00',
+ status: '进行中'
+ };
+
+ const renderAdminister = () => {
+ return render(
+ <MemoryRouter>
+ <Administer />
+ </MemoryRouter>
+ );
+ };
+
+ test('renders Administer component with user tab by default', async () => {
+ axios.get.mockResolvedValueOnce({
+ data: {
+ code: 200,
+ data: { data: mockUsers }
+ }
+ });
+
+ renderAdminister();
+
+ expect(screen.getByText('系统管理')).toBeInTheDocument();
+ expect(screen.getByText('用户管理')).toBeInTheDocument();
+ expect(screen.getByText('折扣管理')).toBeInTheDocument();
+
+ await waitFor(() => {
+ expect(screen.getByText('user1')).toBeInTheDocument();
+ });
+
+ expect(screen.getByText('admin1')).toBeInTheDocument();
+ });
+
+ test('switches between user and discount tabs', async () => {
+ axios.get
+ .mockResolvedValueOnce({
+ data: {
+ code: 200,
+ data: { data: mockUsers }
+ }
+ })
+ .mockResolvedValueOnce({
+ data: {
+ code: 200,
+ data: { data: mockDiscounts }
+ }
+ })
+ .mockResolvedValueOnce({
+ data: {
+ code: 200,
+ data: { data: mockCurrentDiscount }
+ }
+ });
+
+ renderAdminister();
+
+ await waitFor(() => {
+ expect(screen.getByText('user1')).toBeInTheDocument();
+ });
+
+ fireEvent.click(screen.getByText('折扣管理'));
+
+ await waitFor(() => {
+ expect(screen.getByText('五一活动')).toBeInTheDocument();
+ });
+
+ expect(screen.getByText('国庆活动')).toBeInTheDocument();
+ });
+
+
+
+ test('changes user authority', async () => {
+ axios.get.mockResolvedValueOnce({
+ data: {
+ code: 200,
+ data: { data: mockUsers }
+ }
+ });
+ axios.put.mockResolvedValueOnce({
+ data: {
+ code: 200,
+ message: '修改用户权限成功'
+ }
+ });
+
+ renderAdminister();
+
+ await waitFor(() => {
+ expect(screen.getByText('user1')).toBeInTheDocument();
+ });
+
+ const selectElement = screen.getAllByRole('combobox')[0];
+ fireEvent.change(selectElement, { target: { value: 'ADMIN' } });
+
+ await waitFor(() => {
+ expect(axios.put).toHaveBeenCalled();
+ });
+
+ expect(axios.put).toHaveBeenCalledWith(
+ expect.stringContaining('/user/changeAuthority'),
+ {
+ changeUsername: 'user1',
+ authority: 'ADMIN'
+ },
+ expect.any(Object)
+ );
+ });
+
+
+
+ test('shows error messages', async () => {
+ axios.get.mockRejectedValueOnce(new Error('Network Error'));
+
+ renderAdminister();
+
+ await waitFor(() => {
+ expect(screen.getByText(/获取用户列表失败/)).toBeInTheDocument();
+ });
+ });
+});
\ No newline at end of file
diff --git a/src/components/AuthForm.css b/src/components/AuthForm.css
index 8eb1d0a..0348f50 100644
--- a/src/components/AuthForm.css
+++ b/src/components/AuthForm.css
@@ -7,55 +7,58 @@
min-height: 100vh;
background-color: #f5f5f5;
padding: 20px;
- }
-
- .auth-title {
+}
+
+.auth-title {
color: #333;
font-size: 2.5rem;
margin-bottom: 2rem;
text-align: center;
- }
-
- .auth-form-wrapper {
+}
+
+.auth-form-wrapper {
width: 100%;
max-width: 400px;
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
- }
-
- .auth-form {
+}
+
+.auth-form {
display: flex;
flex-direction: column;
gap: 1rem;
- }
-
- .auth-form h2 {
+}
+
+.auth-form h2 {
color: #333;
text-align: center;
margin-bottom: 1.5rem;
- }
-
- .form-group {
+}
+
+.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
- }
-
- .form-group input {
+}
+
+.form-group input {
padding: 0.8rem;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 1rem;
- }
-
- .form-group input:focus {
+ width: 100%;
+ box-sizing: border-box; /* 确保padding不会影响宽度 */
+ height: 42px; /* 固定高度 */
+}
+
+.form-group input:focus {
outline: none;
border-color: #007bff;
- }
-
- .auth-button {
+}
+
+.auth-button {
padding: 0.8rem;
background-color: #007bff;
color: white;
@@ -64,18 +67,19 @@
font-size: 1rem;
cursor: pointer;
transition: background-color 0.3s;
- }
-
- .auth-button:hover {
+ height: 42px; /* 与输入框高度一致 */
+}
+
+.auth-button:hover {
background-color: #0056b3;
- }
-
- .auth-button:disabled {
+}
+
+.auth-button:disabled {
background-color: #cccccc;
cursor: not-allowed;
- }
-
- .auth-switch {
+}
+
+.auth-switch {
display: flex;
justify-content: center;
align-items: center;
@@ -83,9 +87,9 @@
margin-top: 1rem;
font-size: 0.9rem;
color: #666;
- }
-
- .switch-button {
+}
+
+.switch-button {
background: none;
border: none;
color: #007bff;
@@ -93,17 +97,17 @@
text-decoration: underline;
font-size: 0.9rem;
padding: 0;
- }
-
- .switch-button:hover {
+}
+
+.switch-button:hover {
color: #0056b3;
- }
-
- .error-message {
+}
+
+.error-message {
color: #dc3545;
background-color: #f8d7da;
padding: 0.5rem;
border-radius: 4px;
text-align: center;
margin-bottom: 1rem;
- }
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/src/components/Dashboard.css b/src/components/Dashboard.css
index af9e421..09ab88f 100644
--- a/src/components/Dashboard.css
+++ b/src/components/Dashboard.css
@@ -646,4 +646,73 @@
z-index: 100;
padding: 15px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
+}
+
+.admin-center-btn {
+ padding: 5px 10px;
+ margin-right: 10px;
+ background-color: #ff5722;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 14px;
+}
+
+.admin-center-btn:hover {
+ background-color: #e64a19;
+}
+
+/* 区域搜索容器样式 */
+.section-search-container {
+ display: flex;
+ margin-bottom: 20px;
+ gap: 10px;
+}
+
+.section-search-input {
+ flex: 1;
+ padding: 10px 15px;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ font-size: 1rem;
+}
+
+.section-search-button {
+ padding: 10px 20px;
+ background-color: #007bff;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: background-color 0.3s;
+}
+
+.section-search-button:hover {
+ background-color: #0056b3;
+}
+
+/* 分享区顶部操作栏 */
+.share-top-actions {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 20px;
+ gap: 20px;
+}
+
+/* 上传按钮样式 */
+.upload-btn {
+ padding: 10px 20px;
+ background-color: #28a745;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ transition: background-color 0.3s;
+ white-space: nowrap;
+}
+
+.upload-btn:hover {
+ background-color: #218838;
}
\ No newline at end of file
diff --git a/src/components/Dashboard.jsx b/src/components/Dashboard.jsx
index 941210c..97a01cc 100644
--- a/src/components/Dashboard.jsx
+++ b/src/components/Dashboard.jsx
@@ -1,940 +1,941 @@
-import React, { useEffect, useState } from 'react';
-import { useNavigate, useLocation,useParams } from 'react-router-dom';
-// import { getUserInfo } from '../api'; // 保留import但注释掉
-import { createTorrent, getTorrents} from '../api/torrent';
+import 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 './Dashboard.css';
-import { createPost, getPosts } from '../api/helpPost';
+import {createPost, getPosts} from '../api/helpPost';
-const Dashboard = ({ onLogout }) => {
- const location = useLocation();
- const [userInfo, setUserInfo] = useState(null);
- const [loading, setLoading] = useState(false);
- const [error, setError] = useState('');
- const [currentSlide, setCurrentSlide] = useState(0);
- const navigate = useNavigate();
- const {tab} = useParams();
- const [showUploadModal, setShowUploadModal] = useState(false);
- const [isUploading, setIsUploading] = useState(false);
- const [uploadData, setUploadData] = useState({
- name: '',
- type: '',
- region: '',
- subtitle: '',
- resolution: '', // 新增分辨率字段
- file: null,
- description: ''
- });
- const [showPostModal, setShowPostModal] = useState(false);
- const [postTitle, setPostTitle] = useState('');
- const [postContent, setPostContent] = useState('');
- const [selectedImage, setSelectedImage] = useState(null);
- const [helpPosts, setHelpPosts] = useState([]);
- const [helpLoading, setHelpLoading] = useState(false);
- const [helpError, setHelpError] = useState(null);
- const [currentPage, setCurrentPage] = useState(1);
- const [totalPages, setTotalPages] = useState(1);
+const Dashboard = ({onLogout}) => {
+ const location = useLocation();
+ const [userInfo, setUserInfo] = useState(null);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState('');
+ const [currentSlide, setCurrentSlide] = useState(0);
+ const navigate = useNavigate();
+ const {tab} = useParams();
+ const [showUploadModal, setShowUploadModal] = useState(false);
+ const [isUploading, setIsUploading] = useState(false);
+ const [uploadData, setUploadData] = useState({
+ name: '',
+ type: '',
+ region: '',
+ subtitle: '',
+ resolution: '', // 新增分辨率字段
+ file: null,
+ description: ''
+ });
+ const [showPostModal, setShowPostModal] = useState(false);
+ const [postTitle, setPostTitle] = useState('');
+ const [postContent, setPostContent] = useState('');
+ const [selectedImage, setSelectedImage] = useState(null);
+ const [helpPosts, setHelpPosts] = useState([]);
+ const [helpLoading, setHelpLoading] = useState(false);
+ const [helpError, setHelpError] = useState(null);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [totalPages, setTotalPages] = useState(1);
// 添加状态
- const [torrentPosts, setTorrentPosts] = useState([]);
- const [torrentLoading, setTorrentLoading] = useState(false);
- const [torrentError, setTorrentError] = useState(null);
-
- const [filteredResources, setFilteredResources] = useState(torrentPosts);
-
- const activeTab = tab || 'announcement'; // 如果没有tab参数,则默认为announcement
- // 从location.state中初始化状态
-
-
- const handleTabChange = (tabName) => {
- navigate(`/dashboard/${tabName}`, {
- state: {
- savedFilters: selectedFilters, // 使用新的筛选状态 // 保留现有状态
- activeTab: tabName // 可选,如果其他组件依赖这个 state
- }
- });
- };
-
- //公告区
- const [announcements] = useState([
- {
- id: 1,
- title: '系统维护与更新',
- content: '2023-10-15 02:00-06:00将进行系统维护升级,期间网站将无法访问。本次更新包含:\n1. 数据库服务器迁移\n2. 安全补丁更新\n3. CDN节点优化\n\n请提前做好下载安排。',
- author: '系统管理员',
- date: '2023-10-10',
- excerpt: '2023-10-15 02:00-06:00将进行系统维护,期间无法访问',
- category: '系统'
- },
- {
- id: 2,
- title: '资源上新',
- content: '最新热门电影《奥本海默》4K REMUX资源已上线,包含:\n- 4K HDR版本 (56.8GB)\n- 1080P标准版 (12.3GB)\n- 中英双语字幕\n\n欢迎下载保种!',
- author: '资源组',
- date: '2023-10-08',
- excerpt: '最新热门电影《奥本海默》4K资源已上线',
- category: '资源'
- },
- {
- id: 3,
- title: '积分规则调整',
- content: '自11月1日起,上传资源积分奖励提升20%,具体规则如下:\n- 上传电影资源:每GB 10积分\n- 上传电视剧资源:每GB 8积分\n- 上传动漫资源:每GB 6积分\n\n感谢大家的支持与贡献!',
- author: '管理员',
- date: '2023-10-05',
- excerpt: '自11月1日起,上传资源积分奖励提升20%',
- category: '公告'
- },
- {
- id: 4,
- title: '违规处理公告',
- content: '用户user123因发布虚假资源已被封禁,相关资源已删除。请大家遵守社区规则,维护良好的分享环境。',
- author: '管理员',
- date: '2023-10-03',
- excerpt: '用户user123因发布虚假资源已被封禁',
- category: '违规'
- },
- {
- id: 5,
- title: '节日活动',
- content: '国庆期间所有资源下载积分减半,活动时间:2023年10月1日至2023年10月7日。',
- author: '活动组',
- date: '2023-09-30',
- excerpt: '国庆期间所有资源下载积分减半',
- category: '活动'
- },
- {
- id: 6,
- title: '客户端更新',
- content: 'PT客户端v2.5.0已发布,修复多个BUG,新增资源搜索功能。请尽快更新到最新版本以获得更好的使用体验。',
- author: '开发组',
- date: '2023-09-28',
- excerpt: 'PT客户端v2.5.0已发布,修复多个BUG',
- category: '更新'
- },
- // 其他公告...
- ]);
+ const [torrentPosts, setTorrentPosts] = useState([]);
+ const [torrentLoading, setTorrentLoading] = useState(false);
+ const [torrentError, setTorrentError] = useState(null);
-
- const handleAnnouncementClick = (announcement,e) => {
- if (!e.target.closest('.exclude-click')) {
- navigate(`/announcement/${announcement.id}`, {
- state: {
- announcement,
- returnPath: `/dashboard/${activeTab}`,
- scrollPosition: window.scrollY,
- activeTab
+ const [filteredResources, setFilteredResources] = useState(torrentPosts);
+ const [isAdmin, setIsAdmin] = useState(false);
+
+
+ const activeTab = tab || 'announcement'; // 如果没有tab参数,则默认为announcement
+ // 从location.state中初始化状态
+
+
+ const handleTabChange = (tabName) => {
+ navigate(`/dashboard/${tabName}`, {
+ state: {
+ savedFilters: selectedFilters, // 使用新的筛选状态 // 保留现有状态
+ activeTab: tabName // 可选,如果其他组件依赖这个 state
+ }
+ });
+ };
+
+ //公告区
+ const [announcements] = useState([
+ {
+ id: 1,
+ title: '系统维护与更新',
+ content: '2023-10-15 02:00-06:00将进行系统维护升级,期间网站将无法访问。本次更新包含:\n1. 数据库服务器迁移\n2. 安全补丁更新\n3. CDN节点优化\n\n请提前做好下载安排。',
+ author: '系统管理员',
+ date: '2023-10-10',
+ excerpt: '2023-10-15 02:00-06:00将进行系统维护,期间无法访问',
+ category: '系统'
+ },
+ {
+ id: 2,
+ title: '资源上新',
+ content: '最新热门电影《奥本海默》4K REMUX资源已上线,包含:\n- 4K HDR版本 (56.8GB)\n- 1080P标准版 (12.3GB)\n- 中英双语字幕\n\n欢迎下载保种!',
+ author: '资源组',
+ date: '2023-10-08',
+ excerpt: '最新热门电影《奥本海默》4K资源已上线',
+ category: '资源'
+ },
+ {
+ id: 3,
+ title: '积分规则调整',
+ content: '自11月1日起,上传资源积分奖励提升20%,具体规则如下:\n- 上传电影资源:每GB 10积分\n- 上传电视剧资源:每GB 8积分\n- 上传动漫资源:每GB 6积分\n\n感谢大家的支持与贡献!',
+ author: '管理员',
+ date: '2023-10-05',
+ excerpt: '自11月1日起,上传资源积分奖励提升20%',
+ category: '公告'
+ },
+ {
+ id: 4,
+ title: '违规处理公告',
+ content: '用户user123因发布虚假资源已被封禁,相关资源已删除。请大家遵守社区规则,维护良好的分享环境。',
+ author: '管理员',
+ date: '2023-10-03',
+ excerpt: '用户user123因发布虚假资源已被封禁',
+ category: '违规'
+ },
+ {
+ id: 5,
+ title: '节日活动',
+ content: '国庆期间所有资源下载积分减半,活动时间:2023年10月1日至2023年10月7日。',
+ author: '活动组',
+ date: '2023-09-30',
+ excerpt: '国庆期间所有资源下载积分减半',
+ category: '活动'
+ },
+ {
+ id: 6,
+ title: '客户端更新',
+ content: 'PT客户端v2.5.0已发布,修复多个BUG,新增资源搜索功能。请尽快更新到最新版本以获得更好的使用体验。',
+ author: '开发组',
+ date: '2023-09-28',
+ excerpt: 'PT客户端v2.5.0已发布,修复多个BUG',
+ category: '更新'
+ },
+ // 其他公告...
+ ]);
+
+
+ const handleAnnouncementClick = (announcement, e) => {
+ if (!e.target.closest('.exclude-click')) {
+ navigate(`/announcement/${announcement.id}`, {
+ state: {
+ announcement,
+ returnPath: `/dashboard/${activeTab}`,
+ scrollPosition: window.scrollY,
+ activeTab
+ }
+ });
}
- });
- }
- };
+ };
- //资源区
-
+ //资源区
+ const handleFileChange = (e) => {
+ setUploadData({...uploadData, file: e.target.files[0]});
+ };
- const handleFileChange = (e) => {
- setUploadData({...uploadData, file: e.target.files[0]});
- };
+ const handleUploadSubmit = async (e) => {
+ e.preventDefault();
+ try {
+ setIsUploading(true);
- const handleUploadSubmit = async (e) => {
- e.preventDefault();
-
- try {
- setIsUploading(true);
- const username = localStorage.getItem('username');
-
- // 创建种子数据
- const torrentData = {
- torrentName: uploadData.name,
- category: uploadData.type,
- regin: uploadData.region,
- resolution: uploadData.resolution,
- subtitle: uploadData.subtitle,
- description: uploadData.description,
- username: username,
- filePath: "D:\\大学\\大三_下\\实训BT服务器下载\\G2-ptPlatform-backend\\torrents\\[电影天堂www.dytt89.com]两杆大烟枪BD中英双字.mp4.torrent" // 默认路径
- };
-
- // 调用API创建种子
- await createTorrent(
- torrentData.torrentName,
- torrentData.description,
- torrentData.username,
- torrentData.category,
- torrentData.regin,
- torrentData.resolution,
- torrentData.subtitle,
- torrentData.filePath
- );
-
- // 上传成功处理
- setShowUploadModal(false);
- setUploadData({
- name: '',
- type: '',
- region: '',
- subtitle: '',
- resolution: '',
- file: null,
- description: ''
- });
- alert('种子创建成功!');
-
- // 刷新列表
- fetchTorrentPosts(currentPage);
-
- } catch (error) {
- console.error('创建失败:', error);
- alert('创建失败: ' + (error.response?.data?.message || error.message));
- } finally {
- setIsUploading(false);
- }
- };
+ const torrentData = {
+ torrentName: uploadData.name,
+ description: uploadData.description,
+ category: uploadData.type,
+ region: uploadData.region,
+ resolution: uploadData.resolution,
+ subtitle: uploadData.subtitle
+ };
- const handlePostSubmit = async (e) => {
- e.preventDefault();
- try {
- const username = localStorage.getItem('username');
- const response = await createPost(
- postTitle,
- postContent,
- username
- );
-
- if (response.data.code === 200) {
- // 刷新帖子列表
- await fetchHelpPosts();
- // 重置表单
- setShowPostModal(false);
- setPostTitle('');
- setPostContent('');
- setSelectedImage(null);
- } else {
- setHelpError(response.data.message || '发帖失败');
- }
- } catch (err) {
- setHelpError(err.message || '发帖失败');
- }
- };
+ await createTorrent(torrentData, uploadData.file);
+
+ // 上传成功处理
+ setShowUploadModal(false);
+ setUploadData({
+ name: '',
+ type: '',
+ region: '',
+ subtitle: '',
+ resolution: '',
+ file: null,
+ description: ''
+ });
+ alert('种子创建成功!');
+
+ // 刷新列表
+ await fetchTorrentPosts(currentPage);
+
+ } catch (error) {
+ console.error('创建失败:', error);
+ alert('创建失败: ' + (error.response?.data?.message || error.message));
+ } finally {
+ setIsUploading(false);
+ }
+ };
+
+ const handlePostSubmit = async (e) => {
+ e.preventDefault();
+ try {
+ const username = localStorage.getItem('username');
+ const response = await createPost(
+ postTitle,
+ postContent,
+ username
+ );
+
+ if (response.data.code === 200) {
+ // 刷新帖子列表
+ await fetchHelpPosts();
+ // 重置表单
+ setShowPostModal(false);
+ setPostTitle('');
+ setPostContent('');
+ setSelectedImage(null);
+ } else {
+ setHelpError(response.data.message || '发帖失败');
+ }
+ } catch (err) {
+ setHelpError(err.message || '发帖失败');
+ }
+ };
// 获取Torrent帖子列表
- const fetchTorrentPosts = async (page = 1) => {
- setTorrentLoading(true);
- try {
- const response = await getTorrents(page);
- setTorrentPosts(response.data.data.records);
- setTotalPages(Math.ceil(response.data.data.total / 5)); // 假设每页5条
- setCurrentPage(page);
- } catch (err) {
- setTorrentError(err.message);
- } finally {
- setTorrentLoading(false);
- }
-};
+ const fetchTorrentPosts = async (page = 1) => {
+ setTorrentLoading(true);
+ try {
+ const response = await getTorrents(page);
+ setTorrentPosts(response.data.data.records);
+ setTotalPages(Math.ceil(response.data.data.total / 5)); // 假设每页5条
+ setCurrentPage(page);
+ } catch (err) {
+ setTorrentError(err.message);
+ } finally {
+ setTorrentLoading(false);
+ }
+ };
- // 在useEffect中调用
- useEffect(() => {
- if (activeTab === 'share') {
- fetchTorrentPosts();
- }
- }, [activeTab]);
+ // 在useEffect中调用
+ useEffect(() => {
+ if (activeTab === 'share') {
+ fetchTorrentPosts();
+ }
+ }, [activeTab]);
- const handleImageUpload = (e) => {
- setSelectedImage(e.target.files[0]);
- };
+ const handleImageUpload = (e) => {
+ setSelectedImage(e.target.files[0]);
+ };
+ const fetchHelpPosts = async (page = 1) => {
+ setHelpLoading(true);
+ try {
+ const response = await getPosts(page);
+ if (response.data.code === 200) {
+ setHelpPosts(response.data.data.records);
+ setTotalPages(Math.ceil(response.data.data.total / 5)); // 假设每页5条
+ setCurrentPage(page);
+ } else {
+ setHelpError(response.data.message || '获取求助帖失败');
+ }
+ } catch (err) {
+ setHelpError(err.message || '获取求助帖失败');
+ } finally {
+ setHelpLoading(false);
+ }
+ };
+ useEffect(() => {
+ if (activeTab === 'help') {
+ fetchHelpPosts(currentPage);
+ }
+ }, [activeTab, currentPage]); // 添加 currentPage 作为依赖
- const fetchHelpPosts = async (page = 1) => {
- setHelpLoading(true);
- try {
- const response = await getPosts(page);
- if (response.data.code === 200) {
- setHelpPosts(response.data.data.records);
- setTotalPages(Math.ceil(response.data.data.total / 5)); // 假设每页5条
- setCurrentPage(page);
- } else {
- setHelpError(response.data.message || '获取求助帖失败');
- }
- } catch (err) {
- setHelpError(err.message || '获取求助帖失败');
- } finally {
- setHelpLoading(false);
- }
- };
- useEffect(() => {
- if (activeTab === 'help') {
- fetchHelpPosts(currentPage);
- }
- }, [activeTab, currentPage]); // 添加 currentPage 作为依赖
+ // 分类维度配置
+ const filterCategories = {
+ type: {
+ label: '类型',
+ options: {
+ 'all': '全部',
+ '电影': '电影',
+ '电视剧': '电视剧',
+ '动漫': '动漫',
+ '综艺': '综艺'
+ }
+ },
+ subtitle: {
+ label: '字幕',
+ options: {
+ 'all': '全部',
+ 'yes': '有字幕',
+ 'no': '无字幕'
+ }
+ },
+ region: {
+ label: '地区',
+ options: {
+ 'all': '全部',
+ 'cn': '大陆',
+ 'us': '欧美',
+ 'jp': '日本'
+ }
+ }
+ };
-
-
-
- // 分类维度配置
- const filterCategories = {
- type: {
- label: '类型',
- options: {
- 'all': '全部',
- '电影': '电影',
- '电视剧': '电视剧',
- '动漫': '动漫',
- '综艺': '综艺'
- }
- },
- subtitle: {
- label: '字幕',
- options: {
- 'all': '全部',
- 'yes': '有字幕',
- 'no': '无字幕'
- }
- },
- region: {
- label: '地区',
- options: {
- 'all': '全部',
- 'cn': '大陆',
- 'us': '欧美',
- 'jp': '日本'
- }
- }
- };
-
- const [selectedFilters, setSelectedFilters] = useState(
- location.state?.savedFilters ||
- Object.keys(filterCategories).reduce((acc, category) => {
- acc[category] = 'all';
- return acc;
- }, {})
- );
-
+ const [selectedFilters, setSelectedFilters] = useState(
+ location.state?.savedFilters ||
+ Object.keys(filterCategories).reduce((acc, category) => {
+ acc[category] = 'all';
+ return acc;
+ }, {})
+ );
// 处理筛选条件变更
-const handleFilterSelect = (category, value) => {
- setSelectedFilters(prev => ({
- ...prev,
- [category]: prev[category] === value ? null : value // 点击已选中的则取消
- }));
-};
-
-//应用筛选条件
-const applyFilters = () => {
- const result = torrentPosts.filter(resource => {
- return Object.entries(selectedFilters).every(([category, selectedValue]) => {
- if (selectedValue === 'all') return true;
- if (category === 'subtitle') {
- return resource.subtitle === (selectedValue === 'yes');
- }
- return resource[category] === selectedValue;
- });
- });
- setFilteredResources(result);
-};
-
-
- // 恢复滚动位置
- useEffect(() => {
- if (location.state?.scrollPosition) {
- window.scrollTo(0, location.state.scrollPosition);
- }
- }, [location.state]);
-
-
- useEffect(() => {
- const token = localStorage.getItem('token');
- if (!token) {
- navigate('/login');
- return;
- }
-
- /* 保留但注释掉实际的用户信息获取
- const fetchUserInfo = async () => {
- try {
- const response = await getUserInfo(token);
- if (response.data.code === 200) {
- setUserInfo(response.data.data);
- } else {
- setError('获取用户信息失败');
- }
- } catch (err) {
- setError('获取用户信息失败');
- } finally {
- setLoading(false);
- }
+ const handleFilterSelect = (category, value) => {
+ setSelectedFilters(prev => ({
+ ...prev,
+ [category]: prev[category] === value ? null : value // 点击已选中的则取消
+ }));
};
- fetchUserInfo();
- */
-
- // 模拟用户信息
- setUserInfo({
- name: localStorage.getItem('username') || '演示用户', // 确保这里读取的是最新值
- avatar: 'https://via.placeholder.com/40'
- });
- setLoading(false);
- }, [navigate]);
-
- // 轮播图自动切换效果
- useEffect(() => {
- if (activeTab === 'announcement') {
- const timer = setInterval(() => {
- setCurrentSlide(prev => (prev + 1) % 3); // 3张轮播图循环
- }, 3000);
- return () => clearInterval(timer);
- }
- }, [activeTab]);
-
- useEffect(() => {
- if (activeTab === 'help') {
- fetchHelpPosts();
- }
- }, [activeTab]);
-
- const renderContent = () => {
- switch(activeTab) {
- case 'announcement':
- return (
- <div className="content-area" data-testid="announcement-section">
- {/* 轮播图区域 */}
- <div className="carousel-container">
- <div className={`carousel-slide ${currentSlide === 0 ? 'active' : ''}`}>
- <div className="carousel-image gray-bg">促销活动1</div>
- </div>
- <div className={`carousel-slide ${currentSlide === 1 ? 'active' : ''}`}>
- <div className="carousel-image gray-bg">促销活动2</div>
- </div>
- <div className={`carousel-slide ${currentSlide === 2 ? 'active' : ''}`}>
- <div className="carousel-image gray-bg">促销活动3</div>
- </div>
- <div className="carousel-dots">
- <span className={`dot ${currentSlide === 0 ? 'active' : ''}`}></span>
- <span className={`dot ${currentSlide === 1 ? 'active' : ''}`}></span>
- <span className={`dot ${currentSlide === 2 ? 'active' : ''}`}></span>
- </div>
- </div>
-
- {/* 公告区块区域 */}
- <div className="announcement-grid">
- {announcements.map(announcement => (
- <div
- key={announcement.id}
- className="announcement-card"
- onClick={(e) => handleAnnouncementClick(announcement, e)}
- >
- <h3>{announcement.title}</h3>
- <p>{announcement.excerpt}</p>
- <div className="announcement-footer exclude-click">
- <span>{announcement.author}</span>
- <span>{announcement.date}</span>
- </div>
- </div>
- ))}
- </div>
- </div>
- );
- case 'share':
- return (
- <div className="content-area" data-testid="share-section">
- {/* 上传按钮 - 添加在筛选区上方 */}
- <div className="upload-header">
- <button
- className="upload-btn"
- onClick={() => setShowUploadModal(true)}
- >
- + 上传种子
- </button>
- </div>
- {/* 分类筛选区 */}
- <div className="filter-section">
- {Object.entries(filterCategories).map(([category, config]) => (
- <div key={category} className="filter-group">
- <h4>{config.label}:</h4>
- <div className="filter-options">
- {Object.entries(config.options).map(([value, label]) => (
- <button
- key={value}
- className={`filter-btn ${
- selectedFilters[category] === value ? 'active' : ''
- }`}
- onClick={() => handleFilterSelect(category, value)}
- >
- {label}
- </button>
- ))}
- </div>
- </div>
- ))}
-
- <button
- className="confirm-btn"
- onClick={applyFilters}
- >
- 确认筛选
- </button>
- </div>
-
- {/* 上传弹窗 */}
- {showUploadModal && (
- <div className="modal-overlay">
- <div className="upload-modal">
- <h3>上传新种子</h3>
- <button
- className="close-btn"
- onClick={() => setShowUploadModal(false)}
- >
- ×
- </button>
-
- <form onSubmit={handleUploadSubmit}>
- <div className="form-group">
- <label>种子名称</label>
- <input
- type="text"
- value={uploadData.name}
- onChange={(e) => setUploadData({...uploadData, name: e.target.value})}
- required
- />
- </div>
-
- <div className="form-group">
- <label>资源类型</label>
- <select
- value={uploadData.type}
- onChange={(e) => setUploadData({...uploadData, type: e.target.value})}
- required
- >
- <option value="">请选择</option>
- <option value="电影">电影</option>
- <option value="电视剧">电视剧</option>
- <option value="动漫">动漫</option>
- <option value="综艺">综艺</option>
- <option value="音乐">音乐</option>
- </select>
- </div>
-
- {/* 新增地区输入框 */}
- <div className="form-group">
- <label>地区</label>
- <input
- type="text"
- value={uploadData.region || ''}
- onChange={(e) => setUploadData({...uploadData, region: e.target.value})}
- placeholder="例如: 美国, 中国, 日本等"
- required
- />
- </div>
-
- {/* 添加分辨率下拉框 */}
- <div className="form-group">
- <label>分辨率</label>
- <select
- value={uploadData.resolution || ''}
- onChange={(e) => setUploadData({...uploadData, resolution: e.target.value})}
- required
- >
- <option value="">请选择</option>
- <option value="4K">4K</option>
- <option value="2K">2K</option>
- <option value="1080P">1080P</option>
- <option value="720P">720P</option>
- <option value="SD">SD</option>
- <option value="无损音源">无损音源</option>
- <option value="杜比全景声">杜比全景声</option>
- <option value="其他">其他</option>
- </select>
- </div>
-
-
-
- {/* 新增字幕语言下拉框 */}
- <div className="form-group">
- <label>字幕语言</label>
- <select
- value={uploadData.subtitle || ''}
- onChange={(e) => setUploadData({...uploadData, subtitle: e.target.value})}
- required
- >
- <option value="">请选择</option>
- <option value="无需字幕">无需字幕</option>
- <option value="暂无字幕">暂无字幕</option>
- <option value="自带中文字幕">自带中文字幕</option>
- <option value="自带双语字幕(含中文)">自带双语字幕(含中文)</option>
- <option value="附件中文字幕">附件中文字幕</option>
- <option value="附件双语字幕">附件双语字幕</option>
- </select>
- </div>
-
- <div className="form-group">
- <label>种子文件</label>
- <input
- type="file"
- accept=".torrent"
- onChange={handleFileChange}
-
- />
- </div>
-
- <div className="form-group">
- <label>简介</label>
- <textarea
- value={uploadData.description}
- onChange={(e) => setUploadData({...uploadData, description: e.target.value})}
- />
- </div>
-
- <div className="form-actions">
- <button
- type="button"
- className="cancel-btn"
- onClick={() => setShowUploadModal(false)}
- >
- 取消
- </button>
- <button
- type="submit"
- className="confirm-btn"
- disabled={isUploading}
- >
- {isUploading ? '上传中...' : '确认上传'}
- </button>
- </div>
- </form>
- </div>
- </div>
- )}
-
- <div className="resource-list">
- {torrentPosts.map(torrent => (
- <div
- key={torrent.id}
- className="resource-item"
- onClick={() => navigate(`/torrent/${torrent.id}`)}
- >
- <div className="resource-poster">
- <div className="poster-image gray-bg">{torrent.torrentName.charAt(0)}</div>
- </div>
- <div className="resource-info">
- <h3 className="resource-title">{torrent.torrentName}</h3>
- <p className="resource-meta">
- {torrent.resolution} | {torrent.region} | {torrent.category}
- </p>
- <p className="resource-subtitle">字幕: {torrent.subtitle}</p>
- </div>
- <div className="resource-stats">
- <span className="stat">{torrent.size}</span>
- <span className="stat">发布者: {torrent.username}</span>
- </div>
- <button
- className="download-btn"
- onClick={(e) => {
- e.stopPropagation();
- // 下载逻辑
- }}
- >
- 立即下载
- </button>
- </div>
- ))}
- </div>
-
- {/* 分页控件 */}
- <div className="pagination">
- <button
- onClick={() => fetchTorrentPosts(currentPage - 1)}
- disabled={currentPage === 1}
- >
- 上一页
- </button>
-
- {Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
- <button
- key={page}
- onClick={() => fetchTorrentPosts(page)}
- className={currentPage === page ? 'active' : ''}
- >
- {page}
- </button>
- ))}
-
- <button
- onClick={() => fetchTorrentPosts(currentPage + 1)}
- disabled={currentPage === totalPages}
- >
- 下一页
- </button>
- </div>
- </div>
- );
- // 在Dashboard.jsx的renderContent函数中修改case 'request'部分
- case 'request':
- return (
- <div className="content-area" data-testid="request-section">
- {/* 求种区帖子列表 */}
- <div className="request-list">
- {[
- {
- id: 1,
- title: '求《药屋少女的呢喃》第二季全集',
- content: '求1080P带中文字幕版本,最好是内嵌字幕不是外挂的',
- author: '动漫爱好者',
- authorAvatar: 'https://via.placeholder.com/40',
- date: '2023-10-15',
- likeCount: 24,
- commentCount: 8
- },
- {
- id: 2,
- title: '求《奥本海默》IMAX版',
- content: '最好是原盘或者高码率的版本,谢谢各位大佬',
- author: '电影收藏家',
- authorAvatar: 'https://via.placeholder.com/40',
- date: '2023-10-14',
- likeCount: 15,
- commentCount: 5
+//应用筛选条件
+ 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');
}
- ].map(post => (
- <div
- key={post.id}
- className="request-post"
- onClick={() => navigate(`/request/${post.id}`)}
- >
- <div className="post-header">
- <img src={post.authorAvatar} alt={post.author} className="post-avatar" />
- <div className="post-author">{post.author}</div>
- <div className="post-date">{post.date}</div>
- </div>
- <h3 className="post-title">{post.title}</h3>
- <p className="post-content">{post.content}</p>
- <div className="post-stats">
- <span className="post-likes">👍 {post.likeCount}</span>
- <span className="post-comments">💬 {post.commentCount}</span>
- </div>
- </div>
- ))}
- </div>
- </div>
- );
- // 在Dashboard.jsx的renderContent函数中修改case 'help'部分
- case 'help':
- return (
- <div className="content-area" data-testid="help-section">
- {/* 新增发帖按钮 */}
- <div className="post-header">
- <button
- className="create-post-btn"
- onClick={() => setShowPostModal(true)}
- >
- + 发帖求助
- </button>
- </div>
+ return resource[category] === selectedValue;
+ });
+ });
+ setFilteredResources(result);
+ };
- {/* 加载状态和错误提示 */}
- {helpLoading && <div className="loading">加载中...</div>}
- {helpError && <div className="error">{helpError}</div>}
- {/* 求助区帖子列表 */}
- <div className="help-list">
- {helpPosts.map(post => (
- <div
- key={post.id}
- className={`help-post ${post.isSolved ? 'solved' : ''}`}
- onClick={() => navigate(`/help/${post.id}`)}
- >
- <div className="post-header">
- <img
- src={post.authorAvatar || 'https://via.placeholder.com/40'}
- alt={post.authorId}
- className="post-avatar"
- />
- <div className="post-author">{post.authorId}</div>
- <div className="post-date">
- {new Date(post.createTime).toLocaleDateString()}
- </div>
- {post.isSolved && <span className="solved-badge">已解决</span>}
- </div>
- <h3 className="post-title">{post.title}</h3>
- <p className="post-content">{post.content}</p>
- <div className="post-stats">
- <span className="post-likes">👍 {post.likeCount || 0}</span>
- <span className="post-comments">💬 {post.replyCount || 0}</span>
- </div>
- </div>
- ))}
- </div>
+ // 恢复滚动位置
+ useEffect(() => {
+ if (location.state?.scrollPosition) {
+ window.scrollTo(0, location.state.scrollPosition);
+ }
+ }, [location.state]);
- {/* 在帖子列表后添加分页控件 */}
- <div className="pagination">
- <button
- onClick={() => fetchHelpPosts(currentPage - 1)}
- disabled={currentPage === 1}
- >
- 上一页
- </button>
-
- {Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (
- <button
- key={page}
- onClick={() => fetchHelpPosts(page)}
- className={currentPage === page ? 'active' : ''}
- >
- {page}
- </button>
- ))}
-
- <button
- onClick={() => fetchHelpPosts(currentPage + 1)}
- disabled={currentPage === totalPages}
- >
- 下一页
- </button>
- </div>
-
- {/* 新增发帖弹窗 */}
- {showPostModal && (
- <div className="post-modal-overlay">
- <div className="post-modal">
- <h3>发布求助帖</h3>
- <button
- className="modal-close-btn"
- onClick={() => setShowPostModal(false)}
- >
- ×
- </button>
-
- <form onSubmit={handlePostSubmit}>
- <div className="form-group">
- <label>帖子标题</label>
- <input
- type="text"
- value={postTitle}
- onChange={(e) => setPostTitle(e.target.value)}
- placeholder="请输入标题"
- required
- />
+
+ useEffect(() => {
+ const token = localStorage.getItem('token');
+ if (!token) {
+ navigate('/login');
+ return;
+ }
+
+ /* 保留但注释掉实际的用户信息获取
+ const fetchUserInfo = async () => {
+ try {
+ const response = await getUserInfo(token);
+ if (response.data.code === 200) {
+ setUserInfo(response.data.data);
+ } else {
+ setError('获取用户信息失败');
+ }
+ } catch (err) {
+ setError('获取用户信息失败');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchUserInfo();
+ */
+
+ // 模拟用户信息
+ setUserInfo({
+ name: localStorage.getItem('username') || '演示用户', // 确保这里读取的是最新值
+ avatar: 'https://via.placeholder.com/40',
+ isAdmin: true
+ });
+ setLoading(false);
+ }, [navigate]);
+
+ // 轮播图自动切换效果
+ useEffect(() => {
+ if (activeTab === 'announcement') {
+ const timer = setInterval(() => {
+ setCurrentSlide(prev => (prev + 1) % 3); // 3张轮播图循环
+ }, 3000);
+ return () => clearInterval(timer);
+ }
+ }, [activeTab]);
+
+ useEffect(() => {
+ if (activeTab === 'help') {
+ fetchHelpPosts();
+ }
+ }, [activeTab]);
+
+ const renderContent = () => {
+ switch (activeTab) {
+ case 'announcement':
+ return (
+ <div className="content-area" data-testid="announcement-section">
+ {/* 轮播图区域 */}
+ <div className="carousel-container">
+ <div className={`carousel-slide ${currentSlide === 0 ? 'active' : ''}`}>
+ <div className="carousel-image gray-bg">促销活动1</div>
+ </div>
+ <div className={`carousel-slide ${currentSlide === 1 ? 'active' : ''}`}>
+ <div className="carousel-image gray-bg">促销活动2</div>
+ </div>
+ <div className={`carousel-slide ${currentSlide === 2 ? 'active' : ''}`}>
+ <div className="carousel-image gray-bg">促销活动3</div>
+ </div>
+ <div className="carousel-dots">
+ <span className={`dot ${currentSlide === 0 ? 'active' : ''}`}></span>
+ <span className={`dot ${currentSlide === 1 ? 'active' : ''}`}></span>
+ <span className={`dot ${currentSlide === 2 ? 'active' : ''}`}></span>
+ </div>
+ </div>
+
+ {/* 公告区块区域 */}
+ <div className="announcement-grid">
+ {announcements.map(announcement => (
+ <div
+ key={announcement.id}
+ className="announcement-card"
+ onClick={(e) => handleAnnouncementClick(announcement, e)}
+ >
+ <h3>{announcement.title}</h3>
+ <p>{announcement.excerpt}</p>
+ <div className="announcement-footer exclude-click">
+ <span>{announcement.author}</span>
+ <span>{announcement.date}</span>
+ </div>
+ </div>
+ ))}
+ </div>
</div>
-
- <div className="form-group">
- <label>帖子内容</label>
- <textarea
- value={postContent}
- onChange={(e) => setPostContent(e.target.value)}
- placeholder="详细描述你的问题"
- required
- />
- </div>
-
- <div className="form-group">
- <label>上传图片</label>
- <div className="upload-image-btn">
- <input
- type="file"
- id="image-upload"
- accept="image/*"
- onChange={handleImageUpload}
- style={{ display: 'none' }}
- />
- <label htmlFor="image-upload">
- {selectedImage ? '已选择图片' : '选择图片'}
- </label>
- {selectedImage && (
- <span className="image-name">{selectedImage.name}</span>
+ );
+ case 'share':
+ return (
+ <div className="content-area" data-testid="share-section">
+ {/* 上传按钮 - 添加在筛选区上方 */}
+ <div className="upload-header">
+ <button
+ className="upload-btn"
+ onClick={() => setShowUploadModal(true)}
+ >
+ + 上传种子
+ </button>
+ </div>
+ {/* 分类筛选区 */}
+ <div className="filter-section">
+ {Object.entries(filterCategories).map(([category, config]) => (
+ <div key={category} className="filter-group">
+ <h4>{config.label}:</h4>
+ <div className="filter-options">
+ {Object.entries(config.options).map(([value, label]) => (
+ <button
+ key={value}
+ className={`filter-btn ${
+ selectedFilters[category] === value ? 'active' : ''
+ }`}
+ onClick={() => handleFilterSelect(category, value)}
+ >
+ {label}
+ </button>
+ ))}
+ </div>
+ </div>
+ ))}
+
+ <button
+ className="confirm-btn"
+ onClick={applyFilters}
+ >
+ 确认筛选
+ </button>
+ </div>
+
+ {/* 上传弹窗 */}
+ {showUploadModal && (
+ <div className="modal-overlay">
+ <div className="upload-modal">
+ <h3>上传新种子</h3>
+ <button
+ className="close-btn"
+ onClick={() => setShowUploadModal(false)}
+ >
+ ×
+ </button>
+
+ <form onSubmit={handleUploadSubmit}>
+ <div className="form-group">
+ <label>种子名称</label>
+ <input
+ type="text"
+ value={uploadData.name}
+ onChange={(e) => setUploadData({...uploadData, name: e.target.value})}
+ required
+ />
+ </div>
+
+ <div className="form-group">
+ <label>资源类型</label>
+ <select
+ value={uploadData.type}
+ onChange={(e) => setUploadData({...uploadData, type: e.target.value})}
+ required
+ >
+ <option value="">请选择</option>
+ <option value="电影">电影</option>
+ <option value="电视剧">电视剧</option>
+ <option value="动漫">动漫</option>
+ <option value="综艺">综艺</option>
+ <option value="音乐">音乐</option>
+ </select>
+ </div>
+
+ {/* 新增地区输入框 */}
+ <div className="form-group">
+ <label>地区</label>
+ <input
+ type="text"
+ value={uploadData.region || ''}
+ onChange={(e) => setUploadData({...uploadData, region: e.target.value})}
+ placeholder="例如: 美国, 中国, 日本等"
+ required
+ />
+ </div>
+
+ {/* 添加分辨率下拉框 */}
+ <div className="form-group">
+ <label>分辨率</label>
+ <select
+ value={uploadData.resolution || ''}
+ onChange={(e) => setUploadData({
+ ...uploadData,
+ resolution: e.target.value
+ })}
+ required
+ >
+ <option value="">请选择</option>
+ <option value="4K">4K</option>
+ <option value="2K">2K</option>
+ <option value="1080P">1080P</option>
+ <option value="720P">720P</option>
+ <option value="SD">SD</option>
+ <option value="无损音源">无损音源</option>
+ <option value="杜比全景声">杜比全景声</option>
+ <option value="其他">其他</option>
+ </select>
+ </div>
+
+
+ {/* 新增字幕语言下拉框 */}
+ <div className="form-group">
+ <label>字幕语言</label>
+ <select
+ value={uploadData.subtitle || ''}
+ onChange={(e) => setUploadData({
+ ...uploadData,
+ subtitle: e.target.value
+ })}
+ required
+ >
+ <option value="">请选择</option>
+ <option value="无需字幕">无需字幕</option>
+ <option value="暂无字幕">暂无字幕</option>
+ <option value="自带中文字幕">自带中文字幕</option>
+ <option value="自带双语字幕(含中文)">自带双语字幕(含中文)</option>
+ <option value="附件中文字幕">附件中文字幕</option>
+ <option value="附件双语字幕">附件双语字幕</option>
+ </select>
+ </div>
+
+ <div className="form-group">
+ <label>种子文件</label>
+ <input
+ type="file"
+ accept=".torrent"
+ onChange={handleFileChange}
+
+ />
+ </div>
+
+ <div className="form-group">
+ <label>简介</label>
+ <textarea
+ value={uploadData.description}
+ onChange={(e) => setUploadData({
+ ...uploadData,
+ description: e.target.value
+ })}
+ />
+ </div>
+
+ <div className="form-actions">
+ <button
+ type="button"
+ className="cancel-btn"
+ onClick={() => setShowUploadModal(false)}
+ >
+ 取消
+ </button>
+ <button
+ type="submit"
+ className="confirm-btn"
+ disabled={isUploading}
+ >
+ {isUploading ? '上传中...' : '确认上传'}
+ </button>
+ </div>
+ </form>
+ </div>
+ </div>
)}
- </div>
+
+ <div className="resource-list">
+ {torrentPosts.map(torrent => (
+ <div
+ key={torrent.id}
+ className="resource-item"
+ onClick={() => navigate(`/torrent/${torrent.id}`)}
+ >
+ <div className="resource-poster">
+ <div className="poster-image gray-bg">{torrent.torrentName.charAt(0)}</div>
+ </div>
+ <div className="resource-info">
+ <h3 className="resource-title">{torrent.torrentName}</h3>
+ <p className="resource-meta">
+ {torrent.resolution} | {torrent.region} | {torrent.category}
+ </p>
+ <p className="resource-subtitle">字幕: {torrent.subtitle}</p>
+ </div>
+ <div className="resource-stats">
+ <span className="stat">{torrent.size}</span>
+ <span className="stat">发布者: {torrent.username}</span>
+ </div>
+ <button
+ className="download-btn"
+ onClick={(e) => {
+ e.stopPropagation();
+ // 下载逻辑
+ }}
+ >
+ 立即下载
+ </button>
+ </div>
+ ))}
+ </div>
+
+ {/* 分页控件 */}
+ <div className="pagination">
+ <button
+ onClick={() => fetchTorrentPosts(currentPage - 1)}
+ disabled={currentPage === 1}
+ >
+ 上一页
+ </button>
+
+ {Array.from({length: totalPages}, (_, i) => i + 1).map(page => (
+ <button
+ key={page}
+ onClick={() => fetchTorrentPosts(page)}
+ className={currentPage === page ? 'active' : ''}
+ >
+ {page}
+ </button>
+ ))}
+
+ <button
+ onClick={() => fetchTorrentPosts(currentPage + 1)}
+ disabled={currentPage === totalPages}
+ >
+ 下一页
+ </button>
+ </div>
</div>
-
- <div className="form-actions">
- <button
- type="button"
- className="cancel-btn"
- onClick={() => setShowPostModal(false)}
- >
- 取消
- </button>
- <button
- type="submit"
- className="submit-btn"
- >
- 确认发帖
- </button>
+ );
+ // 在Dashboard.jsx的renderContent函数中修改case 'request'部分
+ case 'request':
+ return (
+ <div className="content-area" data-testid="request-section">
+ {/* 求种区帖子列表 */}
+ <div className="request-list">
+ {[
+ {
+ id: 1,
+ title: '求《药屋少女的呢喃》第二季全集',
+ content: '求1080P带中文字幕版本,最好是内嵌字幕不是外挂的',
+ author: '动漫爱好者',
+ authorAvatar: 'https://via.placeholder.com/40',
+ date: '2023-10-15',
+ likeCount: 24,
+ commentCount: 8
+ },
+ {
+ id: 2,
+ title: '求《奥本海默》IMAX版',
+ content: '最好是原盘或者高码率的版本,谢谢各位大佬',
+ author: '电影收藏家',
+ authorAvatar: 'https://via.placeholder.com/40',
+ date: '2023-10-14',
+ likeCount: 15,
+ commentCount: 5
+ }
+ ].map(post => (
+ <div
+ key={post.id}
+ className="request-post"
+ onClick={() => navigate(`/request/${post.id}`)}
+ >
+ <div className="post-header">
+ <img src={post.authorAvatar} alt={post.author} className="post-avatar"/>
+ <div className="post-author">{post.author}</div>
+ <div className="post-date">{post.date}</div>
+ </div>
+ <h3 className="post-title">{post.title}</h3>
+ <p className="post-content">{post.content}</p>
+ <div className="post-stats">
+ <span className="post-likes">👍 {post.likeCount}</span>
+ <span className="post-comments">💬 {post.commentCount}</span>
+ </div>
+ </div>
+ ))}
+ </div>
</div>
- </form>
+ );
+ // 在Dashboard.jsx的renderContent函数中修改case 'help'部分
+ case 'help':
+ return (
+ <div className="content-area" data-testid="help-section">
+ {/* 新增发帖按钮 */}
+ <div className="post-header">
+ <button
+ className="create-post-btn"
+ onClick={() => setShowPostModal(true)}
+ >
+ + 发帖求助
+ </button>
+ </div>
+
+ {/* 加载状态和错误提示 */}
+ {helpLoading && <div className="loading">加载中...</div>}
+ {helpError && <div className="error">{helpError}</div>}
+
+ {/* 求助区帖子列表 */}
+ <div className="help-list">
+ {helpPosts.map(post => (
+ <div
+ key={post.id}
+ className={`help-post ${post.isSolved ? 'solved' : ''}`}
+ onClick={() => navigate(`/help/${post.id}`)}
+ >
+ <div className="post-header">
+ <img
+ src={post.authorAvatar || 'https://via.placeholder.com/40'}
+ alt={post.authorId}
+ className="post-avatar"
+ />
+ <div className="post-author">{post.authorId}</div>
+ <div className="post-date">
+ {new Date(post.createTime).toLocaleDateString()}
+ </div>
+ {post.isSolved && <span className="solved-badge">已解决</span>}
+ </div>
+ <h3 className="post-title">{post.title}</h3>
+ <p className="post-content">{post.content}</p>
+ <div className="post-stats">
+ <span className="post-likes">👍 {post.likeCount || 0}</span>
+ <span className="post-comments">💬 {post.replyCount || 0}</span>
+ </div>
+ </div>
+ ))}
+ </div>
+
+ {/* 在帖子列表后添加分页控件 */}
+ <div className="pagination">
+ <button
+ onClick={() => fetchHelpPosts(currentPage - 1)}
+ disabled={currentPage === 1}
+ >
+ 上一页
+ </button>
+
+ {Array.from({length: totalPages}, (_, i) => i + 1).map(page => (
+ <button
+ key={page}
+ onClick={() => fetchHelpPosts(page)}
+ className={currentPage === page ? 'active' : ''}
+ >
+ {page}
+ </button>
+ ))}
+
+ <button
+ onClick={() => fetchHelpPosts(currentPage + 1)}
+ disabled={currentPage === totalPages}
+ >
+ 下一页
+ </button>
+ </div>
+
+ {/* 新增发帖弹窗 */}
+ {showPostModal && (
+ <div className="post-modal-overlay">
+ <div className="post-modal">
+ <h3>发布求助帖</h3>
+ <button
+ className="modal-close-btn"
+ onClick={() => setShowPostModal(false)}
+ >
+ ×
+ </button>
+
+ <form onSubmit={handlePostSubmit}>
+ <div className="form-group">
+ <label>帖子标题</label>
+ <input
+ type="text"
+ value={postTitle}
+ onChange={(e) => setPostTitle(e.target.value)}
+ placeholder="请输入标题"
+ required
+ />
+ </div>
+
+ <div className="form-group">
+ <label>帖子内容</label>
+ <textarea
+ value={postContent}
+ onChange={(e) => setPostContent(e.target.value)}
+ placeholder="详细描述你的问题"
+ required
+ />
+ </div>
+
+ <div className="form-group">
+ <label>上传图片</label>
+ <div className="upload-image-btn">
+ <input
+ type="file"
+ id="image-upload"
+ accept="image/*"
+ onChange={handleImageUpload}
+ style={{display: 'none'}}
+ />
+ <label htmlFor="image-upload">
+ {selectedImage ? '已选择图片' : '选择图片'}
+ </label>
+ {selectedImage && (
+ <span className="image-name">{selectedImage.name}</span>
+ )}
+ </div>
+ </div>
+
+ <div className="form-actions">
+ <button
+ type="button"
+ className="cancel-btn"
+ onClick={() => setShowPostModal(false)}
+ >
+ 取消
+ </button>
+ <button
+ type="submit"
+ className="submit-btn"
+ >
+ 确认发帖
+ </button>
+ </div>
+ </form>
+ </div>
+ </div>
+ )}
+ </div>
+ );
+ default:
+ return <div className="content-area" data-testid="default-section">公告区内容</div>;
+ }
+ };
+
+ if (loading) return <div className="loading">加载中...</div>;
+ if (error) return <div className="error">{error}</div>;
+
+ return (
+ <div className="dashboard-container" data-testid="dashboard-container">
+ {/* 顶部栏 */}
+ <div className="top-bar" data-testid="top-bar">
+ {/* 搜索框 */}
+ <div className="search-container">
+ <input
+ type="text"
+ placeholder="搜索种子、用户..."
+ className="search-input"
+ />
+ <button className="search-button">搜索</button>
</div>
- </div>
- )}
- </div>
- );
- default:
- return <div className="content-area" data-testid="default-section">公告区内容</div>;
- }
- };
- if (loading) return <div className="loading">加载中...</div>;
- if (error) return <div className="error">{error}</div>;
+ <div className="user-actions">
+ {/* 新增管理员按钮 - 只有管理员可见 */}
+ {userInfo?.isAdmin && (
+ <button
+ className="admin-center-button"
+ onClick={() => navigate('/administer')}
+ >
+ 管理员中心
+ </button>
+ )}
- return (
- <div className="dashboard-container" data-testid="dashboard-container">
- {/* 顶部栏 */}
- <div className="top-bar" data-testid="top-bar">
- {/* 搜索框 */}
- <div className="search-container">
- <input
- type="text"
- placeholder="搜索种子、用户..."
- className="search-input"
- />
- <button className="search-button">搜索</button>
+ <div className="user-info" data-testid="user-info">
+ <img
+ src={userInfo?.avatar || 'https://via.placeholder.com/40'}
+ alt="用户头像"
+ className="user-avatar"
+ onClick={() => navigate('/personal')}
+ style={{cursor: 'pointer'}}
+ />
+ <span className="username">{userInfo?.name || '用户'}</span>
+ <button onClick={onLogout} className="logout-button">退出</button>
+ </div>
+ </div>
+ </div>
+
+ {/* 导航栏 */}
+ {/* handleTabchange函数替换了原本的setactivetab函数 */}
+ <div className="nav-tabs">
+ <button
+ className={`tab-button ${activeTab === 'announcement' ? 'active' : ''}`}
+ onClick={() => handleTabChange('announcement')}
+ >
+ 公告区
+ </button>
+ <button
+ className={`tab-button ${activeTab === 'share' ? 'active' : ''}`}
+ onClick={() => handleTabChange('share')}
+ >
+ 分享区
+ </button>
+ <button
+ className={`tab-button ${activeTab === 'request' ? 'active' : ''}`}
+ onClick={() => handleTabChange('request')}
+ >
+ 求种区
+ </button>
+ <button
+ className={`tab-button ${activeTab === 'help' ? 'active' : ''}`}
+ onClick={() => handleTabChange('help')}
+ >
+ 求助区
+ </button>
+ </div>
+
+ {/* 内容区 */}
+ {renderContent()}
</div>
-
- <div className="user-info" data-testid="user-info">
- <img
- src={userInfo?.avatar || 'https://via.placeholder.com/40'}
- alt="用户头像"
- className="user-avatar"
- onClick={() => navigate('/personal')} // 添加点击跳转
- style={{ cursor: 'pointer' }} // 添加手型指针
- />
- <span className="username">{userInfo?.name || '用户'}</span>
- <button onClick={onLogout} className="logout-button">退出</button>
- </div>
- </div>
-
- {/* 导航栏 */}
- {/* handleTabchange函数替换了原本的setactivetab函数 */}
- <div className="nav-tabs">
- <button
- className={`tab-button ${activeTab === 'announcement' ? 'active' : ''}`}
- onClick={() => handleTabChange('announcement')}
- >
- 公告区
- </button>
- <button
- className={`tab-button ${activeTab === 'share' ? 'active' : ''}`}
- onClick={() => handleTabChange('share')}
- >
- 分享区
- </button>
- <button
- className={`tab-button ${activeTab === 'request' ? 'active' : ''}`}
- onClick={() => handleTabChange('request')}
- >
- 求种区
- </button>
- <button
- className={`tab-button ${activeTab === 'help' ? 'active' : ''}`}
- onClick={() => handleTabChange('help')}
- >
- 求助区
- </button>
- </div>
-
- {/* 内容区 */}
- {renderContent()}
- </div>
- );
+ );
};
export default Dashboard;
\ No newline at end of file
diff --git a/src/components/RequestDetail.jsx b/src/components/RequestDetail.jsx
index 993fe8e..022b0cb 100644
--- a/src/components/RequestDetail.jsx
+++ b/src/components/RequestDetail.jsx
@@ -78,6 +78,9 @@
};
const handleDownloadTorrent = (commentId) => {
+
+
+
console.log('下载种子', commentId);
// 实际下载逻辑
};