mirror of
https://github.com/StepanovPlaton/AboutMe.git
synced 2026-04-03 20:30:49 +04:00
Initial commit
This commit is contained in:
522
src/layouts/base.astro
Normal file
522
src/layouts/base.astro
Normal file
@@ -0,0 +1,522 @@
|
||||
---
|
||||
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>
|
||||
248
src/layouts/grid.astro
Normal file
248
src/layouts/grid.astro
Normal file
@@ -0,0 +1,248 @@
|
||||
---
|
||||
import type { MarkdownHeading } from "astro";
|
||||
import { Icon } from "astro-icon/components";
|
||||
|
||||
import { siteConfig } from "@/config";
|
||||
import {
|
||||
BANNER_HEIGHT,
|
||||
MAIN_PANEL_OVERLAPS_BANNER_HEIGHT,
|
||||
} from "@constants/constants";
|
||||
import { widgetManager } from "@utils/widget";
|
||||
import IconifyLoader from "@components/common/iconifyLoader.astro";
|
||||
import Navbar from "@components/navbar.astro";
|
||||
import FullscreenWallpaper from "@components/fullscreenWallpaper.astro";
|
||||
import Banner from "@components/banner.astro";
|
||||
import Sidebar from "@components/sidebar.astro";
|
||||
import Footer from "@components/footer.astro";
|
||||
import TocButton from "@components/tocButton.astro";
|
||||
import BackToTopButton from "@components/back2TopButton.astro";
|
||||
import BaseLayout from "./base.astro";
|
||||
import "@styles/grid.css";
|
||||
|
||||
/**
|
||||
* Wallpaper
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
banner?: string;
|
||||
description?: string;
|
||||
lang?: string;
|
||||
setOGTypeArticle?: boolean;
|
||||
postSlug?: string;
|
||||
headings?: MarkdownHeading[];
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
banner,
|
||||
description,
|
||||
lang,
|
||||
setOGTypeArticle,
|
||||
postSlug,
|
||||
headings = [],
|
||||
} = Astro.props;
|
||||
const hasBannerCredit = siteConfig.wallpaper.mode === "banner" && siteConfig.wallpaper.banner?.credit?.enable;
|
||||
const hasBannerLink = !!siteConfig.wallpaper.banner?.credit?.url;
|
||||
|
||||
// 检查是否为首页
|
||||
const isHomePage = Astro.url.pathname === "/" || Astro.url.pathname === "";
|
||||
|
||||
// 计算主内容区域位置
|
||||
const mainPanelTop = siteConfig.wallpaper.mode === "banner"
|
||||
? `calc(${BANNER_HEIGHT}vh - ${MAIN_PANEL_OVERLAPS_BANNER_HEIGHT}rem)`
|
||||
: "5.5rem";
|
||||
|
||||
// 当banner被禁用时,主内容区域应该始终从顶栏下面开始
|
||||
const finalMainPanelTop = siteConfig.wallpaper.mode === "banner" ? mainPanelTop : "5.5rem";
|
||||
|
||||
// 检查是否应该启用半透明效果
|
||||
const shouldEnableTransparency = siteConfig.wallpaper.mode === "banner" || siteConfig.wallpaper.mode === "fullscreen";
|
||||
|
||||
// 为组件添加半透明效果的CSS类
|
||||
const transparentClass = shouldEnableTransparency ? "wallpaper-transparent" : "";
|
||||
|
||||
/**
|
||||
* Sidebar
|
||||
*/
|
||||
|
||||
// 检查侧边栏是否启用,动态调整网格布局
|
||||
const {
|
||||
hasLeftSidebar,
|
||||
hasRightSidebar,
|
||||
hasAnyComponents,
|
||||
gridCols,
|
||||
leftSidebarClass,
|
||||
rightSidebarClass,
|
||||
mainContentClass,
|
||||
mobileFooterClass,
|
||||
middleSidebarClass,
|
||||
} = widgetManager.getGridLayout(headings);
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={title}
|
||||
banner={banner}
|
||||
description={description}
|
||||
lang={lang}
|
||||
setOGTypeArticle={setOGTypeArticle}
|
||||
postSlug={postSlug}
|
||||
>
|
||||
<!-- 全局图标加载器 -->
|
||||
<IconifyLoader />
|
||||
|
||||
<!-- Navbar -->
|
||||
<slot slot="head" name="head" />
|
||||
<div
|
||||
id="top-row"
|
||||
class="z-50 pointer-events-none relative transition-all duration-600 max-w-(--page-width) px-0 md:px-4 mx-auto"
|
||||
class:list={[""]}
|
||||
>
|
||||
<div
|
||||
id="navbar-wrapper"
|
||||
class="pointer-events-auto sticky top-0 transition-all"
|
||||
>
|
||||
<Navbar />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 全屏壁纸 - 总是渲染但通过CSS控制可见性 -->
|
||||
<FullscreenWallpaper
|
||||
config={siteConfig.wallpaper}
|
||||
class={siteConfig.wallpaper.mode === "fullscreen" ? "" : "hidden"}
|
||||
/>
|
||||
|
||||
<!-- 为全屏壁纸模式添加body类 -->
|
||||
{
|
||||
shouldEnableTransparency && (
|
||||
<script>
|
||||
document.body.classList.add('wallpaper-transparent');
|
||||
</script>
|
||||
)
|
||||
}
|
||||
|
||||
<!-- Banner - 总是渲染但通过CSS控制可见性 -->
|
||||
<Banner
|
||||
config={siteConfig.wallpaper}
|
||||
isHomePage={isHomePage}
|
||||
class={siteConfig.wallpaper.mode === "banner" ? "" : "hidden"}
|
||||
/>
|
||||
|
||||
<!-- Main content -->
|
||||
<div
|
||||
class={`absolute w-full z-30 pointer-events-none transition-all duration-600 ${siteConfig.wallpaper.mode !== "banner" ? "no-banner-layout" : ""} ${transparentClass}`}
|
||||
style={`top: ${finalMainPanelTop}`}
|
||||
>
|
||||
<div
|
||||
class="relative max-w-(--page-width) mx-auto pointer-events-auto"
|
||||
>
|
||||
<div
|
||||
id="main-grid"
|
||||
class={`transition-all duration-600 w-full left-0 right-0 grid ${gridCols} grid-rows-[auto] mx-auto gap-4 px-0 md:px-4 ${!hasAnyComponents ? "mobile-no-sidebar" : ""} ${(hasLeftSidebar || hasRightSidebar) ? "mobile-both-sidebar" : ""}`}
|
||||
>
|
||||
<!-- Banner image credit -->
|
||||
{
|
||||
hasBannerCredit && (
|
||||
<a
|
||||
href={siteConfig.wallpaper.banner?.credit?.url}
|
||||
id="banner-credit"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
aria-label="Visit image source"
|
||||
class:list={[
|
||||
"group onload-animation-up transition-all absolute flex justify-center items-center rounded-full " +
|
||||
"px-3 right-4 -top-13 bg-black/60 hover:bg-black/70 h-9",
|
||||
{
|
||||
"hover:pr-9 active:bg-black/80":
|
||||
hasBannerLink,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Icon
|
||||
class="text-white/75 text-[1.25rem] mr-1"
|
||||
name="material-symbols:copyright-outline-rounded"
|
||||
/>
|
||||
<div class="text-white/75 text-xs">
|
||||
{siteConfig.wallpaper.banner?.credit?.text}
|
||||
</div>
|
||||
<Icon
|
||||
class:list={[
|
||||
"transition absolute text-[oklch(0.75_0.14_var(--hue))] right-4 text-[0.75rem] opacity-0",
|
||||
{
|
||||
"group-hover:opacity-100":
|
||||
hasBannerLink,
|
||||
},
|
||||
]}
|
||||
name="fa6-solid:arrow-up-right-from-square"
|
||||
/>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
<div class="contents lg:contents md:flex md:flex-col md:col-start-1 md:row-start-1 md:gap-4">
|
||||
{
|
||||
hasLeftSidebar ? (
|
||||
<Sidebar
|
||||
id="left-sidebar"
|
||||
side="left"
|
||||
class={`${leftSidebarClass} ${transparentClass} onload-animation-up`}
|
||||
headings={headings}
|
||||
/>
|
||||
) : (
|
||||
<div id="left-sidebar" class="hidden"></div>
|
||||
)
|
||||
}
|
||||
{
|
||||
hasRightSidebar ? (
|
||||
<Sidebar
|
||||
id="right-sidebar"
|
||||
side="right"
|
||||
class={`${rightSidebarClass} ${transparentClass} onload-animation-up`}
|
||||
headings={headings}
|
||||
/>
|
||||
) : (
|
||||
<div id="right-sidebar" class="hidden"></div>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
<main
|
||||
id="swup-container"
|
||||
class={`${mainContentClass} transition-swup-fade`}
|
||||
>
|
||||
<div id="grid-class-carrier" data-grid-class={gridCols} class="hidden"></div>
|
||||
<div
|
||||
id="content-wrapper"
|
||||
class="onload-animation-up transition-leaving"
|
||||
>
|
||||
<slot />
|
||||
<div
|
||||
class="footer col-span-2 onload-animation-up hidden lg:block transition-swup-fade"
|
||||
>
|
||||
<Footer />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div class={middleSidebarClass}>
|
||||
<Sidebar
|
||||
id="mobile-sidebar"
|
||||
side="middle"
|
||||
class={`${transparentClass} onload-animation-up`}
|
||||
headings={headings}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class={mobileFooterClass}>
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TocButton />
|
||||
|
||||
<BackToTopButton />
|
||||
|
||||
</BaseLayout>
|
||||
Reference in New Issue
Block a user