frontend: add community

Change-Id: I929c21d82ddab39d8b210b229ff7559320c1d853
diff --git "a/src/app/community/thread-detail/\133threadId\135/page.tsx" "b/src/app/community/thread-detail/\133threadId\135/page.tsx"
new file mode 100644
index 0000000..2189402
--- /dev/null
+++ "b/src/app/community/thread-detail/\133threadId\135/page.tsx"
@@ -0,0 +1,396 @@
+'use client';
+
+import React, { useEffect, useState, useRef } from 'react';
+import { Image } from 'primereact/image';
+import { Avatar } from 'primereact/avatar';
+import { Button } from 'primereact/button';
+import { InputText } from "primereact/inputtext";
+// 页面跳转
+import { useParams } from 'next/navigation'
+import { useRouter } from 'next/navigation';
+// 接口传输
+import axios from 'axios';
+// 回复评论
+import { OverlayPanel } from 'primereact/overlaypanel';
+import { Sidebar } from 'primereact/sidebar';
+// 分页
+import { Paginator, type PaginatorPageChangeEvent } from 'primereact/paginator';
+// 消息提醒
+import { Toast } from 'primereact/toast';
+// 样式
+import './thread.scss';
+
+
+// 评论信息
+interface Comment {
+    commentId: number;
+    userId: number | null;
+    replyId: number;
+    content: string;
+    createdAt: string;
+}
+// 评论列表
+interface CommentList {
+    records: Comment[];      // 当前页评论数组
+}
+// 帖子信息
+interface ThreadInfo {
+    threadId: number;
+    userId: number;
+    threadPicture: string;
+    title: string;
+    content: string;
+    likes: number;
+    isLike: boolean;
+    createdAt: string;
+    commentNumber: number;
+    communityId: number;
+}
+// 用户信息
+interface UserInfo {
+    userId: number;
+    username: string;
+    avatar: string;
+    signature: string;
+}
+// 新评论接口
+interface NewComment {
+    userId: number;
+    threadId: number;
+    resourceId: number;
+    replyId: number;
+    content: string;
+    createdAt: string;
+}
+
+
+//帖子详情界面
+export default function ThreadDetailPage() {
+    // 获取URL参数,页面跳转
+    const params = useParams<{ threadId: string }>()
+    const threadId = decodeURIComponent(params.threadId); // 防止中文路径乱码
+    const router = useRouter();
+    // 消息提醒
+    const toast = useRef<Toast>(null);
+    // 帖子信息
+    const [threadInfo, setThreadInfo] = useState<ThreadInfo | null>(null);
+    // 发帖人信息
+    const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
+    // 评论人信息
+    const [commentUserInfos, setCommentUserInfos] = useState<Map<number, UserInfo>>(new Map());
+    //评论
+    const [comments, setComments] = useState<Comment[]>([]);
+    const [commentValue, setCommentValue] = useState<string>('');
+    const [totalComments, setTotalComments] = useState<number>(0);
+    // 回复
+    const [replyValue, setReplyValue] = useState<string>('');
+    const [visibleReply, setVisibleReply] = useState<boolean>(false);// 回复评论可视
+    // 评论选择框
+    const ops = useRef<OverlayPanel[]>([]);
+
+    // 分页
+    const [first, setFirst] = useState<number>(0);
+    const [rows, setRows] = useState<number>(5);
+    const onPageChange = (event: PaginatorPageChangeEvent) => {
+        setFirst(event.first);
+        setRows(event.rows);
+    };
+
+    // 获取帖子信息
+    useEffect(() => {
+        fetchThreadInfo();
+    }, [threadId, threadInfo]);
+
+    const fetchThreadInfo = async () => {
+        try {
+            const { data } = await axios.get(`http://127.0.0.1:4523/m1/6387307-6083949-default/thread?threadId=${threadId}`);
+            setThreadInfo(data);
+            setTotalComments(data.commentNumber);
+        } catch (err) {
+            console.error(err);
+            toast.current?.show({ severity: 'error', summary: 'error', detail: '获取帖子信息失败' });
+        }
+    };
+
+    // 获取发帖人
+    useEffect(() => {
+        if (!threadInfo) return;
+        // 发帖人
+        axios.get(`http://127.0.0.1:4523/m1/6387307-6083949-default/user/info?userId=${threadInfo.userId}`)
+            .then(res => setUserInfo(res.data))
+            .catch(console.error);
+    }, [threadInfo]);
+
+    // 处理点赞
+    const handleLike = async () => {
+        if (!threadInfo) return;
+        if (!threadInfo.isLike) {
+            try {
+                const response = await axios.post(
+                    `http://127.0.0.1:4523/m1/6387307-6083949-default/thread/like`, {
+                    params: { threadId, userId: 22301145 }
+                }
+                );
+                fetchThreadInfo(); // 刷新帖子信息
+                if (response.status === 200) {
+                    console.log('点赞成功:', response.data);
+                    toast.current?.show({ severity: 'success', summary: 'Success', detail: '点赞成功' });
+                }
+            } catch (error) {
+                console.error('点赞失败:', error);
+                toast.current?.show({ severity: 'error', summary: 'error', detail: '点赞失败' });
+            }
+        } else {
+            try {
+                const response = await axios.delete(
+                    `http://127.0.0.1:4523/m1/6387307-6083949-default/thread/like`, {
+                    params: { threadId, userId: 22301145 }
+                }
+                );
+                fetchThreadInfo(); // 刷新帖子信息
+                if (response.status === 200) {
+                    console.log('取消点赞成功:', response.data);
+                    toast.current?.show({ severity: 'success', summary: 'Success', detail: '取消点赞成功' });
+                }
+            } catch (error) {
+                console.error('取消点赞失败:', error);
+                toast.current?.show({ severity: 'error', summary: 'error', detail: '取消点赞失败' });
+            }
+        }
+
+    };
+
+
+    // 当 threadId 或分页参数变化时重新拉评论
+    useEffect(() => {
+        if (!threadId) return;
+
+        fetchComments();
+    }, [threadId, first, rows]);
+
+
+    //通过评论ID获取评论人信息
+    const getReplyUserName = (replyId: number) => {
+        if (replyId == null || replyId == 0) return '';
+        const replyComment = comments.find(comment => comment.commentId === replyId);
+        if (!replyComment?.userId) return '匿名用户';
+        return "回复 " + commentUserInfos.get(replyComment.userId)?.username || '匿名用户';
+    };
+
+    // 获取评论列表
+    const fetchComments = async () => {
+        try {
+            const page = first / rows + 1;
+            console.log("当前页" + page + "size" + rows);
+            const response = await axios.get<CommentList>(
+                `http://127.0.0.1:4523/m1/6387307-6083949-default/comments`, {
+                params: { threadId, page, rows }
+            }
+            );
+            console.log('获取评论列表:', response.data.records);
+            setComments(response.data.records);
+            // 拉取评论对应用户信息
+            response.data.records.forEach(comment => {
+                if (comment.userId != null && !commentUserInfos.has(comment.userId)) {
+                    axios.get<UserInfo>(
+                        `http://127.0.0.1:4523/m1/6387307-6083949-default/user/info`,
+                        { params: { userId: comment.userId } }
+                    ).then(res => {
+                        setCommentUserInfos(prev => new Map(prev).set(comment.userId!, res.data));
+                    });
+                }
+            });
+        } catch (err) {
+            console.error('获取评论失败', err);
+            toast.current?.show({ severity: 'error', summary: 'error', detail: '获取评论失败' });
+        }
+    };
+
+    // 回复评论接口
+    const publishReply = async (commentId: number) => {
+        if (!replyValue.trim() || !threadInfo) return;
+        console.log('发布评论:', commentId);
+        try {
+            const newComment: NewComment = {
+                userId: 22301145,
+                threadId: threadInfo.threadId,
+                resourceId: 0,
+                replyId: commentId,
+                content: commentValue,
+                createdAt: new Date().toISOString().slice(0, 19).replace('T', ' ')
+            };
+
+            const response = await axios.post('http://127.0.0.1:4523/m1/6387307-6083949-default/comment', newComment);
+
+            if (response.status === 200) {
+                toast.current?.show({ severity: 'success', summary: 'Success', detail: '回复成功' });
+                // 更新评论列表
+                fetchComments();
+                setVisibleReply(false)
+                // 清空输入框
+                setReplyValue('');
+            }
+        } catch (error) {
+            console.error('发布评论失败:', error);
+            toast.current?.show({ severity: 'error', summary: 'error', detail: '回复失败' });
+        }
+    };
+
+    // 发布评论接口
+    const publishComment = async () => {
+        if (!commentValue.trim() || !threadInfo) return;
+
+        try {
+            const newComment: NewComment = {
+                userId: 22301145,
+                threadId: threadInfo.threadId,
+                resourceId: 0,
+                replyId: 0, // 直接评论,不是回复
+                content: commentValue,
+                createdAt: new Date().toISOString().slice(0, 19).replace('T', ' ')
+            };
+
+            const response = await axios.post('http://127.0.0.1:4523/m1/6387307-6083949-default/comment', newComment);
+
+            if (response.status === 200) {
+                toast.current?.show({ severity: 'success', summary: 'Success', detail: '评论成功' });
+                // 更新评论列表
+                fetchComments();
+                // 清空输入框
+                setCommentValue('');
+            }
+        } catch (error) {
+            console.error('发布评论失败:', error);
+            toast.current?.show({ severity: 'error', summary: 'error', detail: '发布评论失败' });
+        }
+    };
+
+    // 删除评论接口
+    const deleteComment = async (commentId: number) => {
+        if (!threadInfo) return;
+
+        try {
+            // 调用 DELETE 接口,URL 中最后一段是要删除的 commentId
+            const response = await axios.delete(
+                `http://127.0.0.1:4523/m1/6387307-6083949-default/comment?commentId=${commentId}`
+            );
+
+            if (response.status === 200) {
+                fetchComments();
+                toast.current?.show({ severity: 'success', summary: 'Success', detail: '删除评论成功' });
+            } else {
+                toast.current?.show({ severity: 'error', summary: 'error', detail: '删除评论失败' });
+                console.error('删除评论失败,状态码:', response.status);
+            }
+        } catch (error) {
+            console.error('删除评论接口报错:', error);
+        }
+    };
+
+    const ReplyHeader = (
+        <div className="flex align-items-center gap-1">
+            <h3>回复评论</h3>
+        </div>
+    );
+    if (!threadInfo || !userInfo) return <div>Loading...</div>;
+    return (
+        <div className="thread-detail">
+            <Toast ref={toast}></Toast>
+            {/* 帖子头部 */}
+            <div className="thread-header">
+                <div className="user-info">
+                    <Avatar image={process.env.NEXT_PUBLIC_NGINX_URL + "users/" + userInfo.avatar} size="large" shape="circle" />
+                    <div className="user-meta">
+                        <h3>{userInfo.username}</h3>
+                        <span>{userInfo.signature}</span>
+                    </div>
+                </div>
+                <span className="post-time">{threadInfo.createdAt}</span>
+            </div>
+
+            {/* 帖子内容 */}
+            <div className="thread-content">
+                <h1>{threadInfo.title}</h1>
+                <div className="content-body">
+                    <Image
+                        src={process.env.NEXT_PUBLIC_NGINX_URL + threadInfo.threadPicture}
+                        alt={threadInfo.title}
+                        width="800"
+                        height="400"
+                        className="thread-image"
+                    />
+                    <p>{threadInfo.content}</p>
+                </div>
+                <div className="thread-actions">
+                    <Button
+                        icon={"pi pi-face-smile"}
+                        onClick={handleLike}
+                        className={`like-button ${threadInfo.isLike ? 'liked' : ''}`}
+                        label={threadInfo.likes.toString()}
+                    />
+                </div>
+            </div>
+            {/* 评论列表 */}
+            <div className="comments-section">
+                <div className="comments-header">
+                    <h2>评论 ({totalComments})</h2>
+                    <Button label="返回社区" link onClick={() => router.push(`/community/community-detail/${threadInfo.communityId}`)} />
+                </div>
+                <div className="comments-input">
+                    <Avatar image={process.env.NEXT_PUBLIC_NGINX_URL + "users/" + userInfo.avatar} size="large" shape="circle" />
+                    <InputText value={commentValue} placeholder="发布你的评论" onChange={(e) => setCommentValue(e.target.value)} />
+                    <Button label="发布评论" onClick={publishComment} disabled={!commentValue.trim()} />
+                </div>
+                <div className="comments-list">
+                    {comments.map((comment, index) => (
+                        <div key={comment.commentId} className="comment-item">
+                            <div className="comment-user">
+                                <Avatar
+                                    image={comment.userId ? process.env.NEXT_PUBLIC_NGINX_URL + "users/" + commentUserInfos.get(comment.userId)?.avatar : '/default-avatar.png'}
+                                    size="normal"
+                                    shape="circle"
+                                />
+                                <div className="comment-meta">
+                                    <span className="username">
+                                        {comment.userId ? commentUserInfos.get(comment.userId)?.username : '匿名用户'}
+                                    </span>
+                                    <div className="comment-time">
+                                        <span className="floor">#{index + 1}楼</span>
+                                        <span className="time">{comment.createdAt}</span>
+                                    </div>
+                                </div>
+                                <i className='pi pi-ellipsis-v' onClick={(e) => ops.current[index].toggle(e)} />
+                            </div>
+                            <div className="comment-content">
+                                {<span className="reply-to">{getReplyUserName(comment.replyId)}</span>}
+                                <p>{comment.content}</p>
+                            </div>
+                            <OverlayPanel       // 回调 ref:把实例放到 ops.current 对应的位置
+                                ref={el => {
+                                    if (el) ops.current[index] = el;
+                                }}>
+                                <Button label="回复" text size="small" onClick={() => setVisibleReply(true)} />
+                                {comment.userId === 22301145 &&
+                                    <Button
+                                        label="删除"
+                                        text
+                                        size="small"
+                                        onClick={() => { console.log('Deleting comment:', comment.commentId, 'by user:', comment.userId); deleteComment(comment.commentId) }}
+                                    />
+                                }
+                            </OverlayPanel>
+                            <Sidebar className='reply' header={ReplyHeader} visible={visibleReply} position="bottom" onHide={() => setVisibleReply(false)}>
+                                <div className="reply-input">
+                                    <Avatar image={process.env.NEXT_PUBLIC_NGINX_URL + "users/" + userInfo.avatar} size="large" shape="circle" />
+                                    <InputText value={replyValue} placeholder="发布你的评论" onChange={(e) => setReplyValue(e.target.value)} />
+                                    <Button label="发布评论" onClick={() => publishReply(comment.commentId)} disabled={!replyValue.trim()} />
+                                </div>
+                            </Sidebar>
+                        </div>
+                    ))}
+                    {totalComments > 5 && (<Paginator className="Paginator" first={first} rows={rows} totalRecords={totalComments} rowsPerPageOptions={[5, 10]} onPageChange={onPageChange} />)}
+                </div>
+            </div>
+        </div>
+    );
+}
\ No newline at end of file