blob: 61111f3b64b6238da9c10f3ef9a685a7ef9101d5 [file] [log] [blame]
22301080a93bebb2025-05-27 19:48:11 +08001import React, {useEffect, useState} from 'react';
Akane12173a7bb972025-06-01 01:05:27 +08002import {useNavigate, useLocation, useParams} from 'react-router-dom';
DREWae420b22025-06-02 14:07:20 +08003import {createTorrent, getTorrents, downloadTorrent, getDownloadProgress, deleteTorrent, searchTorrents} from '../api/torrent';
Akane121765b61a72025-05-17 13:52:25 +08004import './Dashboard.css';
DREWae420b22025-06-02 14:07:20 +08005import {createHelpPost, getHelpPosts, getHelpPostDetail, searchHelpPosts} from '../api/helpPost';
6import {createRequestPost, getRequestPosts, getRequestPostDetail, searchRequestPosts} from '../api/requestPost';
7import { message } from 'antd'; // 用于显示提示消息
8import { getAnnouncements,getLatestAnnouncements,getAnnouncementDetail } from '../api/announcement';
9import { getAllDiscounts } from '../api/administer';
DREW7c7c6a02025-06-05 19:58:55 +080010import { getUserInfo, isAdmin } from '../api/auth';
Akane12173a7bb972025-06-01 01:05:27 +080011import { api } from '../api/auth';
DREW7c7c6a02025-06-05 19:58:55 +080012import { getRecommendations, markRecommendationShown, markRecommendationClicked } from '../api/recommend';
13
Akane121765b61a72025-05-17 13:52:25 +080014
15
22301080a93bebb2025-05-27 19:48:11 +080016const Dashboard = ({onLogout}) => {
17 const location = useLocation();
18 const [userInfo, setUserInfo] = useState(null);
19 const [loading, setLoading] = useState(false);
20 const [error, setError] = useState('');
21 const [currentSlide, setCurrentSlide] = useState(0);
22 const navigate = useNavigate();
23 const {tab} = useParams();
24 const [showUploadModal, setShowUploadModal] = useState(false);
25 const [isUploading, setIsUploading] = useState(false);
26 const [uploadData, setUploadData] = useState({
27 name: '',
28 type: '',
29 region: '',
30 subtitle: '',
31 resolution: '', // 新增分辨率字段
32 file: null,
33 description: ''
34 });
35 const [showPostModal, setShowPostModal] = useState(false);
36 const [postTitle, setPostTitle] = useState('');
37 const [postContent, setPostContent] = useState('');
38 const [selectedImage, setSelectedImage] = useState(null);
39 const [helpPosts, setHelpPosts] = useState([]);
40 const [helpLoading, setHelpLoading] = useState(false);
41 const [helpError, setHelpError] = useState(null);
42 const [currentPage, setCurrentPage] = useState(1);
43 const [totalPages, setTotalPages] = useState(1);
Akane1217d1e9f712025-05-29 14:36:56 +080044 const [likedPosts,setLikedPosts] = useState({});
DREWae420b22025-06-02 14:07:20 +080045 const [announcements, setAnnouncements] = useState([]);
46 const [carouselDiscounts, setCarouselDiscounts] = useState([]);
47 const [requestLoading, setRequestLoading] = useState(false);
48 const [requestError, setRequestError] = useState(null);
49 const [requestPosts, setRequestPosts] = useState([]);
Akane1217d1e9f712025-05-29 14:36:56 +080050
Akane121765b61a72025-05-17 13:52:25 +080051
52 // 添加状态
22301080a93bebb2025-05-27 19:48:11 +080053 const [torrentPosts, setTorrentPosts] = useState([]);
54 const [torrentLoading, setTorrentLoading] = useState(false);
55 const [torrentError, setTorrentError] = useState(null);
Akane121765b61a72025-05-17 13:52:25 +080056
22301080a93bebb2025-05-27 19:48:11 +080057 const [filteredResources, setFilteredResources] = useState(torrentPosts);
58 const [isAdmin, setIsAdmin] = useState(false);
59
DREWae420b22025-06-02 14:07:20 +080060 // 在组件状态中添加
61 const [showDownloadModal, setShowDownloadModal] = useState(false);
62 const [selectedTorrent, setSelectedTorrent] = useState(null);
63 const [downloadProgress, setDownloadProgress] = useState(0);
64 const [isDownloading, setIsDownloading] = useState(false);
65 const [downloadPath, setDownloadPath] = useState('D:/studies/ptPlatform/torrent');
66
Akane12173a7bb972025-06-01 01:05:27 +080067 // 新增搜索状态
68 const [announcementSearch, setAnnouncementSearch] = useState('');
69 const [shareSearch, setShareSearch] = useState('');
70 const [requestSearch, setRequestSearch] = useState('');
71 const [helpSearch, setHelpSearch] = useState('');
DREW7c7c6a02025-06-05 19:58:55 +080072 const [recommendations, setRecommendations] = useState([]);
73 const [recommendLoading, setRecommendLoading] = useState(false);
74 const [recommendError, setRecommendError] = useState(null);
75
Akane12173a7bb972025-06-01 01:05:27 +080076
22301080a93bebb2025-05-27 19:48:11 +080077
78 const activeTab = tab || 'announcement'; // 如果没有tab参数,则默认为announcement
79 // 从location.state中初始化状态
80
81
82 const handleTabChange = (tabName) => {
83 navigate(`/dashboard/${tabName}`, {
84 state: {
85 savedFilters: selectedFilters, // 使用新的筛选状态 // 保留现有状态
86 activeTab: tabName // 可选,如果其他组件依赖这个 state
87 }
88 });
89 };
90
Akane12173a7bb972025-06-01 01:05:27 +080091
DREWae420b22025-06-02 14:07:20 +080092
93 //公告区
94 // 添加获取公告的方法
95 const fetchAnnouncements = async () => {
96 try {
97 const response = await getLatestAnnouncements();
98 setAnnouncements(response.data.data.announcements || []);
99 } catch (error) {
100 console.error('获取公告失败:', error);
101 }
Akane12173a7bb972025-06-01 01:05:27 +0800102 };
103
DREWae420b22025-06-02 14:07:20 +0800104 useEffect(() => {
105 if (activeTab === 'announcement') {
106 fetchAnnouncements();
107 fetchDiscountsForCarousel();
DREW7c7c6a02025-06-05 19:58:55 +0800108 fetchRecommendations();
DREWae420b22025-06-02 14:07:20 +0800109 }
110 }, [activeTab]);
Akane12173a7bb972025-06-01 01:05:27 +0800111
DREWae420b22025-06-02 14:07:20 +0800112 const fetchDiscountsForCarousel = async () => {
113 try {
114 const all = await getAllDiscounts();
115 console.log("返回的折扣数据:", all);
116 const now = new Date();
22301080a93bebb2025-05-27 19:48:11 +0800117
DREWae420b22025-06-02 14:07:20 +0800118 // ⚠️ 使用 Date.parse 确保兼容 ISO 格式
119 const ongoing = all.filter(d =>
120 Date.parse(d.startTime) <= now.getTime() && Date.parse(d.endTime) >= now.getTime()
121 );
122
123 const upcoming = all
124 .filter(d => Date.parse(d.startTime) > now.getTime())
125 .sort((a, b) => Date.parse(a.startTime) - Date.parse(b.startTime));
126
127 const selected = [...ongoing.slice(0, 3)];
128
129 while (selected.length < 3 && upcoming.length > 0) {
130 selected.push(upcoming.shift());
131 }
132
133 setCarouselDiscounts(selected);
134 } catch (e) {
135 console.error("获取折扣失败:", e);
136 }
137 };
138
139 // 修改handleAnnouncementClick函数中的state传递,移除不必要的字段
22301080a93bebb2025-05-27 19:48:11 +0800140 const handleAnnouncementClick = (announcement, e) => {
141 if (!e.target.closest('.exclude-click')) {
142 navigate(`/announcement/${announcement.id}`, {
143 state: {
144 announcement,
145 returnPath: `/dashboard/${activeTab}`,
146 scrollPosition: window.scrollY,
147 activeTab
148 }
149 });
Akane121765b61a72025-05-17 13:52:25 +0800150 }
22301080a93bebb2025-05-27 19:48:11 +0800151 };
Akane121765b61a72025-05-17 13:52:25 +0800152
DREW7c7c6a02025-06-05 19:58:55 +0800153 const handleRecommendClick = async (torrent) => {
154 try {
155 // 标记为已点击
156 await markRecommendationClicked(torrent.id);
157
158 // 导航到种子详情页
159 navigate(`/torrent/${torrent.id}`, {
160 state: {
161 fromTab: 'announcement',
162 scrollPosition: window.scrollY
163 }
164 });
165 } catch (error) {
166 console.error('标记推荐点击失败:', error);
167 }
168 };
169
DREWae420b22025-06-02 14:07:20 +0800170
171   // 公告区搜索处理
172    const handleSearchAnnouncement = (e) => {
173        setAnnouncementSearch(e.target.value);
174    };
175
176    // 修改后的搜索函数
177    const handleSearchShare = async () => {
178        try {
179            setTorrentLoading(true);
180            const response = await searchTorrents(shareSearch, 1);
181            if (response.data.code === 200) {
182                setTorrentPosts(response.data.data.records);
183                const total = response.data.data.total;
184                setTotalPages(Math.ceil(total / 5));
185                setCurrentPage(1);
186            } else {
187                setTorrentError(response.data.message || '搜索失败');
188            }
189        } catch (err) {
190            setTorrentError(err.message || '搜索失败');
191        } finally {
192            setTorrentLoading(false);
193        }
194    };
195
196    const handleResetShareSearch = async () => {
197        setShareSearch('');
198        setSelectedFilters(
199            Object.keys(filterCategories).reduce((acc, category) => {
200                acc[category] = 'all';
201                return acc;
202            }, {})
203        );
204        await fetchTorrentPosts(1, true);
205    };
206
207    // 添加搜索函数
208    const handleSearchRequest = async () => {
209        try {
210        setRequestLoading(true);
211        const response = await searchRequestPosts(requestSearch, currentPage);
212        if (response.data.code === 200) {
213            const postsWithCounts = await Promise.all(
214            response.data.data.records.map(async (post) => {
215                try {
216                const detailResponse = await getRequestPostDetail(post.id);
217                if (detailResponse.data.code === 200) {
218                    return {
219                    ...post,
220                    replyCount: detailResponse.data.data.post.replyCount || 0,
221                    isLiked: false
222                    };
223                }
224                return post;
225                } catch (err) {
226                console.error(`获取帖子${post.id}详情失败:`, err);
227                return post;
228                }
229            })
230            );
231            setRequestPosts(postsWithCounts);
232            setTotalPages(Math.ceil(response.data.data.total / 5));
233        } else {
234            setRequestError(response.data.message || '搜索失败');
235        }
236        } catch (err) {
237        setRequestError(err.message || '搜索失败');
238        } finally {
239        setRequestLoading(false);
240        }
241    };
242   
243    // 添加重置搜索函数
244    const handleResetRequestSearch = async () => {
245        setRequestSearch('');
246 await fetchRequestPosts(1); // 重置到第一页
247 };
248
249   // 添加搜索函数
250    const handleSearchHelp = async () => {
251        try {
252        setHelpLoading(true);
253        const response = await searchHelpPosts(helpSearch, currentPage);
254        if (response.data.code === 200) {
255            const postsWithCounts = await Promise.all(
256            response.data.data.records.map(async (post) => {
257                try {
258                const detailResponse = await getHelpPostDetail(post.id);
259                if (detailResponse.data.code === 200) {
260                    return {
261                    ...post,
262                    replyCount: detailResponse.data.data.post.replyCount || 0,
263                    isLiked: false
264                    };
265                }
266                return post;
267                } catch (err) {
268                console.error(`获取帖子${post.id}详情失败:`, err);
269                return post;
270                }
271            })
272            );
273            setHelpPosts(postsWithCounts);
274            setTotalPages(Math.ceil(response.data.data.total / 5));
275        } else {
276            setHelpError(response.data.message || '搜索失败');
277        }
278        } catch (err) {
279        setHelpError(err.message || '搜索失败');
280        } finally {
281        setHelpLoading(false);
282        }
283    };
284   
285    // 添加重置搜索函数
286    const handleResetHelpSearch = async () => {
287        setHelpSearch('');
288 await fetchHelpPosts(1); // 重置到第一页
289 };
290
291
292
Akane121765b61a72025-05-17 13:52:25 +0800293
22301080a93bebb2025-05-27 19:48:11 +0800294 //资源区
295 const handleFileChange = (e) => {
296 setUploadData({...uploadData, file: e.target.files[0]});
297 };
Akane121765b61a72025-05-17 13:52:25 +0800298
22301080a93bebb2025-05-27 19:48:11 +0800299 const handleUploadSubmit = async (e) => {
300 e.preventDefault();
301 try {
302 setIsUploading(true);
Akane121765b61a72025-05-17 13:52:25 +0800303
22301080a93bebb2025-05-27 19:48:11 +0800304 const torrentData = {
305 torrentName: uploadData.name,
306 description: uploadData.description,
307 category: uploadData.type,
308 region: uploadData.region,
309 resolution: uploadData.resolution,
310 subtitle: uploadData.subtitle
311 };
Akane121765b61a72025-05-17 13:52:25 +0800312
DREWae420b22025-06-02 14:07:20 +0800313 await createTorrent(uploadData.file, torrentData, (progress) => {
314 console.log(`上传进度: ${progress}%`);
315 // 这里可以添加进度条更新逻辑
316 });
22301080a93bebb2025-05-27 19:48:11 +0800317
318 // 上传成功处理
319 setShowUploadModal(false);
320 setUploadData({
321 name: '',
322 type: '',
323 region: '',
324 subtitle: '',
325 resolution: '',
326 file: null,
327 description: ''
328 });
329 alert('种子创建成功!');
330
331 // 刷新列表
332 await fetchTorrentPosts(currentPage);
333
334 } catch (error) {
335 console.error('创建失败:', error);
336 alert('创建失败: ' + (error.response?.data?.message || error.message));
337 } finally {
338 setIsUploading(false);
339 }
340 };
341
DREWae420b22025-06-02 14:07:20 +0800342 // 处理下载按钮点击
343 const handleDownloadClick = (torrent, e) => {
344 e.stopPropagation();
345 setSelectedTorrent(torrent);
346 setShowDownloadModal(true);
347 };
348
349 // 执行下载
350 const handleDownload = async () => {
351 if (!selectedTorrent || !downloadPath) return;
352
353 setIsDownloading(true);
354 setDownloadProgress(0);
355
356 try {
357 // 标准化路径
358 const cleanPath = downloadPath
359 .replace(/\\/g, '/') // 统一使用正斜杠
360 .replace(/\/+/g, '/') // 去除多余斜杠
361 .trim();
362
363 // 确保路径以斜杠结尾
364 const finalPath = cleanPath.endsWith('/') ? cleanPath : cleanPath + '/';
365
366 // 发起下载请求
367 await downloadTorrent(selectedTorrent.id, finalPath);
368
369 // 开始轮询进度
370 const interval = setInterval(async () => {
371 try {
372 const res = await getDownloadProgress();
373 const progresses = res.data.progresses;
374
375 if (progresses) {
376 // 使用完整的 torrent 文件路径作为键
377 const torrentPath = selectedTorrent.filePath.replace(/\\/g, '/');
378 const torrentHash = selectedTorrent.hash;
379 // 查找匹配的进度
380 let foundProgress = null;
381 for (const [key, value] of Object.entries(progresses)) {
382 const normalizedKey = key.replace(/\\/g, '/');
383 if (normalizedKey.includes(selectedTorrent.hash) ||
384 normalizedKey.includes(selectedTorrent.torrentName)) {
385 foundProgress = value;
386 break;
387 }
388 }
389 if (foundProgress !== null) {
390 const newProgress = Math.round(foundProgress * 100);
391 setDownloadProgress(newProgress);
392
393 // 检查是否下载完成
394 if (newProgress >= 100) {
395 clearInterval(interval);
396 setIsDownloading(false);
397 message.success('下载完成!');
398 setTimeout(() => setShowDownloadModal(false), 2000);
399 }
400 } else {
401 console.log('当前下载进度:', progresses); // 添加日志
402 }
403 }
404 } catch (error) {
405 console.error('获取进度失败:', error);
406 // 如果获取进度失败但文件已存在,也视为完成
407 const filePath = `${finalPath}${selectedTorrent.torrentName || 'downloaded_file'}`;
408 try {
409 const exists = await checkFileExists(filePath);
410 if (exists) {
411 clearInterval(interval);
412 setDownloadProgress(100);
413 setIsDownloading(false);
414 message.success('下载完成!');
415 setTimeout(() => setShowDownloadModal(false), 2000);
416 }
417 } catch (e) {
418 console.error('文件检查失败:', e);
419 }
420 }
421 }, 2000);
422
423 return () => clearInterval(interval);
424 } catch (error) {
425 setIsDownloading(false);
426 if (error.response && error.response.status === 409) {
427 message.error('分享率不足,无法下载此资源');
428 } else {
429 message.error('下载失败: ' + (error.message || '未知错误'));
430 }
431 }
432 };
433
434 const checkFileExists = async (filePath) => {
435 try {
436 // 这里需要根据您的实际环境实现文件存在性检查
437 // 如果是Electron应用,可以使用Node.js的fs模块
438 // 如果是纯前端,可能需要通过API请求后端检查
439 return true; // 暂时返回true,实际实现需要修改
440 } catch (e) {
441 console.error('检查文件存在性失败:', e);
442 return false;
443 }
444 };
445
446 const handleDeleteTorrent = async (torrentId, e) => {
447 e.stopPropagation(); // 阻止事件冒泡,避免触发资源项的点击事件
448
449 try {
450 // 确认删除
451 if (!window.confirm('确定要删除这个种子吗?此操作不可撤销!')) {
452 return;
453 }
454
455 // 调用删除API
456 await deleteTorrent(torrentId);
457
458 // 删除成功后刷新列表
459 message.success('种子删除成功');
460 await fetchTorrentPosts(currentPage);
461 } catch (error) {
462 console.error('删除种子失败:', error);
463 message.error('删除种子失败: ' + (error.response?.data?.message || error.message));
464 }
465 };
466
467 const handleRequestPostSubmit = async (e) => {
22301080a93bebb2025-05-27 19:48:11 +0800468 e.preventDefault();
469 try {
Akane1217d1e9f712025-05-29 14:36:56 +0800470 const username = localStorage.getItem('username');
DREWae420b22025-06-02 14:07:20 +0800471 const response = await createRequestPost(
472 postTitle,
473 postContent,
474 username,
475 selectedImage
476 );
477
478 if (response.data.code === 200) {
479 // 刷新帖子列表
480
481 await fetchRequestPosts(currentPage);
482 // 重置表单
483 setShowPostModal(false);
484 setPostTitle('');
485 setPostContent('');
486 setSelectedImage(null);
487 } else {
488 setHelpError(response.data.message || '发帖失败');
489 }
490 } catch (err) {
491 setHelpError(err.message || '发帖失败');
492 }
493 };
494
495 const handleHelpPostSubmit = async (e) => {
496 e.preventDefault();
497 try {
498 const username = localStorage.getItem('username');
499 const response = await createHelpPost(
Akane1217d1e9f712025-05-29 14:36:56 +0800500 postTitle,
501 postContent,
502 username,
503 selectedImage
504 );
505
506 if (response.data.code === 200) {
507 // 刷新帖子列表
508 await fetchHelpPosts(currentPage);
DREWae420b22025-06-02 14:07:20 +0800509
Akane1217d1e9f712025-05-29 14:36:56 +0800510 // 重置表单
511 setShowPostModal(false);
512 setPostTitle('');
513 setPostContent('');
514 setSelectedImage(null);
515 } else {
516 setHelpError(response.data.message || '发帖失败');
517 }
22301080a93bebb2025-05-27 19:48:11 +0800518 } catch (err) {
Akane1217d1e9f712025-05-29 14:36:56 +0800519 setHelpError(err.message || '发帖失败');
22301080a93bebb2025-05-27 19:48:11 +0800520 }
Akane1217d1e9f712025-05-29 14:36:56 +0800521 };
Akane121765b61a72025-05-17 13:52:25 +0800522
Akane12173a7bb972025-06-01 01:05:27 +0800523
524const fetchTorrentPosts = async (page = 1, isReset = false) => {
525 setTorrentLoading(true);
526 try {
527 const params = {
528 page,
529 size: 5
530 };
531
532 // 如果有筛选条件且不是重置操作
533 if (!isReset && Object.values(selectedFilters).some(v => v !== 'all')) {
534 if (selectedFilters.type !== 'all') params.category = selectedFilters.type;
535 if (selectedFilters.subtitle !== 'all') params.subtitle = selectedFilters.subtitle;
536 if (selectedFilters.region !== 'all') params.region = selectedFilters.region;
537 if (selectedFilters.resolution !== 'all') params.resolution = selectedFilters.resolution;
22301080a93bebb2025-05-27 19:48:11 +0800538 }
Akane12173a7bb972025-06-01 01:05:27 +0800539
540 const response = (shareSearch && !isReset)
541 ? await searchTorrents(shareSearch, page)
DREW7c7c6a02025-06-05 19:58:55 +0800542 : await api.get('/torrent', { params });
Akane12173a7bb972025-06-01 01:05:27 +0800543
544 if (response.data.code === 200) {
545 setTorrentPosts(response.data.data.records);
546 const total = response.data.data.total;
547 setTotalPages(Math.ceil(total / 5));
548 setCurrentPage(page);
549 } else {
550 setTorrentError(response.data.message);
551 }
552 } catch (err) {
553 setTorrentError(err.message);
554 } finally {
555 setTorrentLoading(false);
556 }
557};
Akane121765b61a72025-05-17 13:52:25 +0800558
22301080a93bebb2025-05-27 19:48:11 +0800559 // 在useEffect中调用
560 useEffect(() => {
561 if (activeTab === 'share') {
562 fetchTorrentPosts();
563 }
564 }, [activeTab]);
Akane121765b61a72025-05-17 13:52:25 +0800565
DREWae420b22025-06-02 14:07:20 +0800566const fetchRequestPosts = async (page = 1) => {
567 setRequestLoading(true);
568 try {
569 const response = await getRequestPosts(page);
570 if (response.data.code === 200) {
571 const postsWithCounts = await Promise.all(
572 response.data.data.records.map(async (post) => {
573 try {
574 const detailResponse = await getRequestPostDetail(post.id);
575 if (detailResponse.data.code === 200) {
576 return {
577 ...post,
578 replyCount: detailResponse.data.data.post.replyCount || 0,
579 isLiked: false // 根据需要添加其他字段
580 };
581 }
582 return post; // 如果获取详情失败,返回原始帖子数据
583 } catch (err) {
584 console.error(`获取帖子${post.id}详情失败:`, err);
585 return post;
586 }
587 })
588 );
589 setRequestPosts(postsWithCounts);
590 setTotalPages(Math.ceil(response.data.data.total / 5)); // 假设每页5条
591 setCurrentPage(page);
592 } else {
593 setRequestError(response.data.message || '获取求助帖失败');
594 }
595 } catch (err) {
596 setRequestError(err.message || '获取求助帖失败');
597 } finally {
598 setRequestLoading(false);
599 }
600 };
601
22301080a93bebb2025-05-27 19:48:11 +0800602 const handleImageUpload = (e) => {
603 setSelectedImage(e.target.files[0]);
604 };
Akane121765b61a72025-05-17 13:52:25 +0800605
606
22301080a93bebb2025-05-27 19:48:11 +0800607 const fetchHelpPosts = async (page = 1) => {
608 setHelpLoading(true);
609 try {
DREWae420b22025-06-02 14:07:20 +0800610 const response = await getHelpPosts(page);
Akane1217d1e9f712025-05-29 14:36:56 +0800611 if (response.data.code === 200) {
612 const postsWithCounts = await Promise.all(
613 response.data.data.records.map(async (post) => {
614 try {
DREWae420b22025-06-02 14:07:20 +0800615 const detailResponse = await getHelpPostDetail(post.id);
Akane1217d1e9f712025-05-29 14:36:56 +0800616 if (detailResponse.data.code === 200) {
617 return {
618 ...post,
619 replyCount: detailResponse.data.data.post.replyCount || 0,
620 isLiked: false // 根据需要添加其他字段
621 };
622 }
623 return post; // 如果获取详情失败,返回原始帖子数据
624 } catch (err) {
625 console.error(`获取帖子${post.id}详情失败:`, err);
626 return post;
627 }
628 })
629 );
630 setHelpPosts(postsWithCounts);
631 setTotalPages(Math.ceil(response.data.data.total / 5)); // 假设每页5条
632 setCurrentPage(page);
633 } else {
634 setHelpError(response.data.message || '获取求助帖失败');
635 }
22301080a93bebb2025-05-27 19:48:11 +0800636 } catch (err) {
Akane1217d1e9f712025-05-29 14:36:56 +0800637 setHelpError(err.message || '获取求助帖失败');
22301080a93bebb2025-05-27 19:48:11 +0800638 } finally {
Akane1217d1e9f712025-05-29 14:36:56 +0800639 setHelpLoading(false);
22301080a93bebb2025-05-27 19:48:11 +0800640 }
DREW7c7c6a02025-06-05 19:58:55 +0800641 };
642
643 const fetchRecommendations = async () => {
644 try {
645 setRecommendLoading(true);
646 const response = await getRecommendations(5);
647 if (response.code === 200) {
648 setRecommendations(response.data || []);
649
650 // 标记这些推荐为已显示
651 response.data.forEach(torrent => {
652 markRecommendationShown(torrent.id);
653 });
654 }
655 } catch (error) {
656 setRecommendError(error.message || '获取推荐失败');
657 } finally {
658 setRecommendLoading(false);
659 }
660 };
Akane1217d1e9f712025-05-29 14:36:56 +0800661
662
22301080a93bebb2025-05-27 19:48:11 +0800663 useEffect(() => {
DREWae420b22025-06-02 14:07:20 +0800664 if (activeTab === 'request') {
665 fetchRequestPosts(currentPage);
22301080a93bebb2025-05-27 19:48:11 +0800666 }
667 }, [activeTab, currentPage]); // 添加 currentPage 作为依赖
Akane121765b61a72025-05-17 13:52:25 +0800668
669
Akane12173a7bb972025-06-01 01:05:27 +0800670 // 分类维度配置
22301080a93bebb2025-05-27 19:48:11 +0800671 const filterCategories = {
672 type: {
673 label: '类型',
674 options: {
675 'all': '全部',
676 '电影': '电影',
677 '电视剧': '电视剧',
678 '动漫': '动漫',
Akane12173a7bb972025-06-01 01:05:27 +0800679 '综艺': '综艺',
680 '音乐': '音乐',
681 '其他': '其他'
22301080a93bebb2025-05-27 19:48:11 +0800682 }
683 },
684 subtitle: {
685 label: '字幕',
686 options: {
687 'all': '全部',
Akane12173a7bb972025-06-01 01:05:27 +0800688 '无需字幕': '无需字幕',
689 '暂无字幕': '暂无字幕',
690 '自带中文字幕': '自带中文字幕',
691 '自带双语字幕(含中文)': '自带双语字幕(含中文)',
692 '附件中文字幕': '附件中文字幕',
693 '附件双语字幕': '附件双语字幕'
22301080a93bebb2025-05-27 19:48:11 +0800694 }
695 },
696 region: {
697 label: '地区',
698 options: {
699 'all': '全部',
Akane12173a7bb972025-06-01 01:05:27 +0800700 '中国': '中国',
701 '英国': '英国',
702 '美国': '美国',
703 '日本': '日本',
704 '韩国': '韩国',
705 '其他': '其他'
706 }
707 },
708 resolution: {
709 label: '分辨率',
710 options: {
711 'all': '全部',
712 '4K': '4K',
713 '2K': '2K',
714 '1080P': '1080P',
715 '720P': '720P',
716 'SD': 'SD',
717 '无损音源': '无损音源',
718 '杜比全景声': '杜比全景声',
719 '其他': '其他'
22301080a93bebb2025-05-27 19:48:11 +0800720 }
721 }
722 };
22301080a93bebb2025-05-27 19:48:11 +0800723 const [selectedFilters, setSelectedFilters] = useState(
724 location.state?.savedFilters ||
725 Object.keys(filterCategories).reduce((acc, category) => {
726 acc[category] = 'all';
727 return acc;
728 }, {})
729 );
Akane121765b61a72025-05-17 13:52:25 +0800730
Akane12173a7bb972025-06-01 01:05:27 +0800731 // 处理筛选条件变更
22301080a93bebb2025-05-27 19:48:11 +0800732 const handleFilterSelect = (category, value) => {
733 setSelectedFilters(prev => ({
734 ...prev,
Akane12173a7bb972025-06-01 01:05:27 +0800735 [category]: prev[category] === value ? 'all' : value
22301080a93bebb2025-05-27 19:48:11 +0800736 }));
Akane121765b61a72025-05-17 13:52:25 +0800737 };
738
Akane12173a7bb972025-06-01 01:05:27 +0800739 // 应用筛选条件
740 const applyFilters = async () => {
741 try {
742 setTorrentLoading(true);
743
744 // 构建查询参数
745 const params = {
746 page: 1, // 从第一页开始
747 size: 5
748 };
749
750 // 添加筛选条件
751 if (selectedFilters.type !== 'all') {
752 params.category = selectedFilters.type;
753 }
754 if (selectedFilters.subtitle !== 'all') {
755 params.subtitle = selectedFilters.subtitle;
756 }
757 if (selectedFilters.region !== 'all') {
758 params.region = selectedFilters.region;
759 }
760 if (selectedFilters.resolution !== 'all') {
761 params.resolution = selectedFilters.resolution;
762 }
763
764 // 调用API获取筛选结果
DREW7c7c6a02025-06-05 19:58:55 +0800765 const response = await api.get('/torrent', { params });
Akane12173a7bb972025-06-01 01:05:27 +0800766
767 if (response.data.code === 200) {
768 setTorrentPosts(response.data.data.records);
769 setTotalPages(Math.ceil(response.data.data.total / 5));
770 setCurrentPage(1);
771 } else {
772 setTorrentError(response.data.message || '筛选失败');
773 }
774 } catch (err) {
775 setTorrentError(err.message || '筛选失败');
776 } finally {
777 setTorrentLoading(false);
778 }
22301080a93bebb2025-05-27 19:48:11 +0800779 };
Akane121765b61a72025-05-17 13:52:25 +0800780
Akane121765b61a72025-05-17 13:52:25 +0800781
22301080a93bebb2025-05-27 19:48:11 +0800782 // 恢复滚动位置
783 useEffect(() => {
784 if (location.state?.scrollPosition) {
785 window.scrollTo(0, location.state.scrollPosition);
786 }
787 }, [location.state]);
Akane121765b61a72025-05-17 13:52:25 +0800788
Akane12173a7bb972025-06-01 01:05:27 +0800789 // 在Dashboard.jsx中修改useEffect
790useEffect(() => {
791 const token = localStorage.getItem('token');
792 if (!token) {
793 navigate('/login');
794 return;
795 }
796
797 const fetchUserInfo = async () => {
798 try {
799 setLoading(true);
800 const backendData = await getUserInfo(); // 调用修改后的方法
801 console.log('后端返回的用户数据:', backendData); // 调试用
802
803 setUserInfo({
804 name: backendData.username || '演示用户',
22301080a93bebb2025-05-27 19:48:11 +0800805 avatar: 'https://via.placeholder.com/40',
Akane12173a7bb972025-06-01 01:05:27 +0800806 isAdmin: backendData.authority === 'ADMIN' // 检查 authority 是否为 "ADMIN"
807 });
808 } catch (error) {
809 console.error('获取用户信息失败:', error);
810 setError('获取用户信息失败');
811 } finally {
812 setLoading(false);
813 }
814 };
815
816 fetchUserInfo();
817}, [navigate]);
22301080a93bebb2025-05-27 19:48:11 +0800818
Akane12173a7bb972025-06-01 01:05:27 +0800819
22301080a93bebb2025-05-27 19:48:11 +0800820 useEffect(() => {
821 if (activeTab === 'announcement') {
822 const timer = setInterval(() => {
DREWae420b22025-06-02 14:07:20 +0800823 setCurrentSlide(prev => {
824 const count = carouselDiscounts.length || 1;
825 return (prev + 1) % count;
826 });
22301080a93bebb2025-05-27 19:48:11 +0800827 }, 3000);
828 return () => clearInterval(timer);
829 }
830 }, [activeTab]);
831
832 useEffect(() => {
833 if (activeTab === 'help') {
834 fetchHelpPosts();
835 }
DREWae420b22025-06-02 14:07:20 +0800836 }, [activeTab, currentPage]); // 添加 currentPage 作为依赖
837
838
22301080a93bebb2025-05-27 19:48:11 +0800839
840 const renderContent = () => {
841 switch (activeTab) {
842 case 'announcement':
843 return (
844 <div className="content-area" data-testid="announcement-section">
Akane12173a7bb972025-06-01 01:05:27 +0800845 <div className="section-search-container">
846 <input
847 type="text"
848 placeholder="搜索公告..."
849 value={announcementSearch}
850 onChange={(e) => setAnnouncementSearch(e.target.value)}
851 className="section-search-input"
852 onKeyPress={(e) => e.key === 'Enter' && handleSearchAnnouncement()}
853 />
854 <button
855 className="search-button"
856 onClick={handleSearchAnnouncement}
857 >
858 搜索
859 </button>
860 </div>
22301080a93bebb2025-05-27 19:48:11 +0800861 {/* 轮播图区域 */}
DREWae420b22025-06-02 14:07:20 +0800862 <div className="carousel-container">
863 {carouselDiscounts.length === 0 ? (
864 <div className="carousel-slide active">
865 <div className="carousel-image gray-bg">暂无折扣活动</div>
22301080a93bebb2025-05-27 19:48:11 +0800866 </div>
DREWae420b22025-06-02 14:07:20 +0800867 ) : (
868 carouselDiscounts.map((discount, index) => (
869 <div key={index} className={`carousel-slide ${currentSlide === index ? 'active' : ''}`}>
870 <div className="carousel-image gray-bg">
871 <h3>{discount.type}</h3>
872 <p>{discount.name}</p>
873 <p>{new Date(discount.startTime).toLocaleDateString()} ~ {new Date(discount.endTime).toLocaleDateString()}</p>
874 </div>
22301080a93bebb2025-05-27 19:48:11 +0800875 </div>
DREWae420b22025-06-02 14:07:20 +0800876 ))
877 )}
878 <div className="carousel-dots">
879 {carouselDiscounts.map((_, index) => (
880 <span key={index} className={`dot ${currentSlide === index ? 'active' : ''}`}></span>
881 ))}
22301080a93bebb2025-05-27 19:48:11 +0800882 </div>
DREWae420b22025-06-02 14:07:20 +0800883 </div>
884
885
22301080a93bebb2025-05-27 19:48:11 +0800886
887 {/* 公告区块区域 */}
888 <div className="announcement-grid">
889 {announcements.map(announcement => (
890 <div
891 key={announcement.id}
892 className="announcement-card"
893 onClick={(e) => handleAnnouncementClick(announcement, e)}
894 >
895 <h3>{announcement.title}</h3>
DREWae420b22025-06-02 14:07:20 +0800896 <p>{announcement.content.substring(0, 100)}...</p>
22301080a93bebb2025-05-27 19:48:11 +0800897 <div className="announcement-footer exclude-click">
22301080a93bebb2025-05-27 19:48:11 +0800898 <span>{announcement.date}</span>
899 </div>
900 </div>
901 ))}
902 </div>
DREW7c7c6a02025-06-05 19:58:55 +0800903 {/* 新增的为你推荐区域 */}
904 <div className="recommend-section">
905 <h2 className="section-title">为你推荐</h2>
906
907 {recommendLoading && <div className="loading">加载推荐中...</div>}
908 {recommendError && <div className="error">{recommendError}</div>}
909
910 <div className="recommend-grid">
911 {recommendations.map(torrent => (
912 <div
913 key={torrent.id}
914 className="recommend-card"
915 onClick={() => handleRecommendClick(torrent)}
916 >
917 <div className="card-poster">
918 <div className="poster-image gray-bg">
919 {torrent.torrentName.charAt(0)}
920 </div>
921 </div>
922 <div className="card-info">
923 <h3 className="card-title">{torrent.torrentName}</h3>
924 <p className="card-meta">
925 {torrent.resolution} | {torrent.region} | {torrent.category}
926 </p>
927 <p className="card-subtitle">字幕: {torrent.subtitle}</p>
928 </div>
929 <div className="card-stats">
930 <span className="stat">👍 {torrent.likeCount || 0}</span>
931 </div>
932 </div>
933 ))}
934
935 {!recommendLoading && recommendations.length === 0 && (
936 <div className="no-recommendations">
937 暂无推荐,请先下载一些资源以获取个性化推荐
938 </div>
939 )}
940 </div>
941 </div>
Akane121765b61a72025-05-17 13:52:25 +0800942 </div>
22301080a93bebb2025-05-27 19:48:11 +0800943 );
944 case 'share':
945 return (
946 <div className="content-area" data-testid="share-section">
Akane12173a7bb972025-06-01 01:05:27 +0800947 {/* 分享区搜索框 */}
948 <div className="section-search-container">
949 <input
950 type="text"
951 placeholder="搜索资源..."
952 value={shareSearch}
953 onChange={(e) => setShareSearch(e.target.value)}
954 className="section-search-input"
955 onKeyPress={(e) => e.key === 'Enter' && handleSearchShare()}
956 />
957 <button
958 className="search-button"
959 onClick={handleSearchShare}
960 >
961 搜索
962 </button>
963 <button
964 className="reset-button"
965 onClick={handleResetShareSearch} // 使用新的重置函数
966 style={{marginLeft: '10px'}}
967 >
968 重置
969 </button>
970 </div>
971
22301080a93bebb2025-05-27 19:48:11 +0800972 {/* 上传按钮 - 添加在筛选区上方 */}
973 <div className="upload-header">
974 <button
975 className="upload-btn"
976 onClick={() => setShowUploadModal(true)}
977 >
978 上传种子
979 </button>
980 </div>
981 {/* 分类筛选区 */}
982 <div className="filter-section">
983 {Object.entries(filterCategories).map(([category, config]) => (
984 <div key={category} className="filter-group">
985 <h4>{config.label}:</h4>
986 <div className="filter-options">
987 {Object.entries(config.options).map(([value, label]) => (
988 <button
989 key={value}
990 className={`filter-btn ${
991 selectedFilters[category] === value ? 'active' : ''
992 }`}
993 onClick={() => handleFilterSelect(category, value)}
994 >
995 {label}
996 </button>
997 ))}
998 </div>
999 </div>
1000 ))}
1001
1002 <button
1003 className="confirm-btn"
1004 onClick={applyFilters}
1005 >
1006 确认筛选
1007 </button>
1008 </div>
1009
1010 {/* 上传弹窗 */}
1011 {showUploadModal && (
1012 <div className="modal-overlay">
1013 <div className="upload-modal">
1014 <h3>上传新种子</h3>
1015 <button
1016 className="close-btn"
1017 onClick={() => setShowUploadModal(false)}
1018 >
1019 ×
1020 </button>
1021
1022 <form onSubmit={handleUploadSubmit}>
1023 <div className="form-group">
1024 <label>种子名称</label>
1025 <input
1026 type="text"
1027 value={uploadData.name}
1028 onChange={(e) => setUploadData({...uploadData, name: e.target.value})}
1029 required
1030 />
1031 </div>
1032
1033 <div className="form-group">
1034 <label>资源类型</label>
1035 <select
1036 value={uploadData.type}
1037 onChange={(e) => setUploadData({...uploadData, type: e.target.value})}
1038 required
1039 >
1040 <option value="">请选择</option>
1041 <option value="电影">电影</option>
1042 <option value="电视剧">电视剧</option>
1043 <option value="动漫">动漫</option>
1044 <option value="综艺">综艺</option>
1045 <option value="音乐">音乐</option>
Akane12173a7bb972025-06-01 01:05:27 +08001046 <option value="其他">其他</option>
22301080a93bebb2025-05-27 19:48:11 +08001047 </select>
1048 </div>
1049
Akane12173a7bb972025-06-01 01:05:27 +08001050 {/* 修改后的地区下拉框 */}
22301080a93bebb2025-05-27 19:48:11 +08001051 <div className="form-group">
1052 <label>地区</label>
Akane12173a7bb972025-06-01 01:05:27 +08001053 <select
22301080a93bebb2025-05-27 19:48:11 +08001054 value={uploadData.region || ''}
1055 onChange={(e) => setUploadData({...uploadData, region: e.target.value})}
22301080a93bebb2025-05-27 19:48:11 +08001056 required
Akane12173a7bb972025-06-01 01:05:27 +08001057 >
1058 <option value="">请选择</option>
1059 <option value="中国">中国</option>
1060 <option value="英国">英国</option>
1061 <option value="美国">美国</option>
1062 <option value="日本">日本</option>
1063 <option value="韩国">韩国</option>
1064 <option value="其他">其他</option>
1065 </select>
22301080a93bebb2025-05-27 19:48:11 +08001066 </div>
1067
1068 {/* 添加分辨率下拉框 */}
1069 <div className="form-group">
1070 <label>分辨率</label>
1071 <select
1072 value={uploadData.resolution || ''}
1073 onChange={(e) => setUploadData({
1074 ...uploadData,
1075 resolution: e.target.value
1076 })}
1077 required
1078 >
1079 <option value="">请选择</option>
1080 <option value="4K">4K</option>
1081 <option value="2K">2K</option>
1082 <option value="1080P">1080P</option>
1083 <option value="720P">720P</option>
1084 <option value="SD">SD</option>
1085 <option value="无损音源">无损音源</option>
1086 <option value="杜比全景声">杜比全景声</option>
1087 <option value="其他">其他</option>
1088 </select>
1089 </div>
1090
1091
1092 {/* 新增字幕语言下拉框 */}
1093 <div className="form-group">
1094 <label>字幕语言</label>
1095 <select
1096 value={uploadData.subtitle || ''}
1097 onChange={(e) => setUploadData({
1098 ...uploadData,
1099 subtitle: e.target.value
1100 })}
1101 required
1102 >
1103 <option value="">请选择</option>
1104 <option value="无需字幕">无需字幕</option>
1105 <option value="暂无字幕">暂无字幕</option>
1106 <option value="自带中文字幕">自带中文字幕</option>
1107 <option value="自带双语字幕(含中文)">自带双语字幕(含中文)</option>
1108 <option value="附件中文字幕">附件中文字幕</option>
1109 <option value="附件双语字幕">附件双语字幕</option>
1110 </select>
1111 </div>
1112
1113 <div className="form-group">
1114 <label>种子文件</label>
1115 <input
1116 type="file"
1117 accept=".torrent"
1118 onChange={handleFileChange}
1119
1120 />
1121 </div>
1122
1123 <div className="form-group">
1124 <label>简介</label>
1125 <textarea
1126 value={uploadData.description}
1127 onChange={(e) => setUploadData({
1128 ...uploadData,
1129 description: e.target.value
1130 })}
1131 />
1132 </div>
1133
1134 <div className="form-actions">
1135 <button
1136 type="button"
1137 className="cancel-btn"
1138 onClick={() => setShowUploadModal(false)}
1139 >
1140 取消
1141 </button>
1142 <button
1143 type="submit"
1144 className="confirm-btn"
1145 disabled={isUploading}
1146 >
1147 {isUploading ? '上传中...' : '确认上传'}
1148 </button>
1149 </div>
1150 </form>
1151 </div>
1152 </div>
Akane121765b61a72025-05-17 13:52:25 +08001153 )}
22301080a93bebb2025-05-27 19:48:11 +08001154
1155 <div className="resource-list">
1156 {torrentPosts.map(torrent => (
1157 <div
1158 key={torrent.id}
1159 className="resource-item"
1160 onClick={() => navigate(`/torrent/${torrent.id}`)}
1161 >
1162 <div className="resource-poster">
1163 <div className="poster-image gray-bg">{torrent.torrentName.charAt(0)}</div>
1164 </div>
1165 <div className="resource-info">
1166 <h3 className="resource-title">{torrent.torrentName}</h3>
1167 <p className="resource-meta">
1168 {torrent.resolution} | {torrent.region} | {torrent.category}
1169 </p>
1170 <p className="resource-subtitle">字幕: {torrent.subtitle}</p>
1171 </div>
1172 <div className="resource-stats">
Akane12173a7bb972025-06-01 01:05:27 +08001173 <span className="stat">{torrent.size}</span>
22301080a93bebb2025-05-27 19:48:11 +08001174 <span className="stat">发布者: {torrent.username}</span>
1175 </div>
1176 <button
1177 className="download-btn"
DREWae420b22025-06-02 14:07:20 +08001178 onClick={(e) => handleDownloadClick(torrent, e)}
22301080a93bebb2025-05-27 19:48:11 +08001179 >
1180 立即下载
1181 </button>
DREWae420b22025-06-02 14:07:20 +08001182 {/* 添加删除按钮 - 只有管理员或发布者可见 */}
1183 {(userInfo?.isAdmin || userInfo?.name === torrent.username) && (
1184 <button
1185 className="delete-btn"
1186 onClick={(e) => handleDeleteTorrent(torrent.id, e)}
1187 >
1188 删除
1189 </button>
1190 )}
22301080a93bebb2025-05-27 19:48:11 +08001191 </div>
1192 ))}
1193 </div>
1194
Akane12173a7bb972025-06-01 01:05:27 +08001195 {totalPages > 1 && (
22301080a93bebb2025-05-27 19:48:11 +08001196 <div className="pagination">
1197 <button
1198 onClick={() => fetchTorrentPosts(currentPage - 1)}
1199 disabled={currentPage === 1}
1200 >
1201 上一页
1202 </button>
1203
1204 {Array.from({length: totalPages}, (_, i) => i + 1).map(page => (
1205 <button
1206 key={page}
1207 onClick={() => fetchTorrentPosts(page)}
1208 className={currentPage === page ? 'active' : ''}
1209 >
1210 {page}
1211 </button>
1212 ))}
1213
1214 <button
1215 onClick={() => fetchTorrentPosts(currentPage + 1)}
1216 disabled={currentPage === totalPages}
1217 >
1218 下一页
1219 </button>
1220 </div>
Akane12173a7bb972025-06-01 01:05:27 +08001221 )}
Akane121765b61a72025-05-17 13:52:25 +08001222 </div>
22301080a93bebb2025-05-27 19:48:11 +08001223 );
1224 // 在Dashboard.jsx的renderContent函数中修改case 'request'部分
1225 case 'request':
1226 return (
1227 <div className="content-area" data-testid="request-section">
DREWae420b22025-06-02 14:07:20 +08001228 {/* 求助区搜索框 */}
Akane12173a7bb972025-06-01 01:05:27 +08001229 <div className="section-search-container">
1230 <input
1231 type="text"
DREWae420b22025-06-02 14:07:20 +08001232 placeholder="搜索求助..."
Akane12173a7bb972025-06-01 01:05:27 +08001233 value={requestSearch}
1234 onChange={(e) => setRequestSearch(e.target.value)}
1235 className="section-search-input"
1236 onKeyPress={(e) => e.key === 'Enter' && handleSearchRequest()}
1237 />
1238 <button
1239 className="search-button"
1240 onClick={handleSearchRequest}
1241 >
1242 搜索
1243 </button>
DREWae420b22025-06-02 14:07:20 +08001244 <button
1245 className="reset-button"
1246 onClick={handleResetRequestSearch}
1247 style={{marginLeft: '10px'}}
1248 >
1249 重置
1250 </button>
Akane12173a7bb972025-06-01 01:05:27 +08001251 </div>
DREWae420b22025-06-02 14:07:20 +08001252
1253 {/* 新增发帖按钮 */}
1254 <div className="post-header">
1255 <button
1256 className="create-post-btn"
1257 onClick={() => setShowPostModal(true)}
1258 >
1259 发帖求助
1260 </button>
1261 </div>
1262
1263 {/* 加载状态和错误提示 */}
1264 {requestLoading && <div className="loading">加载中...</div>}
1265 {requestError && <div className="error">{helpError}</div>}
22301080a93bebb2025-05-27 19:48:11 +08001266 {/* 求种区帖子列表 */}
1267 <div className="request-list">
DREWae420b22025-06-02 14:07:20 +08001268 {requestPosts.map(post => (
22301080a93bebb2025-05-27 19:48:11 +08001269 <div
1270 key={post.id}
DREWae420b22025-06-02 14:07:20 +08001271 className={`request-post ${post.isSolved ? 'solved' : ''}`}
22301080a93bebb2025-05-27 19:48:11 +08001272 onClick={() => navigate(`/request/${post.id}`)}
1273 >
1274 <div className="post-header">
DREWae420b22025-06-02 14:07:20 +08001275 <img
1276 src={post.authorAvatar || 'https://via.placeholder.com/40'}
1277 alt={post.authorId}
1278 className="post-avatar"
1279 />
1280 <div className="post-author">{post.authorId}</div>
1281 <div className="post-date">
1282 {new Date(post.createTime).toLocaleDateString()}
1283 </div>
1284 {post.isSolved && <span className="solved-badge">已解决</span>}
22301080a93bebb2025-05-27 19:48:11 +08001285 </div>
1286 <h3 className="post-title">{post.title}</h3>
1287 <p className="post-content">{post.content}</p>
1288 <div className="post-stats">
DREWae420b22025-06-02 14:07:20 +08001289 <span className="post-likes">👍 {post.likeCount || 0}</span>
1290 <span className="post-comments">💬 {post.replyCount || 0}</span>
22301080a93bebb2025-05-27 19:48:11 +08001291 </div>
1292 </div>
1293 ))}
1294 </div>
DREWae420b22025-06-02 14:07:20 +08001295 {/* 在帖子列表后添加分页控件 */}
1296 <div className="pagination">
1297 <button
1298 onClick={() => fetchRequestPosts(currentPage - 1)}
1299 disabled={currentPage === 1}
1300 >
1301 上一页
1302 </button>
1303
1304 {Array.from({length: totalPages}, (_, i) => i + 1).map(page => (
1305 <button
1306 key={page}
1307 onClick={() => fetchRequestPosts(page)}
1308 className={currentPage === page ? 'active' : ''}
1309 >
1310 {page}
1311 </button>
1312 ))}
1313
1314 <button
1315 onClick={() => fetchRequestPosts(currentPage + 1)}
1316 disabled={currentPage === totalPages}
1317 >
1318 下一页
1319 </button>
1320 </div>
1321 {/* 新增发帖弹窗 */}
1322 {showPostModal && (
1323 <div className="post-modal-overlay">
1324 <div className="post-modal">
1325 <h3>发布求种帖</h3>
1326 <button
1327 className="modal-close-btn"
1328 onClick={() => setShowPostModal(false)}
1329 >
1330 ×
1331 </button>
1332
1333 <form onSubmit={handleRequestPostSubmit}>
1334 <div className="form-group">
1335 <label>帖子标题</label>
1336 <input
1337 type="text"
1338 value={postTitle}
1339 onChange={(e) => setPostTitle(e.target.value)}
1340 placeholder="请输入标题"
1341 required
1342 />
1343 </div>
1344
1345 <div className="form-group">
1346 <label>帖子内容</label>
1347 <textarea
1348 value={postContent}
1349 onChange={(e) => setPostContent(e.target.value)}
1350 placeholder="详细描述你的问题"
1351 required
1352 />
1353 </div>
1354
1355 <div className="form-group">
1356 <label>上传图片</label>
1357 <div className="upload-image-btn">
1358 <input
1359 type="file"
1360 id="image-upload"
1361 accept="image/*"
1362 onChange={handleImageUpload}
1363 style={{display: 'none'}}
1364 />
1365 <label htmlFor="image-upload">
1366 {selectedImage ? '已选择图片' : '选择图片'}
1367 </label>
1368 {selectedImage && (
1369 <span className="image-name">{selectedImage.name}</span>
1370 )}
1371 </div>
1372 </div>
1373
1374 <div className="form-actions">
1375 <button
1376 type="button"
1377 className="cancel-btn"
1378 onClick={() => setShowPostModal(false)}
1379 >
1380 取消
1381 </button>
1382 <button
1383 type="submit"
1384 className="submit-btn"
1385 >
1386 确认发帖
1387 </button>
1388 </div>
1389 </form>
1390 </div>
1391 </div>
1392 )}
Akane121765b61a72025-05-17 13:52:25 +08001393 </div>
DREWae420b22025-06-02 14:07:20 +08001394
1395
1396
1397
22301080a93bebb2025-05-27 19:48:11 +08001398 );
1399 // 在Dashboard.jsx的renderContent函数中修改case 'help'部分
1400 case 'help':
1401 return (
1402 <div className="content-area" data-testid="help-section">
Akane12173a7bb972025-06-01 01:05:27 +08001403 {/* 求助区搜索框 */}
1404 <div className="section-search-container">
1405 <input
1406 type="text"
1407 placeholder="搜索求助..."
1408 value={helpSearch}
1409 onChange={(e) => setHelpSearch(e.target.value)}
1410 className="section-search-input"
1411 onKeyPress={(e) => e.key === 'Enter' && handleSearchHelp()}
1412 />
1413 <button
1414 className="search-button"
1415 onClick={handleSearchHelp}
1416 >
1417 搜索
1418 </button>
1419 <button
1420 className="reset-button"
1421 onClick={handleResetHelpSearch}
1422 style={{marginLeft: '10px'}}
1423 >
1424 重置
1425 </button>
1426 </div>
1427
22301080a93bebb2025-05-27 19:48:11 +08001428 {/* 新增发帖按钮 */}
1429 <div className="post-header">
1430 <button
1431 className="create-post-btn"
1432 onClick={() => setShowPostModal(true)}
1433 >
1434 发帖求助
1435 </button>
1436 </div>
1437
1438 {/* 加载状态和错误提示 */}
1439 {helpLoading && <div className="loading">加载中...</div>}
1440 {helpError && <div className="error">{helpError}</div>}
1441
1442 {/* 求助区帖子列表 */}
1443 <div className="help-list">
1444 {helpPosts.map(post => (
1445 <div
1446 key={post.id}
1447 className={`help-post ${post.isSolved ? 'solved' : ''}`}
1448 onClick={() => navigate(`/help/${post.id}`)}
1449 >
1450 <div className="post-header">
1451 <img
1452 src={post.authorAvatar || 'https://via.placeholder.com/40'}
1453 alt={post.authorId}
1454 className="post-avatar"
1455 />
1456 <div className="post-author">{post.authorId}</div>
1457 <div className="post-date">
1458 {new Date(post.createTime).toLocaleDateString()}
1459 </div>
1460 {post.isSolved && <span className="solved-badge">已解决</span>}
1461 </div>
1462 <h3 className="post-title">{post.title}</h3>
1463 <p className="post-content">{post.content}</p>
1464 <div className="post-stats">
1465 <span className="post-likes">👍 {post.likeCount || 0}</span>
1466 <span className="post-comments">💬 {post.replyCount || 0}</span>
1467 </div>
1468 </div>
1469 ))}
1470 </div>
1471
1472 {/* 在帖子列表后添加分页控件 */}
1473 <div className="pagination">
1474 <button
1475 onClick={() => fetchHelpPosts(currentPage - 1)}
1476 disabled={currentPage === 1}
1477 >
1478 上一页
1479 </button>
1480
1481 {Array.from({length: totalPages}, (_, i) => i + 1).map(page => (
1482 <button
1483 key={page}
1484 onClick={() => fetchHelpPosts(page)}
1485 className={currentPage === page ? 'active' : ''}
1486 >
1487 {page}
1488 </button>
1489 ))}
1490
1491 <button
1492 onClick={() => fetchHelpPosts(currentPage + 1)}
1493 disabled={currentPage === totalPages}
1494 >
1495 下一页
1496 </button>
1497 </div>
1498
1499 {/* 新增发帖弹窗 */}
1500 {showPostModal && (
1501 <div className="post-modal-overlay">
1502 <div className="post-modal">
1503 <h3>发布求助帖</h3>
1504 <button
1505 className="modal-close-btn"
1506 onClick={() => setShowPostModal(false)}
1507 >
1508 ×
1509 </button>
1510
DREWae420b22025-06-02 14:07:20 +08001511 <form onSubmit={handleHelpPostSubmit}>
22301080a93bebb2025-05-27 19:48:11 +08001512 <div className="form-group">
1513 <label>帖子标题</label>
1514 <input
1515 type="text"
1516 value={postTitle}
1517 onChange={(e) => setPostTitle(e.target.value)}
1518 placeholder="请输入标题"
1519 required
1520 />
1521 </div>
1522
1523 <div className="form-group">
1524 <label>帖子内容</label>
1525 <textarea
1526 value={postContent}
1527 onChange={(e) => setPostContent(e.target.value)}
1528 placeholder="详细描述你的问题"
1529 required
1530 />
1531 </div>
1532
1533 <div className="form-group">
1534 <label>上传图片</label>
1535 <div className="upload-image-btn">
1536 <input
1537 type="file"
1538 id="image-upload"
1539 accept="image/*"
1540 onChange={handleImageUpload}
1541 style={{display: 'none'}}
1542 />
1543 <label htmlFor="image-upload">
1544 {selectedImage ? '已选择图片' : '选择图片'}
1545 </label>
1546 {selectedImage && (
1547 <span className="image-name">{selectedImage.name}</span>
1548 )}
1549 </div>
1550 </div>
1551
1552 <div className="form-actions">
1553 <button
1554 type="button"
1555 className="cancel-btn"
1556 onClick={() => setShowPostModal(false)}
1557 >
1558 取消
1559 </button>
1560 <button
1561 type="submit"
1562 className="submit-btn"
1563 >
1564 确认发帖
1565 </button>
1566 </div>
1567 </form>
1568 </div>
1569 </div>
1570 )}
1571 </div>
1572 );
1573 default:
1574 return <div className="content-area" data-testid="default-section">公告区内容</div>;
1575 }
DREWae420b22025-06-02 14:07:20 +08001576
22301080a93bebb2025-05-27 19:48:11 +08001577 };
1578
1579 if (loading) return <div className="loading">加载中...</div>;
1580 if (error) return <div className="error">{error}</div>;
1581
1582 return (
1583 <div className="dashboard-container" data-testid="dashboard-container">
1584 {/* 顶部栏 */}
1585 <div className="top-bar" data-testid="top-bar">
Akane12173a7bb972025-06-01 01:05:27 +08001586 {/* 平台名称替换搜索框 */}
1587 <div className="platform-name">
1588 <h2>PT资源站</h2>
Akane121765b61a72025-05-17 13:52:25 +08001589 </div>
Akane121765b61a72025-05-17 13:52:25 +08001590
22301080a93bebb2025-05-27 19:48:11 +08001591 <div className="user-actions">
1592 {/* 新增管理员按钮 - 只有管理员可见 */}
1593 {userInfo?.isAdmin && (
1594 <button
1595 className="admin-center-button"
1596 onClick={() => navigate('/administer')}
1597 >
1598 管理员中心
1599 </button>
1600 )}
Akane121765b61a72025-05-17 13:52:25 +08001601
22301080a93bebb2025-05-27 19:48:11 +08001602 <div className="user-info" data-testid="user-info">
1603 <img
1604 src={userInfo?.avatar || 'https://via.placeholder.com/40'}
1605 alt="用户头像"
1606 className="user-avatar"
1607 onClick={() => navigate('/personal')}
1608 style={{cursor: 'pointer'}}
1609 />
1610 <span className="username">{userInfo?.name || '用户'}</span>
1611 <button onClick={onLogout} className="logout-button">退出</button>
1612 </div>
1613 </div>
1614 </div>
1615
1616 {/* 导航栏 */}
1617 {/* handleTabchange函数替换了原本的setactivetab函数 */}
1618 <div className="nav-tabs">
1619 <button
1620 className={`tab-button ${activeTab === 'announcement' ? 'active' : ''}`}
1621 onClick={() => handleTabChange('announcement')}
1622 >
1623 公告区
1624 </button>
1625 <button
1626 className={`tab-button ${activeTab === 'share' ? 'active' : ''}`}
1627 onClick={() => handleTabChange('share')}
1628 >
1629 分享区
1630 </button>
1631 <button
1632 className={`tab-button ${activeTab === 'request' ? 'active' : ''}`}
1633 onClick={() => handleTabChange('request')}
1634 >
1635 求种区
1636 </button>
1637 <button
1638 className={`tab-button ${activeTab === 'help' ? 'active' : ''}`}
1639 onClick={() => handleTabChange('help')}
1640 >
1641 求助区
1642 </button>
1643 </div>
1644
1645 {/* 内容区 */}
1646 {renderContent()}
DREWae420b22025-06-02 14:07:20 +08001647 {/* 下载模态框 - 添加在这里 */}
1648 {showDownloadModal && selectedTorrent && (
1649 <div className="modal-overlay">
1650 <div className="download-modal">
1651 <h3>下载 {selectedTorrent.torrentName}</h3>
1652 <button
1653 className="close-btn"
1654 onClick={() => !isDownloading && setShowDownloadModal(false)}
1655 disabled={isDownloading}
1656 >
1657 ×
1658 </button>
1659
1660 <div className="form-group">
1661 <label>下载路径:</label>
1662 <input
1663 type="text"
1664 value={downloadPath}
1665 onChange={(e) => {
1666 // 实时格式化显示
1667 let path = e.target.value
1668 .replace(/\t/g, '')
1669 .replace(/\\/g, '/')
1670 .replace(/\s+/g, ' ');
1671 setDownloadPath(path);
1672 }}
1673 disabled={isDownloading}
1674 placeholder="例如: D:/downloads/"
1675 />
1676 </div>
1677
1678 {isDownloading && (
1679 <div className="progress-container">
1680 <div className="progress-bar" style={{ width: `${downloadProgress}%` }}>
1681 {downloadProgress}%
1682 </div>
1683 </div>
1684 )}
1685
1686 <div className="modal-actions">
1687 <button
1688 onClick={() => !isDownloading && setShowDownloadModal(false)}
1689 disabled={isDownloading}
1690 >
1691 取消
1692 </button>
1693 <button
1694 onClick={handleDownload}
1695 disabled={isDownloading || !downloadPath}
1696 >
1697 {isDownloading ? '下载中...' : '开始下载'}
1698 </button>
1699 </div>
1700 </div>
1701 </div>
1702 )}
Akane121765b61a72025-05-17 13:52:25 +08001703 </div>
22301080a93bebb2025-05-27 19:48:11 +08001704 );
Akane121765b61a72025-05-17 13:52:25 +08001705};
1706
1707export default Dashboard;