"use client"; import { useRef, useState } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { Trash2, MessagesSquare, ChevronRight, ChevronDown } from "lucide-react"; import ReactMarkdown from "react-markdown"; import { apiCreateComment, apiDeleteComment, type CommentNode, type User } from "@/lib/api"; import { canModerateBoard } from "@/lib/roles"; import { toast } from "@/lib/toast"; import Avatar from "./Avatar"; import Pagination from "./Pagination"; interface CommentSectionProps { postId: string; /** 帖子所属板块:板块管理员仅可管理授权板块的评论 */ boardId: number; comments: CommentNode[]; total: number; // 楼层数(主评论数)→ 分页与楼层号计算 totalComments: number; // 全部评论数(含回复)→ 头部徽标 page: number; size: number; // SSR 直出的登录用户(layout 已取,避免客户端再请求一次 /api/me) user: User | null; } // 删除按钮的两段式确认:点一下进入确认态,3 秒未确认自动复原 const CONFIRM_RESET_MS = 3000; // 递归统计子树回复数(删除级联提示用) function countReplies(node: CommentNode): number { return node.replies?.reduce((acc, r) => acc + 1 + countReplies(r), 0) ?? 0; } export default function CommentSection({ postId, boardId, comments, total, totalComments, page, size, user, }: CommentSectionProps) { const router = useRouter(); const [content, setContent] = useState(""); // 内联回复目标:在该评论下方就地发子评论(同一时间只有一个回复框) const [replyTarget, setReplyTarget] = useState<{ id: number; nickname: string } | null>(null); const [replyContent, setReplyContent] = useState(""); const [submitting, setSubmitting] = useState(false); const [deletingId, setDeletingId] = useState(null); const [confirmingId, setConfirmingId] = useState(null); // 折叠状态:哪些楼层展开了子回复(默认全部折叠,SSR 初始为收起) const [expandedFloors, setExpandedFloors] = useState>(new Set()); const textareaRef = useRef(null); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!content.trim()) return; setSubmitting(true); try { const res = await apiCreateComment(postId, content); if (res.comment) { setContent(""); if (res.comment.status === "pending") { toast("已提交,等待审核通过后公开"); } else { // 新楼层在最后一页:刷新后由服务端直出最新分页 router.refresh(); } } else if (res.error === "未登录") { router.push(`/login?redirect=${encodeURIComponent(`/post/${postId}`)}`); } else { toast(res.error || "评论失败"); } } catch { toast("网络错误,评论失败"); } finally { setSubmitting(false); } }; const handleSubmitReply = async (e: React.FormEvent) => { e.preventDefault(); if (!replyTarget || !replyContent.trim()) return; setSubmitting(true); try { const res = await apiCreateComment(postId, replyContent, replyTarget.id); if (res.comment) { setReplyTarget(null); setReplyContent(""); if (res.comment.status === "pending") { toast("已提交,等待审核通过后公开"); } else { // 刷新后由 SSR 直出新树(子评论全量挂在本楼) router.refresh(); } } else if (res.error === "未登录") { router.push(`/login?redirect=${encodeURIComponent(`/post/${postId}`)}`); } else { toast(res.error || "回复失败"); } } catch { toast("网络错误,回复失败"); } finally { setSubmitting(false); } }; const handleDelete = async (commentId: number) => { if (confirmingId !== commentId) { setConfirmingId(commentId); setTimeout(() => { setConfirmingId((cur) => (cur === commentId ? null : cur)); }, CONFIRM_RESET_MS); return; } setConfirmingId(null); setDeletingId(commentId); try { const res = await apiDeleteComment(postId, commentId); if (res.message) { router.refresh(); } else { toast(res.error || "删除失败"); } } catch { toast("网络错误,删除失败"); } finally { setDeletingId(null); } }; // 引用功能已移除:与"回复"功能重叠,且会割裂讨论上下文(抖音风格无此功能) const canDelete = (comment: CommentNode) => !!user && (user.id === comment.user.id || canModerateBoard(user, boardId)); const openReply = (c: CommentNode) => { setReplyTarget({ id: c.id, nickname: c.user.nickname || c.user.username }); setReplyContent(""); }; // 切换某楼层子回复的展开/折叠 const toggleFloor = (floorId: number) => { setExpandedFloors((prev) => { const next = new Set(prev); if (next.has(floorId)) next.delete(floorId); else next.add(floorId); return next; }); }; const totalPages = Math.max(1, Math.ceil(total / size)); // 内联回复表单(出现在目标评论正下方) const inlineReplyForm = replyTarget && (
回复 @{replyTarget.nickname}