blob: e393fcd99a787f1ab1ac387fe4bfb5d81376b10b [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);
DREW5b1883e2025-06-07 10:41:32 +080075 const [isRecommend, setIsRecommend] = useState(true);
76 const [needNewRecommend, setNeedNewRecommend] = useState(false);
77
DREW7c7c6a02025-06-05 19:58:55 +080078
Akane12173a7bb972025-06-01 01:05:27 +080079
22301080a93bebb2025-05-27 19:48:11 +080080
81 const activeTab = tab || 'announcement'; // 如果没有tab参数,则默认为announcement
82 // 从location.state中初始化状态
83
84
85 const handleTabChange = (tabName) => {
86 navigate(`/dashboard/${tabName}`, {
87 state: {
88 savedFilters: selectedFilters, // 使用新的筛选状态 // 保留现有状态
89 activeTab: tabName // 可选,如果其他组件依赖这个 state
90 }
91 });
92 };
93
Akane12173a7bb972025-06-01 01:05:27 +080094
DREWae420b22025-06-02 14:07:20 +080095
96 //公告区
97 // 添加获取公告的方法
98 const fetchAnnouncements = async () => {
99 try {
100 const response = await getLatestAnnouncements();
101 setAnnouncements(response.data.data.announcements || []);
102 } catch (error) {
103 console.error('获取公告失败:', error);
104 }
Akane12173a7bb972025-06-01 01:05:27 +0800105 };
106
DREW5b1883e2025-06-07 10:41:32 +0800107 // 修改 useEffect 钩子
DREWae420b22025-06-02 14:07:20 +0800108 useEffect(() => {
109 if (activeTab === 'announcement') {
110 fetchAnnouncements();
111 fetchDiscountsForCarousel();
DREW5b1883e2025-06-07 10:41:32 +0800112
113 // 只在需要时获取推荐
114 if (isRecommend || needNewRecommend) {
115 fetchRecommendations();
116 }
DREWae420b22025-06-02 14:07:20 +0800117 }
DREW5b1883e2025-06-07 10:41:32 +0800118 }, [activeTab, isRecommend, needNewRecommend]);
119
120 // 添加刷新推荐的功能
121 const handleRefreshRecommendations = () => {
122 setNeedNewRecommend(true);
123 };
Akane12173a7bb972025-06-01 01:05:27 +0800124
DREWae420b22025-06-02 14:07:20 +0800125 const fetchDiscountsForCarousel = async () => {
126 try {
127 const all = await getAllDiscounts();
DREW5b1883e2025-06-07 10:41:32 +0800128 // console.log("返回的折扣数据:", all);
DREWae420b22025-06-02 14:07:20 +0800129 const now = new Date();
22301080a93bebb2025-05-27 19:48:11 +0800130
DREWae420b22025-06-02 14:07:20 +0800131 // ⚠️ 使用 Date.parse 确保兼容 ISO 格式
132 const ongoing = all.filter(d =>
133 Date.parse(d.startTime) <= now.getTime() && Date.parse(d.endTime) >= now.getTime()
134 );
135
136 const upcoming = all
137 .filter(d => Date.parse(d.startTime) > now.getTime())
138 .sort((a, b) => Date.parse(a.startTime) - Date.parse(b.startTime));
139
140 const selected = [...ongoing.slice(0, 3)];
141
142 while (selected.length < 3 && upcoming.length > 0) {
143 selected.push(upcoming.shift());
144 }
145
146 setCarouselDiscounts(selected);
147 } catch (e) {
148 console.error("获取折扣失败:", e);
149 }
150 };
151
152 // 修改handleAnnouncementClick函数中的state传递,移除不必要的字段
22301080a93bebb2025-05-27 19:48:11 +0800153 const handleAnnouncementClick = (announcement, e) => {
154 if (!e.target.closest('.exclude-click')) {
155 navigate(`/announcement/${announcement.id}`, {
156 state: {
157 announcement,
158 returnPath: `/dashboard/${activeTab}`,
159 scrollPosition: window.scrollY,
160 activeTab
161 }
162 });
Akane121765b61a72025-05-17 13:52:25 +0800163 }
22301080a93bebb2025-05-27 19:48:11 +0800164 };
Akane121765b61a72025-05-17 13:52:25 +0800165
DREW7c7c6a02025-06-05 19:58:55 +0800166 const handleRecommendClick = async (torrent) => {
167 try {
168 // 标记为已点击
169 await markRecommendationClicked(torrent.id);
170
171 // 导航到种子详情页
172 navigate(`/torrent/${torrent.id}`, {
173 state: {
174 fromTab: 'announcement',
175 scrollPosition: window.scrollY
176 }
177 });
178 } catch (error) {
179 console.error('标记推荐点击失败:', error);
180 }
181 };
182
DREWae420b22025-06-02 14:07:20 +0800183
184   // 公告区搜索处理
185    const handleSearchAnnouncement = (e) => {
186        setAnnouncementSearch(e.target.value);
187    };
188
189    // 修改后的搜索函数
190    const handleSearchShare = async () => {
191        try {
192            setTorrentLoading(true);
193            const response = await searchTorrents(shareSearch, 1);
194            if (response.data.code === 200) {
195                setTorrentPosts(response.data.data.records);
196                const total = response.data.data.total;
197                setTotalPages(Math.ceil(total / 5));
198                setCurrentPage(1);
199            } else {
200                setTorrentError(response.data.message || '搜索失败');
201            }
202        } catch (err) {
203            setTorrentError(err.message || '搜索失败');
204        } finally {
205            setTorrentLoading(false);
206        }
207    };
208
209    const handleResetShareSearch = async () => {
210        setShareSearch('');
211        setSelectedFilters(
212            Object.keys(filterCategories).reduce((acc, category) => {
213                acc[category] = 'all';
214                return acc;
215            }, {})
216        );
217        await fetchTorrentPosts(1, true);
218    };
219
220    // 添加搜索函数
221    const handleSearchRequest = async () => {
222        try {
223        setRequestLoading(true);
224        const response = await searchRequestPosts(requestSearch, currentPage);
225        if (response.data.code === 200) {
226            const postsWithCounts = await Promise.all(
227            response.data.data.records.map(async (post) => {
228                try {
229                const detailResponse = await getRequestPostDetail(post.id);
230                if (detailResponse.data.code === 200) {
231                    return {
232                    ...post,
233                    replyCount: detailResponse.data.data.post.replyCount || 0,
234                    isLiked: false
235                    };
236                }
237                return post;
238                } catch (err) {
239                console.error(`获取帖子${post.id}详情失败:`, err);
240                return post;
241                }
242            })
243            );
244            setRequestPosts(postsWithCounts);
245            setTotalPages(Math.ceil(response.data.data.total / 5));
246        } else {
247            setRequestError(response.data.message || '搜索失败');
248        }
249        } catch (err) {
250        setRequestError(err.message || '搜索失败');
251        } finally {
252        setRequestLoading(false);
253        }
254    };
255   
256    // 添加重置搜索函数
257    const handleResetRequestSearch = async () => {
258        setRequestSearch('');
259 await fetchRequestPosts(1); // 重置到第一页
260 };
261
262   // 添加搜索函数
263    const handleSearchHelp = async () => {
264        try {
265        setHelpLoading(true);
266        const response = await searchHelpPosts(helpSearch, currentPage);
267        if (response.data.code === 200) {
268            const postsWithCounts = await Promise.all(
269            response.data.data.records.map(async (post) => {
270                try {
271                const detailResponse = await getHelpPostDetail(post.id);
272                if (detailResponse.data.code === 200) {
273                    return {
274                    ...post,
275                    replyCount: detailResponse.data.data.post.replyCount || 0,
276                    isLiked: false
277                    };
278                }
279                return post;
280                } catch (err) {
281                console.error(`获取帖子${post.id}详情失败:`, err);
282                return post;
283                }
284            })
285            );
286            setHelpPosts(postsWithCounts);
287            setTotalPages(Math.ceil(response.data.data.total / 5));
288        } else {
289            setHelpError(response.data.message || '搜索失败');
290        }
291        } catch (err) {
292        setHelpError(err.message || '搜索失败');
293        } finally {
294        setHelpLoading(false);
295        }
296    };
297   
298    // 添加重置搜索函数
299    const handleResetHelpSearch = async () => {
300        setHelpSearch('');
301 await fetchHelpPosts(1); // 重置到第一页
302 };
303
304
305
Akane121765b61a72025-05-17 13:52:25 +0800306
22301080a93bebb2025-05-27 19:48:11 +0800307 //资源区
308 const handleFileChange = (e) => {
309 setUploadData({...uploadData, file: e.target.files[0]});
310 };
Akane121765b61a72025-05-17 13:52:25 +0800311
22301080a93bebb2025-05-27 19:48:11 +0800312 const handleUploadSubmit = async (e) => {
313 e.preventDefault();
314 try {
315 setIsUploading(true);
Akane121765b61a72025-05-17 13:52:25 +0800316
22301080a93bebb2025-05-27 19:48:11 +0800317 const torrentData = {
318 torrentName: uploadData.name,
319 description: uploadData.description,
320 category: uploadData.type,
321 region: uploadData.region,
322 resolution: uploadData.resolution,
323 subtitle: uploadData.subtitle
324 };
Akane121765b61a72025-05-17 13:52:25 +0800325
DREWae420b22025-06-02 14:07:20 +0800326 await createTorrent(uploadData.file, torrentData, (progress) => {
327 console.log(`上传进度: ${progress}%`);
328 // 这里可以添加进度条更新逻辑
329 });
22301080a93bebb2025-05-27 19:48:11 +0800330
331 // 上传成功处理
332 setShowUploadModal(false);
333 setUploadData({
334 name: '',
335 type: '',
336 region: '',
337 subtitle: '',
338 resolution: '',
339 file: null,
340 description: ''
341 });
342 alert('种子创建成功!');
343
344 // 刷新列表
345 await fetchTorrentPosts(currentPage);
346
347 } catch (error) {
348 console.error('创建失败:', error);
349 alert('创建失败: ' + (error.response?.data?.message || error.message));
350 } finally {
351 setIsUploading(false);
352 }
353 };
354
DREWae420b22025-06-02 14:07:20 +0800355 // 处理下载按钮点击
356 const handleDownloadClick = (torrent, e) => {
357 e.stopPropagation();
358 setSelectedTorrent(torrent);
359 setShowDownloadModal(true);
360 };
361
362 // 执行下载
363 const handleDownload = async () => {
364 if (!selectedTorrent || !downloadPath) return;
365
366 setIsDownloading(true);
367 setDownloadProgress(0);
368
DREW5b1883e2025-06-07 10:41:32 +0800369 try {
DREWae420b22025-06-02 14:07:20 +0800370 // 发起下载请求
DREW5b1883e2025-06-07 10:41:32 +0800371 await downloadTorrent(selectedTorrent.id, downloadPath);
372 message.success("下载任务已提交");
DREWae420b22025-06-02 14:07:20 +0800373 // 开始轮询进度
374 const interval = setInterval(async () => {
375 try {
376 const res = await getDownloadProgress();
377 const progresses = res.data.progresses;
378
379 if (progresses) {
380 // 使用完整的 torrent 文件路径作为键
DREW5b1883e2025-06-07 10:41:32 +0800381 const torrentPath = selectedTorrent.downloadPath.replace(/\\/g, '/');
DREWae420b22025-06-02 14:07:20 +0800382 const torrentHash = selectedTorrent.hash;
383 // 查找匹配的进度
384 let foundProgress = null;
385 for (const [key, value] of Object.entries(progresses)) {
386 const normalizedKey = key.replace(/\\/g, '/');
387 if (normalizedKey.includes(selectedTorrent.hash) ||
388 normalizedKey.includes(selectedTorrent.torrentName)) {
389 foundProgress = value;
390 break;
391 }
392 }
393 if (foundProgress !== null) {
394 const newProgress = Math.round(foundProgress * 100);
395 setDownloadProgress(newProgress);
396
397 // 检查是否下载完成
398 if (newProgress >= 100) {
399 clearInterval(interval);
400 setIsDownloading(false);
401 message.success('下载完成!');
402 setTimeout(() => setShowDownloadModal(false), 2000);
403 }
404 } else {
405 console.log('当前下载进度:', progresses); // 添加日志
406 }
407 }
408 } catch (error) {
409 console.error('获取进度失败:', error);
410 // 如果获取进度失败但文件已存在,也视为完成
DREW5b1883e2025-06-07 10:41:32 +0800411 const filePath = `${downloadPath}${selectedTorrent.torrentName || 'downloaded_file'}`;
DREWae420b22025-06-02 14:07:20 +0800412 try {
413 const exists = await checkFileExists(filePath);
414 if (exists) {
415 clearInterval(interval);
416 setDownloadProgress(100);
417 setIsDownloading(false);
418 message.success('下载完成!');
419 setTimeout(() => setShowDownloadModal(false), 2000);
420 }
421 } catch (e) {
422 console.error('文件检查失败:', e);
423 }
424 }
425 }, 2000);
426
427 return () => clearInterval(interval);
428 } catch (error) {
429 setIsDownloading(false);
430 if (error.response && error.response.status === 409) {
431 message.error('分享率不足,无法下载此资源');
432 } else {
433 message.error('下载失败: ' + (error.message || '未知错误'));
434 }
435 }
436 };
437
438 const checkFileExists = async (filePath) => {
439 try {
440 // 这里需要根据您的实际环境实现文件存在性检查
441 // 如果是Electron应用,可以使用Node.js的fs模块
442 // 如果是纯前端,可能需要通过API请求后端检查
443 return true; // 暂时返回true,实际实现需要修改
444 } catch (e) {
445 console.error('检查文件存在性失败:', e);
446 return false;
447 }
448 };
449
450 const handleDeleteTorrent = async (torrentId, e) => {
451 e.stopPropagation(); // 阻止事件冒泡,避免触发资源项的点击事件
452
453 try {
454 // 确认删除
455 if (!window.confirm('确定要删除这个种子吗?此操作不可撤销!')) {
456 return;
457 }
458
459 // 调用删除API
460 await deleteTorrent(torrentId);
461
462 // 删除成功后刷新列表
463 message.success('种子删除成功');
464 await fetchTorrentPosts(currentPage);
465 } catch (error) {
466 console.error('删除种子失败:', error);
467 message.error('删除种子失败: ' + (error.response?.data?.message || error.message));
468 }
469 };
470
471 const handleRequestPostSubmit = async (e) => {
22301080a93bebb2025-05-27 19:48:11 +0800472 e.preventDefault();
473 try {
Akane1217d1e9f712025-05-29 14:36:56 +0800474 const username = localStorage.getItem('username');
DREWae420b22025-06-02 14:07:20 +0800475 const response = await createRequestPost(
476 postTitle,
477 postContent,
478 username,
479 selectedImage
480 );
481
482 if (response.data.code === 200) {
483 // 刷新帖子列表
484
485 await fetchRequestPosts(currentPage);
486 // 重置表单
487 setShowPostModal(false);
488 setPostTitle('');
489 setPostContent('');
490 setSelectedImage(null);
491 } else {
492 setHelpError(response.data.message || '发帖失败');
493 }
494 } catch (err) {
495 setHelpError(err.message || '发帖失败');
496 }
497 };
498
499 const handleHelpPostSubmit = async (e) => {
500 e.preventDefault();
501 try {
502 const username = localStorage.getItem('username');
503 const response = await createHelpPost(
Akane1217d1e9f712025-05-29 14:36:56 +0800504 postTitle,
505 postContent,
506 username,
507 selectedImage
508 );
509
510 if (response.data.code === 200) {
511 // 刷新帖子列表
512 await fetchHelpPosts(currentPage);
DREWae420b22025-06-02 14:07:20 +0800513
Akane1217d1e9f712025-05-29 14:36:56 +0800514 // 重置表单
515 setShowPostModal(false);
516 setPostTitle('');
517 setPostContent('');
518 setSelectedImage(null);
519 } else {
520 setHelpError(response.data.message || '发帖失败');
521 }
22301080a93bebb2025-05-27 19:48:11 +0800522 } catch (err) {
Akane1217d1e9f712025-05-29 14:36:56 +0800523 setHelpError(err.message || '发帖失败');
22301080a93bebb2025-05-27 19:48:11 +0800524 }
Akane1217d1e9f712025-05-29 14:36:56 +0800525 };
Akane121765b61a72025-05-17 13:52:25 +0800526
Akane12173a7bb972025-06-01 01:05:27 +0800527
528const fetchTorrentPosts = async (page = 1, isReset = false) => {
529 setTorrentLoading(true);
530 try {
531 const params = {
532 page,
533 size: 5
534 };
535
536 // 如果有筛选条件且不是重置操作
537 if (!isReset && Object.values(selectedFilters).some(v => v !== 'all')) {
538 if (selectedFilters.type !== 'all') params.category = selectedFilters.type;
539 if (selectedFilters.subtitle !== 'all') params.subtitle = selectedFilters.subtitle;
540 if (selectedFilters.region !== 'all') params.region = selectedFilters.region;
541 if (selectedFilters.resolution !== 'all') params.resolution = selectedFilters.resolution;
22301080a93bebb2025-05-27 19:48:11 +0800542 }
Akane12173a7bb972025-06-01 01:05:27 +0800543
544 const response = (shareSearch && !isReset)
545 ? await searchTorrents(shareSearch, page)
DREW7c7c6a02025-06-05 19:58:55 +0800546 : await api.get('/torrent', { params });
Akane12173a7bb972025-06-01 01:05:27 +0800547
548 if (response.data.code === 200) {
549 setTorrentPosts(response.data.data.records);
550 const total = response.data.data.total;
551 setTotalPages(Math.ceil(total / 5));
552 setCurrentPage(page);
553 } else {
554 setTorrentError(response.data.message);
555 }
556 } catch (err) {
557 setTorrentError(err.message);
558 } finally {
559 setTorrentLoading(false);
560 }
561};
Akane121765b61a72025-05-17 13:52:25 +0800562
22301080a93bebb2025-05-27 19:48:11 +0800563 // 在useEffect中调用
564 useEffect(() => {
565 if (activeTab === 'share') {
566 fetchTorrentPosts();
567 }
568 }, [activeTab]);
Akane121765b61a72025-05-17 13:52:25 +0800569
DREWae420b22025-06-02 14:07:20 +0800570const fetchRequestPosts = async (page = 1) => {
571 setRequestLoading(true);
572 try {
573 const response = await getRequestPosts(page);
574 if (response.data.code === 200) {
575 const postsWithCounts = await Promise.all(
576 response.data.data.records.map(async (post) => {
577 try {
578 const detailResponse = await getRequestPostDetail(post.id);
579 if (detailResponse.data.code === 200) {
580 return {
581 ...post,
582 replyCount: detailResponse.data.data.post.replyCount || 0,
583 isLiked: false // 根据需要添加其他字段
584 };
585 }
586 return post; // 如果获取详情失败,返回原始帖子数据
587 } catch (err) {
588 console.error(`获取帖子${post.id}详情失败:`, err);
589 return post;
590 }
591 })
592 );
593 setRequestPosts(postsWithCounts);
594 setTotalPages(Math.ceil(response.data.data.total / 5)); // 假设每页5条
595 setCurrentPage(page);
596 } else {
597 setRequestError(response.data.message || '获取求助帖失败');
598 }
599 } catch (err) {
600 setRequestError(err.message || '获取求助帖失败');
601 } finally {
602 setRequestLoading(false);
603 }
604 };
605
22301080a93bebb2025-05-27 19:48:11 +0800606 const handleImageUpload = (e) => {
607 setSelectedImage(e.target.files[0]);
608 };
Akane121765b61a72025-05-17 13:52:25 +0800609
610
22301080a93bebb2025-05-27 19:48:11 +0800611 const fetchHelpPosts = async (page = 1) => {
612 setHelpLoading(true);
613 try {
DREWae420b22025-06-02 14:07:20 +0800614 const response = await getHelpPosts(page);
Akane1217d1e9f712025-05-29 14:36:56 +0800615 if (response.data.code === 200) {
616 const postsWithCounts = await Promise.all(
617 response.data.data.records.map(async (post) => {
618 try {
DREWae420b22025-06-02 14:07:20 +0800619 const detailResponse = await getHelpPostDetail(post.id);
Akane1217d1e9f712025-05-29 14:36:56 +0800620 if (detailResponse.data.code === 200) {
621 return {
622 ...post,
623 replyCount: detailResponse.data.data.post.replyCount || 0,
624 isLiked: false // 根据需要添加其他字段
625 };
626 }
627 return post; // 如果获取详情失败,返回原始帖子数据
628 } catch (err) {
629 console.error(`获取帖子${post.id}详情失败:`, err);
630 return post;
631 }
632 })
633 );
634 setHelpPosts(postsWithCounts);
635 setTotalPages(Math.ceil(response.data.data.total / 5)); // 假设每页5条
636 setCurrentPage(page);
637 } else {
638 setHelpError(response.data.message || '获取求助帖失败');
639 }
22301080a93bebb2025-05-27 19:48:11 +0800640 } catch (err) {
Akane1217d1e9f712025-05-29 14:36:56 +0800641 setHelpError(err.message || '获取求助帖失败');
22301080a93bebb2025-05-27 19:48:11 +0800642 } finally {
Akane1217d1e9f712025-05-29 14:36:56 +0800643 setHelpLoading(false);
22301080a93bebb2025-05-27 19:48:11 +0800644 }
DREW7c7c6a02025-06-05 19:58:55 +0800645 };
646
DREW5b1883e2025-06-07 10:41:32 +0800647 // 修改 fetchRecommendations 函数
648const fetchRecommendations = async () => {
649 try {
650 setRecommendLoading(true);
651 const response = await getRecommendations(5);
652
653 if (Array.isArray(response)) {
654 setRecommendations(response);
DREW7c7c6a02025-06-05 19:58:55 +0800655
DREW5b1883e2025-06-07 10:41:32 +0800656 if (response.length === 0) {
657 console.warn('暂无推荐内容');
658 } else {
659 // 标记这些推荐为已显示
660 response.forEach(torrent => {
661 markRecommendationShown(torrent.id);
662 });
DREW7c7c6a02025-06-05 19:58:55 +0800663 }
DREW5b1883e2025-06-07 10:41:32 +0800664
665 // 获取成功后重置标志
666 setIsRecommend(false);
667 setNeedNewRecommend(false);
668 } else {
669 console.warn('返回格式异常:', response);
670 setRecommendations([]);
DREW7c7c6a02025-06-05 19:58:55 +0800671 }
DREW5b1883e2025-06-07 10:41:32 +0800672 } catch (error) {
673 setRecommendError(error.message || '获取推荐失败');
674 } finally {
675 setRecommendLoading(false);
676 }
677};
Akane1217d1e9f712025-05-29 14:36:56 +0800678
679
22301080a93bebb2025-05-27 19:48:11 +0800680 useEffect(() => {
DREWae420b22025-06-02 14:07:20 +0800681 if (activeTab === 'request') {
682 fetchRequestPosts(currentPage);
22301080a93bebb2025-05-27 19:48:11 +0800683 }
684 }, [activeTab, currentPage]); // 添加 currentPage 作为依赖
Akane121765b61a72025-05-17 13:52:25 +0800685
686
Akane12173a7bb972025-06-01 01:05:27 +0800687 // 分类维度配置
22301080a93bebb2025-05-27 19:48:11 +0800688 const filterCategories = {
689 type: {
690 label: '类型',
691 options: {
692 'all': '全部',
693 '电影': '电影',
694 '电视剧': '电视剧',
695 '动漫': '动漫',
Akane12173a7bb972025-06-01 01:05:27 +0800696 '综艺': '综艺',
697 '音乐': '音乐',
698 '其他': '其他'
22301080a93bebb2025-05-27 19:48:11 +0800699 }
700 },
701 subtitle: {
702 label: '字幕',
703 options: {
704 'all': '全部',
Akane12173a7bb972025-06-01 01:05:27 +0800705 '无需字幕': '无需字幕',
706 '暂无字幕': '暂无字幕',
707 '自带中文字幕': '自带中文字幕',
708 '自带双语字幕(含中文)': '自带双语字幕(含中文)',
709 '附件中文字幕': '附件中文字幕',
710 '附件双语字幕': '附件双语字幕'
22301080a93bebb2025-05-27 19:48:11 +0800711 }
712 },
713 region: {
714 label: '地区',
715 options: {
716 'all': '全部',
Akane12173a7bb972025-06-01 01:05:27 +0800717 '中国': '中国',
718 '英国': '英国',
719 '美国': '美国',
720 '日本': '日本',
721 '韩国': '韩国',
722 '其他': '其他'
723 }
724 },
725 resolution: {
726 label: '分辨率',
727 options: {
728 'all': '全部',
729 '4K': '4K',
730 '2K': '2K',
731 '1080P': '1080P',
732 '720P': '720P',
733 'SD': 'SD',
734 '无损音源': '无损音源',
735 '杜比全景声': '杜比全景声',
736 '其他': '其他'
22301080a93bebb2025-05-27 19:48:11 +0800737 }
738 }
739 };
22301080a93bebb2025-05-27 19:48:11 +0800740 const [selectedFilters, setSelectedFilters] = useState(
741 location.state?.savedFilters ||
742 Object.keys(filterCategories).reduce((acc, category) => {
743 acc[category] = 'all';
744 return acc;
745 }, {})
746 );
Akane121765b61a72025-05-17 13:52:25 +0800747
Akane12173a7bb972025-06-01 01:05:27 +0800748 // 处理筛选条件变更
22301080a93bebb2025-05-27 19:48:11 +0800749 const handleFilterSelect = (category, value) => {
750 setSelectedFilters(prev => ({
751 ...prev,
Akane12173a7bb972025-06-01 01:05:27 +0800752 [category]: prev[category] === value ? 'all' : value
22301080a93bebb2025-05-27 19:48:11 +0800753 }));
Akane121765b61a72025-05-17 13:52:25 +0800754 };
755
Akane12173a7bb972025-06-01 01:05:27 +0800756 // 应用筛选条件
757 const applyFilters = async () => {
758 try {
759 setTorrentLoading(true);
760
761 // 构建查询参数
762 const params = {
763 page: 1, // 从第一页开始
764 size: 5
765 };
766
767 // 添加筛选条件
768 if (selectedFilters.type !== 'all') {
769 params.category = selectedFilters.type;
770 }
771 if (selectedFilters.subtitle !== 'all') {
772 params.subtitle = selectedFilters.subtitle;
773 }
774 if (selectedFilters.region !== 'all') {
775 params.region = selectedFilters.region;
776 }
777 if (selectedFilters.resolution !== 'all') {
778 params.resolution = selectedFilters.resolution;
779 }
780
781 // 调用API获取筛选结果
DREW7c7c6a02025-06-05 19:58:55 +0800782 const response = await api.get('/torrent', { params });
Akane12173a7bb972025-06-01 01:05:27 +0800783
784 if (response.data.code === 200) {
785 setTorrentPosts(response.data.data.records);
786 setTotalPages(Math.ceil(response.data.data.total / 5));
787 setCurrentPage(1);
788 } else {
789 setTorrentError(response.data.message || '筛选失败');
790 }
791 } catch (err) {
792 setTorrentError(err.message || '筛选失败');
793 } finally {
794 setTorrentLoading(false);
795 }
22301080a93bebb2025-05-27 19:48:11 +0800796 };
Akane121765b61a72025-05-17 13:52:25 +0800797
Akane121765b61a72025-05-17 13:52:25 +0800798
22301080a93bebb2025-05-27 19:48:11 +0800799 // 恢复滚动位置
800 useEffect(() => {
801 if (location.state?.scrollPosition) {
802 window.scrollTo(0, location.state.scrollPosition);
803 }
804 }, [location.state]);
Akane121765b61a72025-05-17 13:52:25 +0800805
Akane12173a7bb972025-06-01 01:05:27 +0800806 // 在Dashboard.jsx中修改useEffect
807useEffect(() => {
808 const token = localStorage.getItem('token');
809 if (!token) {
810 navigate('/login');
811 return;
812 }
813
814 const fetchUserInfo = async () => {
815 try {
816 setLoading(true);
817 const backendData = await getUserInfo(); // 调用修改后的方法
DREW5b1883e2025-06-07 10:41:32 +0800818 // console.log('后端返回的用户数据:', backendData); // 调试用
Akane12173a7bb972025-06-01 01:05:27 +0800819
820 setUserInfo({
821 name: backendData.username || '演示用户',
22301080a93bebb2025-05-27 19:48:11 +0800822 avatar: 'https://via.placeholder.com/40',
Akane12173a7bb972025-06-01 01:05:27 +0800823 isAdmin: backendData.authority === 'ADMIN' // 检查 authority 是否为 "ADMIN"
824 });
825 } catch (error) {
826 console.error('获取用户信息失败:', error);
827 setError('获取用户信息失败');
828 } finally {
829 setLoading(false);
830 }
831 };
832
833 fetchUserInfo();
834}, [navigate]);
22301080a93bebb2025-05-27 19:48:11 +0800835
Akane12173a7bb972025-06-01 01:05:27 +0800836
22301080a93bebb2025-05-27 19:48:11 +0800837 useEffect(() => {
838 if (activeTab === 'announcement') {
839 const timer = setInterval(() => {
DREWae420b22025-06-02 14:07:20 +0800840 setCurrentSlide(prev => {
841 const count = carouselDiscounts.length || 1;
842 return (prev + 1) % count;
843 });
22301080a93bebb2025-05-27 19:48:11 +0800844 }, 3000);
845 return () => clearInterval(timer);
846 }
847 }, [activeTab]);
848
849 useEffect(() => {
850 if (activeTab === 'help') {
851 fetchHelpPosts();
852 }
DREWae420b22025-06-02 14:07:20 +0800853 }, [activeTab, currentPage]); // 添加 currentPage 作为依赖
854
855
22301080a93bebb2025-05-27 19:48:11 +0800856
857 const renderContent = () => {
858 switch (activeTab) {
859 case 'announcement':
860 return (
861 <div className="content-area" data-testid="announcement-section">
Akane12173a7bb972025-06-01 01:05:27 +0800862 <div className="section-search-container">
863 <input
864 type="text"
865 placeholder="搜索公告..."
866 value={announcementSearch}
867 onChange={(e) => setAnnouncementSearch(e.target.value)}
868 className="section-search-input"
869 onKeyPress={(e) => e.key === 'Enter' && handleSearchAnnouncement()}
870 />
871 <button
872 className="search-button"
873 onClick={handleSearchAnnouncement}
874 >
875 搜索
876 </button>
877 </div>
22301080a93bebb2025-05-27 19:48:11 +0800878 {/* 轮播图区域 */}
DREWae420b22025-06-02 14:07:20 +0800879 <div className="carousel-container">
880 {carouselDiscounts.length === 0 ? (
881 <div className="carousel-slide active">
882 <div className="carousel-image gray-bg">暂无折扣活动</div>
22301080a93bebb2025-05-27 19:48:11 +0800883 </div>
DREWae420b22025-06-02 14:07:20 +0800884 ) : (
885 carouselDiscounts.map((discount, index) => (
886 <div key={index} className={`carousel-slide ${currentSlide === index ? 'active' : ''}`}>
887 <div className="carousel-image gray-bg">
888 <h3>{discount.type}</h3>
889 <p>{discount.name}</p>
890 <p>{new Date(discount.startTime).toLocaleDateString()} ~ {new Date(discount.endTime).toLocaleDateString()}</p>
891 </div>
22301080a93bebb2025-05-27 19:48:11 +0800892 </div>
DREWae420b22025-06-02 14:07:20 +0800893 ))
894 )}
895 <div className="carousel-dots">
896 {carouselDiscounts.map((_, index) => (
897 <span key={index} className={`dot ${currentSlide === index ? 'active' : ''}`}></span>
898 ))}
22301080a93bebb2025-05-27 19:48:11 +0800899 </div>
DREWae420b22025-06-02 14:07:20 +0800900 </div>
901
902
22301080a93bebb2025-05-27 19:48:11 +0800903
904 {/* 公告区块区域 */}
905 <div className="announcement-grid">
906 {announcements.map(announcement => (
907 <div
908 key={announcement.id}
909 className="announcement-card"
910 onClick={(e) => handleAnnouncementClick(announcement, e)}
911 >
912 <h3>{announcement.title}</h3>
DREWae420b22025-06-02 14:07:20 +0800913 <p>{announcement.content.substring(0, 100)}...</p>
22301080a93bebb2025-05-27 19:48:11 +0800914 <div className="announcement-footer exclude-click">
22301080a93bebb2025-05-27 19:48:11 +0800915 <span>{announcement.date}</span>
916 </div>
917 </div>
918 ))}
919 </div>
DREW7c7c6a02025-06-05 19:58:55 +0800920 {/* 新增的为你推荐区域 */}
921 <div className="recommend-section">
DREW5b1883e2025-06-07 10:41:32 +0800922 <div className="section-header">
923 <h2 className="section-title">为你推荐</h2>
924 <button
925 className="refresh-btn"
926 onClick={handleRefreshRecommendations}
927 disabled={recommendLoading}
928 >
929 {recommendLoading ? '刷新中...' : '刷新推荐'}
930 </button>
931 </div>
DREW7c7c6a02025-06-05 19:58:55 +0800932
933 {recommendLoading && <div className="loading">加载推荐中...</div>}
934 {recommendError && <div className="error">{recommendError}</div>}
DREW5b1883e2025-06-07 10:41:32 +0800935
DREW7c7c6a02025-06-05 19:58:55 +0800936 <div className="recommend-grid">
937 {recommendations.map(torrent => (
938 <div
939 key={torrent.id}
940 className="recommend-card"
941 onClick={() => handleRecommendClick(torrent)}
942 >
943 <div className="card-poster">
944 <div className="poster-image gray-bg">
945 {torrent.torrentName.charAt(0)}
946 </div>
947 </div>
948 <div className="card-info">
949 <h3 className="card-title">{torrent.torrentName}</h3>
950 <p className="card-meta">
951 {torrent.resolution} | {torrent.region} | {torrent.category}
952 </p>
953 <p className="card-subtitle">字幕: {torrent.subtitle}</p>
954 </div>
955 <div className="card-stats">
956 <span className="stat">👍 {torrent.likeCount || 0}</span>
957 </div>
958 </div>
959 ))}
960
961 {!recommendLoading && recommendations.length === 0 && (
962 <div className="no-recommendations">
963 暂无推荐,请先下载一些资源以获取个性化推荐
964 </div>
965 )}
966 </div>
967 </div>
Akane121765b61a72025-05-17 13:52:25 +0800968 </div>
22301080a93bebb2025-05-27 19:48:11 +0800969 );
970 case 'share':
971 return (
972 <div className="content-area" data-testid="share-section">
Akane12173a7bb972025-06-01 01:05:27 +0800973 {/* 分享区搜索框 */}
974 <div className="section-search-container">
975 <input
976 type="text"
977 placeholder="搜索资源..."
978 value={shareSearch}
979 onChange={(e) => setShareSearch(e.target.value)}
980 className="section-search-input"
981 onKeyPress={(e) => e.key === 'Enter' && handleSearchShare()}
982 />
983 <button
984 className="search-button"
985 onClick={handleSearchShare}
986 >
987 搜索
988 </button>
989 <button
990 className="reset-button"
991 onClick={handleResetShareSearch} // 使用新的重置函数
992 style={{marginLeft: '10px'}}
993 >
994 重置
995 </button>
996 </div>
997
22301080a93bebb2025-05-27 19:48:11 +0800998 {/* 上传按钮 - 添加在筛选区上方 */}
999 <div className="upload-header">
1000 <button
1001 className="upload-btn"
1002 onClick={() => setShowUploadModal(true)}
1003 >
1004 上传种子
1005 </button>
1006 </div>
1007 {/* 分类筛选区 */}
1008 <div className="filter-section">
1009 {Object.entries(filterCategories).map(([category, config]) => (
1010 <div key={category} className="filter-group">
1011 <h4>{config.label}:</h4>
1012 <div className="filter-options">
1013 {Object.entries(config.options).map(([value, label]) => (
1014 <button
1015 key={value}
1016 className={`filter-btn ${
1017 selectedFilters[category] === value ? 'active' : ''
1018 }`}
1019 onClick={() => handleFilterSelect(category, value)}
1020 >
1021 {label}
1022 </button>
1023 ))}
1024 </div>
1025 </div>
1026 ))}
1027
1028 <button
1029 className="confirm-btn"
1030 onClick={applyFilters}
1031 >
1032 确认筛选
1033 </button>
1034 </div>
1035
1036 {/* 上传弹窗 */}
1037 {showUploadModal && (
1038 <div className="modal-overlay">
1039 <div className="upload-modal">
1040 <h3>上传新种子</h3>
1041 <button
1042 className="close-btn"
1043 onClick={() => setShowUploadModal(false)}
1044 >
1045 ×
1046 </button>
1047
1048 <form onSubmit={handleUploadSubmit}>
1049 <div className="form-group">
1050 <label>种子名称</label>
1051 <input
1052 type="text"
1053 value={uploadData.name}
1054 onChange={(e) => setUploadData({...uploadData, name: e.target.value})}
1055 required
1056 />
1057 </div>
1058
1059 <div className="form-group">
1060 <label>资源类型</label>
1061 <select
1062 value={uploadData.type}
1063 onChange={(e) => setUploadData({...uploadData, type: e.target.value})}
1064 required
1065 >
1066 <option value="">请选择</option>
1067 <option value="电影">电影</option>
1068 <option value="电视剧">电视剧</option>
1069 <option value="动漫">动漫</option>
1070 <option value="综艺">综艺</option>
1071 <option value="音乐">音乐</option>
Akane12173a7bb972025-06-01 01:05:27 +08001072 <option value="其他">其他</option>
22301080a93bebb2025-05-27 19:48:11 +08001073 </select>
1074 </div>
1075
Akane12173a7bb972025-06-01 01:05:27 +08001076 {/* 修改后的地区下拉框 */}
22301080a93bebb2025-05-27 19:48:11 +08001077 <div className="form-group">
1078 <label>地区</label>
Akane12173a7bb972025-06-01 01:05:27 +08001079 <select
22301080a93bebb2025-05-27 19:48:11 +08001080 value={uploadData.region || ''}
1081 onChange={(e) => setUploadData({...uploadData, region: e.target.value})}
22301080a93bebb2025-05-27 19:48:11 +08001082 required
Akane12173a7bb972025-06-01 01:05:27 +08001083 >
1084 <option value="">请选择</option>
1085 <option value="中国">中国</option>
1086 <option value="英国">英国</option>
1087 <option value="美国">美国</option>
1088 <option value="日本">日本</option>
1089 <option value="韩国">韩国</option>
1090 <option value="其他">其他</option>
1091 </select>
22301080a93bebb2025-05-27 19:48:11 +08001092 </div>
1093
1094 {/* 添加分辨率下拉框 */}
1095 <div className="form-group">
1096 <label>分辨率</label>
1097 <select
1098 value={uploadData.resolution || ''}
1099 onChange={(e) => setUploadData({
1100 ...uploadData,
1101 resolution: e.target.value
1102 })}
1103 required
1104 >
1105 <option value="">请选择</option>
1106 <option value="4K">4K</option>
1107 <option value="2K">2K</option>
1108 <option value="1080P">1080P</option>
1109 <option value="720P">720P</option>
1110 <option value="SD">SD</option>
1111 <option value="无损音源">无损音源</option>
1112 <option value="杜比全景声">杜比全景声</option>
1113 <option value="其他">其他</option>
1114 </select>
1115 </div>
1116
1117
1118 {/* 新增字幕语言下拉框 */}
1119 <div className="form-group">
1120 <label>字幕语言</label>
1121 <select
1122 value={uploadData.subtitle || ''}
1123 onChange={(e) => setUploadData({
1124 ...uploadData,
1125 subtitle: e.target.value
1126 })}
1127 required
1128 >
1129 <option value="">请选择</option>
1130 <option value="无需字幕">无需字幕</option>
1131 <option value="暂无字幕">暂无字幕</option>
1132 <option value="自带中文字幕">自带中文字幕</option>
1133 <option value="自带双语字幕(含中文)">自带双语字幕(含中文)</option>
1134 <option value="附件中文字幕">附件中文字幕</option>
1135 <option value="附件双语字幕">附件双语字幕</option>
1136 </select>
1137 </div>
1138
1139 <div className="form-group">
1140 <label>种子文件</label>
1141 <input
1142 type="file"
1143 accept=".torrent"
1144 onChange={handleFileChange}
1145
1146 />
1147 </div>
1148
1149 <div className="form-group">
1150 <label>简介</label>
1151 <textarea
1152 value={uploadData.description}
1153 onChange={(e) => setUploadData({
1154 ...uploadData,
1155 description: e.target.value
1156 })}
1157 />
1158 </div>
1159
1160 <div className="form-actions">
1161 <button
1162 type="button"
1163 className="cancel-btn"
1164 onClick={() => setShowUploadModal(false)}
1165 >
1166 取消
1167 </button>
1168 <button
1169 type="submit"
1170 className="confirm-btn"
1171 disabled={isUploading}
1172 >
1173 {isUploading ? '上传中...' : '确认上传'}
1174 </button>
1175 </div>
1176 </form>
1177 </div>
1178 </div>
Akane121765b61a72025-05-17 13:52:25 +08001179 )}
22301080a93bebb2025-05-27 19:48:11 +08001180
1181 <div className="resource-list">
1182 {torrentPosts.map(torrent => (
1183 <div
1184 key={torrent.id}
1185 className="resource-item"
1186 onClick={() => navigate(`/torrent/${torrent.id}`)}
1187 >
1188 <div className="resource-poster">
1189 <div className="poster-image gray-bg">{torrent.torrentName.charAt(0)}</div>
1190 </div>
1191 <div className="resource-info">
1192 <h3 className="resource-title">{torrent.torrentName}</h3>
1193 <p className="resource-meta">
1194 {torrent.resolution} | {torrent.region} | {torrent.category}
1195 </p>
1196 <p className="resource-subtitle">字幕: {torrent.subtitle}</p>
1197 </div>
1198 <div className="resource-stats">
Akane12173a7bb972025-06-01 01:05:27 +08001199 <span className="stat">{torrent.size}</span>
22301080a93bebb2025-05-27 19:48:11 +08001200 <span className="stat">发布者: {torrent.username}</span>
1201 </div>
1202 <button
1203 className="download-btn"
DREWae420b22025-06-02 14:07:20 +08001204 onClick={(e) => handleDownloadClick(torrent, e)}
22301080a93bebb2025-05-27 19:48:11 +08001205 >
1206 立即下载
1207 </button>
DREWae420b22025-06-02 14:07:20 +08001208 {/* 添加删除按钮 - 只有管理员或发布者可见 */}
1209 {(userInfo?.isAdmin || userInfo?.name === torrent.username) && (
1210 <button
1211 className="delete-btn"
1212 onClick={(e) => handleDeleteTorrent(torrent.id, e)}
1213 >
1214 删除
1215 </button>
1216 )}
22301080a93bebb2025-05-27 19:48:11 +08001217 </div>
1218 ))}
1219 </div>
1220
Akane12173a7bb972025-06-01 01:05:27 +08001221 {totalPages > 1 && (
22301080a93bebb2025-05-27 19:48:11 +08001222 <div className="pagination">
1223 <button
1224 onClick={() => fetchTorrentPosts(currentPage - 1)}
1225 disabled={currentPage === 1}
1226 >
1227 上一页
1228 </button>
1229
1230 {Array.from({length: totalPages}, (_, i) => i + 1).map(page => (
1231 <button
1232 key={page}
1233 onClick={() => fetchTorrentPosts(page)}
1234 className={currentPage === page ? 'active' : ''}
1235 >
1236 {page}
1237 </button>
1238 ))}
1239
1240 <button
1241 onClick={() => fetchTorrentPosts(currentPage + 1)}
1242 disabled={currentPage === totalPages}
1243 >
1244 下一页
1245 </button>
1246 </div>
Akane12173a7bb972025-06-01 01:05:27 +08001247 )}
Akane121765b61a72025-05-17 13:52:25 +08001248 </div>
22301080a93bebb2025-05-27 19:48:11 +08001249 );
1250 // 在Dashboard.jsx的renderContent函数中修改case 'request'部分
1251 case 'request':
1252 return (
1253 <div className="content-area" data-testid="request-section">
DREW5b1883e2025-06-07 10:41:32 +08001254 {/* 求种区搜索框 */}
Akane12173a7bb972025-06-01 01:05:27 +08001255 <div className="section-search-container">
1256 <input
1257 type="text"
DREW5b1883e2025-06-07 10:41:32 +08001258 placeholder="搜索求种..."
Akane12173a7bb972025-06-01 01:05:27 +08001259 value={requestSearch}
1260 onChange={(e) => setRequestSearch(e.target.value)}
1261 className="section-search-input"
1262 onKeyPress={(e) => e.key === 'Enter' && handleSearchRequest()}
1263 />
1264 <button
1265 className="search-button"
1266 onClick={handleSearchRequest}
1267 >
1268 搜索
1269 </button>
DREWae420b22025-06-02 14:07:20 +08001270 <button
1271 className="reset-button"
1272 onClick={handleResetRequestSearch}
1273 style={{marginLeft: '10px'}}
1274 >
1275 重置
1276 </button>
Akane12173a7bb972025-06-01 01:05:27 +08001277 </div>
DREWae420b22025-06-02 14:07:20 +08001278
1279 {/* 新增发帖按钮 */}
1280 <div className="post-header">
1281 <button
1282 className="create-post-btn"
1283 onClick={() => setShowPostModal(true)}
1284 >
1285 发帖求助
1286 </button>
1287 </div>
1288
1289 {/* 加载状态和错误提示 */}
1290 {requestLoading && <div className="loading">加载中...</div>}
1291 {requestError && <div className="error">{helpError}</div>}
22301080a93bebb2025-05-27 19:48:11 +08001292 {/* 求种区帖子列表 */}
1293 <div className="request-list">
DREWae420b22025-06-02 14:07:20 +08001294 {requestPosts.map(post => (
22301080a93bebb2025-05-27 19:48:11 +08001295 <div
1296 key={post.id}
DREWae420b22025-06-02 14:07:20 +08001297 className={`request-post ${post.isSolved ? 'solved' : ''}`}
22301080a93bebb2025-05-27 19:48:11 +08001298 onClick={() => navigate(`/request/${post.id}`)}
1299 >
1300 <div className="post-header">
DREWae420b22025-06-02 14:07:20 +08001301 <img
1302 src={post.authorAvatar || 'https://via.placeholder.com/40'}
1303 alt={post.authorId}
1304 className="post-avatar"
1305 />
1306 <div className="post-author">{post.authorId}</div>
1307 <div className="post-date">
1308 {new Date(post.createTime).toLocaleDateString()}
1309 </div>
1310 {post.isSolved && <span className="solved-badge">已解决</span>}
22301080a93bebb2025-05-27 19:48:11 +08001311 </div>
1312 <h3 className="post-title">{post.title}</h3>
1313 <p className="post-content">{post.content}</p>
1314 <div className="post-stats">
DREWae420b22025-06-02 14:07:20 +08001315 <span className="post-likes">👍 {post.likeCount || 0}</span>
1316 <span className="post-comments">💬 {post.replyCount || 0}</span>
22301080a93bebb2025-05-27 19:48:11 +08001317 </div>
1318 </div>
1319 ))}
1320 </div>
DREWae420b22025-06-02 14:07:20 +08001321 {/* 在帖子列表后添加分页控件 */}
1322 <div className="pagination">
1323 <button
1324 onClick={() => fetchRequestPosts(currentPage - 1)}
1325 disabled={currentPage === 1}
1326 >
1327 上一页
1328 </button>
1329
1330 {Array.from({length: totalPages}, (_, i) => i + 1).map(page => (
1331 <button
1332 key={page}
1333 onClick={() => fetchRequestPosts(page)}
1334 className={currentPage === page ? 'active' : ''}
1335 >
1336 {page}
1337 </button>
1338 ))}
1339
1340 <button
1341 onClick={() => fetchRequestPosts(currentPage + 1)}
1342 disabled={currentPage === totalPages}
1343 >
1344 下一页
1345 </button>
1346 </div>
1347 {/* 新增发帖弹窗 */}
1348 {showPostModal && (
1349 <div className="post-modal-overlay">
1350 <div className="post-modal">
1351 <h3>发布求种帖</h3>
1352 <button
1353 className="modal-close-btn"
1354 onClick={() => setShowPostModal(false)}
1355 >
1356 ×
1357 </button>
1358
1359 <form onSubmit={handleRequestPostSubmit}>
1360 <div className="form-group">
1361 <label>帖子标题</label>
1362 <input
1363 type="text"
1364 value={postTitle}
1365 onChange={(e) => setPostTitle(e.target.value)}
1366 placeholder="请输入标题"
1367 required
1368 />
1369 </div>
1370
1371 <div className="form-group">
1372 <label>帖子内容</label>
1373 <textarea
1374 value={postContent}
1375 onChange={(e) => setPostContent(e.target.value)}
1376 placeholder="详细描述你的问题"
1377 required
1378 />
1379 </div>
1380
1381 <div className="form-group">
1382 <label>上传图片</label>
1383 <div className="upload-image-btn">
1384 <input
1385 type="file"
1386 id="image-upload"
1387 accept="image/*"
1388 onChange={handleImageUpload}
1389 style={{display: 'none'}}
1390 />
1391 <label htmlFor="image-upload">
1392 {selectedImage ? '已选择图片' : '选择图片'}
1393 </label>
1394 {selectedImage && (
1395 <span className="image-name">{selectedImage.name}</span>
1396 )}
1397 </div>
1398 </div>
1399
1400 <div className="form-actions">
1401 <button
1402 type="button"
1403 className="cancel-btn"
1404 onClick={() => setShowPostModal(false)}
1405 >
1406 取消
1407 </button>
1408 <button
1409 type="submit"
1410 className="submit-btn"
1411 >
1412 确认发帖
1413 </button>
1414 </div>
1415 </form>
1416 </div>
1417 </div>
1418 )}
Akane121765b61a72025-05-17 13:52:25 +08001419 </div>
DREWae420b22025-06-02 14:07:20 +08001420
1421
1422
1423
22301080a93bebb2025-05-27 19:48:11 +08001424 );
1425 // 在Dashboard.jsx的renderContent函数中修改case 'help'部分
1426 case 'help':
1427 return (
1428 <div className="content-area" data-testid="help-section">
Akane12173a7bb972025-06-01 01:05:27 +08001429 {/* 求助区搜索框 */}
1430 <div className="section-search-container">
1431 <input
1432 type="text"
1433 placeholder="搜索求助..."
1434 value={helpSearch}
1435 onChange={(e) => setHelpSearch(e.target.value)}
1436 className="section-search-input"
1437 onKeyPress={(e) => e.key === 'Enter' && handleSearchHelp()}
1438 />
1439 <button
1440 className="search-button"
1441 onClick={handleSearchHelp}
1442 >
1443 搜索
1444 </button>
1445 <button
1446 className="reset-button"
1447 onClick={handleResetHelpSearch}
1448 style={{marginLeft: '10px'}}
1449 >
1450 重置
1451 </button>
1452 </div>
1453
22301080a93bebb2025-05-27 19:48:11 +08001454 {/* 新增发帖按钮 */}
1455 <div className="post-header">
1456 <button
1457 className="create-post-btn"
1458 onClick={() => setShowPostModal(true)}
1459 >
1460 发帖求助
1461 </button>
1462 </div>
1463
1464 {/* 加载状态和错误提示 */}
1465 {helpLoading && <div className="loading">加载中...</div>}
1466 {helpError && <div className="error">{helpError}</div>}
1467
1468 {/* 求助区帖子列表 */}
1469 <div className="help-list">
1470 {helpPosts.map(post => (
1471 <div
1472 key={post.id}
1473 className={`help-post ${post.isSolved ? 'solved' : ''}`}
1474 onClick={() => navigate(`/help/${post.id}`)}
1475 >
1476 <div className="post-header">
1477 <img
1478 src={post.authorAvatar || 'https://via.placeholder.com/40'}
1479 alt={post.authorId}
1480 className="post-avatar"
1481 />
1482 <div className="post-author">{post.authorId}</div>
1483 <div className="post-date">
1484 {new Date(post.createTime).toLocaleDateString()}
1485 </div>
1486 {post.isSolved && <span className="solved-badge">已解决</span>}
1487 </div>
1488 <h3 className="post-title">{post.title}</h3>
1489 <p className="post-content">{post.content}</p>
1490 <div className="post-stats">
1491 <span className="post-likes">👍 {post.likeCount || 0}</span>
1492 <span className="post-comments">💬 {post.replyCount || 0}</span>
1493 </div>
1494 </div>
1495 ))}
1496 </div>
1497
1498 {/* 在帖子列表后添加分页控件 */}
1499 <div className="pagination">
1500 <button
1501 onClick={() => fetchHelpPosts(currentPage - 1)}
1502 disabled={currentPage === 1}
1503 >
1504 上一页
1505 </button>
1506
1507 {Array.from({length: totalPages}, (_, i) => i + 1).map(page => (
1508 <button
1509 key={page}
1510 onClick={() => fetchHelpPosts(page)}
1511 className={currentPage === page ? 'active' : ''}
1512 >
1513 {page}
1514 </button>
1515 ))}
1516
1517 <button
1518 onClick={() => fetchHelpPosts(currentPage + 1)}
1519 disabled={currentPage === totalPages}
1520 >
1521 下一页
1522 </button>
1523 </div>
1524
1525 {/* 新增发帖弹窗 */}
1526 {showPostModal && (
1527 <div className="post-modal-overlay">
1528 <div className="post-modal">
1529 <h3>发布求助帖</h3>
1530 <button
1531 className="modal-close-btn"
1532 onClick={() => setShowPostModal(false)}
1533 >
1534 ×
1535 </button>
1536
DREWae420b22025-06-02 14:07:20 +08001537 <form onSubmit={handleHelpPostSubmit}>
22301080a93bebb2025-05-27 19:48:11 +08001538 <div className="form-group">
1539 <label>帖子标题</label>
1540 <input
1541 type="text"
1542 value={postTitle}
1543 onChange={(e) => setPostTitle(e.target.value)}
1544 placeholder="请输入标题"
1545 required
1546 />
1547 </div>
1548
1549 <div className="form-group">
1550 <label>帖子内容</label>
1551 <textarea
1552 value={postContent}
1553 onChange={(e) => setPostContent(e.target.value)}
1554 placeholder="详细描述你的问题"
1555 required
1556 />
1557 </div>
1558
1559 <div className="form-group">
1560 <label>上传图片</label>
1561 <div className="upload-image-btn">
1562 <input
1563 type="file"
1564 id="image-upload"
1565 accept="image/*"
1566 onChange={handleImageUpload}
1567 style={{display: 'none'}}
1568 />
1569 <label htmlFor="image-upload">
1570 {selectedImage ? '已选择图片' : '选择图片'}
1571 </label>
1572 {selectedImage && (
1573 <span className="image-name">{selectedImage.name}</span>
1574 )}
1575 </div>
1576 </div>
1577
1578 <div className="form-actions">
1579 <button
1580 type="button"
1581 className="cancel-btn"
1582 onClick={() => setShowPostModal(false)}
1583 >
1584 取消
1585 </button>
1586 <button
1587 type="submit"
1588 className="submit-btn"
1589 >
1590 确认发帖
1591 </button>
1592 </div>
1593 </form>
1594 </div>
1595 </div>
1596 )}
1597 </div>
1598 );
1599 default:
1600 return <div className="content-area" data-testid="default-section">公告区内容</div>;
1601 }
DREWae420b22025-06-02 14:07:20 +08001602
22301080a93bebb2025-05-27 19:48:11 +08001603 };
1604
1605 if (loading) return <div className="loading">加载中...</div>;
1606 if (error) return <div className="error">{error}</div>;
1607
1608 return (
1609 <div className="dashboard-container" data-testid="dashboard-container">
1610 {/* 顶部栏 */}
1611 <div className="top-bar" data-testid="top-bar">
Akane12173a7bb972025-06-01 01:05:27 +08001612 {/* 平台名称替换搜索框 */}
1613 <div className="platform-name">
1614 <h2>PT资源站</h2>
Akane121765b61a72025-05-17 13:52:25 +08001615 </div>
Akane121765b61a72025-05-17 13:52:25 +08001616
22301080a93bebb2025-05-27 19:48:11 +08001617 <div className="user-actions">
1618 {/* 新增管理员按钮 - 只有管理员可见 */}
1619 {userInfo?.isAdmin && (
1620 <button
1621 className="admin-center-button"
1622 onClick={() => navigate('/administer')}
1623 >
1624 管理员中心
1625 </button>
1626 )}
Akane121765b61a72025-05-17 13:52:25 +08001627
22301080a93bebb2025-05-27 19:48:11 +08001628 <div className="user-info" data-testid="user-info">
1629 <img
1630 src={userInfo?.avatar || 'https://via.placeholder.com/40'}
1631 alt="用户头像"
1632 className="user-avatar"
1633 onClick={() => navigate('/personal')}
1634 style={{cursor: 'pointer'}}
1635 />
1636 <span className="username">{userInfo?.name || '用户'}</span>
1637 <button onClick={onLogout} className="logout-button">退出</button>
1638 </div>
1639 </div>
1640 </div>
1641
1642 {/* 导航栏 */}
1643 {/* handleTabchange函数替换了原本的setactivetab函数 */}
1644 <div className="nav-tabs">
1645 <button
1646 className={`tab-button ${activeTab === 'announcement' ? 'active' : ''}`}
1647 onClick={() => handleTabChange('announcement')}
1648 >
1649 公告区
1650 </button>
1651 <button
1652 className={`tab-button ${activeTab === 'share' ? 'active' : ''}`}
1653 onClick={() => handleTabChange('share')}
1654 >
1655 分享区
1656 </button>
1657 <button
1658 className={`tab-button ${activeTab === 'request' ? 'active' : ''}`}
1659 onClick={() => handleTabChange('request')}
1660 >
1661 求种区
1662 </button>
1663 <button
1664 className={`tab-button ${activeTab === 'help' ? 'active' : ''}`}
1665 onClick={() => handleTabChange('help')}
1666 >
1667 求助区
1668 </button>
1669 </div>
1670
1671 {/* 内容区 */}
1672 {renderContent()}
DREWae420b22025-06-02 14:07:20 +08001673 {/* 下载模态框 - 添加在这里 */}
1674 {showDownloadModal && selectedTorrent && (
1675 <div className="modal-overlay">
1676 <div className="download-modal">
1677 <h3>下载 {selectedTorrent.torrentName}</h3>
1678 <button
1679 className="close-btn"
1680 onClick={() => !isDownloading && setShowDownloadModal(false)}
1681 disabled={isDownloading}
1682 >
1683 ×
1684 </button>
1685
1686 <div className="form-group">
1687 <label>下载路径:</label>
1688 <input
1689 type="text"
1690 value={downloadPath}
1691 onChange={(e) => {
1692 // 实时格式化显示
1693 let path = e.target.value
1694 .replace(/\t/g, '')
1695 .replace(/\\/g, '/')
1696 .replace(/\s+/g, ' ');
1697 setDownloadPath(path);
1698 }}
1699 disabled={isDownloading}
1700 placeholder="例如: D:/downloads/"
1701 />
1702 </div>
1703
1704 {isDownloading && (
1705 <div className="progress-container">
1706 <div className="progress-bar" style={{ width: `${downloadProgress}%` }}>
1707 {downloadProgress}%
1708 </div>
1709 </div>
1710 )}
1711
1712 <div className="modal-actions">
1713 <button
1714 onClick={() => !isDownloading && setShowDownloadModal(false)}
1715 disabled={isDownloading}
1716 >
1717 取消
1718 </button>
1719 <button
1720 onClick={handleDownload}
1721 disabled={isDownloading || !downloadPath}
1722 >
1723 {isDownloading ? '下载中...' : '开始下载'}
1724 </button>
1725 </div>
1726 </div>
1727 </div>
1728 )}
Akane121765b61a72025-05-17 13:52:25 +08001729 </div>
22301080a93bebb2025-05-27 19:48:11 +08001730 );
Akane121765b61a72025-05-17 13:52:25 +08001731};
1732
1733export default Dashboard;