| 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; |