diff --git a/frontend/src/components/PostContent.tsx b/frontend/src/components/PostContent.tsx index 28604ac..a8e8986 100644 --- a/frontend/src/components/PostContent.tsx +++ b/frontend/src/components/PostContent.tsx @@ -1,7 +1,6 @@ -import { useMemo, useCallback, useEffect, useState } from 'react'; +import { useMemo, useCallback, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { renderPostContentHtml } from '../utils/postContent'; -import { extractHeadingsFromHtml, type PostHeading } from '../utils/postHeadings'; import { loginPath, registerPath } from '../utils/authRedirect'; import { useForumLimits } from '../hooks/useForumLimits'; import { notify } from '@/lib/notify'; @@ -12,8 +11,6 @@ interface Props { html: string; isLoggedIn: boolean; className?: string; - /** 正文标题树变化时回调(用于侧栏目录) */ - onHeadingsChange?: (headings: PostHeading[]) => void; /** 点击「回复可见」门控的「去回复」 */ onRequestReply?: () => void; /** 积分解锁成功后刷新正文 */ @@ -26,7 +23,6 @@ export default function PostContent({ html, isLoggedIn, className = 'post-detail-content', - onHeadingsChange, onRequestReply, onUnlocked, postId: postIdProp, @@ -39,19 +35,12 @@ export default function PostContent({ const [lightboxAlt, setLightboxAlt] = useState(''); const [unlocking, setUnlocking] = useState(false); - const prepared = useMemo(() => { - const rendered = renderPostContentHtml(html, isLoggedIn, { + const preparedHtml = useMemo( + () => renderPostContentHtml(html, isLoggedIn, { openLinksInNewTab: limits.open_content_links_in_new_tab, - }); - return { - html: rendered, - headings: extractHeadingsFromHtml(rendered), - }; - }, [html, isLoggedIn, limits.open_content_links_in_new_tab]); - - useEffect(() => { - onHeadingsChange?.(prepared.headings); - }, [prepared.headings, onHeadingsChange]); + }), + [html, isLoggedIn, limits.open_content_links_in_new_tab], + ); const openLightbox = useCallback((img: HTMLImageElement) => { const full = img.getAttribute('data-full') || img.currentSrc || img.src; @@ -167,7 +156,7 @@ export default function PostContent({ className={className} onClick={handleClick} onKeyDown={handleKeyDown} - dangerouslySetInnerHTML={{ __html: prepared.html }} + dangerouslySetInnerHTML={{ __html: preparedHtml }} /> ([]); const [reportOpen, setReportOpen] = useState(false); const [reportReason, setReportReason] = useState('spam'); const [reportDetail, setReportDetail] = useState(''); @@ -173,6 +173,15 @@ export default function PostDetailPage() { const brand = getCachedSiteBranding(); const postContent = post?.content ?? ''; + const isLoggedIn = !!user; + // 同步从正文派生目录,避免预取缓存命中后 setHeadings([]) 盖掉子组件上报且不再回调 + const headings = useMemo(() => { + if (!postContent.trim()) return []; + const rendered = renderPostContentHtml(postContent, isLoggedIn, { + openLinksInNewTab: limits.open_content_links_in_new_tab, + }); + return extractHeadingsFromHtml(rendered); + }, [postContent, isLoggedIn, limits.open_content_links_in_new_tab]); const postSEO = post ? { title: post.title, description: excerptFromHTML(postContent), @@ -196,10 +205,6 @@ export default function PostDetailPage() { } : null; usePageSEO(postSEO); - const handleHeadingsChange = useCallback((next: PostHeading[]) => { - setHeadings(next); - }, []); - useEffect(() => { if (loading || !post) { setPostOutline({ headings: [], scrollRoot: null, title: '文章目录' }); @@ -263,7 +268,6 @@ export default function PostDetailPage() { setReplyTo(null); setEditingCommentId(null); setComposerOpen(false); - setHeadings([]); setLoading(false); return; } @@ -272,7 +276,6 @@ export default function PostDetailPage() { setReplyTo(null); setEditingCommentId(null); setComposerOpen(false); - setHeadings([]); if (!keep) { setLoading(true); setPost(null); @@ -1124,9 +1127,8 @@ export default function PostDetailPage() { { void reloadPostContent(); }} />