blob: 836daa7622ead0daac015df62d6ba267f8d99195 [file] [log] [blame]
22301080a93bebb2025-05-27 19:48:11 +08001import React, {useEffect, useState} from 'react';
2import {useNavigate, useLocation, useParams} from 'react-router-dom';
3// import { getUserInfo } from '../api/auth';
4import {createTorrent, getTorrents} from '../api/torrent';
Akane121765b61a72025-05-17 13:52:25 +08005import './Dashboard.css';
Akane1217d1e9f712025-05-29 14:36:56 +08006import {createPost, getPosts, getPostDetail} from '../api/helpPost';
Akane121765b61a72025-05-17 13:52:25 +08007
8
22301080a93bebb2025-05-27 19:48:11 +08009const Dashboard = ({onLogout}) => {
10 const location = useLocation();
11 const [userInfo, setUserInfo] = useState(null);
12 const [loading, setLoading] = useState(false);
13 const [error, setError] = useState('');
14 const [currentSlide, setCurrentSlide] = useState(0);
15 const navigate = useNavigate();
16 const {tab} = useParams();
17 const [showUploadModal, setShowUploadModal] = useState(false);
18 const [isUploading, setIsUploading] = useState(false);
19 const [uploadData, setUploadData] = useState({
20 name: '',
21 type: '',
22 region: '',
23 subtitle: '',
24 resolution: '', // 新增分辨率字段
25 file: null,
26 description: ''
27 });
28 const [showPostModal, setShowPostModal] = useState(false);
29 const [postTitle, setPostTitle] = useState('');
30 const [postContent, setPostContent] = useState('');
31 const [selectedImage, setSelectedImage] = useState(null);
32 const [helpPosts, setHelpPosts] = useState([]);
33 const [helpLoading, setHelpLoading] = useState(false);
34 const [helpError, setHelpError] = useState(null);
35 const [currentPage, setCurrentPage] = useState(1);
36 const [totalPages, setTotalPages] = useState(1);
Akane1217d1e9f712025-05-29 14:36:56 +080037 const [likedPosts,setLikedPosts] = useState({});
38
Akane121765b61a72025-05-17 13:52:25 +080039
40 // 添加状态
22301080a93bebb2025-05-27 19:48:11 +080041 const [torrentPosts, setTorrentPosts] = useState([]);
42 const [torrentLoading, setTorrentLoading] = useState(false);
43 const [torrentError, setTorrentError] = useState(null);
Akane121765b61a72025-05-17 13:52:25 +080044
22301080a93bebb2025-05-27 19:48:11 +080045 const [filteredResources, setFilteredResources] = useState(torrentPosts);
46 const [isAdmin, setIsAdmin] = useState(false);
47
48
49 const activeTab = tab || 'announcement'; // 如果没有tab参数,则默认为announcement
50 // 从location.state中初始化状态
51
52
53 const handleTabChange = (tabName) => {
54 navigate(`/dashboard/${tabName}`, {
55 state: {
56 savedFilters: selectedFilters, // 使用新的筛选状态 // 保留现有状态
57 activeTab: tabName // 可选,如果其他组件依赖这个 state
58 }
59 });
60 };
61
62 //公告区
63 const [announcements] = useState([
64 {
65 id: 1,
66 title: '系统维护与更新',
67 content: '2023-10-15 02:00-06:00将进行系统维护升级,期间网站将无法访问。本次更新包含:\n1. 数据库服务器迁移\n2. 安全补丁更新\n3. CDN节点优化\n\n请提前做好下载安排。',
68 author: '系统管理员',
69 date: '2023-10-10',
70 excerpt: '2023-10-15 02:00-06:00将进行系统维护,期间无法访问',
71 category: '系统'
72 },
73 {
74 id: 2,
75 title: '资源上新',
76 content: '最新热门电影《奥本海默》4K REMUX资源已上线,包含:\n- 4K HDR版本 (56.8GB)\n- 1080P标准版 (12.3GB)\n- 中英双语字幕\n\n欢迎下载保种!',
77 author: '资源组',
78 date: '2023-10-08',
79 excerpt: '最新热门电影《奥本海默》4K资源已上线',
80 category: '资源'
81 },
82 {
83 id: 3,
84 title: '积分规则调整',
85 content: '自11月1日起,上传资源积分奖励提升20%,具体规则如下:\n- 上传电影资源:每GB 10积分\n- 上传电视剧资源:每GB 8积分\n- 上传动漫资源:每GB 6积分\n\n感谢大家的支持与贡献!',
86 author: '管理员',
87 date: '2023-10-05',
88 excerpt: '自11月1日起,上传资源积分奖励提升20%',
89 category: '公告'
90 },
91 {
92 id: 4,
93 title: '违规处理公告',
94 content: '用户user123因发布虚假资源已被封禁,相关资源已删除。请大家遵守社区规则,维护良好的分享环境。',
95 author: '管理员',
96 date: '2023-10-03',
97 excerpt: '用户user123因发布虚假资源已被封禁',
98 category: '违规'
99 },
100 {
101 id: 5,
102 title: '节日活动',
103 content: '国庆期间所有资源下载积分减半,活动时间:2023年10月1日至2023年10月7日。',
104 author: '活动组',
105 date: '2023-09-30',
106 excerpt: '国庆期间所有资源下载积分减半',
107 category: '活动'
108 },
109 {
110 id: 6,
111 title: '客户端更新',
112 content: 'PT客户端v2.5.0已发布,修复多个BUG,新增资源搜索功能。请尽快更新到最新版本以获得更好的使用体验。',
113 author: '开发组',
114 date: '2023-09-28',
115 excerpt: 'PT客户端v2.5.0已发布,修复多个BUG',
116 category: '更新'
117 },
118 // 其他公告...
119 ]);
120
121
122 const handleAnnouncementClick = (announcement, e) => {
123 if (!e.target.closest('.exclude-click')) {
124 navigate(`/announcement/${announcement.id}`, {
125 state: {
126 announcement,
127 returnPath: `/dashboard/${activeTab}`,
128 scrollPosition: window.scrollY,
129 activeTab
130 }
131 });
Akane121765b61a72025-05-17 13:52:25 +0800132 }
22301080a93bebb2025-05-27 19:48:11 +0800133 };
Akane121765b61a72025-05-17 13:52:25 +0800134
135
22301080a93bebb2025-05-27 19:48:11 +0800136 //资源区
137 const handleFileChange = (e) => {
138 setUploadData({...uploadData, file: e.target.files[0]});
139 };
Akane121765b61a72025-05-17 13:52:25 +0800140
22301080a93bebb2025-05-27 19:48:11 +0800141 const handleUploadSubmit = async (e) => {
142 e.preventDefault();
143 try {
144 setIsUploading(true);
Akane121765b61a72025-05-17 13:52:25 +0800145
22301080a93bebb2025-05-27 19:48:11 +0800146 const torrentData = {
147 torrentName: uploadData.name,
148 description: uploadData.description,
149 category: uploadData.type,
150 region: uploadData.region,
151 resolution: uploadData.resolution,
152 subtitle: uploadData.subtitle
153 };
Akane121765b61a72025-05-17 13:52:25 +0800154
22301080a93bebb2025-05-27 19:48:11 +0800155 await createTorrent(torrentData, uploadData.file);
156
157 // 上传成功处理
158 setShowUploadModal(false);
159 setUploadData({
160 name: '',
161 type: '',
162 region: '',
163 subtitle: '',
164 resolution: '',
165 file: null,
166 description: ''
167 });
168 alert('种子创建成功!');
169
170 // 刷新列表
171 await fetchTorrentPosts(currentPage);
172
173 } catch (error) {
174 console.error('创建失败:', error);
175 alert('创建失败: ' + (error.response?.data?.message || error.message));
176 } finally {
177 setIsUploading(false);
178 }
179 };
180
181 const handlePostSubmit = async (e) => {
182 e.preventDefault();
183 try {
Akane1217d1e9f712025-05-29 14:36:56 +0800184 const username = localStorage.getItem('username');
185 const response = await createPost(
186 postTitle,
187 postContent,
188 username,
189 selectedImage
190 );
191
192 if (response.data.code === 200) {
193 // 刷新帖子列表
194 await fetchHelpPosts(currentPage);
195 // 重置表单
196 setShowPostModal(false);
197 setPostTitle('');
198 setPostContent('');
199 setSelectedImage(null);
200 } else {
201 setHelpError(response.data.message || '发帖失败');
202 }
22301080a93bebb2025-05-27 19:48:11 +0800203 } catch (err) {
Akane1217d1e9f712025-05-29 14:36:56 +0800204 setHelpError(err.message || '发帖失败');
22301080a93bebb2025-05-27 19:48:11 +0800205 }
Akane1217d1e9f712025-05-29 14:36:56 +0800206 };
Akane121765b61a72025-05-17 13:52:25 +0800207
208 // 获取Torrent帖子列表
22301080a93bebb2025-05-27 19:48:11 +0800209 const fetchTorrentPosts = async (page = 1) => {
210 setTorrentLoading(true);
211 try {
212 const response = await getTorrents(page);
213 setTorrentPosts(response.data.data.records);
214 setTotalPages(Math.ceil(response.data.data.total / 5)); // 假设每页5条
215 setCurrentPage(page);
216 } catch (err) {
217 setTorrentError(err.message);
218 } finally {
219 setTorrentLoading(false);
220 }
221 };
Akane121765b61a72025-05-17 13:52:25 +0800222
22301080a93bebb2025-05-27 19:48:11 +0800223 // 在useEffect中调用
224 useEffect(() => {
225 if (activeTab === 'share') {
226 fetchTorrentPosts();
227 }
228 }, [activeTab]);
Akane121765b61a72025-05-17 13:52:25 +0800229
22301080a93bebb2025-05-27 19:48:11 +0800230 const handleImageUpload = (e) => {
231 setSelectedImage(e.target.files[0]);
232 };
Akane121765b61a72025-05-17 13:52:25 +0800233
234
22301080a93bebb2025-05-27 19:48:11 +0800235 const fetchHelpPosts = async (page = 1) => {
236 setHelpLoading(true);
237 try {
Akane1217d1e9f712025-05-29 14:36:56 +0800238 const response = await getPosts(page);
239 if (response.data.code === 200) {
240 const postsWithCounts = await Promise.all(
241 response.data.data.records.map(async (post) => {
242 try {
243 const detailResponse = await getPostDetail(post.id);
244 if (detailResponse.data.code === 200) {
245 return {
246 ...post,
247 replyCount: detailResponse.data.data.post.replyCount || 0,
248 isLiked: false // 根据需要添加其他字段
249 };
250 }
251 return post; // 如果获取详情失败,返回原始帖子数据
252 } catch (err) {
253 console.error(`获取帖子${post.id}详情失败:`, err);
254 return post;
255 }
256 })
257 );
258 setHelpPosts(postsWithCounts);
259 setTotalPages(Math.ceil(response.data.data.total / 5)); // 假设每页5条
260 setCurrentPage(page);
261 } else {
262 setHelpError(response.data.message || '获取求助帖失败');
263 }
22301080a93bebb2025-05-27 19:48:11 +0800264 } catch (err) {
Akane1217d1e9f712025-05-29 14:36:56 +0800265 setHelpError(err.message || '获取求助帖失败');
22301080a93bebb2025-05-27 19:48:11 +0800266 } finally {
Akane1217d1e9f712025-05-29 14:36:56 +0800267 setHelpLoading(false);
22301080a93bebb2025-05-27 19:48:11 +0800268 }
Akane1217d1e9f712025-05-29 14:36:56 +0800269 };
270
271
22301080a93bebb2025-05-27 19:48:11 +0800272 useEffect(() => {
273 if (activeTab === 'help') {
274 fetchHelpPosts(currentPage);
275 }
276 }, [activeTab, currentPage]); // 添加 currentPage 作为依赖
Akane121765b61a72025-05-17 13:52:25 +0800277
278
22301080a93bebb2025-05-27 19:48:11 +0800279 // 分类维度配置
280 const filterCategories = {
281 type: {
282 label: '类型',
283 options: {
284 'all': '全部',
285 '电影': '电影',
286 '电视剧': '电视剧',
287 '动漫': '动漫',
288 '综艺': '综艺'
289 }
290 },
291 subtitle: {
292 label: '字幕',
293 options: {
294 'all': '全部',
295 'yes': '有字幕',
296 'no': '无字幕'
297 }
298 },
299 region: {
300 label: '地区',
301 options: {
302 'all': '全部',
303 'cn': '大陆',
304 'us': '欧美',
305 'jp': '日本'
306 }
307 }
308 };
Akane121765b61a72025-05-17 13:52:25 +0800309
22301080a93bebb2025-05-27 19:48:11 +0800310 const [selectedFilters, setSelectedFilters] = useState(
311 location.state?.savedFilters ||
312 Object.keys(filterCategories).reduce((acc, category) => {
313 acc[category] = 'all';
314 return acc;
315 }, {})
316 );
Akane121765b61a72025-05-17 13:52:25 +0800317
318
319// 处理筛选条件变更
22301080a93bebb2025-05-27 19:48:11 +0800320 const handleFilterSelect = (category, value) => {
321 setSelectedFilters(prev => ({
322 ...prev,
323 [category]: prev[category] === value ? null : value // 点击已选中的则取消
324 }));
Akane121765b61a72025-05-17 13:52:25 +0800325 };
326
22301080a93bebb2025-05-27 19:48:11 +0800327//应用筛选条件
328 const applyFilters = () => {
329 const result = torrentPosts.filter(resource => {
330 return Object.entries(selectedFilters).every(([category, selectedValue]) => {
331 if (selectedValue === 'all') return true;
332 if (category === 'subtitle') {
333 return resource.subtitle === (selectedValue === 'yes');
Akane121765b61a72025-05-17 13:52:25 +0800334 }
22301080a93bebb2025-05-27 19:48:11 +0800335 return resource[category] === selectedValue;
336 });
337 });
338 setFilteredResources(result);
339 };
Akane121765b61a72025-05-17 13:52:25 +0800340
Akane121765b61a72025-05-17 13:52:25 +0800341
22301080a93bebb2025-05-27 19:48:11 +0800342 // 恢复滚动位置
343 useEffect(() => {
344 if (location.state?.scrollPosition) {
345 window.scrollTo(0, location.state.scrollPosition);
346 }
347 }, [location.state]);
Akane121765b61a72025-05-17 13:52:25 +0800348
22301080a93bebb2025-05-27 19:48:11 +0800349
350 useEffect(() => {
351 const token = localStorage.getItem('token');
352 if (!token) {
353 navigate('/login');
354 return;
355 }
356
357 /* 保留但注释掉实际的用户信息获取
358     const fetchUserInfo = async () => {
359       try {
360         const response = await getUserInfo(token);
361         if (response.data.code === 200) {
362           setUserInfo(response.data.data);
363         } else {
364           setError('获取用户信息失败');
365         }
366       } catch (err) {
367         setError('获取用户信息失败');
368       } finally {
369         setLoading(false);
370       }
371     };
372
373     fetchUserInfo();
374     */
375
376 // 模拟用户信息
377 setUserInfo({
378 name: localStorage.getItem('username') || '演示用户', // 确保这里读取的是最新值
379 avatar: 'https://via.placeholder.com/40',
380 isAdmin: true
381 });
382 setLoading(false);
383 }, [navigate]);
384
385 // 轮播图自动切换效果
386 useEffect(() => {
387 if (activeTab === 'announcement') {
388 const timer = setInterval(() => {
389 setCurrentSlide(prev => (prev + 1) % 3); // 3张轮播图循环
390 }, 3000);
391 return () => clearInterval(timer);
392 }
393 }, [activeTab]);
394
395 useEffect(() => {
396 if (activeTab === 'help') {
397 fetchHelpPosts();
398 }
399 }, [activeTab]);
400
401 const renderContent = () => {
402 switch (activeTab) {
403 case 'announcement':
404 return (
405 <div className="content-area" data-testid="announcement-section">
406 {/* 轮播图区域 */}
407 <div className="carousel-container">
408 <div className={`carousel-slide ${currentSlide === 0 ? 'active' : ''}`}>
409 <div className="carousel-image gray-bg">促销活动1</div>
410 </div>
411 <div className={`carousel-slide ${currentSlide === 1 ? 'active' : ''}`}>
412 <div className="carousel-image gray-bg">促销活动2</div>
413 </div>
414 <div className={`carousel-slide ${currentSlide === 2 ? 'active' : ''}`}>
415 <div className="carousel-image gray-bg">促销活动3</div>
416 </div>
417 <div className="carousel-dots">
418 <span className={`dot ${currentSlide === 0 ? 'active' : ''}`}></span>
419 <span className={`dot ${currentSlide === 1 ? 'active' : ''}`}></span>
420 <span className={`dot ${currentSlide === 2 ? 'active' : ''}`}></span>
421 </div>
422 </div>
423
424 {/* 公告区块区域 */}
425 <div className="announcement-grid">
426 {announcements.map(announcement => (
427 <div
428 key={announcement.id}
429 className="announcement-card"
430 onClick={(e) => handleAnnouncementClick(announcement, e)}
431 >
432 <h3>{announcement.title}</h3>
433 <p>{announcement.excerpt}</p>
434 <div className="announcement-footer exclude-click">
435 <span>{announcement.author}</span>
436 <span>{announcement.date}</span>
437 </div>
438 </div>
439 ))}
440 </div>
Akane121765b61a72025-05-17 13:52:25 +0800441 </div>
22301080a93bebb2025-05-27 19:48:11 +0800442 );
443 case 'share':
444 return (
445 <div className="content-area" data-testid="share-section">
446 {/* 上传按钮 - 添加在筛选区上方 */}
447 <div className="upload-header">
448 <button
449 className="upload-btn"
450 onClick={() => setShowUploadModal(true)}
451 >
452 上传种子
453 </button>
454 </div>
455 {/* 分类筛选区 */}
456 <div className="filter-section">
457 {Object.entries(filterCategories).map(([category, config]) => (
458 <div key={category} className="filter-group">
459 <h4>{config.label}:</h4>
460 <div className="filter-options">
461 {Object.entries(config.options).map(([value, label]) => (
462 <button
463 key={value}
464 className={`filter-btn ${
465 selectedFilters[category] === value ? 'active' : ''
466 }`}
467 onClick={() => handleFilterSelect(category, value)}
468 >
469 {label}
470 </button>
471 ))}
472 </div>
473 </div>
474 ))}
475
476 <button
477 className="confirm-btn"
478 onClick={applyFilters}
479 >
480 确认筛选
481 </button>
482 </div>
483
484 {/* 上传弹窗 */}
485 {showUploadModal && (
486 <div className="modal-overlay">
487 <div className="upload-modal">
488 <h3>上传新种子</h3>
489 <button
490 className="close-btn"
491 onClick={() => setShowUploadModal(false)}
492 >
493 ×
494 </button>
495
496 <form onSubmit={handleUploadSubmit}>
497 <div className="form-group">
498 <label>种子名称</label>
499 <input
500 type="text"
501 value={uploadData.name}
502 onChange={(e) => setUploadData({...uploadData, name: e.target.value})}
503 required
504 />
505 </div>
506
507 <div className="form-group">
508 <label>资源类型</label>
509 <select
510 value={uploadData.type}
511 onChange={(e) => setUploadData({...uploadData, type: e.target.value})}
512 required
513 >
514 <option value="">请选择</option>
515 <option value="电影">电影</option>
516 <option value="电视剧">电视剧</option>
517 <option value="动漫">动漫</option>
518 <option value="综艺">综艺</option>
519 <option value="音乐">音乐</option>
520 </select>
521 </div>
522
523 {/* 新增地区输入框 */}
524 <div className="form-group">
525 <label>地区</label>
526 <input
527 type="text"
528 value={uploadData.region || ''}
529 onChange={(e) => setUploadData({...uploadData, region: e.target.value})}
530 placeholder="例如: 美国, 中国, 日本等"
531 required
532 />
533 </div>
534
535 {/* 添加分辨率下拉框 */}
536 <div className="form-group">
537 <label>分辨率</label>
538 <select
539 value={uploadData.resolution || ''}
540 onChange={(e) => setUploadData({
541 ...uploadData,
542 resolution: e.target.value
543 })}
544 required
545 >
546 <option value="">请选择</option>
547 <option value="4K">4K</option>
548 <option value="2K">2K</option>
549 <option value="1080P">1080P</option>
550 <option value="720P">720P</option>
551 <option value="SD">SD</option>
552 <option value="无损音源">无损音源</option>
553 <option value="杜比全景声">杜比全景声</option>
554 <option value="其他">其他</option>
555 </select>
556 </div>
557
558
559 {/* 新增字幕语言下拉框 */}
560 <div className="form-group">
561 <label>字幕语言</label>
562 <select
563 value={uploadData.subtitle || ''}
564 onChange={(e) => setUploadData({
565 ...uploadData,
566 subtitle: e.target.value
567 })}
568 required
569 >
570 <option value="">请选择</option>
571 <option value="无需字幕">无需字幕</option>
572 <option value="暂无字幕">暂无字幕</option>
573 <option value="自带中文字幕">自带中文字幕</option>
574 <option value="自带双语字幕(含中文)">自带双语字幕(含中文)</option>
575 <option value="附件中文字幕">附件中文字幕</option>
576 <option value="附件双语字幕">附件双语字幕</option>
577 </select>
578 </div>
579
580 <div className="form-group">
581 <label>种子文件</label>
582 <input
583 type="file"
584 accept=".torrent"
585 onChange={handleFileChange}
586
587 />
588 </div>
589
590 <div className="form-group">
591 <label>简介</label>
592 <textarea
593 value={uploadData.description}
594 onChange={(e) => setUploadData({
595 ...uploadData,
596 description: e.target.value
597 })}
598 />
599 </div>
600
601 <div className="form-actions">
602 <button
603 type="button"
604 className="cancel-btn"
605 onClick={() => setShowUploadModal(false)}
606 >
607 取消
608 </button>
609 <button
610 type="submit"
611 className="confirm-btn"
612 disabled={isUploading}
613 >
614 {isUploading ? '上传中...' : '确认上传'}
615 </button>
616 </div>
617 </form>
618 </div>
619 </div>
Akane121765b61a72025-05-17 13:52:25 +0800620 )}
22301080a93bebb2025-05-27 19:48:11 +0800621
622 <div className="resource-list">
623 {torrentPosts.map(torrent => (
624 <div
625 key={torrent.id}
626 className="resource-item"
627 onClick={() => navigate(`/torrent/${torrent.id}`)}
628 >
629 <div className="resource-poster">
630 <div className="poster-image gray-bg">{torrent.torrentName.charAt(0)}</div>
631 </div>
632 <div className="resource-info">
633 <h3 className="resource-title">{torrent.torrentName}</h3>
634 <p className="resource-meta">
635 {torrent.resolution} | {torrent.region} | {torrent.category}
636 </p>
637 <p className="resource-subtitle">字幕: {torrent.subtitle}</p>
638 </div>
639 <div className="resource-stats">
640 <span className="stat">{torrent.size}</span>
641 <span className="stat">发布者: {torrent.username}</span>
642 </div>
643 <button
644 className="download-btn"
645 onClick={(e) => {
646 e.stopPropagation();
647 // 下载逻辑
648 }}
649 >
650 立即下载
651 </button>
652 </div>
653 ))}
654 </div>
655
656 {/* 分页控件 */}
657 <div className="pagination">
658 <button
659 onClick={() => fetchTorrentPosts(currentPage - 1)}
660 disabled={currentPage === 1}
661 >
662 上一页
663 </button>
664
665 {Array.from({length: totalPages}, (_, i) => i + 1).map(page => (
666 <button
667 key={page}
668 onClick={() => fetchTorrentPosts(page)}
669 className={currentPage === page ? 'active' : ''}
670 >
671 {page}
672 </button>
673 ))}
674
675 <button
676 onClick={() => fetchTorrentPosts(currentPage + 1)}
677 disabled={currentPage === totalPages}
678 >
679 下一页
680 </button>
681 </div>
Akane121765b61a72025-05-17 13:52:25 +0800682 </div>
22301080a93bebb2025-05-27 19:48:11 +0800683 );
684 // 在Dashboard.jsx的renderContent函数中修改case 'request'部分
685 case 'request':
686 return (
687 <div className="content-area" data-testid="request-section">
688 {/* 求种区帖子列表 */}
689 <div className="request-list">
690 {[
691 {
692 id: 1,
693 title: '求《药屋少女的呢喃》第二季全集',
694 content: '求1080P带中文字幕版本,最好是内嵌字幕不是外挂的',
695 author: '动漫爱好者',
696 authorAvatar: 'https://via.placeholder.com/40',
697 date: '2023-10-15',
698 likeCount: 24,
699 commentCount: 8
700 },
701 {
702 id: 2,
703 title: '求《奥本海默》IMAX版',
704 content: '最好是原盘或者高码率的版本,谢谢各位大佬',
705 author: '电影收藏家',
706 authorAvatar: 'https://via.placeholder.com/40',
707 date: '2023-10-14',
708 likeCount: 15,
709 commentCount: 5
710 }
711 ].map(post => (
712 <div
713 key={post.id}
714 className="request-post"
715 onClick={() => navigate(`/request/${post.id}`)}
716 >
717 <div className="post-header">
718 <img src={post.authorAvatar} alt={post.author} className="post-avatar"/>
719 <div className="post-author">{post.author}</div>
720 <div className="post-date">{post.date}</div>
721 </div>
722 <h3 className="post-title">{post.title}</h3>
723 <p className="post-content">{post.content}</p>
724 <div className="post-stats">
725 <span className="post-likes">👍 {post.likeCount}</span>
726 <span className="post-comments">💬 {post.commentCount}</span>
727 </div>
728 </div>
729 ))}
730 </div>
Akane121765b61a72025-05-17 13:52:25 +0800731 </div>
22301080a93bebb2025-05-27 19:48:11 +0800732 );
733 // 在Dashboard.jsx的renderContent函数中修改case 'help'部分
734 case 'help':
735 return (
736 <div className="content-area" data-testid="help-section">
737 {/* 新增发帖按钮 */}
738 <div className="post-header">
739 <button
740 className="create-post-btn"
741 onClick={() => setShowPostModal(true)}
742 >
743 发帖求助
744 </button>
745 </div>
746
747 {/* 加载状态和错误提示 */}
748 {helpLoading && <div className="loading">加载中...</div>}
749 {helpError && <div className="error">{helpError}</div>}
750
751 {/* 求助区帖子列表 */}
752 <div className="help-list">
753 {helpPosts.map(post => (
754 <div
755 key={post.id}
756 className={`help-post ${post.isSolved ? 'solved' : ''}`}
757 onClick={() => navigate(`/help/${post.id}`)}
758 >
759 <div className="post-header">
760 <img
761 src={post.authorAvatar || 'https://via.placeholder.com/40'}
762 alt={post.authorId}
763 className="post-avatar"
764 />
765 <div className="post-author">{post.authorId}</div>
766 <div className="post-date">
767 {new Date(post.createTime).toLocaleDateString()}
768 </div>
769 {post.isSolved && <span className="solved-badge">已解决</span>}
770 </div>
771 <h3 className="post-title">{post.title}</h3>
772 <p className="post-content">{post.content}</p>
773 <div className="post-stats">
774 <span className="post-likes">👍 {post.likeCount || 0}</span>
775 <span className="post-comments">💬 {post.replyCount || 0}</span>
776 </div>
777 </div>
778 ))}
779 </div>
780
781 {/* 在帖子列表后添加分页控件 */}
782 <div className="pagination">
783 <button
784 onClick={() => fetchHelpPosts(currentPage - 1)}
785 disabled={currentPage === 1}
786 >
787 上一页
788 </button>
789
790 {Array.from({length: totalPages}, (_, i) => i + 1).map(page => (
791 <button
792 key={page}
793 onClick={() => fetchHelpPosts(page)}
794 className={currentPage === page ? 'active' : ''}
795 >
796 {page}
797 </button>
798 ))}
799
800 <button
801 onClick={() => fetchHelpPosts(currentPage + 1)}
802 disabled={currentPage === totalPages}
803 >
804 下一页
805 </button>
806 </div>
807
808 {/* 新增发帖弹窗 */}
809 {showPostModal && (
810 <div className="post-modal-overlay">
811 <div className="post-modal">
812 <h3>发布求助帖</h3>
813 <button
814 className="modal-close-btn"
815 onClick={() => setShowPostModal(false)}
816 >
817 ×
818 </button>
819
820 <form onSubmit={handlePostSubmit}>
821 <div className="form-group">
822 <label>帖子标题</label>
823 <input
824 type="text"
825 value={postTitle}
826 onChange={(e) => setPostTitle(e.target.value)}
827 placeholder="请输入标题"
828 required
829 />
830 </div>
831
832 <div className="form-group">
833 <label>帖子内容</label>
834 <textarea
835 value={postContent}
836 onChange={(e) => setPostContent(e.target.value)}
837 placeholder="详细描述你的问题"
838 required
839 />
840 </div>
841
842 <div className="form-group">
843 <label>上传图片</label>
844 <div className="upload-image-btn">
845 <input
846 type="file"
847 id="image-upload"
848 accept="image/*"
849 onChange={handleImageUpload}
850 style={{display: 'none'}}
851 />
852 <label htmlFor="image-upload">
853 {selectedImage ? '已选择图片' : '选择图片'}
854 </label>
855 {selectedImage && (
856 <span className="image-name">{selectedImage.name}</span>
857 )}
858 </div>
859 </div>
860
861 <div className="form-actions">
862 <button
863 type="button"
864 className="cancel-btn"
865 onClick={() => setShowPostModal(false)}
866 >
867 取消
868 </button>
869 <button
870 type="submit"
871 className="submit-btn"
872 >
873 确认发帖
874 </button>
875 </div>
876 </form>
877 </div>
878 </div>
879 )}
880 </div>
881 );
882 default:
883 return <div className="content-area" data-testid="default-section">公告区内容</div>;
884 }
885 };
886
887 if (loading) return <div className="loading">加载中...</div>;
888 if (error) return <div className="error">{error}</div>;
889
890 return (
891 <div className="dashboard-container" data-testid="dashboard-container">
892 {/* 顶部栏 */}
893 <div className="top-bar" data-testid="top-bar">
894 {/* 搜索框 */}
895 <div className="search-container">
896 <input
897 type="text"
898 placeholder="搜索种子、用户..."
899 className="search-input"
900 />
901 <button className="search-button">搜索</button>
Akane121765b61a72025-05-17 13:52:25 +0800902 </div>
Akane121765b61a72025-05-17 13:52:25 +0800903
22301080a93bebb2025-05-27 19:48:11 +0800904 <div className="user-actions">
905 {/* 新增管理员按钮 - 只有管理员可见 */}
906 {userInfo?.isAdmin && (
907 <button
908 className="admin-center-button"
909 onClick={() => navigate('/administer')}
910 >
911 管理员中心
912 </button>
913 )}
Akane121765b61a72025-05-17 13:52:25 +0800914
22301080a93bebb2025-05-27 19:48:11 +0800915 <div className="user-info" data-testid="user-info">
916 <img
917 src={userInfo?.avatar || 'https://via.placeholder.com/40'}
918 alt="用户头像"
919 className="user-avatar"
920 onClick={() => navigate('/personal')}
921 style={{cursor: 'pointer'}}
922 />
923 <span className="username">{userInfo?.name || '用户'}</span>
924 <button onClick={onLogout} className="logout-button">退出</button>
925 </div>
926 </div>
927 </div>
928
929 {/* 导航栏 */}
930 {/* handleTabchange函数替换了原本的setactivetab函数 */}
931 <div className="nav-tabs">
932 <button
933 className={`tab-button ${activeTab === 'announcement' ? 'active' : ''}`}
934 onClick={() => handleTabChange('announcement')}
935 >
936 公告区
937 </button>
938 <button
939 className={`tab-button ${activeTab === 'share' ? 'active' : ''}`}
940 onClick={() => handleTabChange('share')}
941 >
942 分享区
943 </button>
944 <button
945 className={`tab-button ${activeTab === 'request' ? 'active' : ''}`}
946 onClick={() => handleTabChange('request')}
947 >
948 求种区
949 </button>
950 <button
951 className={`tab-button ${activeTab === 'help' ? 'active' : ''}`}
952 onClick={() => handleTabChange('help')}
953 >
954 求助区
955 </button>
956 </div>
957
958 {/* 内容区 */}
959 {renderContent()}
Akane121765b61a72025-05-17 13:52:25 +0800960 </div>
22301080a93bebb2025-05-27 19:48:11 +0800961 );
Akane121765b61a72025-05-17 13:52:25 +0800962};
963
964export default Dashboard;