mirror of
https://github.com/StepanovPlaton/AboutMe.git
synced 2026-04-04 12:50:49 +04:00
Initial commit
This commit is contained in:
364
src/components/banner.astro
Normal file
364
src/components/banner.astro
Normal 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>
|
||||
Reference in New Issue
Block a user