mirror of
https://github.com/StepanovPlaton/AboutMe.git
synced 2026-04-04 12:50:49 +04:00
522 lines
20 KiB
Plaintext
522 lines
20 KiB
Plaintext
---
|
||
import "katex/dist/katex.css";
|
||
|
||
import { siteConfig, profileConfig, umamiConfig, musicPlayerConfig, pioConfig } from "@/config";
|
||
import type { Favicon } from "@/types/config";
|
||
import { defaultFavicons } from "@constants/icon";
|
||
import {
|
||
BANNER_HEIGHT,
|
||
BANNER_HEIGHT_EXTEND,
|
||
BANNER_HEIGHT_HOME,
|
||
LIGHT_MODE,
|
||
DARK_MODE,
|
||
SYSTEM_MODE,
|
||
PAGE_WIDTH,
|
||
NAVBAR_HEIGHT,
|
||
} from "@constants/constants";
|
||
import { pathsEqual, url } from "@utils/url";
|
||
import FontLoader from "@components/fontLoader.astro";
|
||
import LoadingOverlay from "@components/loadingOverlay.astro";
|
||
import ConfigCarrier from "@components/configCarrier.astro";
|
||
import MusicPlayer from "@components/musicPlayer.svelte";
|
||
import Pio from "@components/pio.svelte";
|
||
import "@styles/navbar.css";
|
||
import "@styles/banner.css";
|
||
import "@styles/fancybox.css";
|
||
|
||
|
||
interface Props {
|
||
title?: string;
|
||
banner?: string;
|
||
description?: string;
|
||
lang?: string;
|
||
setOGTypeArticle?: boolean;
|
||
postSlug?: string;
|
||
}
|
||
|
||
let { title, banner, description, lang, setOGTypeArticle, postSlug } = Astro.props;
|
||
|
||
// apply a class to the body element to decide the height of the banner, only used for initial page load
|
||
const isHomePage = pathsEqual(Astro.url.pathname, url("/"));
|
||
|
||
// defines global css variables
|
||
// why doing this in BaseLayout instead of GlobalStyles: https://github.com/withastro/astro/issues/6728#issuecomment-1502203757
|
||
const configHue = siteConfig.themeColor.hue;
|
||
|
||
// 获取导航栏透明模式配置
|
||
const navbarTransparentMode =
|
||
siteConfig.wallpaper.banner?.navbar?.transparentMode || "semi";
|
||
// 判断是否应该显示顶部高光效果(只在full和semifull模式下显示)
|
||
const shouldShowTopHighlight =
|
||
navbarTransparentMode === "full" || navbarTransparentMode === "semifull";
|
||
|
||
// 获取默认banner图片的辅助函数
|
||
const getDefaultBanner = (): string => {
|
||
const src = siteConfig.wallpaper.src;
|
||
if (typeof src === "string") {
|
||
return src;
|
||
}
|
||
if (Array.isArray(src)) {
|
||
return src[0] || "";
|
||
}
|
||
if (src && typeof src === "object") {
|
||
// 优先使用desktop,如果没有则使用mobile
|
||
const desktopSrc = src.desktop;
|
||
const mobileSrc = src.mobile;
|
||
if (typeof desktopSrc === "string") {
|
||
return desktopSrc;
|
||
}
|
||
if (Array.isArray(desktopSrc) && desktopSrc.length > 0) {
|
||
return desktopSrc[0];
|
||
}
|
||
if (typeof mobileSrc === "string") {
|
||
return mobileSrc;
|
||
}
|
||
if (Array.isArray(mobileSrc) && mobileSrc.length > 0) {
|
||
return mobileSrc[0];
|
||
}
|
||
}
|
||
return "";
|
||
};
|
||
|
||
// TODO don't use post cover as banner for now
|
||
banner = getDefaultBanner();
|
||
|
||
const enableBanner = siteConfig.wallpaper.mode === "banner";
|
||
|
||
let pageTitle: string;
|
||
if (title) {
|
||
pageTitle = `${title} - ${siteConfig.title}`;
|
||
} else {
|
||
pageTitle = siteConfig.subtitle
|
||
? `${siteConfig.title} - ${siteConfig.subtitle}`
|
||
: siteConfig.title;
|
||
}
|
||
|
||
let ogImageUrl: string | undefined;
|
||
if (siteConfig.generateOgImages && postSlug) {
|
||
ogImageUrl = new URL(`/og/${postSlug}.png`, Astro.site).toString();
|
||
}
|
||
|
||
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.wallpaper.position || "center"];
|
||
|
||
const umamiEnabled = umamiConfig.enabled || false;
|
||
const umamiScripts = umamiConfig.scripts || ""; // 获取Umami scripts配置
|
||
---
|
||
|
||
<!DOCTYPE html>
|
||
<html lang={siteLang} class="bg-(--page-bg) text-[14px] md:text-[16px] is-loading"
|
||
>
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<!-- Resource Preconnect -->
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link rel="preconnect" href="https://code.iconify.design">
|
||
<!-- Font Loader -->
|
||
<FontLoader />
|
||
<!-- title -->
|
||
<title>{pageTitle}</title>
|
||
<!-- description -->
|
||
<meta name="description" content={description || pageTitle}>
|
||
<!-- keywords -->
|
||
{siteConfig.keywords && siteConfig.keywords.length > 0 && (
|
||
<meta name="keywords" content={siteConfig.keywords.join(', ')} />
|
||
)}
|
||
<!-- author -->
|
||
<meta name="author" content={profileConfig.name}>
|
||
<!-- Open Graph -->
|
||
<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 || pageTitle}>
|
||
{ogImageUrl && <meta property="og:image" content={ogImageUrl} />}
|
||
{
|
||
setOGTypeArticle ? (
|
||
<meta property="og:type" content="article" />
|
||
) : (
|
||
<meta property="og:type" content="website" />
|
||
)
|
||
}
|
||
<!-- Twitter -->
|
||
<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 || pageTitle}>
|
||
<!-- viewport -->
|
||
<meta name="viewport" content="width=device-width" />
|
||
<!-- generator -->
|
||
<meta name="generator" content={Astro.generator} />
|
||
<!-- favicons -->
|
||
{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={{LIGHT_MODE, DARK_MODE, SYSTEM_MODE, BANNER_HEIGHT_EXTEND, PAGE_WIDTH, NAVBAR_HEIGHT, configHue, defaultTheme: siteConfig.defaultTheme}}>
|
||
// Load the theme from local storage
|
||
const theme = localStorage.getItem('theme') || defaultTheme;
|
||
// Apply the theme to the document
|
||
let isDark = false;
|
||
switch (theme) {
|
||
case LIGHT_MODE:
|
||
document.documentElement.classList.remove('dark');
|
||
isDark = false;
|
||
break
|
||
case DARK_MODE:
|
||
document.documentElement.classList.add('dark');
|
||
isDark = true;
|
||
break
|
||
case SYSTEM_MODE:
|
||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||
document.documentElement.classList.add('dark');
|
||
isDark = true;
|
||
} else {
|
||
document.documentElement.classList.remove('dark');
|
||
isDark = false;
|
||
}
|
||
}
|
||
const expressiveTheme = isDark ? "github-dark" : "github-light";
|
||
document.documentElement.setAttribute("data-theme", expressiveTheme);
|
||
// Load the hue from local storage
|
||
const hue = localStorage.getItem('hue') || configHue;
|
||
// Apply the hue to the document
|
||
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>
|
||
<!-- defines global css variables. This will be applied to <html> <body> and some other elements idk why -->
|
||
<style define:vars={{
|
||
configHue,
|
||
'page-width': `${PAGE_WIDTH}rem`,
|
||
}}></style>
|
||
<!-- custom head -->
|
||
<slot name="head"></slot>
|
||
<!-- Pio 看板娘样式 - 仅在启用时加载 -->
|
||
{pioConfig.enable && <link rel="stylesheet" href="/pio/static/pio.css" />}
|
||
<!-- rss feed -->
|
||
<link rel="alternate" type="application/rss+xml" title={profileConfig.name} href={`${Astro.site}rss.xml`}/>
|
||
<!-- Umami Analytics -->
|
||
{umamiEnabled && umamiScripts && <Fragment set:html={umamiScripts} />}
|
||
</head>
|
||
|
||
<body class=" min-h-screen " class:list={[{"is-home": isHomePage, "enable-banner": enableBanner}]}
|
||
>
|
||
<!-- 页面加载动画 -->
|
||
<LoadingOverlay />
|
||
<!-- 全局配置载体 -->
|
||
<ConfigCarrier />
|
||
<!-- 页面顶部渐变高光效果 - 只在full和semifull模式下显示 -->
|
||
{shouldShowTopHighlight && <div class="top-gradient-highlight"></div>}
|
||
<!-- 页面内容 -->
|
||
<slot />
|
||
<!-- Music Player - 仅在启用时加载 -->
|
||
{musicPlayerConfig.enable && <MusicPlayer client:idle />}
|
||
<!-- Pio 看板娘组件 - 如果启用了侧边栏,可能需要调整 z-index -->
|
||
{pioConfig.enable && <Pio client:idle />}
|
||
<!-- 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`,
|
||
}}>
|
||
@reference "../styles/main.css";
|
||
@layer components {
|
||
.enable-banner.is-home #banner-wrapper {
|
||
@apply h-(--banner-height-home) translate-y-(--banner-height-extend)
|
||
}
|
||
.enable-banner #banner-wrapper {
|
||
@apply h-(--banner-height-home)
|
||
}
|
||
.enable-banner.is-home #banner {
|
||
@apply h-(--banner-height-home) translate-y-0
|
||
}
|
||
.enable-banner #banner {
|
||
@apply h-(--banner-height-home) translate-y-(--bannerOffset)
|
||
}
|
||
.enable-banner.is-home #main-grid {
|
||
translate: 0 var(--banner-height-extend);
|
||
}
|
||
.enable-banner #top-row {
|
||
height: calc(var(--banner-height-home) - 5.5rem);
|
||
}
|
||
.enable-banner.is-home #sidebar-sticky {
|
||
top: calc(1rem - var(--banner-height-extend)) !important;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
|
||
<script>
|
||
import {
|
||
NAVBAR_HEIGHT,
|
||
BANNER_HEIGHT,
|
||
BANNER_HEIGHT_HOME,
|
||
BANNER_HEIGHT_EXTEND,
|
||
} from "@/constants/constants";
|
||
import { pathsEqual, url } from "@utils/url";
|
||
import { initWallpaperMode } from "@utils/wallpaper";
|
||
import { initTheme } from "@utils/theme";
|
||
import { initHue } from "@utils/hue";
|
||
import { initMarkdownActions } from "@/utils/markdown";
|
||
import { initFancybox, cleanupFancybox } from "@utils/fancybox";
|
||
import { loadAndInitTranslate } from "@utils/language";
|
||
import { setupParticleEffects } from "@/utils/particle";
|
||
|
||
|
||
// 初始化Swup
|
||
const setup = () => {
|
||
// 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
|
||
window.swup.hooks.on('link:click', () => {
|
||
// 简化 navbar 处理逻辑
|
||
const bannerEnabled = !!document.getElementById('banner-wrapper')
|
||
if (bannerEnabled) {
|
||
const navbar = document.getElementById('navbar-wrapper')
|
||
if (navbar && document.body.classList.contains('is-home')) {
|
||
const threshold = window.innerHeight * (BANNER_HEIGHT / 100) - NAVBAR_HEIGHT
|
||
if (document.documentElement.scrollTop >= threshold) {
|
||
navbar.classList.add('navbar-hidden')
|
||
}
|
||
}
|
||
}
|
||
})
|
||
window.swup.hooks.on('content:replace', () => {
|
||
// Update main grid layout class name after page transition
|
||
const gridClassCarrier = document.getElementById("grid-class-carrier");
|
||
if (gridClassCarrier) {
|
||
const gridClass = gridClassCarrier.getAttribute("data-grid-class");
|
||
const mainGrid = document.getElementById("main-grid");
|
||
if (mainGrid && gridClass) {
|
||
const currentClasses = mainGrid.className.split(" ");
|
||
const newClasses = currentClasses.filter(c => !c.startsWith("lg:grid-cols-") && !c.startsWith("md:grid-cols-") && !c.startsWith("grid-cols-"));
|
||
const gridClasses = gridClass.split(" ");
|
||
mainGrid.className = [...newClasses, ...gridClasses].join(" ");
|
||
}
|
||
}
|
||
// Reinitialize semifull mode scroll detection after page transition
|
||
const navbar = document.getElementById('navbar')
|
||
if (navbar) {
|
||
const transparentMode = navbar.getAttribute('data-transparent-mode')
|
||
if (transparentMode === 'semifull') {
|
||
// Re-call the initialization function to rebind scroll events
|
||
if (typeof (window as any).initSemifullScrollDetection === 'function') {
|
||
(window as any).initSemifullScrollDetection()
|
||
}
|
||
}
|
||
}
|
||
})
|
||
window.swup.hooks.on('visit:start', (visit: {to: {url: string}}) => {
|
||
// Disable scroll protection when a link is clicked to prevent interference with page jump scrolling to top
|
||
if ((window as any).scrollProtectionManager && typeof (window as any).scrollProtectionManager.disable === 'function') {
|
||
(window as any).scrollProtectionManager.disable();
|
||
}
|
||
// Change banner height immediately when a link is clicked
|
||
const bodyElement = document.querySelector('body')
|
||
const isHomePage = pathsEqual(visit.to.url, url('/'))
|
||
if (isHomePage) {
|
||
bodyElement!.classList.add('is-home');
|
||
} else {
|
||
bodyElement!.classList.remove('is-home');
|
||
}
|
||
// Control banner text visibility based on page
|
||
const bannerTextOverlay = document.querySelector('.banner-text-overlay')
|
||
if (bannerTextOverlay) {
|
||
if (isHomePage) {
|
||
bannerTextOverlay.classList.remove('hidden')
|
||
} else {
|
||
bannerTextOverlay.classList.add('hidden')
|
||
}
|
||
}
|
||
// Control navbar transparency based on page
|
||
const navbar = document.getElementById('navbar')
|
||
if (navbar) {
|
||
navbar.setAttribute('data-is-home', isHomePage.toString())
|
||
const transparentMode = navbar.getAttribute('data-transparent-mode')
|
||
if (transparentMode === 'semifull') {
|
||
// Re-call the initialization function to rebind scroll events
|
||
if (typeof window.initSemifullScrollDetection === 'function') {
|
||
window.initSemifullScrollDetection()
|
||
}
|
||
}
|
||
}
|
||
// 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')
|
||
}
|
||
});
|
||
window.swup.hooks.on('visit:end', (_visit: {to: {url: string}}) => {
|
||
setTimeout(() => {
|
||
const heightExtend = document.getElementById('page-height-extend')
|
||
if (heightExtend) {
|
||
heightExtend.classList.add('hidden')
|
||
}
|
||
}, 300)
|
||
});
|
||
window.swup.hooks.on('page:view', () => {
|
||
// Scroll to top of the page after page transition
|
||
window.scrollTo({
|
||
top: 0,
|
||
behavior: 'smooth'
|
||
});
|
||
// Check if the current page is a post page, if so, trigger a custom event to initialize the comment system
|
||
setTimeout(() => {
|
||
if (document.getElementById('tcomment')) {
|
||
const pageLoadedEvent = new CustomEvent('twilight:page:loaded', {
|
||
detail: {
|
||
path: window.location.pathname,
|
||
timestamp: Date.now()
|
||
}
|
||
});
|
||
document.dispatchEvent(pageLoadedEvent);
|
||
}
|
||
}, 300);
|
||
// Initialize Fancybox
|
||
initFancybox();
|
||
});
|
||
initFancybox();
|
||
window.swup.hooks.on("content:replace", () => {
|
||
cleanupFancybox();
|
||
},
|
||
{ before: true },
|
||
)
|
||
}
|
||
if (window?.swup?.hooks) {
|
||
setup()
|
||
} else {
|
||
document.addEventListener('swup:enable', setup)
|
||
}
|
||
|
||
// 初始化 navbar 元素,用于控制 banner 隐藏
|
||
let navbar = document.getElementById('navbar-wrapper')
|
||
// 初始化返回顶部按钮元素,用于控制返回顶部按钮的显示与隐藏
|
||
let backToTopBtn = document.getElementById('back-to-top-btn');
|
||
// 节流函数
|
||
function throttle(func: (...args: any[]) => void, limit: number) {
|
||
let inThrottle: boolean;
|
||
return function(this: any, ...args: any[]) {
|
||
if (!inThrottle) {
|
||
func.apply(this, args);
|
||
inThrottle = true;
|
||
setTimeout(() => inThrottle = false, limit);
|
||
}
|
||
}
|
||
}
|
||
// 滚动事件处理函数,用于控制返回顶部按钮和banner隐藏
|
||
function scrollFunction() {
|
||
const scrollTop = document.documentElement.scrollTop;
|
||
const bannerHeight = window.innerHeight * (BANNER_HEIGHT / 100);
|
||
const bannerEnabled = !!document.getElementById('banner-wrapper')
|
||
// 批量处理DOM操作
|
||
requestAnimationFrame(() => {
|
||
if (backToTopBtn) {
|
||
if (scrollTop > bannerHeight) {
|
||
backToTopBtn.classList.remove('hide')
|
||
} else {
|
||
backToTopBtn.classList.add('hide')
|
||
}
|
||
}
|
||
if (bannerEnabled && navbar) {
|
||
const isHome = document.body.classList.contains('is-home');
|
||
const currentBannerHeight = isHome ? BANNER_HEIGHT_HOME : BANNER_HEIGHT;
|
||
const threshold = window.innerHeight * (currentBannerHeight / 100) - NAVBAR_HEIGHT;
|
||
if (scrollTop >= threshold) {
|
||
navbar.classList.add('navbar-hidden')
|
||
} else {
|
||
navbar.classList.remove('navbar-hidden')
|
||
}
|
||
}
|
||
});
|
||
}
|
||
// 使用节流优化滚动性能
|
||
window.onscroll = throttle(scrollFunction, 16); // 约60fps
|
||
// 监听窗口 resize 事件,用于更新 banner 高度相关的 CSS 变量
|
||
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`);
|
||
}
|
||
|
||
// Initialize theme
|
||
initTheme();
|
||
|
||
// Initialize hue
|
||
initHue();
|
||
|
||
// Initialize wallpaper mode
|
||
initWallpaperMode();
|
||
|
||
// 初始化 Markdown 相关操作以防止因 Swup 无刷新跳转的机制限制而导致操作失效
|
||
initMarkdownActions();
|
||
|
||
// Initialize translate.js
|
||
loadAndInitTranslate();
|
||
|
||
// Initialize particle effects
|
||
setupParticleEffects();
|
||
|
||
// Remove is-loading class and trigger initial-animation after page load
|
||
if (typeof window !== 'undefined') {
|
||
window.addEventListener('load', () => {
|
||
const overlay = document.getElementById('loading-overlay');
|
||
const enableBanner = document.body.classList.contains('enable-banner');
|
||
// 触发 LoadingOverlay 淡出
|
||
if (overlay) {
|
||
overlay.classList.add('fade-out');
|
||
}
|
||
// 等待 LoadingOverlay 淡出(600ms)
|
||
setTimeout(() => {
|
||
if (overlay) {
|
||
overlay.style.display = 'none';
|
||
}
|
||
if (enableBanner) {
|
||
// banner 开始恢复过渡:添加 banner-restoring 类(解除 CSS 中的 height: 0)
|
||
document.documentElement.classList.add('banner-restoring');
|
||
// 等待 banner 恢复过渡完成(600ms)
|
||
setTimeout(() => {
|
||
// 触发各容器淡入动画
|
||
document.documentElement.classList.remove('is-loading');
|
||
document.documentElement.classList.add('show-initial-animation');
|
||
setTimeout(() => {
|
||
document.documentElement.classList.remove('show-initial-animation');
|
||
document.documentElement.classList.remove('banner-restoring');
|
||
}, 1200);
|
||
}, 600);
|
||
} else {
|
||
// 非 banner 模式,直接触发淡入
|
||
document.documentElement.classList.remove('is-loading');
|
||
document.documentElement.classList.add('show-initial-animation');
|
||
setTimeout(() => {
|
||
document.documentElement.classList.remove('show-initial-animation');
|
||
}, 1200);
|
||
}
|
||
}, 600);
|
||
});
|
||
}
|
||
</script> |