import { X } from "lucide-react"; import { useState, useEffect, useRef, useCallback } from "react"; import type { Comment, User } from "../types.ts"; import { CommentList } from "./CommentList"; interface CommentPanelProps { open: boolean; onClose: () => void; author: User; createdAt: string | Date; awemeId: string; mounted: boolean; } export function CommentPanel({ open, onClose, author, createdAt, awemeId, mounted }: CommentPanelProps) { const [comments, setComments] = useState([]); const [total, setTotal] = useState(0); const [loading, setLoading] = useState(false); const [hasMore, setHasMore] = useState(true); // ranked 排序的稳定参数(由后端返回,前端透传保证会话内稳定) const [rankParams, setRankParams] = useState(null); // 两套滚动容器与哨兵,分别对应横屏与竖屏面板 const scrollRefLandscape = useRef(null); const sentinelRefLandscape = useRef(null); const scrollRefPortrait = useRef(null); const sentinelRefPortrait = useRef(null); const loadingRef = useRef(false); // 加载评论 const loadComments = useCallback(async (reset = false) => { if (loadingRef.current || (!reset && !hasMore)) return; loadingRef.current = true; setLoading(true); try { const skip = reset ? 0 : comments.length; const query = new URLSearchParams({ skip: String(skip), take: String(20), mode: 'ranked', }); if (rankParams) { query.set('seed', rankParams.seed); query.set('snapshot', rankParams.snapshot); } const response = await fetch(`/api/comments/${awemeId}?${query.toString()}`); const data = await response.json(); // 统一做一次基于 cid 的去重,避免分页偶发重复 if (reset) { setComments(() => { const seen = new Set(); return (data.comments as Comment[]).filter((c) => { if (seen.has(c.cid)) return false; seen.add(c.cid); return true; }); }); } else { setComments((prev) => { const merged = [...prev, ...(data.comments as Comment[])]; const seen = new Set(); // 保留首次出现的项,既保证顺序也避免重复 return merged.filter((c) => { if (seen.has(c.cid)) return false; seen.add(c.cid); return true; }); }); } setTotal(data.total); setHasMore(data.hasMore); if (data.mode === 'ranked' && data.seed && data.snapshot) { // 初始化或重置时更新稳定参数 setRankParams((prev) => { if (reset) return { seed: String(data.seed), snapshot: String(data.snapshot) }; return prev ?? { seed: String(data.seed), snapshot: String(data.snapshot) }; }); } else if (reset) { setRankParams(null); } } catch (error) { console.error("加载评论失败:", error); } finally { setLoading(false); loadingRef.current = false; } }, [awemeId, comments.length, hasMore]); // 面板打开时加载初始评论 useEffect(() => { if (open && comments.length === 0) { loadComments(true); } }, [open]); // Intersection Observer 监听底部触发加载(针对横屏与竖屏两个容器分别监听) useEffect(() => { if (!open) return; const observers: IntersectionObserver[] = []; const setup = (rootEl: HTMLDivElement | null, targetEl: HTMLDivElement | null) => { if (!rootEl || !targetEl) return; const io = new IntersectionObserver( (entries) => { const entry = entries[0]; if (entry.isIntersecting && !loadingRef.current && hasMore) { loadComments(); } }, { root: rootEl, rootMargin: '0px 0px 200px 0px', // 距底部 200px 触发 threshold: 0, } ); io.observe(targetEl); observers.push(io); }; setup(scrollRefLandscape.current, sentinelRefLandscape.current); setup(scrollRefPortrait.current, sentinelRefPortrait.current); return () => { for (const io of observers) io.disconnect(); }; }, [open, hasMore, loadComments]); return ( <> {/* 横屏评论面板:并排分栏 */} {/* 竖屏评论面板:bottom sheet */} ); }