merge
Change-Id: I5227831adac7f85854cbe7321c2a3aa39d8c1d7a
diff --git a/src/views/postDetail/postDetail.module.css b/src/views/postDetail/postDetail.module.css
index e69de29..46c2176 100644
--- a/src/views/postDetail/postDetail.module.css
+++ b/src/views/postDetail/postDetail.module.css
@@ -0,0 +1,89 @@
+.commentList .ant-list-item {
+ min-height: 300px;
+ height: 300px;
+ box-sizing: border-box;
+ display: flex;
+ align-items: flex-start;
+ /* 可选:让内容垂直居中可用 align-items: center; */
+}
+.contentArea {
+ width: 100%;
+ max-width: 900px;
+ margin: 32px auto 0 auto;
+ padding: 0 16px 32px 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+}
+
+.card {
+ border-radius: 10px;
+ background: var(--card-bg);
+}
+
+.metaRow {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 16px;
+ margin-bottom: 8px;
+}
+
+.locked {
+ margin: 12px 0;
+ color: #d4380d;
+ font-weight: bold;
+}
+
+.contentText {
+ font-size: 17px;
+ color: var(--text-color);
+ line-height: 1.8;
+ margin-top: 12px;
+}
+
+.addCommentCard {
+ border-radius: 10px;
+ background: var(--card-bg);
+}
+
+.textarea {
+ font-size: 15px;
+}
+
+.commentListCard {
+ border-radius: 10px;
+ background: var(--card-bg);
+}
+
+.commentList .ant-list-item,
+.replyList .ant-list-item {
+ min-height: 120px;
+ height: auto;
+ box-sizing: border-box;
+ display: flex;
+ align-items: flex-start;
+ border-bottom: 1px solid var(--border-color);
+ padding: 24px 16px;
+}
+
+.replyList {
+ margin-left: 32px;
+ background: transparent;
+}
+
+.replyItem {
+ background: #f6f8fa;
+ border-radius: 6px;
+ margin-bottom: 8px;
+ padding: 12px 16px;
+}
+
+@media (max-width: 600px) {
+ .contentArea {
+ max-width: 100%;
+ padding: 0 4px 24px 4px;
+ }
+ .card, .addCommentCard, .commentListCard {
+ padding: 0;
+ }
+}
\ No newline at end of file
diff --git a/src/views/postDetail/postDetail.tsx b/src/views/postDetail/postDetail.tsx
index a40fb68..d6029a8 100644
--- a/src/views/postDetail/postDetail.tsx
+++ b/src/views/postDetail/postDetail.tsx
@@ -1,114 +1,206 @@
import React, { useEffect, useState } from 'react';
-import { useParams } from 'react-router-dom';
import styles from './PostDetail.module.css';
-import { Card, List, Typography, Button, Input, Spin, Empty } from 'antd';
-type CommentProps = {
- children?: React.ReactNode;
-};
+import { Card, List, Typography, Button, Input, Spin, Empty, Divider } from 'antd';
import { getPostDetail } from '@/api/post';
import { getPostComments } 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 } from '@ant-design/icons';
const { Title, Text, Paragraph } = Typography;
const { TextArea } = Input;
export interface PostResponse {
- createdAt?: number;
- hotScore?: number;
- lastCalculated?: number;
- postContent?: string;
- postId?: number;
- postTitle?: string;
- postType?: string;
- userId?: number;
- viewCount?: number;
- [property: string]: any;
+ 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;
+ commentId?: number;
+ content?: string;
+ createdAt?: number;
+ parentCommentId?: number | null;
+ postId?: number;
+ replies?: CommentResponse[];
+ userId?: number;
+ [property: string]: any;
}
const PostDetail: React.FC = () => {
- 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 [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);
- useEffect(() => {
- console.log('postId', postId);
- if (!postId) return;
- const fetchData = async () => {
- setLoading(true);
- const res = await getPostDetailRefresh();
- if (res == null || (res as any).error) {
- setLoading(false);
- return;
- }
- setPost(res as PostResponse);
- await getPostCommentsRefresh();
- setComments(res as CommentResponse[]);
- setLoading(false);
- };
- fetchData();
- }, [postId]);
+ 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);
- if (loading) return <div className={styles.center}><Spin /></div>;
- if (!post) return <div className={styles.center}><Empty description="未找到帖子" /></div>;
+ const commentsRes = await getPostCommentsRefresh();
+ setComments(commentsRes as CommentResponse[]);
+ setLoading(false);
+ };
+ fetchData();
+ }, [postId]);
- return (
- <div className={styles.container}>
- <div className={styles.nav}>
- <Navbar current={post.postType} />
- </div>
- <div className={styles.content}>
- <div className={styles.postDetail}>
-
- </div >
- <Card title={post.postTitle} className={styles.card}>
- <Paragraph>{post.postContent}</Paragraph>
- <div className={styles.actions}>
- <Button type="primary" onClick={() => setNewComment('')}>评论</Button>
- </div>
- </Card>
+ if (loading) return <div className={styles.center}><Spin /></div>;
+ if (!post) return <div className={styles.center}><Empty description="未找到帖子" /></div>;
- <List
- className={styles.commentList}
- header={<Title level={4}>评论区</Title>}
- dataSource={comments}
- renderItem={(item) => (
- <List.Item key={item.commentId}>
- <List.Item.Meta
- title={<Text strong>{item.userId}</Text>}
- description={<Text>{item.content}</Text>}
- />
- </List.Item>
- )}
- />
+ return (
+ <div className={styles.container}>
+ {/* 固定导航栏 */}
+ <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}
+ bordered={false}
+ 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={() => {
+ // 下载逻辑
+ window.open(`/api/download/post/${post.postId}`, '_blank');
+ }}
+ >
+ 下载
+ </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>
+ <Text type="secondary">类型: {post.postType}</Text>
+ <Text type="secondary">热度: {post.hotScore}</Text>
+ <Text type="secondary">最后计算: {post.lastCalculated ? new Date(post.lastCalculated).toLocaleString() : "无"}</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>
- <TextArea
- rows={4}
- value={newComment}
- onChange={(e) => setNewComment(e.target.value)}
- placeholder="写下你的评论..."
- />
- </div>
- </div>
- );
+ {/* 发布评论区域 */}
+ <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={() => setNewComment('')}
+ disabled={!newComment.trim()}
+ >
+ 评论
+ </Button>
+ <div style={{ clear: 'both' }} />
+ </Card>
+
+ <Card
+ className={styles.commentListCard}
+ title={<Title level={4} style={{ margin: 0 }}>评论区</Title>}
+ bodyStyle={{ padding: 0 }}
+ >
+ <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;
\ No newline at end of file