blob: d6029a88d10f8b4a281a5a572b51e2367a3bcb49 [file] [log] [blame]
San3yuana2ee30b2025-06-05 21:20:17 +08001import React, { useEffect, useState } from 'react';
San3yuana2ee30b2025-06-05 21:20:17 +08002import styles from './PostDetail.module.css';
San3yuan8166d1b2025-06-05 23:15:53 +08003import { Card, List, Typography, Button, Input, Spin, Empty, Divider } from 'antd';
San3yuana2ee30b2025-06-05 21:20:17 +08004import { getPostDetail } from '@/api/post';
5import { getPostComments } from '@/api/comment';
6import { useSearchParams } from 'react-router-dom';
7import request from '@/utils/request';
8import { useApi } from '@/hooks/request';
9import Navbar from '@/components/navbar/navbar';
San3yuan8166d1b2025-06-05 23:15:53 +080010import { DownloadOutlined, LikeOutlined, LikeFilled } from '@ant-design/icons';
San3yuana2ee30b2025-06-05 21:20:17 +080011
12const { Title, Text, Paragraph } = Typography;
13const { TextArea } = Input;
14
15export interface PostResponse {
San3yuan8166d1b2025-06-05 23:15:53 +080016 postId?: number;
17 userId?: number;
18 postTitle?: string;
19 postContent?: string;
20 createdAt?: number;
21 postType?: string;
22 isLocked?: boolean;
23 lockedReason?: string;
24 lockedAt?: string;
25 lockedBy?: number;
26 viewCount?: number;
27 hotScore?: number;
28 lastCalculated?: number;
29 [property: string]: any;
San3yuana2ee30b2025-06-05 21:20:17 +080030}
31
32export interface CommentResponse {
San3yuan8166d1b2025-06-05 23:15:53 +080033 commentId?: number;
34 content?: string;
35 createdAt?: number;
36 parentCommentId?: number | null;
37 postId?: number;
38 replies?: CommentResponse[];
39 userId?: number;
40 [property: string]: any;
San3yuana2ee30b2025-06-05 21:20:17 +080041}
42
43const PostDetail: React.FC = () => {
San3yuan8166d1b2025-06-05 23:15:53 +080044 const [searchParams] = useSearchParams();
45 const postId = searchParams.get('postId');
46 const { refresh: getPostDetailRefresh } = useApi(() => request.get(getPostDetail + `/${postId}`), false);
47 const { refresh: getPostCommentsRefresh } = useApi(() => request.get(getPostComments + `/${postId}`), false);
48 const [post, setPost] = useState<PostResponse | null>(null);
49 const [comments, setComments] = useState<CommentResponse[]>([]);
50 const [newComment, setNewComment] = useState<string>('');
51 const [loading, setLoading] = useState<boolean>(true);
52 const [liked, setLiked] = useState(false);
San3yuana2ee30b2025-06-05 21:20:17 +080053
San3yuan8166d1b2025-06-05 23:15:53 +080054 useEffect(() => {
55 if (!postId) return;
56 const fetchData = async () => {
57 setLoading(true);
58 const postRes = await getPostDetailRefresh();
59 if (!postRes || (postRes as any).error) {
60 setLoading(false);
61 return;
62 }
63 setPost(postRes as PostResponse);
San3yuana2ee30b2025-06-05 21:20:17 +080064
San3yuan8166d1b2025-06-05 23:15:53 +080065 const commentsRes = await getPostCommentsRefresh();
66 setComments(commentsRes as CommentResponse[]);
67 setLoading(false);
68 };
69 fetchData();
70 }, [postId]);
San3yuana2ee30b2025-06-05 21:20:17 +080071
San3yuan8166d1b2025-06-05 23:15:53 +080072 if (loading) return <div className={styles.center}><Spin /></div>;
73 if (!post) return <div className={styles.center}><Empty description="未找到帖子" /></div>;
San3yuana2ee30b2025-06-05 21:20:17 +080074
San3yuan8166d1b2025-06-05 23:15:53 +080075 return (
76 <div className={styles.container}>
77 {/* 固定导航栏 */}
78 <div className={styles.nav}>
79 <Navbar current={post.postType} />
80 </div>
81 {/* 内容区域 */}
82 <div className={styles.contentArea}>
83 <Card
84 title={<Title level={3} style={{ margin: 0 }}>{post.postTitle || "帖子标题"}</Title>}
85 className={styles.card}
86 bordered={false}
87 style={{ marginBottom: 24, boxShadow: '0 4px 24px rgba(0,0,0,0.08)' }}
88 extra={
89 <div style={{ display: 'flex', gap: 16 }}>
90 <Button
91 type="primary"
92 icon={<DownloadOutlined />}
93 onClick={() => {
94 // 下载逻辑
95 window.open(`/api/download/post/${post.postId}`, '_blank');
96 }}
97 >
98 下载
99 </Button>
100 <Button
101 type="primary"
102 icon={liked ? <LikeFilled /> : <LikeOutlined />}
103 style={liked ? { background: '#ccc', borderColor: '#ccc', color: '#888', cursor: 'not-allowed' } : {}}
104 disabled={liked}
105 onClick={() => setLiked(true)}
106 >
107 {liked ? '已点赞' : '点赞'}
108 </Button>
109 </div>
110 }
111 >
112 <div className={styles.metaRow}>
113 <Text type="secondary">作者ID: {post.userId}</Text>
114 <Text type="secondary">发布时间: {post.createdAt ? new Date(post.createdAt).toLocaleString() : "未知"}</Text>
115 <Text type="secondary">浏览量: {post.viewCount}</Text>
116 <Text type="secondary">类型: {post.postType}</Text>
117 <Text type="secondary">热度: {post.hotScore}</Text>
118 <Text type="secondary">最后计算: {post.lastCalculated ? new Date(post.lastCalculated).toLocaleString() : "无"}</Text>
119 </div>
120 {post.isLocked && (
121 <div className={styles.locked}>
122 <Text type="danger">本帖已锁定</Text>
123 {post.lockedReason && <Text type="secondary">(原因:{post.lockedReason})</Text>}
124 {post.lockedAt && <Text style={{ marginLeft: 8 }}>锁定时间: {post.lockedAt}</Text>}
125 {post.lockedBy !== 0 && <Text style={{ marginLeft: 8 }}>锁定人ID: {post.lockedBy}</Text>}
126 </div>
127 )}
128 <Divider style={{ margin: '16px 0' }} />
129 <Paragraph className={styles.contentText}>{post.postContent || "暂无内容"}</Paragraph>
130 </Card>
San3yuana2ee30b2025-06-05 21:20:17 +0800131
San3yuan8166d1b2025-06-05 23:15:53 +0800132 {/* 发布评论区域 */}
133 <Card className={styles.addCommentCard} style={{ marginBottom: 32, boxShadow: '0 2px 12px rgba(0,0,0,0.06)' }}>
134 <Title level={5} style={{ marginBottom: 12 }}>发布评论</Title>
135 <TextArea
136 rows={4}
137 value={newComment}
138 onChange={(e) => setNewComment(e.target.value)}
139 placeholder="写下你的评论..."
140 className={styles.textarea}
141 />
142 <Button
143 type="primary"
144 style={{ marginTop: 12, float: 'right' }}
145 onClick={() => setNewComment('')}
146 disabled={!newComment.trim()}
147 >
148 评论
149 </Button>
150 <div style={{ clear: 'both' }} />
151 </Card>
152
153 <Card
154 className={styles.commentListCard}
155 title={<Title level={4} style={{ margin: 0 }}>评论区</Title>}
156 bodyStyle={{ padding: 0 }}
157 >
158 <List
159 className={styles.commentList}
160 dataSource={comments}
161 locale={{ emptyText: <Empty description="暂无评论" /> }}
162 renderItem={(item) => (
163 <List.Item className={styles.commentItem} key={item.commentId}>
164 <List.Item.Meta
165 title={<Text strong>用户ID: {item.userId}</Text>}
166 description={
167 <>
168 <Text>{item.content}</Text>
169 <div style={{ fontSize: 12, color: '#888', marginTop: 8 }}>
170 {item.createdAt && new Date(item.createdAt).toLocaleString()}
171 </div>
172 </>
173 }
174 />
175 {/* 可递归渲染子评论 */}
176 {item.replies && item.replies.length > 0 && (
177 <List
178 className={styles.replyList}
179 dataSource={item.replies}
180 renderItem={reply => (
181 <List.Item className={styles.replyItem} key={reply.commentId}>
182 <List.Item.Meta
183 title={<Text strong>用户ID: {reply.userId}</Text>}
184 description={
185 <>
186 <Text>{reply.content}</Text>
187 <div style={{ fontSize: 12, color: '#888', marginTop: 8 }}>
188 {reply.createdAt && new Date(reply.createdAt).toLocaleString()}
189 </div>
190 </>
191 }
192 />
193 </List.Item>
194 )}
195 />
196 )}
197 </List.Item>
198 )}
199 />
200 </Card>
201 </div>
202 </div>
203 );
San3yuana2ee30b2025-06-05 21:20:17 +0800204};
205
206export default PostDetail;