blob: 591d9f18814e0c7d9f238f4a3185713ed5f56c9d [file] [log] [blame]
import React, { useCallback, useEffect, useState } from 'react';
import styles from './PostDetail.module.css';
import { Card, List, Typography, Button, Input, Spin, Empty, Divider, message } from 'antd';
import { getPostDetail } from '@/api/post';
import { getPostComments, postPostComments } from '@/api/comment';
import { useSearchParams } from 'react-router-dom';
import request from '@/utils/request';
import { useApi } from '@/hooks/request';
import Navbar from '@/components/navbar/navbar';
import { DownloadOutlined, LikeOutlined, LikeFilled, PayCircleOutlined } from '@ant-design/icons';
import instance from '@/utils/axios';
import { useAppSelector } from '@/hooks/store';
const { Title, Text, Paragraph } = Typography;
const { TextArea } = Input;
export interface PostResponse {
postId?: number;
userId?: number;
postTitle?: string;
postContent?: string;
createdAt?: number;
postType?: string;
isLocked?: boolean;
lockedReason?: string;
lockedAt?: string;
lockedBy?: number;
viewCount?: number;
hotScore?: number;
lastCalculated?: number;
[property: string]: any;
}
export interface CommentResponse {
commentId?: number;
content?: string;
createdAt?: number;
parentCommentId?: number | null;
postId?: number;
replies?: CommentResponse[];
userId?: number;
[property: string]: any;
}
const PostDetail: React.FC = () => {
const [messageApi, placeholder] = message.useMessage();
const [searchParams] = useSearchParams();
const postId = searchParams.get('postId');
const { refresh: getPostDetailRefresh } = useApi(() => request.get(getPostDetail + `/${postId}`), false);
const { refresh: getPostCommentsRefresh } = useApi(() => request.get(getPostComments + `/${postId}`), false);
const [post, setPost] = useState<PostResponse | null>(null);
const [comments, setComments] = useState<CommentResponse[]>([]);
const [newComment, setNewComment] = useState<string>('');
const [loading, setLoading] = useState<boolean>(true);
const [liked, setLiked] = useState(false);
const userId = useAppSelector((state)=> state.user.userId)
const {refresh:postComment} = useApi((payload)=>request.post(postPostComments, payload), false)
const handleDownload = async (torrentId: string, torrentName:string) => {
console.log(torrentId)
try {
const token = localStorage.getItem('token'); // 或从状态管理中获取
const response = await instance.get(`/torrent/download/${torrentId}`, {
responseType: 'blob',
headers: {
Authorization: `Bearer ${token}`,
},
});
console.log(response)
const blob = new Blob([response.data], { type: response.headers['content-type'] });
const downloadUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = downloadUrl;
a.download = `资源_${torrentName}.torrent`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(downloadUrl);
} catch (error) {
console.error('下载失败', error);
alert('下载失败,请检查网络或登录状态');
}
};
useEffect(() => {
if (!postId) return;
const fetchData = async () => {
setLoading(true);
const postRes = await getPostDetailRefresh();
if (!postRes || (postRes as any).error) {
setLoading(false);
return;
}
setPost(postRes as PostResponse);
const commentsRes = await getPostCommentsRefresh();
setComments(commentsRes as CommentResponse[]);
setLoading(false);
};
fetchData();
}, [postId]);
const refreshComment = useCallback(async ()=>{
if(!postId) return;
const commentsRes = await getPostCommentsRefresh();
setComments(commentsRes as CommentResponse[]);
},[postId, setComments])
if (loading) return <div className={styles.center}><Spin /></div>;
if (!post) return <div className={styles.center}><Empty description="未找到帖子" /></div>;
return (
<div className={styles.container}>
{placeholder}
<div className={styles.nav}>
<Navbar current={post.postType} />
</div>
<div className={styles.contentArea}>
{/**帖子信息 */}
<Card
title={<Title level={3} style={{ margin: 0 }}>{post.postTitle || "帖子标题"}</Title>}
className={styles.card}
style={{ marginBottom: 24, boxShadow: '0 4px 24px rgba(0,0,0,0.08)' }}
extra={
<div style={{ display: 'flex', gap: 16 }}>
<Button
type="primary"
icon={<DownloadOutlined />}
onClick={() =>
post.torrentId && post.postTitle
? handleDownload(post.torrentId, post.postTitle)
: undefined}
>
下载
</Button>
<Button
type="primary"
icon={liked ? <LikeFilled /> : <LikeOutlined />}
style={liked ? { background: '#ccc', borderColor: '#ccc', color: '#888', cursor: 'not-allowed' } : {}}
disabled={liked}
onClick={() => setLiked(true)}
>
{liked ? '已点赞' : '点赞'}
</Button>
</div>
}
>
<div className={styles.metaRow}>
<Text type="secondary">作者ID: {post.userId}</Text>
<Text type="secondary">发布时间: {post.createdAt ? new Date(post.createdAt).toLocaleString() : "未知"}</Text>
<Text type="secondary">浏览量: {post.viewCount}</Text>
</div>
{post.isLocked && (
<div className={styles.locked}>
<Text type="danger">本帖已锁定</Text>
{post.lockedReason && <Text type="secondary">(原因:{post.lockedReason})</Text>}
{post.lockedAt && <Text style={{ marginLeft: 8 }}>锁定时间: {post.lockedAt}</Text>}
{post.lockedBy !== 0 && <Text style={{ marginLeft: 8 }}>锁定人ID: {post.lockedBy}</Text>}
</div>
)}
<Divider style={{ margin: '16px 0' }} />
<Paragraph className={styles.contentText}>{post.postContent || "暂无内容"}</Paragraph>
</Card>
{/* 发布评论区域 */}
<Card className={styles.addCommentCard} style={{ marginBottom: 32, boxShadow: '0 2px 12px rgba(0,0,0,0.06)' }}>
<Title level={5} style={{ marginBottom: 12 }}>发布评论</Title>
<TextArea
rows={4}
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
placeholder="写下你的评论..."
className={styles.textarea}
/>
<Button
type="primary"
style={{ marginTop: 12, float: 'right' }}
onClick={async() => {
try{
await postComment({
postId,
userId,
content:newComment,
parentCommentId:''
})
setNewComment('')
refreshComment();
messageApi.success('发布成功');
}catch(err){
messageApi.error((err as Error).message);
}
}}
disabled={!newComment.trim()}
>
评论
</Button>
<div style={{ clear: 'both' }} />
</Card>
{/* 评论区 */}
<Card
className={styles.commentListCard}
title={<Title level={4} style={{ margin: 0 }}>评论区</Title>}
>
{
comments && comments.length &&
<List
className={styles.commentList}
dataSource={comments}
locale={{ emptyText: <Empty description="暂无评论" /> }}
renderItem={(item) => (
<List.Item className={styles.commentItem} key={item.commentId}>
<List.Item.Meta
title={<Text strong>用户ID: {item.userId}</Text>}
description={
<>
<Text>{item.content}</Text>
<div style={{ fontSize: 12, color: '#888', marginTop: 8 }}>
{item.createdAt && new Date(item.createdAt).toLocaleString()}
</div>
</>
}
/>
{/* 可递归渲染子评论 */}
{item.replies && item.replies.length > 0 && (
<List
className={styles.replyList}
dataSource={item.replies}
renderItem={reply => (
<List.Item className={styles.replyItem} key={reply.commentId}>
<List.Item.Meta
title={<Text strong>用户ID: {reply.userId}</Text>}
description={
<>
<Text>{reply.content}</Text>
<div style={{ fontSize: 12, color: '#888', marginTop: 8 }}>
{reply.createdAt && new Date(reply.createdAt).toLocaleString()}
</div>
</>
}
/>
</List.Item>
)}
/>
)}
</List.Item>
)}
/>
}
</Card>
</div>
</div>
);
};
export default PostDetail;