blob: a5262fc47cb5bb04e7f471055701a8abda70c7af [file] [log] [blame]
Jiarenxiang36728482025-06-07 21:51:26 +08001import React, { useEffect, useState } from 'react';
2import { useParams, useNavigate } from 'react-router-dom';
3import { Button, Input, message, Spin, Tag, Card, Avatar, List, Pagination } from 'antd';
4import { DownloadOutlined, ArrowLeftOutlined, SendOutlined } from '@ant-design/icons';
5import {
6 getTorrentInfo,
7 downloadTorrent,
8 addComment,
9 getComments,
10 getCategories,
11} from '../../services/bt/index';
12
13const PAGE_SIZE = 10;
14
15const TorrentDetail: React.FC = () => {
16const { id } = useParams<{ id: string }>();
17const navigate = useNavigate();
18
19const [loading, setLoading] = useState(true);
20const [torrent, setTorrent] = useState<any>(null);
21const [categories, setCategories] = useState<any[]>([]);
22const [comments, setComments] = useState<any[]>([]);
23const [commentLoading, setCommentLoading] = useState(false);
24const [comment, setComment] = useState('');
25const [commentsTotal, setCommentsTotal] = useState(0);
26const [pageNum, setPageNum] = useState(1);
27
28useEffect(() => {
29 setLoading(true);
30 getTorrentInfo({ id: id! })
31 .then((res) => setTorrent(res.data))
32 .finally(() => setLoading(false));
33 getCategories().then((res) => setCategories(res.data || []));
34}, [id]);
35
36useEffect(() => {
37 fetchComments(pageNum);
38 // eslint-disable-next-line
39}, [id, pageNum]);
40
41const fetchComments = (page: number) => {
42 setCommentLoading(true);
43 getComments({ torrentId: Number(id), pageNum: page, pageSize: PAGE_SIZE })
44 .then((res) => {0
45 setComments(res.data?.list || []);
46 setCommentsTotal(res.data?.total || 0);
47 })
48 .finally(() => setCommentLoading(false));
49};
50
51const handleDownload = async () => {
52 try {
53 const res = await downloadTorrent({ id: id! });
54 const blob = new Blob([res], { type: 'application/x-bittorrent' });
55 const url = window.URL.createObjectURL(blob);
56 const a = document.createElement('a');
57 a.href = url;
58 a.download = `${torrent?.title || 'torrent'}.torrent`;
59 a.click();
60 window.URL.revokeObjectURL(url);
61 } catch {
62 message.error('下载失败');
63 }
64};
65
66const handleAddComment = async () => {
67 if (!comment.trim()) return;
68 await addComment({ torrentId: Number(id), comment });
69 setComment('');
70 fetchComments(1);
71 setPageNum(1);
72 message.success('评论成功');
73};
74
75const getCategoryName = (catId: number) => {
76 return categories.find((c) => c.id === catId)?.name || '未知分类';
77};
78
79const statusMap: Record<number, string> = {
Jiarenxiang24d681b2025-06-08 19:27:05 +080080 0: '审核中',
Jiarenxiang36728482025-06-07 21:51:26 +080081 1: '已发布',
82 2: '审核不通过',
83 3: '已上架修改重审中',
84 10: '已下架',
85};
86
87// 星球主题样式
88const planetBg = {
89 background: 'radial-gradient(circle at 60% 40%, #2b6cb0 0%, #1a202c 100%)',
90 minHeight: '100vh',
91 padding: '32px 0',
92};
Jiarenxiang2e3c2672025-06-08 20:46:35 +080093// 创建独立的评论项组件
94interface CommentItemProps {
95 item: any;
96 torrentId: string | undefined;
97 refreshComments: () => void;
98}
99const CommentItem: React.FC<CommentItemProps> = ({ item, torrentId, refreshComments }) => {
100 const [expanded, setExpanded] = useState(false);
101 const [replyContent, setReplyContent] = useState('');
102 const [replying, setReplying] = useState(false);
Jiarenxiang36728482025-06-07 21:51:26 +0800103
Jiarenxiang2e3c2672025-06-08 20:46:35 +0800104 const handleReply = async () => {
105 if (!replyContent.trim()) return;
106 setReplying(true);
107 try {
108 await addComment({
109 torrentId: Number(torrentId),
110 comment: replyContent,
111 pid: item.id
112 });
113 setReplyContent('');
114 setReplying(false);
115 refreshComments();
116 message.success('回复成功');
117 } catch (error) {
118 message.error('回复失败');
119 setReplying(false);
120 }
121 };
122
123 return (
124 <List.Item
125 style={{
126 alignItems: 'flex-start',
127 border: 'none',
128 background: 'transparent',
129 padding: '20px 0',
130 borderBottom: '1px solid rgba(255,255,255,0.08)',
131 }}
132 >
133 <List.Item.Meta
134 avatar={
135 <Avatar
136 src={item.avatar ?
137 (item.avatar.startsWith('http') ?
138 item.avatar :
139 `${item.avatar}`) :
140 'https://img.icons8.com/color/48/planet.png'
141 }
142 size={48}
143 style={{ boxShadow: '0 2px 8px #4299e1' }}
144 />
145 }
146 title={
147 <span style={{ color: '#fff', fontWeight: 500 }}>
148 {item.username || '匿名用户'}
149 </span>
150 }
151 description={
152 <div>
153 <span style={{ color: '#cbd5e1', fontSize: 16, display: 'block' }}>
154 {item.comment}
155 </span>
156 <div style={{ marginTop: 8 }}>
157 <span style={{ fontSize: 12, color: '#a0aec0' }}>
158 {item.createTime}
159 </span>
160 <Button
161 type="link"
162 size="small"
163 style={{ marginLeft: 16, color: '#4299e1' }}
164 onClick={() => setExpanded(v => !v)}
165 >
166 {expanded ? '收起回复' : `展开回复${item.children?.length ? ` (${item.children.length})` : ''}`}
167 </Button>
168 </div>
169 </div>
170 }
171 />
172 {expanded && (
173 <div style={{ width: '100%', marginTop: 12, marginLeft: 56 }}>
174 {item.children?.length > 0 && (
175 <List
176 dataSource={item.children}
177 itemLayout="horizontal"
178 locale={{ emptyText: '暂无子评论' }}
179 renderItem={(child: any) => (
180 <List.Item
181 style={{
182 border: 'none',
183 background: 'transparent',
184 padding: '12px 0 0 0',
185 marginLeft: 0,
186 }}
187 >
188 <List.Item.Meta
189 avatar={
190 <Avatar
191 src={child.avatar ?
192 (child.avatar.startsWith('http') ?
193 child.avatar :
194 `${child.avatar}`) :
195 'https://img.icons8.com/color/48/planet.png'
196 }
197 size={36}
198 style={{ boxShadow: '0 1px 4px #805ad5' }}
199 />
200 }
201 title={
202 <span style={{ color: '#c3bfff', fontWeight: 500, fontSize: 15 }}>
203 {child.username || '匿名用户'}
204 </span>
205 }
206 description={
207 <div>
208 <span style={{ color: '#e0e7ef', fontSize: 15, display: 'block' }}>
209 {child.comment}
210 </span>
211 <span style={{ marginLeft: 12, fontSize: 12, color: '#a0aec0' }}>
212 {child.createTime}
213 </span>
214 </div>
215 }
216 />
217 </List.Item>
218 )}
219 />
220 )}
221 <div style={{ display: 'flex', marginTop: 12, gap: 8 }}>
222 <Input.TextArea
223 value={replyContent}
224 onChange={e => setReplyContent(e.target.value)}
225 placeholder="回复该评论"
226 autoSize={{ minRows: 1, maxRows: 3 }}
227 style={{ background: 'rgba(255,255,255,0.15)', color: '#fff', border: 'none', flex: 1 }}
228 />
229 <Button
230 type="primary"
231 icon={<SendOutlined />}
232 loading={replying}
233 onClick={handleReply}
234 disabled={!replyContent.trim()}
235 style={{ background: 'linear-gradient(90deg,#4299e1,#805ad5)', border: 'none', height: 40 }}
236 >
237 发送
238 </Button>
239 </div>
240 </div>
241 )}
242 </List.Item>
243 );
244};
245
246// 在return中使用修复后的代码
Jiarenxiang36728482025-06-07 21:51:26 +0800247return (
248 <div style={planetBg}>
249 <div style={{ maxWidth: 900, margin: '0 auto', background: 'rgba(255,255,255,0.08)', borderRadius: 16, boxShadow: '0 8px 32px rgba(0,0,0,0.2)', padding: 24 }}>
250 <Button
251 icon={<ArrowLeftOutlined />}
252 type="link"
253 onClick={() => navigate(-1)}
254 style={{ color: '#fff', marginBottom: 16 }}
255 >
256 返回
257 </Button>
258 {loading ? (
259 <Spin size="large" />
260 ) : (
261 <>
262 {torrent?.status !== 1 ? (
263 <div style={{ color: '#fff', fontSize: 24, textAlign: 'center', padding: '80px 0' }}>
264 当前状态:{statusMap[torrent?.status] || '未知状态'}
265 </div>
266 ) : (
267 <>
268 <div style={{ display: 'flex', alignItems: 'center', gap: 24 }}>
269 <Avatar
270 size={96}
271 src={torrent?.cover || 'https://img.icons8.com/color/96/planet.png'}
272 style={{ boxShadow: '0 0 24px #4299e1' }}
273 />
274 <div>
275 <h1 style={{ color: '#fff', fontSize: 32, marginBottom: 8 }}>
276 {torrent?.title}
277 </h1>
278 <div style={{ marginBottom: 8 }}>
279 <Tag color="geekblue">{getCategoryName(torrent?.categoryId)}</Tag>
280 {torrent?.tags?.map((tag: string) => (
281 <Tag key={tag} color="blue">{tag}</Tag>
282 ))}
283 </div>
284 <div style={{ color: '#cbd5e1', marginBottom: 8 }}>
285 上传者:{torrent?.owner} | 上传时间:{torrent?.createdAt}
286 </div>
287 <Button
288 type="primary"
289 icon={<DownloadOutlined />}
290 onClick={handleDownload}
291 style={{ background: 'linear-gradient(90deg,#4299e1,#805ad5)', border: 'none' }}
292 >
293 下载种子
294 </Button>
295 </div>
296 </div>
297 <Card
298 style={{
299 marginTop: 32,
Jiarenxiang36728482025-06-07 21:51:26 +0800300 background: 'rgba(255,255,255,0.10)',
301 border: 'none',
302 borderRadius: 12,
303 color: '#fff',
304 }}
305 title={<span style={{ color: '#fff' }}>星球评论</span>}
306 >
Jiarenxiang2e3c2672025-06-08 20:46:35 +0800307
308 <List
309 loading={commentLoading}
310 dataSource={comments.filter((item: any) => item.pid === null)}
311 locale={{ emptyText: '暂无评论' }}
312 renderItem={(item: any) => (
313 <CommentItem
314 item={item}
315 torrentId={id}
316 refreshComments={() => fetchComments(pageNum)}
317 />
318 )}
319 />
320 <Pagination
321 style={{ marginTop: 16, textAlign: 'right' }}
322 current={pageNum}
323 pageSize={PAGE_SIZE}
324 total={commentsTotal}
325 onChange={setPageNum}
326 showSizeChanger={false}
327 />
Jiarenxiang36728482025-06-07 21:51:26 +0800328 <div style={{ display: 'flex', marginTop: 24, gap: 8 }}>
329 <Input.TextArea
330 value={comment}
331 onChange={e => setComment(e.target.value)}
332 placeholder="在星球上留下你的评论吧~"
333 autoSize={{ minRows: 2, maxRows: 4 }}
Jiarenxiang2e3c2672025-06-08 20:46:35 +0800334 style={{
335 background: 'rgba(255,255,255,0.15)',
336 color: '#fff',
337 border: 'none',
338 flex: 1
339 }}
Jiarenxiang36728482025-06-07 21:51:26 +0800340 />
341 <Button
342 type="primary"
343 icon={<SendOutlined />}
344 onClick={handleAddComment}
345 disabled={!comment.trim()}
Jiarenxiang2e3c2672025-06-08 20:46:35 +0800346 style={{
347 background: 'linear-gradient(90deg,#4299e1,#805ad5)',
348 border: 'none',
349 height: 48,
350 alignSelf: 'flex-end'
351 }}
Jiarenxiang36728482025-06-07 21:51:26 +0800352 >
353 发送
354 </Button>
355 </div>
356 </Card>
357 </>
358 )}
359 </>
360 )}
361 </div>
362 </div>
363);
Jiarenxiang2e3c2672025-06-08 20:46:35 +0800364// return (
365// <div style={planetBg}>
366// <div style={{ maxWidth: 900, margin: '0 auto', background: 'rgba(255,255,255,0.08)', borderRadius: 16, boxShadow: '0 8px 32px rgba(0,0,0,0.2)', padding: 24 }}>
367// <Button
368// icon={<ArrowLeftOutlined />}
369// type="link"
370// onClick={() => navigate(-1)}
371// style={{ color: '#fff', marginBottom: 16 }}
372// >
373// 返回
374// </Button>
375// {loading ? (
376// <Spin size="large" />
377// ) : (
378// <>
379// {torrent?.status !== 1 ? (
380// <div style={{ color: '#fff', fontSize: 24, textAlign: 'center', padding: '80px 0' }}>
381// 当前状态:{statusMap[torrent?.status] || '未知状态'}
382// </div>
383// ) : (
384// <>
385
386
387
388
389
390
391
392// <Card
393// style={{
394// marginTop: 32,
395// background: 'rgba(255,255,255,0.12)',
396// border: 'none',
397// borderRadius: 12,
398// color: '#fff',
399// }}
400// title={<span style={{ color: '#fff' }}>种子详情</span>}
401// >
402// <div dangerouslySetInnerHTML={{ __html: torrent?.description || '' }} />
403// <div style={{ marginTop: 16, color: '#cbd5e1' }}>
404// <span>文件大小:{torrent?.size}</span>
405// <span style={{ marginLeft: 24 }}>做种人数:{torrent?.seeders}</span>
406// <span style={{ marginLeft: 24 }}>下载人数:{torrent?.leechers}</span>
407// <span style={{ marginLeft: 24 }}>完成次数:{torrent?.completed}</span>
408// </div>
409// </Card>
410// <Card
411// style={{
412// marginTop: 32,
413// background: 'rgba(255,255,255,0.10)',
414// border: 'none',
415// borderRadius: 12,
416// color: '#fff',
417// }}
418// title={<span style={{ color: '#fff' }}>星球评论</span>}
419// >
420// <List
421// loading={commentLoading}
422// dataSource={comments}
423// locale={{ emptyText: '暂无评论' }}
424// renderItem={(item: any) => {
425// // 只渲染顶级评论(pid为null)
426// if (item.pid !== null) return null;
427// const [expanded, setExpanded] = useState(false);
428// const [replyContent, setReplyContent] = useState('');
429// const [replying, setReplying] = useState(false);
430
431// const handleReply = async () => {
432// if (!replyContent.trim()) return;
433// setReplying(true);
434// await addComment({ torrentId: Number(id), comment: replyContent, pid: item.id });
435// setReplyContent('');
436// setReplying(false);
437// fetchComments(pageNum);
438// message.success('回复成功');
439// };
440
441// return (
442// <List.Item
443// style={{
444// alignItems: 'flex-start',
445// border: 'none',
446// background: 'transparent',
447// padding: '20px 0',
448// borderBottom: '1px solid rgba(255,255,255,0.08)',
449// }}
450// >
451// <List.Item.Meta
452// avatar={
453// <Avatar
454// src={item.avatar ? item.avatar.startsWith('http') ? item.avatar : `${item.avatar}` : 'https://img.icons8.com/color/48/planet.png'}
455// size={48}
456// style={{ boxShadow: '0 2px 8px #4299e1' }}
457// />
458// }
459// title={
460// <span style={{ color: '#fff', fontWeight: 500 }}>
461// {item.username || '匿名用户'}
462// </span>
463// }
464// description={
465// <span style={{ color: '#cbd5e1', fontSize: 16 }}>
466// {item.comment}
467// <span style={{ marginLeft: 16, fontSize: 12, color: '#a0aec0' }}>
468// {item.createTime}
469// </span>
470// <Button
471// type="link"
472// size="small"
473// style={{ marginLeft: 16, color: '#4299e1' }}
474// onClick={() => setExpanded((v) => !v)}
475// >
476// {expanded ? '收起回复' : `展开回复${item.children?.length ? ` (${item.children.length})` : ''}`}
477// </Button>
478// </span>
479// }
480// />
481// {expanded && (
482// <div style={{ width: '100%', marginTop: 12, marginLeft: 56 }}>
483// <List
484// dataSource={item.children}
485// itemLayout="horizontal"
486// locale={{ emptyText: '暂无子评论' }}
487// renderItem={(child: any) => (
488// <List.Item
489// style={{
490// border: 'none',
491// background: 'transparent',
492// padding: '12px 0 0 0',
493// marginLeft: 0,
494// }}
495// >
496// <List.Item.Meta
497// avatar={
498// <Avatar
499// src={child.avatar ? child.avatar.startsWith('http') ? child.avatar : `${child.avatar}` : 'https://img.icons8.com/color/48/planet.png'}
500// size={36}
501// style={{ boxShadow: '0 1px 4px #805ad5' }}
502// />
503// }
504// title={
505// <span style={{ color: '#c3bfff', fontWeight: 500, fontSize: 15 }}>
506// {child.username || '匿名用户'}
507// </span>
508// }
509// description={
510// <span style={{ color: '#e0e7ef', fontSize: 15 }}>
511// {child.comment}
512// <span style={{ marginLeft: 12, fontSize: 12, color: '#a0aec0' }}>
513// {child.createTime}
514// </span>
515// </span>
516// }
517// />
518// </List.Item>
519// )}
520// />
521// <div style={{ display: 'flex', marginTop: 12, gap: 8 }}>
522// <Input.TextArea
523// value={replyContent}
524// onChange={e => setReplyContent(e.target.value)}
525// placeholder="回复该评论"
526// autoSize={{ minRows: 1, maxRows: 3 }}
527// style={{ background: 'rgba(255,255,255,0.15)', color: '#fff', border: 'none' }}
528// />
529// <Button
530// type="primary"
531// icon={<SendOutlined />}
532// loading={replying}
533// onClick={handleReply}
534// disabled={!replyContent.trim()}
535// style={{ background: 'linear-gradient(90deg,#4299e1,#805ad5)', border: 'none', height: 40 }}
536// >
537// 发送
538// </Button>
539// </div>
540// </div>
541// )}
542// </List.Item>
543// );
544// }}
545// />
546// <Pagination
547// style={{ marginTop: 16, textAlign: 'right' }}
548// current={pageNum}
549// pageSize={PAGE_SIZE}
550// total={commentsTotal}
551// onChange={setPageNum}
552// showSizeChanger={false}
553// />
554// <div style={{ display: 'flex', marginTop: 24, gap: 8 }}>
555// <Input.TextArea
556// value={comment}
557// onChange={e => setComment(e.target.value)}
558// placeholder="在星球上留下你的评论吧~"
559// autoSize={{ minRows: 2, maxRows: 4 }}
560// style={{ background: 'rgba(255,255,255,0.15)', color: '#fff', border: 'none' }}
561// />
562// <Button
563// type="primary"
564// icon={<SendOutlined />}
565// onClick={handleAddComment}
566// disabled={!comment.trim()}
567// style={{ background: 'linear-gradient(90deg,#4299e1,#805ad5)', border: 'none', height: 48 }}
568// >
569// 发送
570// </Button>
571// </div>
572// </Card>
573// </>
574// )}
575// </>
576// )}
577// </div>
578// </div>
579// );
Jiarenxiang36728482025-06-07 21:51:26 +0800580};
581
582export default TorrentDetail;