blob: 7a25162c595996bea67156ff84be7797e57dc73d [file] [log] [blame]
import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { Button, Input, message, Spin, Tag, Card, Avatar, List, Pagination } from 'antd';
import { DownloadOutlined, ArrowLeftOutlined, SendOutlined } from '@ant-design/icons';
import {
getTorrentInfo,
downloadTorrent,
addComment,
getComments,
getCategories,
} from '../../services/bt/index';
const PAGE_SIZE = 10;
const TorrentDetail: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [loading, setLoading] = useState(true);
const [torrent, setTorrent] = useState<any>(null);
const [categories, setCategories] = useState<any[]>([]);
const [comments, setComments] = useState<any[]>([]);
const [commentLoading, setCommentLoading] = useState(false);
const [comment, setComment] = useState('');
const [commentsTotal, setCommentsTotal] = useState(0);
const [pageNum, setPageNum] = useState(1);
useEffect(() => {
setLoading(true);
getTorrentInfo({ id: id! })
.then((res) => setTorrent(res.data))
.finally(() => setLoading(false));
getCategories().then((res) => setCategories(res.data || []));
}, [id]);
useEffect(() => {
fetchComments(pageNum);
// eslint-disable-next-line
}, [id, pageNum]);
const fetchComments = (page: number) => {
setCommentLoading(true);
getComments({ torrentId: Number(id), pageNum: page, pageSize: PAGE_SIZE })
.then((res) => {0
setComments(res.data?.list || []);
setCommentsTotal(res.data?.total || 0);
})
.finally(() => setCommentLoading(false));
};
const handleDownload = async () => {
try {
const res = await downloadTorrent({ id: id! });
const blob = new Blob([res], { type: 'application/x-bittorrent' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${torrent?.title || 'torrent'}.torrent`;
a.click();
window.URL.revokeObjectURL(url);
} catch {
message.error('下载失败');
}
};
const handleAddComment = async () => {
if (!comment.trim()) return;
await addComment({ torrentId: Number(id), comment });
setComment('');
fetchComments(1);
setPageNum(1);
message.success('评论成功');
};
const getCategoryName = (catId: number) => {
return categories.find((c) => c.id === catId)?.name || '未知分类';
};
const statusMap: Record<number, string> = {
0: '审核中',
1: '已发布',
2: '审核不通过',
3: '已上架修改重审中',
10: '已下架',
};
// 星球主题样式
const planetBg = {
background: 'radial-gradient(circle at 60% 40%, #2b6cb0 0%, #1a202c 100%)',
minHeight: '100vh',
padding: '32px 0',
};
return (
<div style={planetBg}>
<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 }}>
<Button
icon={<ArrowLeftOutlined />}
type="link"
onClick={() => navigate(-1)}
style={{ color: '#fff', marginBottom: 16 }}
>
返回
</Button>
{loading ? (
<Spin size="large" />
) : (
<>
{torrent?.status !== 1 ? (
<div style={{ color: '#fff', fontSize: 24, textAlign: 'center', padding: '80px 0' }}>
当前状态:{statusMap[torrent?.status] || '未知状态'}
</div>
) : (
<>
<div style={{ display: 'flex', alignItems: 'center', gap: 24 }}>
<Avatar
size={96}
src={torrent?.cover || 'https://img.icons8.com/color/96/planet.png'}
style={{ boxShadow: '0 0 24px #4299e1' }}
/>
<div>
<h1 style={{ color: '#fff', fontSize: 32, marginBottom: 8 }}>
{torrent?.title}
</h1>
<div style={{ marginBottom: 8 }}>
<Tag color="geekblue">{getCategoryName(torrent?.categoryId)}</Tag>
{torrent?.tags?.map((tag: string) => (
<Tag key={tag} color="blue">{tag}</Tag>
))}
</div>
<div style={{ color: '#cbd5e1', marginBottom: 8 }}>
上传者:{torrent?.owner} | 上传时间:{torrent?.createdAt}
</div>
<Button
type="primary"
icon={<DownloadOutlined />}
onClick={handleDownload}
style={{ background: 'linear-gradient(90deg,#4299e1,#805ad5)', border: 'none' }}
>
下载种子
</Button>
</div>
</div>
<Card
style={{
marginTop: 32,
background: 'rgba(255,255,255,0.12)',
border: 'none',
borderRadius: 12,
color: '#fff',
}}
title={<span style={{ color: '#fff' }}>种子详情</span>}
>
<div dangerouslySetInnerHTML={{ __html: torrent?.description || '' }} />
<div style={{ marginTop: 16, color: '#cbd5e1' }}>
<span>文件大小:{torrent?.size}</span>
<span style={{ marginLeft: 24 }}>做种人数:{torrent?.seeders}</span>
<span style={{ marginLeft: 24 }}>下载人数:{torrent?.leechers}</span>
<span style={{ marginLeft: 24 }}>完成次数:{torrent?.completed}</span>
</div>
</Card>
<Card
style={{
marginTop: 32,
background: 'rgba(255,255,255,0.10)',
border: 'none',
borderRadius: 12,
color: '#fff',
}}
title={<span style={{ color: '#fff' }}>星球评论</span>}
>
<List
loading={commentLoading}
dataSource={comments}
locale={{ emptyText: '暂无评论' }}
renderItem={(item: any) => {
// 只渲染顶级评论(pid为null)
if (item.pid !== null) return null;
const [expanded, setExpanded] = useState(false);
const [replyContent, setReplyContent] = useState('');
const [replying, setReplying] = useState(false);
const handleReply = async () => {
if (!replyContent.trim()) return;
setReplying(true);
await addComment({ torrentId: Number(id), comment: replyContent, pid: item.id });
setReplyContent('');
setReplying(false);
fetchComments(pageNum);
message.success('回复成功');
};
return (
<List.Item
style={{
alignItems: 'flex-start',
border: 'none',
background: 'transparent',
padding: '20px 0',
borderBottom: '1px solid rgba(255,255,255,0.08)',
}}
>
<List.Item.Meta
avatar={
<Avatar
src={item.avatar ? item.avatar.startsWith('http') ? item.avatar : `${item.avatar}` : 'https://img.icons8.com/color/48/planet.png'}
size={48}
style={{ boxShadow: '0 2px 8px #4299e1' }}
/>
}
title={
<span style={{ color: '#fff', fontWeight: 500 }}>
{item.username || '匿名用户'}
</span>
}
description={
<span style={{ color: '#cbd5e1', fontSize: 16 }}>
{item.comment}
<span style={{ marginLeft: 16, fontSize: 12, color: '#a0aec0' }}>
{item.createTime}
</span>
<Button
type="link"
size="small"
style={{ marginLeft: 16, color: '#4299e1' }}
onClick={() => setExpanded((v) => !v)}
>
{expanded ? '收起回复' : `展开回复${item.children?.length ? ` (${item.children.length})` : ''}`}
</Button>
</span>
}
/>
{expanded && (
<div style={{ width: '100%', marginTop: 12, marginLeft: 56 }}>
<List
dataSource={item.children}
itemLayout="horizontal"
locale={{ emptyText: '暂无子评论' }}
renderItem={(child: any) => (
<List.Item
style={{
border: 'none',
background: 'transparent',
padding: '12px 0 0 0',
marginLeft: 0,
}}
>
<List.Item.Meta
avatar={
<Avatar
src={child.avatar ? child.avatar.startsWith('http') ? child.avatar : `${child.avatar}` : 'https://img.icons8.com/color/48/planet.png'}
size={36}
style={{ boxShadow: '0 1px 4px #805ad5' }}
/>
}
title={
<span style={{ color: '#c3bfff', fontWeight: 500, fontSize: 15 }}>
{child.username || '匿名用户'}
</span>
}
description={
<span style={{ color: '#e0e7ef', fontSize: 15 }}>
{child.comment}
<span style={{ marginLeft: 12, fontSize: 12, color: '#a0aec0' }}>
{child.createTime}
</span>
</span>
}
/>
</List.Item>
)}
/>
<div style={{ display: 'flex', marginTop: 12, gap: 8 }}>
<Input.TextArea
value={replyContent}
onChange={e => setReplyContent(e.target.value)}
placeholder="回复该评论"
autoSize={{ minRows: 1, maxRows: 3 }}
style={{ background: 'rgba(255,255,255,0.15)', color: '#fff', border: 'none' }}
/>
<Button
type="primary"
icon={<SendOutlined />}
loading={replying}
onClick={handleReply}
disabled={!replyContent.trim()}
style={{ background: 'linear-gradient(90deg,#4299e1,#805ad5)', border: 'none', height: 40 }}
>
发送
</Button>
</div>
</div>
)}
</List.Item>
);
}}
/>
<Pagination
style={{ marginTop: 16, textAlign: 'right' }}
current={pageNum}
pageSize={PAGE_SIZE}
total={commentsTotal}
onChange={setPageNum}
showSizeChanger={false}
/>
<div style={{ display: 'flex', marginTop: 24, gap: 8 }}>
<Input.TextArea
value={comment}
onChange={e => setComment(e.target.value)}
placeholder="在星球上留下你的评论吧~"
autoSize={{ minRows: 2, maxRows: 4 }}
style={{ background: 'rgba(255,255,255,0.15)', color: '#fff', border: 'none' }}
/>
<Button
type="primary"
icon={<SendOutlined />}
onClick={handleAddComment}
disabled={!comment.trim()}
style={{ background: 'linear-gradient(90deg,#4299e1,#805ad5)', border: 'none', height: 48 }}
>
发送
</Button>
</div>
</Card>
</>
)}
</>
)}
</div>
</div>
);
};
export default TorrentDetail;