import { useToLogin, useToUserProfile } from "@/hooks/use-route"; import { User } from "@/models/user"; import { useLocale, useTranslations } from "next-intl"; import { useState } from "react"; import { toast } from "sonner"; import GravatarAvatar, { getGravatarByUser } from "@/components/common/gravatar"; import { Reply, Trash, Heart, Pencil, Lock } from "lucide-react"; import { Comment } from "@/models/comment"; import { TargetType } from "@/models/types"; import { toggleLike } from "@/api/like"; import { useDoubleConfirm } from "@/hooks/use-double-confirm"; import { CommentInput } from "./comment-input"; import { createComment, deleteComment, listComments, updateComment } from "@/api/comment"; import { OrderBy } from "@/models/common"; import { formatDateTime } from "@/utils/common/datetime"; export function CommentItem( { user, comment, parentComment, onCommentDelete, activeInput, setActiveInputId, onReplySubmitted // 评论区计数更新用 }: { user: User | null, comment: Comment, parentComment: Comment | null, onCommentDelete: ({ commentId }: { commentId: number }) => void, activeInput: { id: number; type: 'reply' | 'edit' } | null, setActiveInputId: (input: { id: number; type: 'reply' | 'edit' } | null) => void, onReplySubmitted: ({ commentContent, isPrivate }: { commentContent: string, isPrivate: boolean }) => void, } ) { const locale = useLocale(); const t = useTranslations("Comment"); const commonT = useTranslations("Common"); const clickToUserProfile = useToUserProfile(); const clickToLogin = useToLogin(); const { confirming, onClick, onBlur } = useDoubleConfirm(); const [likeCount, setLikeCount] = useState(comment.likeCount); const [liked, setLiked] = useState(comment.isLiked); const [canClickLike, setCanClickLike] = useState(true); const [isPrivate, setIsPrivate] = useState(comment.isPrivate); const [replyCount, setReplyCount] = useState(comment.replyCount); const [showReplies, setShowReplies] = useState(false); const [replies, setReplies] = useState([]); const [repliesLoaded, setRepliesLoaded] = useState(false); const handleToggleLike = () => { if (!canClickLike) { return; } setCanClickLike(false); if (!user) { toast.error(t("login_required"), { action:
, }); return; } // 提前转换状态,让用户觉得响应很快 const likedPrev = liked; const likeCountPrev = likeCount; setLiked(prev => !prev); setLikeCount(prev => prev + (likedPrev ? -1 : 1)); toggleLike( { targetType: TargetType.Comment, targetId: comment.id } ).then(res => { toast.success(res.data.status ? t("like_success") : t("unlike_success")); setCanClickLike(true); }).catch(error => { toast.error(t("like_failed") + ": " + error.message); // 失败回滚 setLiked(likedPrev); setLikeCount(likeCountPrev); setCanClickLike(true); }); } const reloadReplies = () => { listComments( { targetType: comment.targetType, targetId: comment.targetId, depth: comment.depth + 1, orderBy: OrderBy.CreatedAt, desc: false, page: 1, size: 999999, commentId: comment.id } ).then(response => { setReplies(response.data.comments); setRepliesLoaded(true); }); } const toggleReplies = () => { if (!showReplies && !repliesLoaded) { reloadReplies(); } setShowReplies(!showReplies); } const onCommentEdit = ({ commentContent: newContent, isPrivate }: { commentContent: string, isPrivate: boolean }) => { updateComment({ id: comment.id, content: newContent, isPrivate }).then(() => { toast.success(t("edit_success")); comment.content = newContent; setIsPrivate(isPrivate); setActiveInputId(null); }).catch(error => { toast.error(t("edit_failed") + ": " + error.message); }); } const onReply = ({ commentContent, isPrivate }: { commentContent: string, isPrivate: boolean }) => { createComment({ targetType: comment.targetType, targetId: comment.targetId, content: commentContent, replyId: comment.id, isPrivate, }).then(() => { toast.success(t("comment_success")); reloadReplies(); setShowReplies(true); setActiveInputId(null); setReplyCount(replyCount + 1); onReplySubmitted({ commentContent, isPrivate }); }).catch(error => { toast.error(t("comment_failed") + ": " + error?.response?.data?.message || error?.message ); }); } const onReplyDelete = ({ commentId: replyId }: { commentId: number }) => { deleteComment({ id: replyId }).then(() => { toast.success(t("delete_success")); setReplyCount(replyCount - 1); setReplies(replies.filter(r => r.id !== replyId)); }).catch(error => { toast.error(t("delete_failed") + ": " + error.message); }); } return (
clickToUserProfile(comment.user.username)} className="cursor-pointer fade-in w-12 h-12">
clickToUserProfile(comment.user.username)} className="font-bold text-base text-slate-800 dark:text-slate-100 cursor-pointer fade-in-up"> {comment.user.nickname}
{formatDateTime({ dateTimeString: comment.createdAt, locale, convertShortAgo: true, unitI18n: { secondsAgo: commonT("secondsAgo"), minutesAgo: commonT("minutesAgo"), hoursAgo: commonT("hoursAgo"), daysAgo: commonT("daysAgo") } })} {comment.createdAt !== comment.updatedAt && {t("edit_at", { time: formatDateTime({ dateTimeString: comment.updatedAt, locale, convertShortAgo: true, unitI18n: { secondsAgo: commonT("secondsAgo"), minutesAgo: commonT("minutesAgo"), hoursAgo: commonT("hoursAgo"), daysAgo: commonT("daysAgo") } }) })}}

{ isPrivate && } { parentComment && <>{t("reply")} : } {comment.content}

{/* 点赞按钮 */} {/* 回复按钮 */} {/* 编辑和删除按钮 仅自己的评论可见 */} {user?.id === comment.user.id && ( <> )} {replyCount > 0 && }
{/* 这俩输入框一次只能显示一个 */} {activeInput && activeInput.type === 'reply' && activeInput.id === comment.id && } {activeInput && activeInput.type === 'edit' && activeInput.id === comment.id && }
{showReplies && replies.length > 0 && (
{replies.map((reply) => ( ))}
)}
) }