--- import { ClientRouter } from "astro:transitions"; import "../styles/globals.css"; import UserMenu from "~/components/UserMenu"; import ThemeSwitch from "~/components/ThemeSwitch"; import LocaleSwitch from "~/components/LocaleSwitch"; import { buildDefaultJsonLd, buildHeadTags, renderHeadTagsHtml, toJsonLdScript, type PageSeoInput, } from "@app/seo-geo"; import { alternatePath, COOKIE_LANG, COOKIE_LANG_MAX_AGE, LOCALES, type Locale, localePath, t, } from "@app/i18n"; import { getSiteSeo } from "~/lib/seo"; export interface Props { title: string; locale: Locale; brand?: string; /** 当前页,用于侧栏高亮 */ active?: "home" | "library" | "browse" | "mounts" | "danmaku"; seo?: PageSeoInput; /** 播放页隐藏模板页脚 */ hideFooter?: boolean; } const { title, locale, brand, active, seo, hideFooter = false } = Astro.props; /** 语言下拉:各语言在当前页的 URL */ const localeHrefs = Object.fromEntries( LOCALES.map((code) => [code, alternatePath(Astro.url.pathname, code)]), ) as Record; const brandName = brand ?? t(locale, "meta.brand"); const siteSeo = getSiteSeo(locale); const pageSeoInput: PageSeoInput = { ...(seo ?? {}), title: seo?.title ?? title, path: seo?.path ?? Astro.url.pathname, }; const headTags = buildHeadTags(siteSeo, pageSeoInput); const headHtml = renderHeadTagsHtml(headTags); const jsonLdHtml = toJsonLdScript(buildDefaultJsonLd(siteSeo, pageSeoInput)); const pageTitle = headTags.find((tTag) => tTag.key === "og:title")?.value ?? title; const homeHref = localePath(locale, "/"); const brandHref = homeHref; const brandInitial = brandName.charAt(0) || "弹"; /** 全站统一侧栏导航 */ const nav = [ { id: "home", n: "00", label: t(locale, "nav.home"), href: homeHref }, { id: "library", n: "01", label: t(locale, "nav.library"), href: localePath(locale, "/library") }, { id: "browse", n: "02", label: t(locale, "nav.browse"), href: localePath(locale, "/browse") }, { id: "mounts", n: "03", label: t(locale, "nav.mounts"), href: localePath(locale, "/mounts") }, { id: "danmaku", n: "04", label: t(locale, "nav.danmaku"), href: localePath(locale, "/danmaku") }, ] as const; --- <Fragment set:html={headHtml} /> <Fragment set:html={jsonLdHtml} /> <ClientRouter /> {/* ClientRouter onPopState 在 history.state 为 null 时会吞掉前进/后退,见 history-guard */} <script> import "@/lib/history-guard"; </script> <script is:inline define:vars={{ cookieName: COOKIE_LANG, maxAge: COOKIE_LANG_MAX_AGE, nextLocale: locale === "zh-CN" ? "en" : "zh-CN" }}> (function () { function apply(mode) { var dark = mode === "dark" || (mode !== "light" && window.matchMedia("(prefers-color-scheme: dark)").matches); document.documentElement.classList.toggle("dark", dark); document.documentElement.classList.toggle("light", !dark); document.documentElement.style.colorScheme = dark ? "dark" : "light"; } function applyFromStorage() { var mode = "system"; try { var t = localStorage.getItem("app-theme"); if (t === "light" || t === "dark" || t === "system") mode = t; } catch (e) {} apply(mode); } applyFromStorage(); // ClientRouter 软导航后 <html> class 会被新页覆盖,需按偏好重放主题 document.addEventListener("astro:after-swap", applyFromStorage); if (!window.__fanmuChromeInit) { window.__fanmuChromeInit = true; window .matchMedia("(prefers-color-scheme: dark)") .addEventListener("change", function () { try { if ((localStorage.getItem("app-theme") || "system") === "system") { apply("system"); } } catch (e) { apply("system"); } }); // 语言切换:写偏好 cookie 后由 <a> 跳转(捕获阶段) document.addEventListener( "click", function (e) { var el = e.target && e.target.closest ? e.target.closest("[data-switch-locale]") : null; if (!el) return; var target = el.getAttribute("data-switch-locale") || nextLocale; document.cookie = cookieName + "=" + target + "; path=/; max-age=" + maxAge + "; SameSite=Lax"; }, true, ); } })(); </script> </head> <body> <div aria-hidden="true" class="bg-base pointer-events-none fixed inset-0 z-0"></div> <div aria-hidden="true" class="u-grid-field pointer-events-none fixed inset-0 z-0" style="opacity: 0.18;" > </div> <div aria-hidden="true" class="bg-glow pointer-events-none fixed inset-0 z-0"></div> <div aria-hidden="true" class="bg-vignette pointer-events-none fixed inset-0 z-0"></div> { <aside class="side-rail" aria-label={t(locale, "nav.primary")}> <a href={brandHref} class="side-rail__brand" aria-label={brandName}> <span class="brand-signal flex size-8 items-center justify-center text-[12px] font-bold" style="background: var(--edge); color: #101110;" > {brandInitial} </span> <span class="side-rail__brand-text">{brandName}</span> </a> <div class="side-rail__rule" aria-hidden="true"></div> <nav class="side-rail__nav"> {nav.map((item) => ( <a href={item.href} class:list={["side-rail__link", { "is-active": active === item.id }]} aria-current={active === item.id ? "page" : undefined} > <span class="u-mono">{item.n}</span> <span>{item.label}</span> </a> ))} </nav> <div class="side-rail__rule" aria-hidden="true"></div> <div class="side-rail__foot"> <ThemeSwitch client:only="react" /> </div> </aside> } <div class="site-shell"> <header class="site-header"> <div class="site-header__inner"> <a href={brandHref} class="site-header__brand lg:hidden" aria-label={brandName}> <span class="brand-signal flex size-7 items-center justify-center text-[11px] font-bold" style="background: var(--edge); color: #101110;" > {brandInitial} </span> <span class="text-[14px] font-semibold tracking-[0.04em]">{brandName}</span> </a> <nav class="site-header__nav" aria-label={t(locale, "nav.primary")}> { nav.map((item) => ( <a href={item.href} class:list={["u-linelink", { "is-active": active === item.id }]} aria-current={active === item.id ? "page" : undefined} > <span class="u-mono">{item.n}</span> <span>{item.label}</span> </a> )) } </nav> <div class="site-header__actions"> <LocaleSwitch client:only="react" locale={locale} hrefs={localeHrefs} /> <span class="lg:hidden"> <ThemeSwitch client:only="react" /> </span> <UserMenu client:only="react" locale={locale} /> </div> </div> <div class="site-header__dash" aria-hidden="true"></div> </header> <main class="page-main"> <slot /> </main> {!hideFooter && ( <footer class="site-footer"> <div class="u-dotline" aria-hidden="true"></div> <div class="site-footer__inner"> <p class="u-mono">{brandName}</p> <p class="u-label">{t(locale, "home.footer")}</p> </div> </footer> )} </div> </body> </html>