blob: 591d9f18814e0c7d9f238f4a3185713ed5f56c9d [file] [log] [blame]
San3yuan292794c2025-06-08 20:02:46 +08001import React, { useCallback, useEffect, useState } from 'react';
San3yuana2ee30b2025-06-05 21:20:17 +08002import styles from './PostDetail.module.css';
San3yuan292794c2025-06-08 20:02:46 +08003import { Card, List, Typography, Button, Input, Spin, Empty, Divider, message } from 'antd';
San3yuana2ee30b2025-06-05 21:20:17 +08004import { getPostDetail } from '@/api/post';
San3yuan292794c2025-06-08 20:02:46 +08005import { getPostComments, postPostComments } from '@/api/comment';
San3yuana2ee30b2025-06-05 21:20:17 +08006import { useSearchParams } from 'react-router-dom';
7import request from '@/utils/request';
8import { useApi } from '@/hooks/request';
9import Navbar from '@/components/navbar/navbar';
San3yuan292794c2025-06-08 20:02:46 +080010import { DownloadOutlined, LikeOutlined, LikeFilled, PayCircleOutlined } from '@ant-design/icons';
11import instance from '@/utils/axios';
12import { useAppSelector } from '@/hooks/store';
13
San3yuana2ee30b2025-06-05 21:20:17 +080014
15const { Title, Text, Paragraph } = Typography;
16const { TextArea } = Input;
17
18export interface PostResponse {
San3yuan8166d1b2025-06-05 23:15:53 +080019 postId?: number;
20 userId?: number;
21 postTitle?: string;
22 postContent?: string;
23 createdAt?: number;
24 postType?: string;
25 isLocked?: boolean;
26 lockedReason?: string;
27 lockedAt?: string;
28 lockedBy?: number;
29 viewCount?: number;
30 hotScore?: number;
31 lastCalculated?: number;
32 [property: string]: any;
San3yuana2ee30b2025-06-05 21:20:17 +080033}
34
35export interface CommentResponse {
San3yuan8166d1b2025-06-05 23:15:53 +080036 commentId?: number;
37 content?: string;
38 createdAt?: number;
39 parentCommentId?: number | null;
40 postId?: number;
41 replies?: CommentResponse[];
42 userId?: number;
43 [property: string]: any;
San3yuana2ee30b2025-06-05 21:20:17 +080044}
45
46const PostDetail: React.FC = () => {
San3yuan292794c2025-06-08 20:02:46 +080047 const [messageApi, placeholder] = message.useMessage();
San3yuan8166d1b2025-06-05 23:15:53 +080048 const [searchParams] = useSearchParams();
49 const postId = searchParams.get('postId');
50 const { refresh: getPostDetailRefresh } = useApi(() => request.get(getPostDetail + `/${postId}`), false);
51 const { refresh: getPostCommentsRefresh } = useApi(() => request.get(getPostComments + `/${postId}`), false);
52 const [post, setPost] = useState<PostResponse | null>(null);
53 const [comments, setComments] = useState<CommentResponse[]>([]);
54 const [newComment, setNewComment] = useState<string>('');
55 const [loading, setLoading] = useState<boolean>(true);
56 const [liked, setLiked] = useState(false);
San3yuan292794c2025-06-08 20:02:46 +080057 const userId = useAppSelector((state)=> state.user.userId)
58
59 const {refresh:postComment} = useApi((payload)=>request.post(postPostComments, payload), false)
60
61 const handleDownload = async (torrentId: string, torrentName:string) => {
62 console.log(torrentId)
63 try {
64 const token = localStorage.getItem('token'); // 或从状态管理中获取
65 const response = await instance.get(`/torrent/download/${torrentId}`, {
66 responseType: 'blob',
67 headers: {
68 Authorization: `Bearer ${token}`,
69 },
70 });
71 console.log(response)
72 const blob = new Blob([response.data], { type: response.headers['content-type'] });
73 const downloadUrl = URL.createObjectURL(blob);
74
75 const a = document.createElement('a');
76 a.href = downloadUrl;
77 a.download = `资源_${torrentName}.torrent`;
78 document.body.appendChild(a);
79 a.click();
80 document.body.removeChild(a);
81 URL.revokeObjectURL(downloadUrl);
82 } catch (error) {
83 console.error('下载失败', error);
84 alert('下载失败,请检查网络或登录状态');
85 }
86 };
San3yuana2ee30b2025-06-05 21:20:17 +080087
San3yuan8166d1b2025-06-05 23:15:53 +080088 useEffect(() => {
89 if (!postId) return;
90 const fetchData = async () => {
91 setLoading(true);
92 const postRes = await getPostDetailRefresh();
93 if (!postRes || (postRes as any).error) {
94 setLoading(false);
95 return;
96 }
97 setPost(postRes as PostResponse);
San3yuana2ee30b2025-06-05 21:20:17 +080098
San3yuan8166d1b2025-06-05 23:15:53 +080099 const commentsRes = await getPostCommentsRefresh();
100 setComments(commentsRes as CommentResponse[]);
101 setLoading(false);
102 };
103 fetchData();
104 }, [postId]);
San3yuana2ee30b2025-06-05 21:20:17 +0800105
San3yuan292794c2025-06-08 20:02:46 +0800106 const refreshComment = useCallback(async ()=>{
107 if(!postId) return;
108 const commentsRes = await getPostCommentsRefresh();
109 setComments(commentsRes as CommentResponse[]);
110 },[postId, setComments])
111
San3yuan8166d1b2025-06-05 23:15:53 +0800112 if (loading) return <div className={styles.center}><Spin /></div>;
113 if (!post) return <div className={styles.center}><Empty description="未找到帖子" /></div>;
San3yuana2ee30b2025-06-05 21:20:17 +0800114
San3yuan8166d1b2025-06-05 23:15:53 +0800115 return (
116 <div className={styles.container}>
San3yuan292794c2025-06-08 20:02:46 +0800117 {placeholder}
San3yuan8166d1b2025-06-05 23:15:53 +0800118 <div className={styles.nav}>
119 <Navbar current={post.postType} />
120 </div>
San3yuan292794c2025-06-08 20:02:46 +0800121
San3yuan8166d1b2025-06-05 23:15:53 +0800122 <div className={styles.contentArea}>
San3yuan292794c2025-06-08 20:02:46 +0800123 {/**帖子信息 */}
San3yuan8166d1b2025-06-05 23:15:53 +0800124 <Card
125 title={<Title level={3} style={{ margin: 0 }}>{post.postTitle || "帖子标题"}</Title>}
126 className={styles.card}
San3yuan8166d1b2025-06-05 23:15:53 +0800127 style={{ marginBottom: 24, boxShadow: '0 4px 24px rgba(0,0,0,0.08)' }}
128 extra={
129 <div style={{ display: 'flex', gap: 16 }}>
130 <Button
131 type="primary"
132 icon={<DownloadOutlined />}
San3yuan292794c2025-06-08 20:02:46 +0800133 onClick={() =>
134 post.torrentId && post.postTitle
135 ? handleDownload(post.torrentId, post.postTitle)
136 : undefined}
San3yuan8166d1b2025-06-05 23:15:53 +0800137 >
138 下载
139 </Button>
140 <Button
141 type="primary"
142 icon={liked ? <LikeFilled /> : <LikeOutlined />}
143 style={liked ? { background: '#ccc', borderColor: '#ccc', color: '#888', cursor: 'not-allowed' } : {}}
144 disabled={liked}
145 onClick={() => setLiked(true)}
146 >
147 {liked ? '已点赞' : '点赞'}
148 </Button>
149 </div>
150 }
151 >
152 <div className={styles.metaRow}>
153 <Text type="secondary">作者ID: {post.userId}</Text>
154 <Text type="secondary">发布时间: {post.createdAt ? new Date(post.createdAt).toLocaleString() : "未知"}</Text>
155 <Text type="secondary">浏览量: {post.viewCount}</Text>
San3yuan8166d1b2025-06-05 23:15:53 +0800156 </div>
157 {post.isLocked && (
158 <div className={styles.locked}>
159 <Text type="danger">本帖已锁定</Text>
160 {post.lockedReason && <Text type="secondary">(原因:{post.lockedReason})</Text>}
161 {post.lockedAt && <Text style={{ marginLeft: 8 }}>锁定时间: {post.lockedAt}</Text>}
162 {post.lockedBy !== 0 && <Text style={{ marginLeft: 8 }}>锁定人ID: {post.lockedBy}</Text>}
163 </div>
164 )}
165 <Divider style={{ margin: '16px 0' }} />
166 <Paragraph className={styles.contentText}>{post.postContent || "暂无内容"}</Paragraph>
167 </Card>
San3yuana2ee30b2025-06-05 21:20:17 +0800168
San3yuan8166d1b2025-06-05 23:15:53 +0800169 {/* 发布评论区域 */}
170 <Card className={styles.addCommentCard} style={{ marginBottom: 32, boxShadow: '0 2px 12px rgba(0,0,0,0.06)' }}>
171 <Title level={5} style={{ marginBottom: 12 }}>发布评论</Title>
172 <TextArea
173 rows={4}
174 value={newComment}
175 onChange={(e) => setNewComment(e.target.value)}
176 placeholder="写下你的评论..."
177 className={styles.textarea}
178 />
179 <Button
180 type="primary"
181 style={{ marginTop: 12, float: 'right' }}
San3yuan292794c2025-06-08 20:02:46 +0800182 onClick={async() => {
183 try{
184 await postComment({
185 postId,
186 userId,
187 content:newComment,
188 parentCommentId:''
189 })
190 setNewComment('')
191 refreshComment();
192 messageApi.success('发布成功');
193 }catch(err){
194 messageApi.error((err as Error).message);
195 }
196
197 }}
San3yuan8166d1b2025-06-05 23:15:53 +0800198 disabled={!newComment.trim()}
199 >
200 评论
201 </Button>
202 <div style={{ clear: 'both' }} />
203 </Card>
204
San3yuan292794c2025-06-08 20:02:46 +0800205 {/* 评论区 */}
San3yuan8166d1b2025-06-05 23:15:53 +0800206 <Card
207 className={styles.commentListCard}
208 title={<Title level={4} style={{ margin: 0 }}>评论区</Title>}
San3yuan8166d1b2025-06-05 23:15:53 +0800209 >
San3yuan292794c2025-06-08 20:02:46 +0800210 {
211 comments && comments.length &&
212 <List
San3yuan8166d1b2025-06-05 23:15:53 +0800213 className={styles.commentList}
214 dataSource={comments}
215 locale={{ emptyText: <Empty description="暂无评论" /> }}
216 renderItem={(item) => (
217 <List.Item className={styles.commentItem} key={item.commentId}>
218 <List.Item.Meta
219 title={<Text strong>用户ID: {item.userId}</Text>}
220 description={
221 <>
222 <Text>{item.content}</Text>
223 <div style={{ fontSize: 12, color: '#888', marginTop: 8 }}>
224 {item.createdAt && new Date(item.createdAt).toLocaleString()}
225 </div>
226 </>
227 }
228 />
229 {/* 可递归渲染子评论 */}
230 {item.replies && item.replies.length > 0 && (
231 <List
232 className={styles.replyList}
233 dataSource={item.replies}
234 renderItem={reply => (
235 <List.Item className={styles.replyItem} key={reply.commentId}>
236 <List.Item.Meta
237 title={<Text strong>用户ID: {reply.userId}</Text>}
238 description={
239 <>
240 <Text>{reply.content}</Text>
241 <div style={{ fontSize: 12, color: '#888', marginTop: 8 }}>
242 {reply.createdAt && new Date(reply.createdAt).toLocaleString()}
243 </div>
244 </>
245 }
246 />
247 </List.Item>
248 )}
249 />
250 )}
251 </List.Item>
252 )}
253 />
San3yuan292794c2025-06-08 20:02:46 +0800254 }
255
San3yuan8166d1b2025-06-05 23:15:53 +0800256 </Card>
257 </div>
258 </div>
259 );
San3yuana2ee30b2025-06-05 21:20:17 +0800260};
261
262export default PostDetail;