blob: d841b0417431fc06dae56fccd80b6ed8b1dcaa63 [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> = {
80 0: '候选中',
81 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};
93
94return (
95 <div style={planetBg}>
96 <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 }}>
97 <Button
98 icon={<ArrowLeftOutlined />}
99 type="link"
100 onClick={() => navigate(-1)}
101 style={{ color: '#fff', marginBottom: 16 }}
102 >
103 返回
104 </Button>
105 {loading ? (
106 <Spin size="large" />
107 ) : (
108 <>
109 {torrent?.status !== 1 ? (
110 <div style={{ color: '#fff', fontSize: 24, textAlign: 'center', padding: '80px 0' }}>
111 当前状态:{statusMap[torrent?.status] || '未知状态'}
112 </div>
113 ) : (
114 <>
115 <div style={{ display: 'flex', alignItems: 'center', gap: 24 }}>
116 <Avatar
117 size={96}
118 src={torrent?.cover || 'https://img.icons8.com/color/96/planet.png'}
119 style={{ boxShadow: '0 0 24px #4299e1' }}
120 />
121 <div>
122 <h1 style={{ color: '#fff', fontSize: 32, marginBottom: 8 }}>
123 {torrent?.title}
124 </h1>
125 <div style={{ marginBottom: 8 }}>
126 <Tag color="geekblue">{getCategoryName(torrent?.categoryId)}</Tag>
127 {torrent?.tags?.map((tag: string) => (
128 <Tag key={tag} color="blue">{tag}</Tag>
129 ))}
130 </div>
131 <div style={{ color: '#cbd5e1', marginBottom: 8 }}>
132 上传者:{torrent?.owner} | 上传时间:{torrent?.createdAt}
133 </div>
134 <Button
135 type="primary"
136 icon={<DownloadOutlined />}
137 onClick={handleDownload}
138 style={{ background: 'linear-gradient(90deg,#4299e1,#805ad5)', border: 'none' }}
139 >
140 下载种子
141 </Button>
142 </div>
143 </div>
144 <Card
145 style={{
146 marginTop: 32,
147 background: 'rgba(255,255,255,0.12)',
148 border: 'none',
149 borderRadius: 12,
150 color: '#fff',
151 }}
152 title={<span style={{ color: '#fff' }}>种子详情</span>}
153 >
154 <div dangerouslySetInnerHTML={{ __html: torrent?.description || '' }} />
155 <div style={{ marginTop: 16, color: '#cbd5e1' }}>
156 <span>文件大小:{torrent?.size}</span>
157 <span style={{ marginLeft: 24 }}>做种人数:{torrent?.seeders}</span>
158 <span style={{ marginLeft: 24 }}>下载人数:{torrent?.leechers}</span>
159 <span style={{ marginLeft: 24 }}>完成次数:{torrent?.completed}</span>
160 </div>
161 </Card>
162 <Card
163 style={{
164 marginTop: 32,
165 background: 'rgba(255,255,255,0.10)',
166 border: 'none',
167 borderRadius: 12,
168 color: '#fff',
169 }}
170 title={<span style={{ color: '#fff' }}>星球评论</span>}
171 >
172 <List
173 loading={commentLoading}
174 dataSource={comments}
175 locale={{ emptyText: '暂无评论' }}
176 renderItem={(item: any) => {
177 // 只渲染顶级评论(pid为null)
178 if (item.pid !== null) return null;
179 const [expanded, setExpanded] = useState(false);
180 const [replyContent, setReplyContent] = useState('');
181 const [replying, setReplying] = useState(false);
182
183 const handleReply = async () => {
184 if (!replyContent.trim()) return;
185 setReplying(true);
186 await addComment({ torrentId: Number(id), comment: replyContent, pid: item.id });
187 setReplyContent('');
188 setReplying(false);
189 fetchComments(pageNum);
190 message.success('回复成功');
191 };
192
193 return (
194 <List.Item
195 style={{
196 alignItems: 'flex-start',
197 border: 'none',
198 background: 'transparent',
199 padding: '20px 0',
200 borderBottom: '1px solid rgba(255,255,255,0.08)',
201 }}
202 >
203 <List.Item.Meta
204 avatar={
205 <Avatar
206 src={item.avatar ? item.avatar.startsWith('http') ? item.avatar : `${item.avatar}` : 'https://img.icons8.com/color/48/planet.png'}
207 size={48}
208 style={{ boxShadow: '0 2px 8px #4299e1' }}
209 />
210 }
211 title={
212 <span style={{ color: '#fff', fontWeight: 500 }}>
213 {item.username || '匿名用户'}
214 </span>
215 }
216 description={
217 <span style={{ color: '#cbd5e1', fontSize: 16 }}>
218 {item.comment}
219 <span style={{ marginLeft: 16, fontSize: 12, color: '#a0aec0' }}>
220 {item.createTime}
221 </span>
222 <Button
223 type="link"
224 size="small"
225 style={{ marginLeft: 16, color: '#4299e1' }}
226 onClick={() => setExpanded((v) => !v)}
227 >
228 {expanded ? '收起回复' : `展开回复${item.children?.length ? ` (${item.children.length})` : ''}`}
229 </Button>
230 </span>
231 }
232 />
233 {expanded && (
234 <div style={{ width: '100%', marginTop: 12, marginLeft: 56 }}>
235 <List
236 dataSource={item.children}
237 itemLayout="horizontal"
238 locale={{ emptyText: '暂无子评论' }}
239 renderItem={(child: any) => (
240 <List.Item
241 style={{
242 border: 'none',
243 background: 'transparent',
244 padding: '12px 0 0 0',
245 marginLeft: 0,
246 }}
247 >
248 <List.Item.Meta
249 avatar={
250 <Avatar
251 src={child.avatar ? child.avatar.startsWith('http') ? child.avatar : `${child.avatar}` : 'https://img.icons8.com/color/48/planet.png'}
252 size={36}
253 style={{ boxShadow: '0 1px 4px #805ad5' }}
254 />
255 }
256 title={
257 <span style={{ color: '#c3bfff', fontWeight: 500, fontSize: 15 }}>
258 {child.username || '匿名用户'}
259 </span>
260 }
261 description={
262 <span style={{ color: '#e0e7ef', fontSize: 15 }}>
263 {child.comment}
264 <span style={{ marginLeft: 12, fontSize: 12, color: '#a0aec0' }}>
265 {child.createTime}
266 </span>
267 </span>
268 }
269 />
270 </List.Item>
271 )}
272 />
273 <div style={{ display: 'flex', marginTop: 12, gap: 8 }}>
274 <Input.TextArea
275 value={replyContent}
276 onChange={e => setReplyContent(e.target.value)}
277 placeholder="回复该评论"
278 autoSize={{ minRows: 1, maxRows: 3 }}
279 style={{ background: 'rgba(255,255,255,0.15)', color: '#fff', border: 'none' }}
280 />
281 <Button
282 type="primary"
283 icon={<SendOutlined />}
284 loading={replying}
285 onClick={handleReply}
286 disabled={!replyContent.trim()}
287 style={{ background: 'linear-gradient(90deg,#4299e1,#805ad5)', border: 'none', height: 40 }}
288 >
289 发送
290 </Button>
291 </div>
292 </div>
293 )}
294 </List.Item>
295 );
296 }}
297 />
298 <Pagination
299 style={{ marginTop: 16, textAlign: 'right' }}
300 current={pageNum}
301 pageSize={PAGE_SIZE}
302 total={commentsTotal}
303 onChange={setPageNum}
304 showSizeChanger={false}
305 />
306 <div style={{ display: 'flex', marginTop: 24, gap: 8 }}>
307 <Input.TextArea
308 value={comment}
309 onChange={e => setComment(e.target.value)}
310 placeholder="在星球上留下你的评论吧~"
311 autoSize={{ minRows: 2, maxRows: 4 }}
312 style={{ background: 'rgba(255,255,255,0.15)', color: '#fff', border: 'none' }}
313 />
314 <Button
315 type="primary"
316 icon={<SendOutlined />}
317 onClick={handleAddComment}
318 disabled={!comment.trim()}
319 style={{ background: 'linear-gradient(90deg,#4299e1,#805ad5)', border: 'none', height: 48 }}
320 >
321 发送
322 </Button>
323 </div>
324 </Card>
325 </>
326 )}
327 </>
328 )}
329 </div>
330 </div>
331);
332};
333
334export default TorrentDetail;