mirror of
https://github.com/StepanovPlaton/AboutMe.git
synced 2026-04-04 12:50:49 +04:00
364 lines
15 KiB
Plaintext
364 lines
15 KiB
Plaintext
---
|
|
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> |