243 lines
8.8 KiB
Plaintext
243 lines
8.8 KiB
Plaintext
---
|
|
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<Locale, string>;
|
|
|
|
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;
|
|
---
|
|
|
|
<!doctype html>
|
|
<html lang={locale} class="dark">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<meta name="color-scheme" content="light dark" />
|
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
<link
|
|
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Noto+Sans+SC:wght@400;500;600;700;900&family=Space+Grotesk:wght@500;600;700&display=swap"
|
|
rel="stylesheet"
|
|
/>
|
|
<link rel="alternate" type="text/plain" href="/llms.txt" title="llms.txt" />
|
|
<link rel="alternate" hreflang="zh-CN" href={`${siteSeo.siteUrl}/zh-CN`} />
|
|
<link rel="alternate" hreflang="en" href={`${siteSeo.siteUrl}/en`} />
|
|
<link rel="alternate" hreflang="x-default" href={`${siteSeo.siteUrl}/zh-CN`} />
|
|
<title set:html={pageTitle} />
|
|
<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>
|