@@ -232,11 +232,13 @@ export default function RightPanel({
|
|||||||
<div className={`aside-panel-inner${isPostDetail ? ' aside-panel-inner--post-detail' : ''}`}>
|
<div className={`aside-panel-inner${isPostDetail ? ' aside-panel-inner--post-detail' : ''}`}>
|
||||||
{isPostDetail && (
|
{isPostDetail && (
|
||||||
<>
|
<>
|
||||||
|
{postDetail.author?.id ? (
|
||||||
<PostAuthorCard
|
<PostAuthorCard
|
||||||
author={postDetail.author}
|
author={postDetail.author}
|
||||||
publishedAt={postDetail.publishedAt}
|
publishedAt={postDetail.publishedAt}
|
||||||
viewCount={postDetail.viewCount}
|
viewCount={postDetail.viewCount}
|
||||||
/>
|
/>
|
||||||
|
) : null}
|
||||||
<div className="widget-card widget-card--outline">
|
<div className="widget-card widget-card--outline">
|
||||||
<div className="widget-card-head">
|
<div className="widget-card-head">
|
||||||
<ListTree className="widget-card-icon widget-card-icon--outline" aria-hidden />
|
<ListTree className="widget-card-icon widget-card-icon--outline" aria-hidden />
|
||||||
|
|||||||
@@ -148,7 +148,10 @@ export default function MainLayout() {
|
|||||||
setSidebarOpen(false);
|
setSidebarOpen(false);
|
||||||
}, [loc.pathname, loc.search]);
|
}, [loc.pathname, loc.search]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!/^\/post\/\d+/.test(loc.pathname)) setPostOutline(null);
|
const isArticleAside =
|
||||||
|
(/^\/post\/\d+/.test(loc.pathname) && !/\/edit$/.test(loc.pathname))
|
||||||
|
|| /^\/page\//.test(loc.pathname);
|
||||||
|
if (!isArticleAside) setPostOutline(null);
|
||||||
}, [loc.pathname]);
|
}, [loc.pathname]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hideAside) setAsideOpen(false);
|
if (!hideAside) setAsideOpen(false);
|
||||||
@@ -477,6 +480,10 @@ export default function MainLayout() {
|
|||||||
const activeChipIndex = Math.max(0, boardChipIds.indexOf(mobileActiveBoard === -1 ? 0 : mobileActiveBoard));
|
const activeChipIndex = Math.max(0, boardChipIds.indexOf(mobileActiveBoard === -1 ? 0 : mobileActiveBoard));
|
||||||
|
|
||||||
const isPostDetail = /^\/post\/\d+/.test(loc.pathname) && !/\/edit$/.test(loc.pathname);
|
const isPostDetail = /^\/post\/\d+/.test(loc.pathname) && !/\/edit$/.test(loc.pathname);
|
||||||
|
const isSitePage = /^\/page\//.test(loc.pathname);
|
||||||
|
const isArticleAside = isPostDetail || isSitePage;
|
||||||
|
// 帖子有作者 →「作者与目录」;单页仅目录
|
||||||
|
const articleAsideLabel = isPostDetail ? '作者与目录' : '文章目录';
|
||||||
const setPostOutlineSafe = useCallback((outline: {
|
const setPostOutlineSafe = useCallback((outline: {
|
||||||
headings: PostHeading[];
|
headings: PostHeading[];
|
||||||
scrollRoot: HTMLElement | null;
|
scrollRoot: HTMLElement | null;
|
||||||
@@ -632,10 +639,10 @@ export default function MainLayout() {
|
|||||||
type="button"
|
type="button"
|
||||||
className="header-icon-btn"
|
className="header-icon-btn"
|
||||||
onClick={openAside}
|
onClick={openAside}
|
||||||
aria-label={isPostDetail ? '打开作者与目录' : '打开社区动态'}
|
aria-label={isArticleAside ? `打开${articleAsideLabel}` : '打开社区动态'}
|
||||||
aria-expanded={asideOpen}
|
aria-expanded={asideOpen}
|
||||||
aria-controls="aside-drawer"
|
aria-controls="aside-drawer"
|
||||||
title={isPostDetail ? '作者与目录' : '社区动态'}
|
title={isArticleAside ? articleAsideLabel : '社区动态'}
|
||||||
>
|
>
|
||||||
<PanelRight size={18} aria-hidden />
|
<PanelRight size={18} aria-hidden />
|
||||||
</button>
|
</button>
|
||||||
@@ -815,7 +822,7 @@ export default function MainLayout() {
|
|||||||
asideWidgets={asideWidgets}
|
asideWidgets={asideWidgets}
|
||||||
onPostClick={openPost}
|
onPostClick={openPost}
|
||||||
|
|
||||||
postDetail={isPostDetail ? {
|
postDetail={isArticleAside ? {
|
||||||
author: postOutline?.author ?? null,
|
author: postOutline?.author ?? null,
|
||||||
publishedAt: postOutline?.publishedAt,
|
publishedAt: postOutline?.publishedAt,
|
||||||
viewCount: postOutline?.viewCount,
|
viewCount: postOutline?.viewCount,
|
||||||
@@ -879,7 +886,7 @@ export default function MainLayout() {
|
|||||||
onClick={() => { closeSidebar(); openAside(); }}
|
onClick={() => { closeSidebar(); openAside(); }}
|
||||||
>
|
>
|
||||||
<PanelRight size={16} aria-hidden />
|
<PanelRight size={16} aria-hidden />
|
||||||
{isPostDetail ? '作者与目录' : '社区动态'}
|
{isArticleAside ? articleAsideLabel : '社区动态'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -910,10 +917,10 @@ export default function MainLayout() {
|
|||||||
className="aside-drawer"
|
className="aside-drawer"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-label={isPostDetail ? '作者与目录' : '社区动态'}
|
aria-label={isArticleAside ? articleAsideLabel : '社区动态'}
|
||||||
>
|
>
|
||||||
<div className="aside-drawer-head">
|
<div className="aside-drawer-head">
|
||||||
<span>{isPostDetail ? '作者与目录' : '社区动态'}</span>
|
<span>{isArticleAside ? articleAsideLabel : '社区动态'}</span>
|
||||||
<button
|
<button
|
||||||
ref={asideCloseRef}
|
ref={asideCloseRef}
|
||||||
type="button"
|
type="button"
|
||||||
@@ -934,7 +941,7 @@ export default function MainLayout() {
|
|||||||
loading={asideLoading}
|
loading={asideLoading}
|
||||||
asideWidgets={asideWidgets}
|
asideWidgets={asideWidgets}
|
||||||
onPostClick={openPost}
|
onPostClick={openPost}
|
||||||
postDetail={isPostDetail ? {
|
postDetail={isArticleAside ? {
|
||||||
author: postOutline?.author ?? null,
|
author: postOutline?.author ?? null,
|
||||||
publishedAt: postOutline?.publishedAt,
|
publishedAt: postOutline?.publishedAt,
|
||||||
viewCount: postOutline?.viewCount,
|
viewCount: postOutline?.viewCount,
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { useParams } from 'react-router-dom';
|
import { useEffect, useMemo, useRef } from 'react';
|
||||||
|
import { useOutletContext, useParams } from 'react-router-dom';
|
||||||
import NotFoundPage from './NotFoundPage';
|
import NotFoundPage from './NotFoundPage';
|
||||||
import { api } from '../api/client';
|
import { api } from '../api/client';
|
||||||
import type { SitePage } from '../api/types';
|
import type { SitePage } from '../api/types';
|
||||||
|
import ArticleOutline from '../components/ArticleOutline';
|
||||||
import PostContent from '../components/PostContent';
|
import PostContent from '../components/PostContent';
|
||||||
import { usePageSEO } from '../hooks/usePageSEO';
|
import { usePageSEO } from '../hooks/usePageSEO';
|
||||||
import { parsePermalinkSlug, pagePath } from '../utils/permalink';
|
import { parsePermalinkSlug, pagePath } from '../utils/permalink';
|
||||||
import { useForumLimits } from '../hooks/useForumLimits';
|
import { useForumLimits } from '../hooks/useForumLimits';
|
||||||
import { useAuth } from '../hooks/useAuth';
|
import { useAuth } from '../hooks/useAuth';
|
||||||
import { useSessionResource } from '../hooks/useSessionResource';
|
import { useSessionResource } from '../hooks/useSessionResource';
|
||||||
|
import type { LayoutCtx } from '../layouts/MainLayout';
|
||||||
|
import { extractHeadingsFromHtml } from '../utils/postHeadings';
|
||||||
|
import { renderPostContentHtml } from '../utils/postContent';
|
||||||
|
|
||||||
/** 自定义单页(关于我们、版规等) */
|
/** 自定义单页(关于我们、版规等) */
|
||||||
export default function SitePageView() {
|
export default function SitePageView() {
|
||||||
@@ -15,12 +20,25 @@ export default function SitePageView() {
|
|||||||
const slug = parsePermalinkSlug(rawSlug);
|
const slug = parsePermalinkSlug(rawSlug);
|
||||||
const { limits } = useForumLimits();
|
const { limits } = useForumLimits();
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
const { setPostOutline, isMobile } = useOutletContext<LayoutCtx>();
|
||||||
|
const pageRef = useRef<HTMLDivElement>(null);
|
||||||
const { data: page, loading } = useSessionResource<SitePage | null>(
|
const { data: page, loading } = useSessionResource<SitePage | null>(
|
||||||
slug ? `sitepage:${slug}` : null,
|
slug ? `sitepage:${slug}` : null,
|
||||||
() => api.page(slug).then(d => d.page),
|
() => api.page(slug).then(d => d.page),
|
||||||
{ enabled: !!slug },
|
{ enabled: !!slug },
|
||||||
);
|
);
|
||||||
const notFound = !slug || (!loading && !page);
|
const notFound = !slug || (!loading && !page);
|
||||||
|
const isLoggedIn = !!user;
|
||||||
|
const pageContent = page?.content ?? '';
|
||||||
|
|
||||||
|
// 同步从正文派生目录,与帖子详情共用右侧栏目录布局
|
||||||
|
const headings = useMemo(() => {
|
||||||
|
if (!pageContent.trim()) return [];
|
||||||
|
const rendered = renderPostContentHtml(pageContent, isLoggedIn, {
|
||||||
|
openLinksInNewTab: limits.open_content_links_in_new_tab,
|
||||||
|
});
|
||||||
|
return extractHeadingsFromHtml(rendered);
|
||||||
|
}, [pageContent, isLoggedIn, limits.open_content_links_in_new_tab]);
|
||||||
|
|
||||||
usePageSEO({
|
usePageSEO({
|
||||||
title: page?.title,
|
title: page?.title,
|
||||||
@@ -29,19 +47,42 @@ export default function SitePageView() {
|
|||||||
ogType: 'article',
|
ogType: 'article',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (loading || !page) {
|
||||||
|
setPostOutline({ headings: [], scrollRoot: null, title: '文章目录' });
|
||||||
|
return () => setPostOutline(null);
|
||||||
|
}
|
||||||
|
setPostOutline({
|
||||||
|
headings,
|
||||||
|
scrollRoot: pageRef.current,
|
||||||
|
title: '文章目录',
|
||||||
|
});
|
||||||
|
return () => setPostOutline(null);
|
||||||
|
}, [headings, loading, page, setPostOutline]);
|
||||||
|
|
||||||
if (!slug) return <NotFoundPage title="页面不存在" />;
|
if (!slug) return <NotFoundPage title="页面不存在" />;
|
||||||
if (loading) return null;
|
if (loading) return null;
|
||||||
if (notFound || !page) return <NotFoundPage title="页面不存在" description="该页面不存在或未发布" />;
|
if (notFound || !page) return <NotFoundPage title="页面不存在" description="该页面不存在或未发布" />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-wrap">
|
<div className="page-wrap" ref={pageRef}>
|
||||||
<article className="site-page">
|
<article className="site-page">
|
||||||
<header className="site-page__head">
|
<header className="site-page__head">
|
||||||
<h1>{page.title}</h1>
|
<h1>{page.title}</h1>
|
||||||
</header>
|
</header>
|
||||||
|
{isMobile && headings.length > 0 && (
|
||||||
|
<details className="post-detail-toc-mobile">
|
||||||
|
<summary>文章目录({headings.length})</summary>
|
||||||
|
<ArticleOutline
|
||||||
|
headings={headings}
|
||||||
|
scrollRoot={pageRef.current}
|
||||||
|
title="目录"
|
||||||
|
/>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
<PostContent
|
<PostContent
|
||||||
html={page.content}
|
html={page.content}
|
||||||
isLoggedIn={!!user}
|
isLoggedIn={isLoggedIn}
|
||||||
className="site-page__body post-detail-content"
|
className="site-page__body post-detail-content"
|
||||||
/>
|
/>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
Reference in New Issue
Block a user