Files
fuwari/src/layouts/Layout.astro
二叉树树 27206a7a9b refactor: (性能优化)使友链页面在桌面端Chromium系浏览器不再卡成史
移除所有图片加载时的进度条动画效果,简化DOM结构和CSS样式
默认使用深色主题,优化主题加载逻辑
清理未使用的代码和注释
2026-01-22 18:20:53 +08:00

637 lines
20 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
import "@fontsource/roboto/400.css";
import "@fontsource/roboto/500.css";
import "@fontsource/roboto/700.css";
import { profileConfig, siteConfig } from "@/config";
import ConfigCarrier from "@components/ConfigCarrier.astro";
import {
AUTO_MODE,
BANNER_HEIGHT,
BANNER_HEIGHT_EXTEND,
BANNER_HEIGHT_HOME,
DARK_MODE,
DEFAULT_THEME,
LIGHT_MODE,
PAGE_WIDTH,
} from "../constants/constants";
import { defaultFavicons } from "../constants/icon";
import type { Favicon } from "../types/config";
import { url, pathsEqual } from "../utils/url-utils";
import "katex/dist/katex.css";
interface Props {
title?: string;
banner?: string;
description?: string;
lang?: string;
setOGTypeArticle?: boolean;
}
let { title, banner, description, lang, setOGTypeArticle } = Astro.props;
// apply a class to the body element to decide the height of the banner, only used for initial page load
// Swup can update the body for each page visit, but it's after the page transition, causing a delay for banner height change
// so use Swup hooks instead to change the height immediately when a link is clicked
const isHomePage = pathsEqual(Astro.url.pathname, url("/"));
// defines global css variables
// why doing this in Layout instead of GlobalStyles: https://github.com/withastro/astro/issues/6728#issuecomment-1502203757
const configHue = siteConfig.themeColor.hue;
if (!banner || typeof banner !== "string" || banner.trim() === "") {
banner = siteConfig.banner.src;
}
// TODO don't use post cover as banner for now
banner = siteConfig.banner.src;
const enableBanner = siteConfig.banner.enable;
let pageTitle: string;
if (title) {
pageTitle = `${title} - ${siteConfig.title}`;
} else {
if (siteConfig.subtitle && siteConfig.subtitle.trim() !== "") {
pageTitle = `${siteConfig.title} - ${siteConfig.subtitle}`;
} else {
pageTitle = siteConfig.title;
}
}
// Use siteConfig.description as fallback for meta description when no description is provided
if (!description) {
description = siteConfig.description || pageTitle;
}
const favicons: Favicon[] =
siteConfig.favicon.length > 0 ? siteConfig.favicon : defaultFavicons;
// const siteLang = siteConfig.lang.replace('_', '-')
if (!lang) {
lang = `${siteConfig.lang}`;
}
const siteLang = lang.replace("_", "-");
const bannerOffsetByPosition = {
top: `${BANNER_HEIGHT_EXTEND}vh`,
center: `${BANNER_HEIGHT_EXTEND / 2}vh`,
bottom: "0",
};
const bannerOffset =
bannerOffsetByPosition[siteConfig.banner.position || "center"];
---
<!DOCTYPE html>
<html lang={siteLang} class="bg-[var(--page-bg)] transition text-[12px] md:text-[16px] dark"
data-overlayscrollbars-initialize> <!-- 手机端适配 -->
<head>
<title>{pageTitle}</title>
<meta charset="UTF-8" />
<meta name="description" content={description}>
<meta name="author" content={profileConfig.name}>
{siteConfig.keywords && (
<meta name="keywords" content={siteConfig.keywords.join(", ")}>
)}
<meta property="og:site_name" content={siteConfig.title}>
<meta property="og:url" content={Astro.url}>
<meta property="og:title" content={pageTitle}>
<meta property="og:description" content={description}>
{setOGTypeArticle ? (
<meta property="og:type" content="article" />
) : (
<meta property="og:type" content="website" />
)}
<meta name="twitter:card" content="summary_large_image">
<meta property="twitter:url" content={Astro.url}>
<meta name="twitter:title" content={pageTitle}>
<meta name="twitter:description" content={description}>
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<script src="/js/umami-share.js" defer></script>
<script data-swup-ignore-script src="https://pic1.acofork.com/random.js" defer></script>
{/* <!-- Content Security Policy -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' https://giscus.app https://hpic.072103.xyz https://umami.2x.nz https://hm.baidu.com https://www.googletagmanager.com https://www.google-analytics.com https://pagead2.googlesyndication.com https://googleads.g.doubleclick.net https://static.cloudflareinsights.com https://*.adtrafficquality.google; style-src 'self' 'unsafe-inline' https://giscus.app https://fonts.googleapis.com https://api.iconify.design; font-src 'self' https://fonts.gstatic.com https://api.iconify.design; img-src 'self' data: https: http:; connect-src 'self' https://umami.2x.nz https://hm.baidu.com https://www.google-analytics.com https://analytics.google.com https://api.iconify.design https://static.cloudflareinsights.com https://pic.2x.nz https://q2.qlogo.cn https://ep1.adtrafficquality.google https://googleads.g.doubleclick.net; frame-src 'self' https://giscus.app *.bilibili.com https://www.google.com https://googleads.g.doubleclick.net https://support.nodeget.com https://*.adtrafficquality.google; object-src 'none'; base-uri 'self'; form-action 'self';"> */}
{favicons.map(favicon => (
<link rel="icon"
href={favicon.src.startsWith('/') ? url(favicon.src) : favicon.src}
sizes={favicon.sizes}
media={favicon.theme && `(prefers-color-scheme: ${favicon.theme})`}
/>
))}
<!-- Set the theme before the page is rendered to avoid a flash -->
<script is:inline define:vars={{DEFAULT_THEME, LIGHT_MODE, DARK_MODE, AUTO_MODE, BANNER_HEIGHT_EXTEND, PAGE_WIDTH, configHue, forceDarkMode: siteConfig.themeColor.forceDarkMode}}>
// Force dark mode if configured
if (forceDarkMode) {
document.documentElement.classList.add('dark');
localStorage.setItem('theme', DARK_MODE);
} else {
// Load the theme from local storage
const theme = localStorage.getItem('theme') || DARK_MODE;
switch (theme) {
case LIGHT_MODE:
document.documentElement.classList.remove('dark');
break
case DARK_MODE:
document.documentElement.classList.add('dark');
break
case AUTO_MODE:
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
}
// Load the hue from local storage
const hue = localStorage.getItem('hue') || configHue;
document.documentElement.style.setProperty('--hue', hue);
// calculate the --banner-height-extend, which needs to be a multiple of 4 to avoid blurry text
let offset = Math.floor(window.innerHeight * (BANNER_HEIGHT_EXTEND / 100));
offset = offset - offset % 4;
document.documentElement.style.setProperty('--banner-height-extend', `${offset}px`);
</script>
<style define:vars={{ configHue }}>
/* Fallback for no-js: Apply default theme hue */
:root {
--hue: var(--configHue);
}
</style>
<style define:vars={{
configHue,
'page-width': `${PAGE_WIDTH}rem`,
'bg-url': siteConfig.background.src ? `url(${siteConfig.background.src})` : 'none',
'bg-enable': siteConfig.background.enable ? '1' : '0',
'bg-position': siteConfig.background.position || 'center',
'bg-size': siteConfig.background.size || 'cover',
'bg-repeat': siteConfig.background.repeat || 'no-repeat',
'bg-attachment': siteConfig.background.attachment || 'fixed',
'bg-opacity': (siteConfig.background.opacity || 0.3).toString()
}}>
:root {
--bg-url: var(--bg-url);
--bg-enable: var(--bg-enable);
--bg-position: var(--bg-position);
--bg-size: var(--bg-size);
--bg-repeat: var(--bg-repeat);
--bg-attachment: var(--bg-attachment);
--bg-opacity: var(--bg-opacity);
}
/* Background image configuration */
body {
--bg-url: var(--bg-url);
--bg-enable: var(--bg-enable);
--bg-position: var(--bg-position);
--bg-size: var(--bg-size);
--bg-repeat: var(--bg-repeat);
--bg-attachment: var(--bg-attachment);
--bg-opacity: var(--bg-opacity);
}
#bg-box {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-position: var(--bg-position);
background-size: var(--bg-size);
background-repeat: var(--bg-repeat);
background-attachment: var(--bg-attachment);
opacity: 0;
pointer-events: none;
z-index: -1;
transition: opacity 0.3s ease-in-out;
}
#bg-box.loaded {
opacity: calc(var(--bg-opacity) * var(--bg-enable));
}
</style>
<slot name="head"></slot>
<link rel="alternate" type="application/rss+xml" title={profileConfig.name} href={`${Astro.site}rss.xml`}/>
<!-- Umami分析自建 -->
<script defer src="https://umami.acofork.com/script.js" data-website-id="5d710dbd-3a2e-43e3-a553-97b415090c63" data-swup-ignore-script></script>
</head>
<body class=" min-h-screen transition " class:list={[{"lg:is-home": isHomePage, "enable-banner": enableBanner}]}
data-overlayscrollbars-initialize
>
<div id="bg-box"></div>
<ConfigCarrier></ConfigCarrier>
<slot />
<!-- increase the page height during page transition to prevent the scrolling animation from jumping -->
<div id="page-height-extend" class="hidden h-[300vh]"></div>
</body>
</html>
<style is:global define:vars={{
bannerOffset,
'banner-height-home': `${BANNER_HEIGHT_HOME}vh`,
'banner-height': `${BANNER_HEIGHT}vh`,
}}>
@tailwind components;
@layer components {
.enable-banner.is-home #banner-wrapper {
@apply h-[var(--banner-height-home)] translate-y-[var(--banner-height-extend)]
}
.enable-banner #banner-wrapper {
@apply h-[var(--banner-height-home)]
}
.enable-banner.is-home #banner {
@apply h-[var(--banner-height-home)] translate-y-0
}
.enable-banner #banner {
@apply h-[var(--banner-height-home)] translate-y-[var(--bannerOffset)]
}
.enable-banner.is-home #main-grid {
@apply translate-y-[var(--banner-height-extend)];
}
.enable-banner #top-row {
@apply h-[calc(var(--banner-height-home)_-_4.5rem)] transition-all duration-300
}
.enable-banner.is-home #sidebar-sticky {
@apply top-[calc(1rem_-_var(--banner-height-extend))]
}
.navbar-hidden {
@apply opacity-0 -translate-y-16
}
}
</style>
<script>
import 'overlayscrollbars/overlayscrollbars.css';
import {
OverlayScrollbars,
// ScrollbarsHidingPlugin,
// SizeObserverPlugin,
// ClickScrollPlugin
} from 'overlayscrollbars';
import {getHue, getStoredTheme, setHue, setTheme, getBgBlur, setBgBlur, getHideBg, setHideBg} from "../utils/setting-utils";
import {pathsEqual, url} from "../utils/url-utils";
import {
BANNER_HEIGHT,
BANNER_HEIGHT_HOME,
BANNER_HEIGHT_EXTEND,
MAIN_PANEL_OVERLAPS_BANNER_HEIGHT
} from "../constants/constants";
import { siteConfig } from '../config';
/* Preload fonts */
// (async function() {
// try {
// await Promise.all([
// document.fonts.load("400 1em Roboto"),
// document.fonts.load("700 1em Roboto"),
// ]);
// document.body.classList.remove("hidden");
// } catch (error) {
// console.log("Failed to load fonts:", error);
// }
// })();
/* TODO This is a temporary solution for style flicker issue when the transition is activated */
/* issue link: https://github.com/withastro/astro/issues/8711, the solution get from here too */
/* update: fixed in Astro 3.2.4 */
/*
function disableAnimation() {
const css = document.createElement('style')
css.appendChild(
document.createTextNode(
`*{
-webkit-transition:none!important;
-moz-transition:none!important;
-o-transition:none!important;
-ms-transition:none!important;
transition:none!important
}`
)
)
document.head.appendChild(css)
return () => {
// Force restyle
;(() => window.getComputedStyle(document.body))()
// Wait for next tick before removing
setTimeout(() => {
document.head.removeChild(css)
}, 1)
}
}
*/
const bannerEnabled = !!document.getElementById('banner-wrapper')
function setClickOutsideToClose(panel: string, ignores: string[]) {
document.addEventListener("click", event => {
let panelDom = document.getElementById(panel);
let tDom = event.target;
if (!(tDom instanceof Node)) return; // Ensure the event target is an HTML Node
for (let ig of ignores) {
let ie = document.getElementById(ig)
if (ie == tDom || (ie?.contains(tDom))) {
return;
}
}
panelDom!.classList.add("float-panel-closed");
});
}
setClickOutsideToClose("display-setting", ["display-setting", "display-settings-switch"])
setClickOutsideToClose("nav-menu-panel", ["nav-menu-panel", "nav-menu-switch"])
setClickOutsideToClose("search-panel", ["search-panel", "search-bar", "search-switch"])
function loadTheme() {
const theme = getStoredTheme()
setTheme(theme)
}
function loadHue() {
setHue(getHue())
}
function loadBgBlur() {
setBgBlur(getBgBlur())
setHideBg(getHideBg())
}
function initCustomScrollbar() {
const bodyElement = document.querySelector('body');
if (!bodyElement) return;
OverlayScrollbars(
// docs say that a initialization to the body element would affect native functionality like window.scrollTo
// but just leave it here for now
{
target: bodyElement,
cancel: {
nativeScrollbarsOverlaid: true, // don't initialize the overlay scrollbar if there is a native one
}
}, {
scrollbars: {
theme: 'scrollbar-base scrollbar-auto py-1',
autoHide: 'move',
autoHideDelay: 500,
autoHideSuspend: false,
},
});
const katexElements = document.querySelectorAll('.katex-display') as NodeListOf<HTMLElement>;
katexElements.forEach((ele) => {
OverlayScrollbars(ele, {
scrollbars: {
theme: 'scrollbar-base scrollbar-auto py-1',
}
});
});
}
function showBanner() {
if (!siteConfig.banner.enable) return;
const banner = document.getElementById('banner');
if (!banner) {
console.error('Banner element not found');
return;
}
banner.classList.remove('opacity-0', 'scale-105');
}
function init() {
// disableAnimation()() // TODO
loadTheme();
loadHue();
loadBgBlur();
initCustomScrollbar();
showBanner();
}
/* Load settings when entering the site */
init();
const setup = () => {
// TODO: temp solution to change the height of the banner
/*
window.swup.hooks.on('animation:out:start', () => {
const path = window.location.pathname
const body = document.querySelector('body')
if (path[path.length - 1] === '/' && !body.classList.contains('is-home')) {
body.classList.add('is-home')
} else if (path[path.length - 1] !== '/' && body.classList.contains('is-home')) {
body.classList.remove('is-home')
}
})
*/
window.swup.hooks.on('link:click', () => {
// Remove the delay for the first time page load
document.documentElement.style.setProperty('--content-delay', '0ms')
// prevent elements from overlapping the navbar
if (!bannerEnabled) {
return
}
let threshold = window.innerHeight * (BANNER_HEIGHT / 100) - 72 - 16
let navbar = document.getElementById('navbar-wrapper')
if (!navbar || !document.body.classList.contains('lg:is-home')) {
return
}
if (document.body.scrollTop >= threshold || document.documentElement.scrollTop >= threshold) {
navbar.classList.add('navbar-hidden')
}
})
window.swup.hooks.on('content:replace', initCustomScrollbar)
window.swup.hooks.on('visit:start', (visit: {to: {url: string}}) => {
// change banner height immediately when a link is clicked
const bodyElement = document.querySelector('body')
if (pathsEqual(visit.to.url, url('/'))) {
bodyElement!.classList.add('lg:is-home');
} else {
bodyElement!.classList.remove('lg:is-home');
}
// increase the page height during page transition to prevent the scrolling animation from jumping
const heightExtend = document.getElementById('page-height-extend')
if (heightExtend) {
heightExtend.classList.remove('hidden')
}
// Hide the TOC while scrolling back to top
let toc = document.getElementById('toc-wrapper');
if (toc) {
toc.classList.add('toc-not-ready')
}
});
window.swup.hooks.on('page:view', () => {
// hide the temp high element when the transition is done
const heightExtend = document.getElementById('page-height-extend')
if (heightExtend) {
heightExtend.classList.remove('hidden')
}
});
window.swup.hooks.on('visit:end', (_visit: {to: {url: string}}) => {
setTimeout(() => {
const heightExtend = document.getElementById('page-height-extend')
if (heightExtend) {
heightExtend.classList.add('hidden')
}
// Just make the transition looks better
const toc = document.getElementById('toc-wrapper');
if (toc) {
toc.classList.remove('toc-not-ready')
}
}, 200)
});
}
if (window?.swup?.hooks) {
setup()
} else {
document.addEventListener('swup:enable', setup)
}
let backToTopBtn = document.getElementById('back-to-top-btn');
let goToCommentsBtn = document.getElementById('go-to-comments-btn');
let toc = document.getElementById('toc-wrapper');
let navbar = document.getElementById('navbar-wrapper')
function scrollFunction() {
let bannerHeight = window.innerHeight * (BANNER_HEIGHT / 100)
if (backToTopBtn) {
if (document.body.scrollTop > bannerHeight || document.documentElement.scrollTop > bannerHeight) {
backToTopBtn.classList.remove('hide')
} else {
backToTopBtn.classList.add('hide')
}
}
if (goToCommentsBtn) {
// Only show if comments exist and scrolled down
const commentsExist = !!document.getElementById('giscus-container');
if (commentsExist && (document.body.scrollTop > bannerHeight || document.documentElement.scrollTop > bannerHeight)) {
goToCommentsBtn.classList.remove('hide')
} else {
goToCommentsBtn.classList.add('hide')
}
}
if (bannerEnabled && toc) {
if (document.body.scrollTop > bannerHeight || document.documentElement.scrollTop > bannerHeight) {
toc.classList.remove('toc-hide')
} else {
toc.classList.add('toc-hide')
}
}
if (!bannerEnabled) return
if (navbar) {
const NAVBAR_HEIGHT = 72
const MAIN_PANEL_EXCESS_HEIGHT = MAIN_PANEL_OVERLAPS_BANNER_HEIGHT * 16 // The height the main panel overlaps the banner
let bannerHeight = BANNER_HEIGHT
if (document.body.classList.contains('lg:is-home') && window.innerWidth >= 1024) {
bannerHeight = BANNER_HEIGHT_HOME
}
let threshold = window.innerHeight * (bannerHeight / 100) - NAVBAR_HEIGHT - MAIN_PANEL_EXCESS_HEIGHT - 16
if (document.body.scrollTop >= threshold || document.documentElement.scrollTop >= threshold) {
navbar.classList.add('navbar-hidden')
} else {
navbar.classList.remove('navbar-hidden')
}
}
}
window.onscroll = scrollFunction
window.onresize = () => {
// calculate the --banner-height-extend, which needs to be a multiple of 4 to avoid blurry text
let offset = Math.floor(window.innerHeight * (BANNER_HEIGHT_EXTEND / 100));
offset = offset - offset % 4;
document.documentElement.style.setProperty('--banner-height-extend', `${offset}px`);
}
</script>
<script>
import { Fancybox } from "@fancyapps/ui"
import "@fancyapps/ui/dist/fancybox/fancybox.css"
const setup = () => {
Fancybox.bind(".custom-md img, #post-cover img", {
wheel: 'zoom',
clickContent: 'close',
dblclickContent: 'zoom',
click: 'close',
dblclick: 'zoom',
Panels: {
display: ['counter', 'zoom']
},
Images: {
panning: true,
zoom: true,
protect: false
}
})
window.swup.hooks.on("page:view", () => {
Fancybox.bind(".custom-md img, #post-cover img", {
wheel: 'zoom',
clickContent: 'close',
dblclickContent: 'zoom',
click: 'close',
dblclick: 'zoom',
Panels: {
display: ['counter', 'zoom']
},
Images: {
panning: true,
zoom: true,
protect: false
}
})
})
window.swup.hooks.on(
"content:replace",
() => {
Fancybox.unbind(".custom-md img, #post-cover img")
},
{ before: true },
)
}
if (window.swup) {
setup()
} else {
document.addEventListener("swup:enable", setup)
}
</script>
<svg style="position: absolute; width: 0; height: 0; overflow: hidden;" aria-hidden="true">
<defs>
<filter id="liquid-glass">
<feTurbulence type="fractalNoise" baseFrequency="0.05" numOctaves="2" result="noise" />
<feDisplacementMap in="SourceGraphic" in2="noise" scale="20" xChannelSelector="R" yChannelSelector="G" />
</filter>
</defs>
</svg>