blob: 2189402b9d7c8902be75ef7f85647a69e9f9d00f [file] [log] [blame]
'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>
);
}