Initial commit

This commit is contained in:
2026-02-02 22:47:52 +03:00
committed by GitHub
commit f53016aeda
239 changed files with 84360 additions and 0 deletions

364
src/components/banner.astro Normal file
View File

@@ -0,0 +1,364 @@
---
import type { SiteConfig } from "@/types/config";
import { BANNER_HEIGHT_EXTEND } from "@constants/constants";
import TypewriterText from "@components/common/typewriterText.astro";
import ImageWrapper from "@components/common/imageWrapper.astro";
interface Props {
config: SiteConfig["wallpaper"];
isHomePage: boolean;
class?: string;
}
const { config, isHomePage, class: className } = Astro.props;
// 获取当前设备类型的图片源
const getImageSources = () => {
const toArray = (src: any) => [src || []].flat();
const { src } = config;
const isObj = src && typeof src === "object" && !Array.isArray(src);
const desktop = toArray(isObj ? (src as any).desktop : src);
const mobile = toArray(isObj ? (src as any).mobile : src);
return {
desktop: desktop.length > 0 ? desktop : mobile,
mobile: mobile.length > 0 ? mobile : desktop,
};
}
const imageSources = getImageSources();
// 轮播配置
const carouselConfig = config.carousel;
const isCarouselEnabled = imageSources.desktop.length > 1 || imageSources.mobile.length > 1;
const carouselInterval = carouselConfig?.interval || 6
// 样式配置
const showHomeText = config.banner?.homeText?.enable && isHomePage;
const showWaves = config.banner?.waves?.enable;
const isPerformanceMode = config.banner?.waves?.performanceMode;
---
<!-- Banner Wrapper -->
<div
id="banner-wrapper"
class:list={[
"absolute z-10 w-full transition-all duration-600 overflow-hidden",
className
]}
style={`top: -${BANNER_HEIGHT_EXTEND}vh`}
>
{isCarouselEnabled ? (
<div id="banner-carousel" class="relative h-full w-full" data-carousel-config={JSON.stringify(carouselConfig)}>
<ul class="carousel-list h-full w-full">
{imageSources.desktop.map((src, index) => (
<li class:list={[
"carousel-item desktop-item hidden md:block absolute inset-0 transition-opacity duration-1200",
index === 0 ? 'opacity-100' : 'opacity-0',
carouselConfig?.kenBurns !== false ? 'ken-burns-enabled' : ''
]}>
<ImageWrapper
alt={`Desktop banner ${index + 1}`}
class:list={["object-cover h-full w-full"]}
src={src}
position={config.position}
loading={index === 0 ? "eager" : "lazy"}
/>
</li>
))}
{imageSources.mobile.map((src, index) => (
<li class:list={[
"carousel-item mobile-item block md:hidden absolute inset-0 transition-opacity duration-1200",
index === 0 ? 'opacity-100' : 'opacity-0',
carouselConfig?.kenBurns !== false ? 'ken-burns-enabled' : ''
]}>
<ImageWrapper
alt={`Mobile banner ${index + 1}`}
class:list={["object-cover h-full w-full"]}
src={src}
position={config.position}
loading={index === 0 ? "eager" : "lazy"}
/>
</li>
))}
</ul>
</div>
) : (
<div class="relative h-full w-full">
<ImageWrapper
alt="Mobile banner"
class:list={["block md:hidden object-cover h-full w-full transition duration-600 opacity-100"]}
src={imageSources.mobile[0] || imageSources.desktop[0] || ''}
position={config.position}
loading="eager"
/>
<ImageWrapper
id="banner"
alt="Desktop banner"
class:list={["hidden md:block object-cover h-full w-full transition duration-600 opacity-100"]}
src={imageSources.desktop[0] || imageSources.mobile[0] || ''}
position={config.position}
loading="eager"
/>
</div>
)}
<!-- Home page text overlay -->
{config.banner?.homeText?.enable && (
<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">
{config.banner?.homeText?.title && (
<h1 class="banner-title text-6xl lg:text-8xl text-white mb-2 lg:mb-4">
{config.banner.homeText.title}
</h1>
)}
{config.banner?.homeText?.subtitle && (
<h2 class="banner-subtitle text-xl lg:text-3xl text-white/90">
{config.banner.homeText.typewriter?.enable ? (
<TypewriterText
text={config.banner.homeText.subtitle}
speed={config.banner.homeText.typewriter.speed}
deleteSpeed={config.banner.homeText.typewriter.deleteSpeed}
pauseTime={config.banner.homeText.typewriter.pauseTime}
/>
) : (
Array.isArray(config.banner.homeText.subtitle)
? config.banner.homeText.subtitle[0]
: config.banner.homeText.subtitle
)}
</h2>
)}
</div>
</div>
</div>
)}
<!-- Water waves effect -->
{showWaves && (
<div class="waves-container absolute -bottom-px h-[10vh] max-h-37.5 min-h-12.5 w-full md:h-[15vh]" id="header-waves">
<svg
class="waves"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 21 150 30"
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">
{isPerformanceMode ? (
// 性能模式:只渲染二个波浪层
<use
xlink:href="#gentle-wave"
x="48"
y="7"
class="fill-(--page-bg)"
></use>
<use
xlink:href="#gentle-wave"
x="48"
y="5"
class="opacity-75 fill-(--page-bg)"
></use>
) : (
// 正常模式:渲染四个波浪层
<>
<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-75 fill-(--page-bg)"
></use>
<use
xlink:href="#gentle-wave"
x="48"
y="7"
class="fill-(--page-bg)"
></use>
</>
)}
</g>
</svg>
</div>
)}
</div>
<script define:vars={{ carouselInterval }}>
// 轮播图初始化函数
window.initBannerCarousel = function() {
const carousel = document.getElementById('banner-carousel');
if (!carousel) return;
// 检查是否是同一个 DOM 元素且定时器正在运行,避免重复初始化导致动画重置
if (window.bannerCarouselTimer && window.currentBannerCarousel === carousel) {
return;
}
window.currentBannerCarousel = carousel;
// 初始化全局状态
if (!window.bannerCarouselState) {
window.bannerCarouselState = {
currentIndex: 0,
lastSwitchTime: Date.now()
};
}
// 清理旧的定时器,防止重复初始化导致的闪烁
if (window.bannerCarouselTimer) {
clearTimeout(window.bannerCarouselTimer);
window.bannerCarouselTimer = null;
}
const carouselConfigData = carousel.getAttribute('data-carousel-config');
const carouselConfig = carouselConfigData ? JSON.parse(carouselConfigData) : {};
const desktopItems = carousel.querySelectorAll('.carousel-item.desktop-item');
const mobileItems = carousel.querySelectorAll('.carousel-item.mobile-item');
function setupCarousel(items, isEnabled) {
if (items.length === 0) return null;
// 如果禁用了轮播但有多张图,则随机显示一张
if (items.length > 1 && !isEnabled) {
items.forEach(item => {
item.classList.add('opacity-0');
item.classList.remove('opacity-100');
});
const randomIndex = Math.floor(Math.random() * items.length);
const randomItem = items[randomIndex];
randomItem.classList.add('opacity-100');
randomItem.classList.remove('opacity-0');
return null;
}
if (items.length > 1 && isEnabled) {
// 使用全局状态中的索引
let currentIndex = window.bannerCarouselState.currentIndex;
// 确保索引有效
if (currentIndex >= items.length) {
currentIndex = 0;
window.bannerCarouselState.currentIndex = 0;
}
function switchToSlide(index) {
const currentItem = items[currentIndex];
currentItem.classList.remove('opacity-100');
currentItem.classList.add('opacity-0');
currentIndex = index;
// 更新全局状态
window.bannerCarouselState.currentIndex = index;
window.bannerCarouselState.lastSwitchTime = Date.now();
const nextItem = items[currentIndex];
nextItem.classList.add('opacity-100');
nextItem.classList.remove('opacity-0');
// 通过切换 is-animating 类来重置下一张图片的动画
if (nextItem.classList.contains('ken-burns-enabled')) {
nextItem.classList.remove('is-animating');
nextItem.offsetHeight; // 强制重绘
nextItem.classList.add('is-animating');
}
}
// 初始化:根据当前索引显示图片
items.forEach((item, index) => {
if (index === currentIndex) {
item.classList.add('opacity-100');
item.classList.remove('opacity-0');
// 初始图片开启动画
if (item.classList.contains('ken-burns-enabled')) {
item.classList.add('is-animating');
}
} else {
item.classList.add('opacity-0');
item.classList.remove('opacity-100');
item.classList.remove('is-animating');
}
});
return {
next: () => switchToSlide((currentIndex + 1) % items.length),
prev: () => switchToSlide((currentIndex - 1 + items.length) % items.length),
};
}
return null;
}
const desktopCtrl = setupCarousel(desktopItems, carouselConfig?.enable);
const mobileCtrl = setupCarousel(mobileItems, carouselConfig?.enable);
if (carouselConfig?.enable && (desktopCtrl || mobileCtrl)) {
function startCarousel() {
if (window.bannerCarouselTimer) clearTimeout(window.bannerCarouselTimer);
const runLoop = () => {
const now = Date.now();
const intervalMs = carouselInterval * 1000;
const elapsed = now - window.bannerCarouselState.lastSwitchTime;
let delay = intervalMs - elapsed;
if (delay < 0) delay = 0;
window.bannerCarouselTimer = setTimeout(() => {
desktopCtrl?.next();
mobileCtrl?.next();
runLoop();
}, delay);
};
runLoop();
}
startCarousel();
}
}
// Banner显示控制函数
function showBanner() {
requestAnimationFrame(() => {
const banner = document.getElementById('banner');
if (banner) {
banner.classList.remove('opacity-0');
}
const mobileBanner = document.querySelector('.block.lg\\:hidden[alt="Mobile banner"]');
if (mobileBanner && !document.getElementById('banner-carousel')) {
mobileBanner.classList.remove('opacity-0');
mobileBanner.classList.add('opacity-100');
}
const carousel = document.getElementById('banner-carousel');
if (carousel) {
window.initBannerCarousel();
}
});
}
// 监听 Astro 页面切换事件
document.addEventListener('astro:after-swap', () => {
showBanner();
});
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', showBanner);
} else {
showBanner();
}
</script>