331 lines
9.8 KiB
Plaintext
331 lines
9.8 KiB
Plaintext
---
|
|
interface Props {
|
|
autoplay?: boolean;
|
|
autoplaySpeed?: number; // 毫秒
|
|
showArrows?: boolean;
|
|
showDots?: boolean;
|
|
cardGap?: number; // px
|
|
className?: string;
|
|
}
|
|
|
|
const {
|
|
autoplay = true,
|
|
autoplaySpeed = 5000,
|
|
showArrows = true,
|
|
showDots = true,
|
|
cardGap = 16,
|
|
className = ""
|
|
} = Astro.props;
|
|
|
|
const carouselId = `carousel-${Math.random().toString(36).substring(2, 11)}`;
|
|
---
|
|
|
|
<div class={`smooth-card-carousel relative overflow-hidden group ${className}`} id={carouselId}>
|
|
<!-- 轮播容器 -->
|
|
<div class="carousel-container relative">
|
|
<!-- 轮播轨道 -->
|
|
<div class="carousel-track flex transition-transform duration-500 ease-out cursor-grab">
|
|
<slot />
|
|
</div>
|
|
|
|
<!-- 控制按钮 -->
|
|
{showArrows && (
|
|
<div class="carousel-controls">
|
|
<button class="carousel-prev absolute left-4 top-1/2 -translate-y-1/2 z-10 bg-white bg-opacity-70 dark:bg-color-dark-card dark:bg-opacity-70 rounded-full p-2 shadow-md opacity-40 group-hover:opacity-100 transition-opacity hover:bg-opacity-90 dark:hover:bg-opacity-90 focus:outline-none hover:scale-110">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-700 dark:text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
|
|
</svg>
|
|
<span class="sr-only">上一个</span>
|
|
</button>
|
|
<button class="carousel-next absolute right-4 top-1/2 -translate-y-1/2 z-10 bg-white bg-opacity-70 dark:bg-color-dark-card dark:bg-opacity-70 rounded-full p-2 shadow-md opacity-40 group-hover:opacity-100 transition-opacity hover:bg-opacity-90 dark:hover:bg-opacity-90 focus:outline-none hover:scale-110">
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-gray-700 dark:text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
</svg>
|
|
<span class="sr-only">下一个</span>
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
<!-- 指示点 -->
|
|
{showDots && (
|
|
<div class="carousel-dots absolute bottom-3 left-0 right-0 flex justify-center space-x-2 z-10">
|
|
<!-- 指示点将通过JS动态生成 -->
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<style define:vars={{ cardGap: `${cardGap}px` }}>
|
|
.smooth-card-carousel {
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.carousel-track {
|
|
display: flex;
|
|
transition: transform 0.5s ease-out;
|
|
gap: var(--cardGap);
|
|
padding: 8px 4px; /* 添加内边距,让阴影效果完全显示 */
|
|
}
|
|
|
|
.carousel-track.grabbing {
|
|
cursor: grabbing;
|
|
transition: none;
|
|
}
|
|
|
|
.carousel-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background-color: rgba(255, 255, 255, 0.5);
|
|
transition: all 0.3s ease;
|
|
}
|
|
|
|
.carousel-dot.active {
|
|
width: 10px;
|
|
height: 10px;
|
|
background-color: white;
|
|
}
|
|
|
|
/* 淡入淡出效果 */
|
|
.carousel-track > * {
|
|
transition: opacity 0.3s ease, transform 0.3s ease;
|
|
}
|
|
|
|
.carousel-track .card-faded {
|
|
opacity: 0.6;
|
|
transform: scale(0.95);
|
|
}
|
|
|
|
/* 导航按钮悬停效果 */
|
|
.carousel-prev,
|
|
.carousel-next {
|
|
transition: opacity 0.3s ease, transform 0.2s ease, background-color 0.3s ease;
|
|
}
|
|
|
|
.carousel-container::before,
|
|
.carousel-container::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
width: 60px;
|
|
z-index: 5;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.carousel-container::before {
|
|
left: 0;
|
|
background: linear-gradient(to right, rgba(255,255,255,0.5), transparent);
|
|
}
|
|
|
|
.carousel-container::after {
|
|
right: 0;
|
|
background: linear-gradient(to left, rgba(255,255,255,0.5), transparent);
|
|
}
|
|
|
|
/* 暗黑模式下的渐变适配 */
|
|
[data-theme='dark'] .carousel-container::before {
|
|
background: linear-gradient(to right, rgba(0,0,0,0.3), transparent);
|
|
}
|
|
|
|
[data-theme='dark'] .carousel-container::after {
|
|
background: linear-gradient(to left, rgba(0,0,0,0.3), transparent);
|
|
}
|
|
</style>
|
|
|
|
<script define:vars={{ carouselId, autoplay, autoplaySpeed }}>
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const carousel = document.getElementById(carouselId);
|
|
if (!carousel) return;
|
|
|
|
const track = carousel.querySelector('.carousel-track');
|
|
const cards = Array.from(track.children);
|
|
const carouselContainer = carousel.querySelector('.carousel-container');
|
|
const prevBtn = carousel.querySelector('.carousel-prev');
|
|
const nextBtn = carousel.querySelector('.carousel-next');
|
|
const dotsContainer = carousel.querySelector('.carousel-dots');
|
|
|
|
let currentIndex = 0;
|
|
let startX, startScrollLeft, cardWidth, totalCards, visibleCards;
|
|
let autoplayInterval;
|
|
let isDragging = false;
|
|
|
|
// 初始化
|
|
const initialize = () => {
|
|
if (cards.length === 0) return;
|
|
|
|
// 计算一张卡片的宽度(包括间距)
|
|
cardWidth = cards[0].offsetWidth + parseInt(getComputedStyle(track).gap);
|
|
|
|
// 计算可见卡片数量和总卡片数
|
|
visibleCards = Math.floor(carouselContainer.offsetWidth / cardWidth);
|
|
totalCards = cards.length;
|
|
|
|
// 克隆卡片以实现无限滚动
|
|
if (totalCards < visibleCards * 2) {
|
|
for (let i = 0; i < totalCards; i++) {
|
|
const clone = cards[i].cloneNode(true);
|
|
track.appendChild(clone);
|
|
}
|
|
// 更新卡片数组
|
|
cards.push(...Array.from(track.children).slice(totalCards));
|
|
totalCards = cards.length;
|
|
}
|
|
|
|
// 创建指示点
|
|
if (dotsContainer) {
|
|
dotsContainer.innerHTML = '';
|
|
const numDots = Math.ceil(totalCards / visibleCards);
|
|
|
|
for (let i = 0; i < numDots; i++) {
|
|
const dot = document.createElement('button');
|
|
dot.classList.add('carousel-dot');
|
|
dot.setAttribute('aria-label', `滑动到第${i + 1}组卡片`);
|
|
|
|
if (i === 0) dot.classList.add('active');
|
|
|
|
dot.addEventListener('click', () => {
|
|
goToSlide(i * visibleCards);
|
|
});
|
|
|
|
dotsContainer.appendChild(dot);
|
|
}
|
|
}
|
|
|
|
// 开始自动播放
|
|
if (autoplay) {
|
|
startAutoplay();
|
|
}
|
|
|
|
applyCardStyles();
|
|
};
|
|
|
|
// 设置卡片样式(淡入淡出效果)
|
|
const applyCardStyles = () => {
|
|
cards.forEach((card, index) => {
|
|
if (index < currentIndex || index >= currentIndex + visibleCards) {
|
|
card.classList.add('card-faded');
|
|
} else {
|
|
card.classList.remove('card-faded');
|
|
}
|
|
});
|
|
};
|
|
|
|
// 滚动到指定滑块
|
|
const goToSlide = (index) => {
|
|
// 防止越界
|
|
if (index < 0) {
|
|
index = totalCards - visibleCards;
|
|
} else if (index >= totalCards) {
|
|
index = 0;
|
|
}
|
|
|
|
currentIndex = index;
|
|
track.style.transform = `translateX(-${currentIndex * cardWidth}px)`;
|
|
|
|
// 更新指示点
|
|
if (dotsContainer) {
|
|
const dots = Array.from(dotsContainer.children);
|
|
const activeDotIndex = Math.floor(currentIndex / visibleCards);
|
|
|
|
dots.forEach((dot, i) => {
|
|
if (i === activeDotIndex) {
|
|
dot.classList.add('active');
|
|
} else {
|
|
dot.classList.remove('active');
|
|
}
|
|
});
|
|
}
|
|
|
|
applyCardStyles();
|
|
};
|
|
|
|
// 下一张
|
|
const nextSlide = () => {
|
|
goToSlide(currentIndex + visibleCards);
|
|
};
|
|
|
|
// 上一张
|
|
const prevSlide = () => {
|
|
goToSlide(currentIndex - visibleCards);
|
|
};
|
|
|
|
// 鼠标/触摸事件处理
|
|
const dragStart = (e) => {
|
|
isDragging = true;
|
|
startX = e.type.includes('mouse') ? e.pageX : e.touches[0].pageX;
|
|
startScrollLeft = currentIndex * cardWidth;
|
|
|
|
track.classList.add('grabbing');
|
|
|
|
// 暂停自动播放
|
|
if (autoplay) {
|
|
clearInterval(autoplayInterval);
|
|
}
|
|
};
|
|
|
|
const dragging = (e) => {
|
|
if (!isDragging) return;
|
|
|
|
e.preventDefault();
|
|
const x = e.type.includes('mouse') ? e.pageX : e.touches[0].pageX;
|
|
const walk = startX - x;
|
|
|
|
track.style.transform = `translateX(-${startScrollLeft + walk}px)`;
|
|
};
|
|
|
|
const dragEnd = () => {
|
|
if (!isDragging) return;
|
|
isDragging = false;
|
|
|
|
track.classList.remove('grabbing');
|
|
|
|
const threshold = cardWidth / 3;
|
|
const walk = parseInt(track.style.transform.match(/-?\d+/) || 0);
|
|
const targetIndex = Math.round(walk / cardWidth);
|
|
|
|
goToSlide(targetIndex);
|
|
|
|
// 重新开始自动播放
|
|
if (autoplay) {
|
|
startAutoplay();
|
|
}
|
|
};
|
|
|
|
// 自动播放
|
|
const startAutoplay = () => {
|
|
if (autoplayInterval) clearInterval(autoplayInterval);
|
|
autoplayInterval = setInterval(nextSlide, autoplaySpeed);
|
|
};
|
|
|
|
// 事件监听
|
|
if (prevBtn) prevBtn.addEventListener('click', prevSlide);
|
|
if (nextBtn) nextBtn.addEventListener('click', nextSlide);
|
|
|
|
// 鼠标拖动
|
|
track.addEventListener('mousedown', dragStart);
|
|
window.addEventListener('mousemove', dragging);
|
|
window.addEventListener('mouseup', dragEnd);
|
|
|
|
// 触摸拖动
|
|
track.addEventListener('touchstart', dragStart);
|
|
window.addEventListener('touchmove', dragging);
|
|
window.addEventListener('touchend', dragEnd);
|
|
|
|
// 鼠标悬停时暂停自动播放
|
|
carousel.addEventListener('mouseenter', () => {
|
|
if (autoplay) clearInterval(autoplayInterval);
|
|
});
|
|
|
|
carousel.addEventListener('mouseleave', () => {
|
|
if (autoplay) startAutoplay();
|
|
});
|
|
|
|
// 窗口调整大小时重新初始化
|
|
window.addEventListener('resize', initialize);
|
|
|
|
// 初始化
|
|
initialize();
|
|
});
|
|
</script> |