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