| ---
|
| import Live2DWidget from "@components/features/Live2DWidget.astro";
|
| import OverlayWallpaper from "@components/features/OverlayWallpaper.astro";
|
| import SpineModel from "@components/features/SpineModel.astro";
|
| import Footer from "@components/layout/Footer.astro";
|
| import Navbar from "@components/layout/Navbar.astro";
|
| import SideBar from "@components/layout/SideBar.astro";
|
| import type { MarkdownHeading } from "astro";
|
| import { Icon } from "astro-icon/components";
|
| import ImageWrapper from "@/components/common/ImageWrapper.astro";
|
| import FloatingControls from "@/components/controls/FloatingControls.astro";
|
| import TypewriterText from "@/components/features/TypewriterText.astro";
|
| import {
|
| backgroundWallpaper,
|
| live2dModelConfig,
|
| sidebarLayoutConfig,
|
| siteConfig,
|
| } from "@/config";
|
| import {
|
| BANNER_HEIGHT,
|
| BANNER_HEIGHT_EXTEND,
|
| MAIN_PANEL_OVERLAPS_BANNER_HEIGHT,
|
| } from "@/constants/constants";
|
| import { getImageQuality } from "@/utils/image-utils";
|
| import { getBackgroundImages, isHomePage } from "@/utils/layout-utils";
|
| import {
|
| generateGridClasses,
|
| generateMainContentClasses,
|
| generateRightSidebarClasses,
|
| generateSidebarClasses,
|
| getResponsiveSidebarConfig,
|
| type ResponsiveSidebarConfig,
|
| } from "@/utils/responsive-utils";
|
| import Layout from "./Layout.astro";
|
|
|
| interface Props {
|
| title?: string;
|
| banner?: string;
|
| description?: string;
|
| lang?: string;
|
| setOGTypeArticle?: boolean;
|
| postSlug?: string;
|
| headings?: MarkdownHeading[];
|
| }
|
|
|
| const backgroundImages = getBackgroundImages();
|
|
|
| const {
|
| title,
|
| banner,
|
| description,
|
| lang,
|
| setOGTypeArticle,
|
| postSlug,
|
| headings = [],
|
| } = Astro.props;
|
|
|
|
|
| const isBannerMode = backgroundWallpaper.mode === "banner";
|
| const isOverlayMode = backgroundWallpaper.mode === "overlay";
|
| const isBackgroundEnabled = backgroundWallpaper.mode !== "none";
|
| const isWallpaperSwitchable = backgroundWallpaper.switchable ?? true;
|
|
|
|
|
| const wavesConfig = backgroundWallpaper.banner?.waves?.enable;
|
|
|
| const wavesEnabledOnDesktop =
|
| typeof wavesConfig === "object" ? wavesConfig.desktop : wavesConfig;
|
| const wavesEnabledOnMobile =
|
| typeof wavesConfig === "object" ? wavesConfig.mobile : wavesConfig;
|
|
|
|
|
| const creditEnableConfig = backgroundWallpaper.banner?.credit?.enable;
|
| const creditEnabledOnDesktop =
|
| typeof creditEnableConfig === "object"
|
| ? creditEnableConfig.desktop
|
| : creditEnableConfig;
|
| const creditEnabledOnMobile =
|
| typeof creditEnableConfig === "object"
|
| ? creditEnableConfig.mobile
|
| : creditEnableConfig;
|
|
|
| const hasBannerCredit =
|
| isBannerMode && isBackgroundEnabled && creditEnableConfig;
|
|
|
|
|
| const creditTextConfig = backgroundWallpaper.banner?.credit?.text;
|
| const creditTextDesktop =
|
| typeof creditTextConfig === "object"
|
| ? creditTextConfig.desktop
|
| : creditTextConfig;
|
| const creditTextMobile =
|
| typeof creditTextConfig === "object"
|
| ? creditTextConfig.mobile
|
| : creditTextConfig;
|
|
|
| const creditUrlConfig = backgroundWallpaper.banner?.credit?.url;
|
| const creditUrlDesktop =
|
| typeof creditUrlConfig === "object"
|
| ? creditUrlConfig.desktop
|
| : creditUrlConfig;
|
| const creditUrlMobile =
|
| typeof creditUrlConfig === "object"
|
| ? creditUrlConfig.mobile
|
| : creditUrlConfig;
|
|
|
|
|
| const isHomePageCheck = isHomePage(Astro.url.pathname);
|
|
|
|
|
| const isPostPage = !!postSlug;
|
|
|
|
|
| const getRandomSubtitle = () => {
|
| const subtitle = backgroundWallpaper.banner?.homeText?.subtitle;
|
| if (Array.isArray(subtitle)) {
|
| const randomIndex = Math.floor(Math.random() * subtitle.length);
|
| return subtitle[randomIndex];
|
| }
|
| return subtitle;
|
| };
|
| const randomSubtitle = getRandomSubtitle();
|
|
|
|
|
| const homeTextEnable = backgroundWallpaper.banner?.homeText?.enable;
|
| const showHomeText = isBannerMode && !!homeTextEnable && isHomePageCheck;
|
|
|
| const mobileNonHomeBannerClass = !isHomePageCheck ? "mobile-hide-banner" : "";
|
|
|
|
|
| const mainPanelTop =
|
| isBannerMode && isBackgroundEnabled
|
| ? `calc(${BANNER_HEIGHT}vh - ${MAIN_PANEL_OVERLAPS_BANNER_HEIGHT}rem)`
|
| : "5.5rem";
|
|
|
|
|
|
|
| const finalMainPanelTop =
|
| isBannerMode && isBackgroundEnabled ? mainPanelTop : "5.5rem";
|
|
|
|
|
| const sidebarConfig = getResponsiveSidebarConfig();
|
|
|
|
|
| const shouldShowRightSidebarOnPostPage: boolean =
|
| isPostPage &&
|
| sidebarLayoutConfig.position === "left" &&
|
| !!sidebarLayoutConfig.showRightSidebarOnPostPage;
|
|
|
| const effectiveIsBothSidebars: boolean =
|
| sidebarConfig.isBothSidebars || shouldShowRightSidebarOnPostPage;
|
| const effectiveHasRightComponents: boolean =
|
| sidebarConfig.hasRightComponents ||
|
| (shouldShowRightSidebarOnPostPage &&
|
| sidebarLayoutConfig.rightComponents.some((comp) => comp.enable));
|
|
|
|
|
| const updatedGridConfig: ResponsiveSidebarConfig = {
|
| ...sidebarConfig,
|
| isBothSidebars: effectiveIsBothSidebars,
|
| hasRightComponents: effectiveHasRightComponents,
|
| };
|
|
|
| const { gridCols } = generateGridClasses(updatedGridConfig);
|
| const sidebarClass = generateSidebarClasses();
|
| const rightSidebarClass = effectiveIsBothSidebars
|
| ? generateRightSidebarClasses()
|
| : "";
|
| const mainContentClass = generateMainContentClasses(updatedGridConfig);
|
|
|
| const footerClass = [
|
| "footer",
|
| "col-span-1",
|
| "md:col-span-2",
|
| "onload-animation",
|
| ];
|
|
|
| if (
|
| updatedGridConfig.isBothSidebars &&
|
| updatedGridConfig.hasLeftComponents &&
|
| updatedGridConfig.hasRightComponents
|
| ) {
|
| footerClass.push("xl:col-start-2 xl:col-span-1");
|
| } else if (
|
| updatedGridConfig.hasLeftComponents &&
|
| !updatedGridConfig.hasRightComponents
|
| ) {
|
| footerClass.push("xl:col-start-2 xl:col-span-1");
|
| } else if (
|
| !updatedGridConfig.hasLeftComponents &&
|
| updatedGridConfig.hasRightComponents
|
| ) {
|
| footerClass.push("xl:col-start-1 xl:col-span-1");
|
| } else {
|
| footerClass.push("xl:col-start-1 xl:col-span-1");
|
| }
|
|
|
| const footerClassName = footerClass.join(" ");
|
|
|
|
|
| const shouldEnableTransparency = isOverlayMode && isBackgroundEnabled;
|
|
|
|
|
| const transparentClass = shouldEnableTransparency
|
| ? "wallpaper-transparent"
|
| : "";
|
|
|
| const navbarWidthFull = siteConfig.navbar.widthFull ?? false;
|
|
|
|
|
| const configQuality = getImageQuality();
|
| const mobileQuality = Math.round(configQuality * 0.9);
|
| ---
|
|
|
| <Layout title={title} banner={banner} description={description} lang={lang} setOGTypeArticle={setOGTypeArticle} postSlug={postSlug}>
|
|
|
|
|
|
|
| {(isWallpaperSwitchable || isOverlayMode) && (
|
| <OverlayWallpaper config={{
|
| src: {
|
| desktop: typeof backgroundImages.desktop === 'string' ? backgroundImages.desktop : '',
|
| mobile: typeof backgroundImages.mobile === 'string' ? backgroundImages.mobile : ''
|
| },
|
| zIndex: backgroundWallpaper.overlay?.zIndex,
|
| opacity: backgroundWallpaper.overlay?.opacity,
|
| blur: backgroundWallpaper.overlay?.blur,
|
| }}
|
| className={isWallpaperSwitchable && !isOverlayMode ? "hidden opacity-0" : undefined} />
|
| )}
|
|
|
|
|
| {shouldEnableTransparency && (
|
| <script>
|
| document.body.classList.add('wallpaper-transparent');
|
| </script>
|
| )}
|
|
|
|
|
| <slot slot="head" name="head"></slot>
|
| <div id="top-row" class="z-50 pointer-events-none relative transition-all duration-700 mx-auto" class:list={[navbarWidthFull ? "" : "max-w-(--page-width) px-0 md:px-4"]}>
|
| <div id="navbar-wrapper" class="pointer-events-auto sticky top-0 transition-all">
|
| <Navbar></Navbar>
|
| </div>
|
| </div>
|
|
|
|
|
| {(isWallpaperSwitchable || isBannerMode) && (
|
| <div id="banner-wrapper" class={`absolute z-10 w-full transition duration-700 overflow-hidden ${mobileNonHomeBannerClass}`} style={isWallpaperSwitchable ? `top: -${BANNER_HEIGHT_EXTEND}vh; display: none;` : `top: -${BANNER_HEIGHT_EXTEND}vh`}>
|
|
|
| <div class="relative h-full w-full">
|
|
|
| <ImageWrapper
|
| alt="Mobile background image of the blog"
|
| class:list={["block lg:hidden object-cover h-full w-full transition duration-700 opacity-100"]}
|
| src={typeof backgroundImages.mobile === 'string' ? backgroundImages.mobile : (typeof backgroundImages.desktop === 'string' ? backgroundImages.desktop : '')}
|
| position={backgroundWallpaper.banner?.position}
|
| widths={[640, 750, 1080]}
|
| sizes="100vw"
|
| loading="eager"
|
| fetchpriority="high"
|
| quality={mobileQuality}
|
| />
|
|
|
| <ImageWrapper
|
| id="banner"
|
| alt="Desktop background image of the blog"
|
| class:list={["hidden lg:block object-cover h-full transition duration-700 opacity-100"]}
|
| src={typeof backgroundImages.desktop === 'string' ? backgroundImages.desktop : (typeof backgroundImages.mobile === 'string' ? backgroundImages.mobile : '')}
|
| position={backgroundWallpaper.banner?.position}
|
| widths={[1280, 1920, 2560]}
|
| sizes="100vw"
|
| loading="eager"
|
| fetchpriority="high"
|
| quality={configQuality}
|
| />
|
| </div>
|
|
|
|
|
| {(isWallpaperSwitchable || showHomeText) && homeTextEnable && (
|
| <div class={`banner-text-overlay absolute inset-0 z-20 flex items-center justify-center ${!showHomeText ? 'hidden' : ''}`}>
|
| <div class="w-4/5 lg:w-3/4 text-center mb-0">
|
| <div class="flex flex-col">
|
| {backgroundWallpaper.banner?.homeText?.title && (
|
| <h1 class="banner-title font-bold text-white mb-2 lg:mb-4 leading-tight" style={{ fontSize: backgroundWallpaper.banner.homeText.titleSize || "3rem" }}>
|
| {backgroundWallpaper.banner.homeText.title}
|
| </h1>
|
| )}
|
| {backgroundWallpaper.banner?.homeText?.subtitle && (
|
| <h2 id="banner-subtitle" class="banner-subtitle text-white/90 leading-snug" style={{ fontSize: backgroundWallpaper.banner.homeText.subtitleSize || "1.5rem" }} data-subtitles={JSON.stringify(backgroundWallpaper.banner.homeText.subtitle)}>
|
| {backgroundWallpaper.banner.homeText.typewriter?.enable ? (
|
| <TypewriterText
|
| text={backgroundWallpaper.banner.homeText.subtitle}
|
| speed={backgroundWallpaper.banner.homeText.typewriter.speed}
|
| deleteSpeed={backgroundWallpaper.banner.homeText.typewriter.deleteSpeed}
|
| pauseTime={backgroundWallpaper.banner.homeText.typewriter.pauseTime}
|
| />
|
| ) : (
|
| randomSubtitle
|
| )}
|
| </h2>
|
| )}
|
| </div>
|
| </div>
|
| </div>
|
| )}
|
|
|
| <script is:inline>
|
| function setRandomSubtitle() {
|
| const subtitleElement = document.getElementById('banner-subtitle');
|
| if (!subtitleElement) return;
|
|
|
| const subtitlesData = subtitleElement.dataset.subtitles;
|
| if (!subtitlesData) return;
|
|
|
| try {
|
| const subtitles = JSON.parse(subtitlesData);
|
|
|
| if (Array.isArray(subtitles) && subtitles.length > 0 && !subtitleElement.querySelector('.typewriter')) {
|
|
|
|
|
| if (!window.fireflyCachedSubtitle) {
|
| const randomIndex = Math.floor(Math.random() * subtitles.length);
|
| window.fireflyCachedSubtitle = subtitles[randomIndex];
|
| }
|
| subtitleElement.textContent = window.fireflyCachedSubtitle;
|
| }
|
| } catch (e) {
|
| console.error("Failed to parse subtitles", e);
|
| }
|
| }
|
|
|
|
|
| setRandomSubtitle();
|
| </script>
|
|
|
|
|
| {(wavesEnabledOnDesktop || wavesEnabledOnMobile) ? (
|
| <div class={`waves absolute -bottom-px h-[10vh] max-h-37.5 min-h-12.5 w-full md:h-[15vh]
|
| ${!wavesEnabledOnMobile ? 'hidden' : ''} ${!wavesEnabledOnDesktop ? 'md:hidden' : ''} ${wavesEnabledOnDesktop ? 'md:block' : ''}`}
|
| id="header-waves"
|
| style="transform: translateZ(0); will-change: fill;">
|
| <svg
|
| class="waves"
|
| xmlns="http://www.w3.org/2000/svg"
|
| xmlns:xlink="http://www.w3.org/1999/xlink"
|
| viewBox="0 24 150 28"
|
| preserveAspectRatio="none"
|
| shape-rendering="geometricPrecision"
|
| >
|
| <defs>
|
| <path
|
| id="gentle-wave"
|
| d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v48h-352z"
|
| >
|
| </path>
|
| </defs>
|
| <g class="parallax">
|
| <use
|
| xlink:href="#gentle-wave"
|
| x="48"
|
| y="0"
|
| class="opacity-25 fill-(--page-bg)"
|
| ></use>
|
| <use
|
| xlink:href="#gentle-wave"
|
| x="48"
|
| y="3"
|
| class="opacity-50 fill-(--page-bg)"
|
| ></use>
|
| <use
|
| xlink:href="#gentle-wave"
|
| x="48"
|
| y="5"
|
| class="opacity-65 fill-(--page-bg)"
|
| ></use>
|
| <use
|
| xlink:href="#gentle-wave"
|
| x="48"
|
| y="7"
|
| class=" opacity-75 fill-(--page-bg)"
|
| ></use>
|
| </g>
|
| </svg>
|
| </div>
|
| ) : null}
|
| </div>
|
| )}
|
|
|
|
|
| <div class={`absolute w-full z-30 pointer-events-none ${mobileNonHomeBannerClass ? 'mobile-main-no-banner' : ''} ${!(isBannerMode && isBackgroundEnabled) ? '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 duration-700 w-full left-0 right-0 grid ${gridCols} grid-rows-[auto_1fr_auto] lg:grid-rows-[auto] mx-auto gap-4 px-2 md:px-4 ${!sidebarConfig.mobileShowSidebar ? 'mobile-no-sidebar' : ''}`}
|
| data-sidebar-position={sidebarLayoutConfig.position}
|
| data-show-right-sidebar-on-post={sidebarLayoutConfig.showRightSidebarOnPostPage ? "true" : "false"}
|
| >
|
|
|
| {hasBannerCredit && creditEnabledOnDesktop && <a
|
| href={creditUrlDesktop || "#"}
|
| id="banner-credit-desktop"
|
| target={creditUrlDesktop ? "_blank" : "_self"}
|
| rel={creditUrlDesktop ? "noopener" : ""}
|
| aria-label="Visit image source"
|
| class:list={[
|
| "group onload-animation transition-all absolute justify-center items-center rounded-full " +
|
| "px-3 right-4 -top-13 bg-black/60 hover:bg-black/70 h-9 hidden md:flex",
|
| {"hover:pr-9 active:bg-black/80": creditUrlDesktop}
|
| ]}
|
| >
|
| <Icon class="text-white/75 text-[1.25rem] mr-1" name="material-symbols:copyright-outline-rounded" ></Icon>
|
| <div class="text-white/75 text-xs">{creditTextDesktop}</div>
|
| {creditUrlDesktop && <Icon class:list={["transition absolute text-[oklch(0.75_0.14_var(--hue))] right-4 text-[0.75rem] opacity-0 group-hover:opacity-100"]}
|
| name="fa7-solid:arrow-up-right-from-square">
|
| </Icon>}
|
| </a>}
|
|
|
|
|
| {hasBannerCredit && creditEnabledOnMobile && <a
|
| href={creditUrlMobile || "#"}
|
| id="banner-credit-mobile"
|
| target={creditUrlMobile ? "_blank" : "_self"}
|
| rel={creditUrlMobile ? "noopener" : ""}
|
| aria-label="Visit image source"
|
| class:list={[
|
| "group onload-animation transition-all absolute justify-center items-center rounded-full " +
|
| "px-3 right-4 -top-13 bg-black/60 hover:bg-black/70 h-9 flex md:hidden",
|
| {"hover:pr-9 active:bg-black/80": creditUrlMobile}
|
| ]}
|
| >
|
| <Icon class="text-white/75 text-[1.25rem] mr-1" name="material-symbols:copyright-outline-rounded" ></Icon>
|
| <div class="text-white/75 text-xs">{creditTextMobile}</div>
|
| {creditUrlMobile && <Icon class:list={["transition absolute text-[oklch(0.75_0.14_var(--hue))] right-4 text-[0.75rem] opacity-0 group-hover:opacity-100"]}
|
| name="fa7-solid:arrow-up-right-from-square">
|
| </Icon>}
|
| </a>}
|
|
|
|
|
| {/* 渲染侧边栏 - 769px及以上显示 */}
|
| <div id="left-sidebar-wrapper" class="contents" style="contain: layout style paint;">
|
| {sidebarConfig.hasLeftComponents && (
|
| <SideBar
|
| side={sidebarConfig.isBothSidebars ? "left" : undefined}
|
| class={`${sidebarClass} ${transparentClass}`}
|
| headings={headings}
|
| />
|
| )}
|
| </div>
|
|
|
| {/* 主内容区 - 始终渲染,确保 Swup 容器存在 */}
|
| <main id="swup-container" class={`${mainContentClass} transition-main`}>
|
| {/* 携带网格布局类名,用于JS更新父容器 */}
|
| <div id="grid-class-carrier" data-grid-class={gridCols} class="hidden"></div>
|
| {/* 备用 h1 标题 - 当主页横幅文本未启用时,提供隐藏的主标题 */}
|
| {isHomePageCheck && !showHomeText && (
|
| <h1 class="sr-only">{siteConfig.title}</h1>
|
| )}
|
| <div id="content-wrapper" class="onload-animation transition-leaving">
|
| <slot></slot>
|
|
|
| </div>
|
| </main>
|
|
|
| {/* 右侧边栏 - 仅双侧边栏模式 */}
|
| {/*
|
| 如果全局配置为双侧栏(position: both),则使用静态容器(不被swup替换),避免闪烁。
|
| 如果全局配置为单侧栏(position: left),则使用动态容器(被swup替换),以便在文章页显示右侧栏。
|
| 注意:为了满足swup要求所有containers必须存在,我们在两种模式下都必须渲染 #right-sidebar-dynamic。
|
| 右侧边栏在1280px以下隐藏(CSS处理)。
|
| */}
|
| {sidebarLayoutConfig.position === "both" ? (
|
| <>
|
| <div id="right-sidebar-static" class="contents">
|
| {effectiveIsBothSidebars && effectiveHasRightComponents && (
|
| <SideBar side="right" class={`${rightSidebarClass} ${transparentClass}`} headings={headings}></SideBar>
|
| )}
|
| </div>
|
| <div id="right-sidebar-dynamic" class="hidden transition-swup-fade"></div>
|
| </>
|
| ) : (
|
| <div id="right-sidebar-dynamic" class="contents transition-swup-fade">
|
| {effectiveIsBothSidebars && effectiveHasRightComponents && (
|
| <SideBar side="right" class={`${rightSidebarClass} ${transparentClass}`} headings={headings}></SideBar>
|
| )}
|
| </div>
|
| )}
|
|
|
| {/* 移动端底部组件 - 768px及以下显示 */}
|
| {sidebarLayoutConfig.mobileBottomComponents && sidebarLayoutConfig.mobileBottomComponents.length > 0 && (
|
| <div id="mobile-bottom-sidebar" class="col-span-1 block md:hidden mt-4">
|
| <SideBar side="bottom" class={`${transparentClass}`} headings={headings}></SideBar>
|
| </div>
|
| )}
|
|
|
| <div class={footerClassName}>
|
| <Footer></Footer>
|
| </div>
|
| </div>
|
|
|
| <SpineModel></SpineModel>
|
| {live2dModelConfig.enable && <Live2DWidget config={live2dModelConfig} />}
|
| </div>
|
| </div>
|
|
|
| <FloatingControls headings={headings} />
|
|
|
|
|
| </Layout>
|
|
|
| <style is:global>
|
|
|
| #right-sidebar {
|
| transition: opacity 0.35s ease-in-out,
|
| transform 0.35s ease-in-out;
|
| }
|
|
|
|
|
| @keyframes swupFadeOut {
|
| from {
|
| opacity: 1;
|
| }
|
| to {
|
| opacity: 0.95;
|
| }
|
| }
|
|
|
|
|
| .banner-title {
|
| text-shadow: 0 4px 24px rgba(0, 0, 0, 0.6);
|
| }
|
|
|
| .banner-subtitle {
|
| text-shadow: 0 2px 16px rgba(0, 0, 0, 0.6);
|
| }
|
| </style>
|
|
|
|
|