feat(ui): 全站画布氛围层、侧栏 sticky 对齐与长页回顶

用顶栏/主栏高度令牌消除 sticky 首滚空走,面包屑叠到中栏上方,公开页增加回顶浮钮。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-18 00:28:25 +08:00
parent 0b60c4299b
commit 1535577eac
21 changed files with 286 additions and 111 deletions

View File

@@ -28,7 +28,6 @@ export default async function AdminLayout({ children }: { children: React.ReactN
id="main"
tabIndex={-1}
className="min-h-screen flex items-center justify-center px-4 outline-none"
style={{ background: "var(--bg)" }}
>
<div className="max-w-md w-full panel p-8 text-center">
<span

View File

@@ -103,6 +103,15 @@ export default async function AnnouncementPage({ params }: PageProps) {
<SiteDocShell
left={left}
right={right}
breadcrumb={
<SiteDocBreadcrumb
items={[
{ href: "/", label: "首页" },
{ href: "/announcements", label: "站点公告" },
{ label: a.title },
]}
/>
}
mobileExtras={
<>
{toc.length > 0 ? <PostToc items={toc} /> : null}
@@ -111,14 +120,6 @@ export default async function AnnouncementPage({ params }: PageProps) {
</>
}
>
<SiteDocBreadcrumb
items={[
{ href: "/", label: "首页" },
{ href: "/announcements", label: "站点公告" },
{ label: a.title },
]}
/>
<article className="panel p-6 sm:p-8">
<SiteDocMasthead
kicker="站点公告"

View File

@@ -132,15 +132,16 @@ export default async function AnnouncementsListPage({ searchParams }: PageProps)
<SiteDocShell
left={left}
right={right}
breadcrumb={
<SiteDocBreadcrumb
items={[
{ href: "/", label: "首页" },
{ label: "站点公告" },
]}
/>
}
mobileExtras={<SiteDocPageNav pages={sidebarPages} title="关于本站" />}
>
<SiteDocBreadcrumb
items={[
{ href: "/", label: "首页" },
{ label: "站点公告" },
]}
/>
<header className="mb-5">
<h1
className="text-[24px] sm:text-[28px] font-extrabold tracking-tight flex items-center gap-2"

View File

@@ -42,7 +42,7 @@ function ComposeInner() {
export default function ComposePage() {
return (
<div className="j13-compose-page">
<nav className="meta mb-4 flex items-center gap-1.5 text-[13px] shrink-0">
<nav className="meta -mt-4 sm:-mt-6 mb-1.5 flex items-center gap-1 text-[12.5px] leading-none shrink-0">
<Link href="/" className="hover:text-[var(--accent)] transition-colors">
首页
</Link>

View File

@@ -56,6 +56,14 @@
--r: 18px;
--r-sm: 12px;
/* 顶栏 68px 条 + 1px 底边;main 桌面 py-9。侧栏 sticky 必须等于
栏在 scrollY=0 时的视口 top,否则第一下滚动会先空走一截再吸住 */
--header-bar: 68px;
--header-h: calc(var(--header-bar) + 1px);
--main-pad-y: 2.25rem;
--rail-top: calc(var(--header-h) + var(--main-pad-y));
--rail-max-h: calc(100vh - var(--rail-top) - 1rem);
color-scheme: light;
}
@@ -103,6 +111,7 @@
html {
scrollbar-color: var(--line-2) transparent;
background-color: var(--bg); /* 橡皮筋 overscroll 与画布同色 */
}
body {
@@ -113,6 +122,59 @@ body {
-webkit-font-smoothing: auto;
text-rendering: optimizeLegibility;
font-synthesis: none; /* 禁止伪粗,避免缺档字重糊成一团 */
isolation: isolate; /* 让 z-index:-1 的画布层停在内容之下、html 之上 */
}
/* 浏览器最后一层画布:氛围光 + 石墨微粒。
纯 CSS(令牌 + 内联 SVG),不跟位图——跟浅/暗/accent 换肤,无限缩放,无额外请求。 */
body::before {
content: "";
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
background-image:
radial-gradient(ellipse 80% 52% at 12% -18%, color-mix(in srgb, var(--accent) 10%, transparent), transparent 62%),
radial-gradient(ellipse 48% 36% at 100% 0%, color-mix(in srgb, var(--ink) 4%, transparent), transparent 58%),
radial-gradient(ellipse 42% 28% at 78% 100%, color-mix(in srgb, var(--accent) 6%, transparent), transparent 60%),
linear-gradient(180deg, color-mix(in srgb, var(--panel) 40%, transparent) 0%, transparent 32%);
}
body::after {
content: "";
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
opacity: 0.032;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 160 160' width='160' height='160'%3E%3Cfilter id='n' x='0' y='0' width='100%25' height='100%25'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.8' numOctaves='4' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
background-size: 160px 160px;
}
.dark body::before {
background-image:
radial-gradient(ellipse 80% 52% at 12% -18%, color-mix(in srgb, var(--accent) 16%, transparent), transparent 62%),
radial-gradient(ellipse 48% 36% at 100% 0%, color-mix(in srgb, var(--ink) 6%, transparent), transparent 58%),
radial-gradient(ellipse 42% 28% at 78% 100%, color-mix(in srgb, var(--accent) 9%, transparent), transparent 60%),
linear-gradient(180deg, color-mix(in srgb, var(--panel) 22%, transparent) 0%, transparent 36%);
}
.dark body::after {
opacity: 0.05;
}
@media print {
body::before,
body::after {
display: none;
}
}
@media (prefers-contrast: more) {
body::before,
body::after {
display: none;
}
}
/* 全站悬停即显提示气泡(配合 TipHost + data-tip,替代原生 title 延迟) */
@@ -2319,7 +2381,6 @@ body:has([data-admin-viewport]) {
height: 100dvh;
max-height: 100dvh;
overflow: hidden;
background: var(--bg);
}
.admin-shell {
@@ -2328,7 +2389,7 @@ body:has([data-admin-viewport]) {
max-height: 100dvh;
min-height: 0;
overflow: hidden;
background: var(--bg);
background: transparent; /* 透出全站画布层 */
}
.admin-rail {
@@ -5136,6 +5197,58 @@ html.dark .prose-content pre[data-theme] span {
font-size: 13px;
}
/* 长文回顶:右下角浮钮 */
.j13-back-top {
position: fixed;
right: 1rem;
bottom: 1.25rem;
z-index: 45;
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.75rem;
height: 2.75rem;
border-radius: 999px;
border: 1px solid var(--line-2);
background: color-mix(in srgb, var(--panel) 92%, transparent);
color: var(--ink-2);
box-shadow: var(--shadow-lg);
backdrop-filter: blur(14px);
-webkit-backdrop-filter: blur(14px);
cursor: pointer;
transition: color 0.15s ease, background 0.15s ease, border-color 0.15s ease;
}
.j13-back-top:hover,
.j13-back-top:focus-visible {
color: var(--accent);
background: var(--accent-soft);
border-color: color-mix(in srgb, var(--accent) 28%, var(--line-2));
outline: none;
}
/* 帖子移动端底栏上方抬高,避免遮挡 */
.j13-back-top.is-lifted {
bottom: 5.25rem;
}
@media (min-width: 1024px) {
.j13-back-top.is-lifted {
bottom: 1.25rem;
}
}
@media (min-width: 640px) {
.j13-back-top {
right: 1.35rem;
bottom: 1.5rem;
}
.j13-back-top.is-lifted {
bottom: 5.25rem;
}
}
@media (min-width: 1024px) {
.j13-back-top.is-lifted {
bottom: 1.5rem;
}
}
/* 评论楼层树:抖音风格
主评论全宽;子回复折叠在 panel-2 背景容器内,点击"展开 X 条回复"显示
子回复之间用淡分隔线;depth≥2 时昵称后显示 ▶ 被回复者 */

View File

@@ -32,7 +32,7 @@ export default async function NotificationsPage({ searchParams }: NotificationsP
return (
<div className="max-w-2xl mx-auto">
<nav className="meta mb-5 flex items-center gap-1.5 text-[13px]">
<nav className="meta -mt-4 sm:-mt-6 mb-1.5 flex items-center gap-1 text-[12.5px] leading-none">
<Link href="/" className="hover:text-[var(--accent)] transition-colors">首页</Link>
<ChevronRight size={13} />
<span style={{ color: "var(--accent)" }}>通知中心</span>

View File

@@ -97,6 +97,15 @@ export default async function SitePagePublic({ params }: PageProps) {
<SiteDocShell
left={left}
right={right}
breadcrumb={
<SiteDocBreadcrumb
items={[
{ href: "/", label: "首页" },
{ label: "关于本站" },
{ label: p.title },
]}
/>
}
mobileExtras={
<>
{toc.length > 0 ? <PostToc items={toc} /> : null}
@@ -107,14 +116,6 @@ export default async function SitePagePublic({ params }: PageProps) {
</>
}
>
<SiteDocBreadcrumb
items={[
{ href: "/", label: "首页" },
{ label: "关于本站" },
{ label: p.title },
]}
/>
<article className="panel p-6 sm:p-8">
<SiteDocMasthead
kicker="官方文件"

View File

@@ -17,6 +17,7 @@ import PostRow from "@/components/PostRow";
import FeedTabs, { type FeedSort } from "@/components/home/FeedTabs";
import FeedNewTip from "@/components/home/FeedNewTip";
import { WorkspaceNav, WorkspaceNavMobile } from "@/components/home/WorkspaceNav";
import { SiteDocBreadcrumb } from "@/components/site-doc";
import { CommunityPanels, CommunityPanelsMobile } from "@/components/home/CommunityPanels";
import {
BoardPanels,
@@ -166,11 +167,21 @@ export default async function HomePage({ searchParams }: HomePageProps) {
<div>
<WorkspaceNavMobile boards={boards} user={user} />
<div className="flex items-start gap-6 xl:gap-7 mt-5 lg:mt-0">
<div className={`flex items-start gap-6 xl:gap-7 ${activeBoard ? "" : "mt-5 lg:mt-0"}`}>
<WorkspaceNav boards={boards} user={user} />
{/* 中栏:tab 栏与帖子流为同一个整体卡片(左栏切板块只替换这一列) */}
<div className="flex-1 min-w-0">
<div className="flex-1 min-w-0 relative">
{activeBoard ? (
<div className="mt-4 lg:mt-0">
<SiteDocBreadcrumb
items={[
{ href: "/", label: "首页" },
{ label: activeBoard.name },
]}
/>
</div>
) : null}
<h1 className="sr-only">{heading}</h1>
<div className="panel overflow-hidden">

View File

@@ -42,7 +42,7 @@ export default function PointsClient() {
return (
<div className="max-w-3xl mx-auto">
<nav className="meta mb-5 flex items-center gap-1.5 text-[13px]">
<nav className="meta -mt-4 sm:-mt-6 mb-1.5 flex items-center gap-1 text-[12.5px] leading-none">
<Link href="/" className="hover:text-[var(--accent)] transition-colors">
首页
</Link>

View File

@@ -39,7 +39,7 @@ export default async function EditPostPage({ params }: PageProps) {
data-compose-viewport
className="j13-compose-page w-full"
>
<nav className="meta mb-4 flex items-center gap-1.5 flex-wrap text-[13px] shrink-0">
<nav className="meta -mt-4 sm:-mt-6 mb-1.5 flex items-center gap-1 flex-wrap text-[12.5px] leading-none shrink-0">
<Link href="/" className="hover:text-[var(--accent)] transition-colors">
首页
</Link>

View File

@@ -30,6 +30,7 @@ import CommentSection from "@/components/CommentSection";
import PostActions from "@/components/PostActions";
import LikeButton from "@/components/LikeButton";
import PostToc from "@/components/PostToc";
import { SiteDocBreadcrumb } from "@/components/site-doc";
import MobilePostBar from "@/components/MobilePostBar";
import Avatar from "@/components/Avatar";
import RoleBadge from "@/components/RoleBadge";
@@ -557,7 +558,7 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
return (
<>
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
{/* 移动端浮动操作条:长文阅读中途可点赞 / 跳评论 / 回顶部 */}
{/* 移动端浮动操作条:长文读到中途可点赞 / 跳评论框 */}
<MobilePostBar
postId={id}
liked={post.liked}
@@ -571,7 +572,7 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
{/* sticky 必须直接挂在 grid 子项上:aside 作为 grid item,其可粘行程
等于行高(正文高度);内层包裹会被 items-start 缩成内容高而失去行程 */}
<aside
className="hidden lg:flex w-56 xl:w-60 shrink-0 sticky top-24 flex-col gap-4 max-h-[calc(100vh_-_7rem)] overflow-hidden"
className="hidden lg:flex w-56 xl:w-60 shrink-0 sticky top-[var(--rail-top)] flex-col gap-4 max-h-[var(--rail-max-h)] overflow-hidden"
aria-label="帖子导航"
>
<BoardCard board={post.board} />
@@ -580,16 +581,14 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
</aside>
{/* 中栏:正文主体 */}
<div className="min-w-0">
<div className="min-w-0 relative">
<SiteDocBreadcrumb
items={[
{ href: "/", label: "首页" },
{ href: `/?board=${post.board.id}`, label: post.board.name },
]}
/>
<article className="w-full">
<nav className="meta mb-5 flex items-center gap-1.5 flex-wrap text-[13px]">
<Link href="/" className="hover:text-[var(--accent)] transition-colors">首页</Link>
<ChevronRight size={13} />
<Link href={`/?board=${post.board.id}`} className="hover:text-[var(--accent)] transition-colors">
{post.board.name}
</Link>
</nav>
<div className="panel p-6 sm:p-8">
<header className="mb-7">
<div className="flex items-center gap-2 mb-4 flex-wrap">
@@ -810,7 +809,7 @@ export default async function PostDetailPage({ params, searchParams }: PageProps
{/* 右栏:目录 / 同板块热议(阅读数据已并入标题下元信息条) */}
<aside
className="hidden lg:flex w-[280px] xl:w-[300px] shrink-0 sticky top-24 flex-col gap-4 max-h-[calc(100vh_-_7rem)] overflow-hidden"
className="hidden lg:flex w-[280px] xl:w-[300px] shrink-0 sticky top-[var(--rail-top)] flex-col gap-4 max-h-[var(--rail-max-h)] overflow-hidden"
aria-label="阅读辅助"
>
{toc.length > 0 && <PostToc items={toc} />}

View File

@@ -121,7 +121,7 @@ export default async function UserPage({ params, searchParams }: UserPageProps)
return (
<div>
<nav className="meta mb-5 flex items-center gap-1.5 text-[13px]">
<nav className="meta -mt-4 sm:-mt-6 mb-1.5 flex items-center gap-1 text-[12.5px] leading-none">
<Link href="/" className="hover:text-[var(--accent)] transition-colors">首页</Link>
<ChevronRight size={13} />
<span style={{ color: "var(--accent)" }}>{u.nickname || u.username}</span>
@@ -276,7 +276,7 @@ export default async function UserPage({ params, searchParams }: UserPageProps)
{/* ═══ 三栏:左导航 / 中内容 / 右信息 ═══ */}
<div className="grid grid-cols-1 items-start lg:grid-cols-[224px_minmax(0,1fr)_280px] xl:grid-cols-[240px_minmax(0,1fr)_300px] gap-x-6 xl:gap-x-7">
{/* 左栏:Tab 导航 */}
<aside className="hidden lg:flex w-56 xl:w-60 shrink-0 sticky top-24 flex-col gap-4">
<aside className="hidden lg:flex w-56 xl:w-60 shrink-0 sticky top-[var(--rail-top)] flex-col gap-4">
<section className="j13-module !p-2">
<p
className="px-2.5 pt-1.5 pb-2 text-[11.5px] font-bold tracking-[0.08em] uppercase"
@@ -341,7 +341,7 @@ export default async function UserPage({ params, searchParams }: UserPageProps)
</div>
{/* 右栏:账号信息 + 积分 + 安全提示 */}
<aside className="hidden lg:flex w-[280px] xl:w-[300px] shrink-0 sticky top-24 flex-col gap-4 max-h-[calc(100vh_-_7rem)] overflow-hidden">
<aside className="hidden lg:flex w-[280px] xl:w-[300px] shrink-0 sticky top-[var(--rail-top)] flex-col gap-4 max-h-[var(--rail-max-h)] overflow-hidden">
<section className="j13-module">
<p className="j13-module-title mb-3">
<Hash size={15} style={{ color: "var(--accent)" }} /> 账号信息