import { useState, useEffect } from 'react'; import { Check, Clock, History, MessageSquare, X, Pencil, Trash2 } from 'lucide-react'; import type { ReactNode } from 'react'; import type { Comment, User } from '../api/types'; import CommentContent from './CommentContent'; import CommentRevisionDialog from './CommentRevisionDialog'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from '@/components/ui/alert-dialog'; import { commentNick, commentInitial, formatCommentDate, isGuestComment, buildCommentTree, type CommentNode, } from '../utils/comment'; import { isTimeDiffSignificant } from '../utils/content'; import { useForumLimits } from '../hooks/useForumLimits'; import UserLink from './UserLink'; function isCommentAuthor(c: Comment, user?: User | null): boolean { return !!user && c.user_id > 0 && c.user_id === user.id; } function canEditComment(c: Comment, user: User | null | undefined, windowHours: number): boolean { if (!user) return false; if (user.role === 'admin') return true; if (!isCommentAuthor(c, user)) return false; if (windowHours <= 0) return true; const created = new Date(c.created_at).getTime(); if (Number.isNaN(created)) return false; return Date.now() - created <= windowHours * 3600_000; } interface ItemProps { node: CommentNode; nested?: boolean; highlightFloor?: number | null; replyToId?: number | null; editingId?: number | null; currentUser?: User | null; onReply: (comment: Comment) => void; onCancelReply: () => void; onStartEdit: (comment: Comment) => void; onCancelEdit: () => void; onSaveEdit: (comment: Comment, content: string) => Promise; onDelete: (comment: Comment) => Promise; onApprove?: (comment: Comment) => Promise; renderReplyBox?: (comment: Comment) => ReactNode; } /** 单条评论(支持嵌套子回复 + 内联回复框 + 编辑/删除) */ function CommentItem({ node, nested, highlightFloor, replyToId, editingId, currentUser, onReply, onCancelReply, onStartEdit, onCancelEdit, onSaveEdit, onDelete, onApprove, renderReplyBox, }: ItemProps) { const { limits } = useForumLimits(); const c = node.comment; const nick = commentNick(c); const guest = isGuestComment(c); const isHighlighted = highlightFloor === c.floor; const hidden = !!c.content_hidden; const isReplying = replyToId === c.id; const isEditing = editingId === c.id; const isAdmin = currentUser?.role === 'admin'; const canEdit = canEditComment(c, currentUser, limits.comment_edit_window_hours ?? 24); const canDelete = isAdmin; const canApprove = isAdmin && (c.status === 'pending' || c.status === 'rejected') && !!onApprove; const showEdited = !hidden && !!c.updated_at && isTimeDiffSignificant(c.created_at, c.updated_at); const [editText, setEditText] = useState(c.content); const [saving, setSaving] = useState(false); const [deleting, setDeleting] = useState(false); const [approving, setApproving] = useState(false); const [revOpen, setRevOpen] = useState(false); useEffect(() => { if (isEditing) setEditText(c.content); }, [isEditing, c.content, c.id]); const handleSave = async () => { const next = editText.trim(); if (!next) return; setSaving(true); try { await onSaveEdit(c, next); } finally { setSaving(false); } }; return (
{!guest && c.user_id ? ( {c.user?.avatar ? ( ) : ( commentInitial(c) )} ) : (
{c.user?.avatar ? ( ) : ( commentInitial(c) )}
)}
{guest && c.guest_url ? ( {nick} ) : !guest && c.user_id ? ( ) : ( {nick} )}
{hidden ? (
该评论为私密评论,仅文章作者与评论发起者可见!
) : isEditing ? (