基于swipper重构产品详情页缩略图切换效果
This commit is contained in:
parent
bf0958db93
commit
f161eda997
@ -136,7 +136,7 @@ export default async function Page({ params, searchParams }) {
|
||||
)}
|
||||
{/* 产品副标题 */}
|
||||
{data.subtitle && (
|
||||
<div className="bg-gray-50 whitespace-pre-line text-gray-700 w-full max-w-full prose prose-sm">
|
||||
<div className="bg-gray-50 whitespace-pre-line text-gray-700 w-full max-w-full prose prose-sm pt-6">
|
||||
{data.subtitle}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1,10 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Swiper, SwiperSlide } from "swiper/react";
|
||||
import { Navigation, Pagination, Thumbs, FreeMode } from "swiper/modules";
|
||||
|
||||
// 导入Swiper样式
|
||||
import "swiper/css";
|
||||
import "swiper/css/navigation";
|
||||
import "swiper/css/pagination";
|
||||
import "swiper/css/thumbs";
|
||||
import "swiper/css/free-mode";
|
||||
|
||||
const ProductImageGallery = ({ data }) => {
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0);
|
||||
const thumbnailContainerRef = useRef(null);
|
||||
const [thumbsSwiper, setThumbsSwiper] = useState(null);
|
||||
const thumbsSwiperRef = useRef(null);
|
||||
|
||||
// 如果没有attachments,不显示组件
|
||||
if (!data?.attachments || data.attachments.length === 0) {
|
||||
@ -30,6 +40,41 @@ const ProductImageGallery = ({ data }) => {
|
||||
setCurrentImageIndex(mainImageIndex);
|
||||
}, [mainImageIndex]);
|
||||
|
||||
// 键盘导航支持
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event) => {
|
||||
if (data.attachments.length <= 1) return;
|
||||
|
||||
switch (event.key) {
|
||||
case 'ArrowLeft':
|
||||
event.preventDefault();
|
||||
changeMainImage('prev');
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
event.preventDefault();
|
||||
changeMainImage('next');
|
||||
break;
|
||||
case 'Home':
|
||||
event.preventDefault();
|
||||
setCurrentImageIndex(0);
|
||||
break;
|
||||
case 'End':
|
||||
event.preventDefault();
|
||||
setCurrentImageIndex(data.attachments.length - 1);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 添加键盘事件监听器
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [data.attachments.length]);
|
||||
|
||||
const changeMainImage = (direction) => {
|
||||
if (data.attachments.length <= 1) return;
|
||||
|
||||
@ -42,149 +87,201 @@ const ProductImageGallery = ({ data }) => {
|
||||
|
||||
const changeMainImageByIndex = (index) => {
|
||||
setCurrentImageIndex(index);
|
||||
|
||||
// 智能滚动缩略图到可见区域
|
||||
if (thumbsSwiperRef.current && thumbsSwiperRef.current.swiper) {
|
||||
const swiper = thumbsSwiperRef.current.swiper;
|
||||
const slideIndex = index;
|
||||
|
||||
// 计算目标滚动位置
|
||||
const slideWidth = 64; // 64px 缩略图宽度
|
||||
const spaceBetween = 16; // 16px 间距
|
||||
const totalSlideWidth = slideWidth + spaceBetween;
|
||||
|
||||
// 获取当前可见的slides数量
|
||||
const visibleSlides = swiper.params.slidesPerView;
|
||||
|
||||
// 计算当前滚动位置
|
||||
const currentTranslate = swiper.translate;
|
||||
const currentSlideIndex = Math.round(Math.abs(currentTranslate) / totalSlideWidth);
|
||||
|
||||
// 判断需要向左还是向右滚动
|
||||
let targetSlideIndex;
|
||||
|
||||
if (slideIndex < currentSlideIndex) {
|
||||
// 向左滚动:让选中的缩略图显示在左侧
|
||||
targetSlideIndex = Math.max(0, slideIndex - 1);
|
||||
} else if (slideIndex >= currentSlideIndex + visibleSlides) {
|
||||
// 向右滚动:让选中的缩略图显示在右侧
|
||||
targetSlideIndex = slideIndex - visibleSlides + 1;
|
||||
} else {
|
||||
// 当前缩略图已经在可见区域内,不需要滚动
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算目标滚动位置
|
||||
const targetTranslate = targetSlideIndex * totalSlideWidth;
|
||||
|
||||
// 确保不超出边界
|
||||
const maxTranslate = swiper.maxTranslate();
|
||||
const finalTranslate = Math.max(0, Math.min(targetTranslate, maxTranslate));
|
||||
|
||||
// 平滑滚动到目标位置
|
||||
swiper.slideTo(Math.round(finalTranslate / totalSlideWidth), 300);
|
||||
}
|
||||
};
|
||||
|
||||
// 智能滚动到缩略图 - 业内最佳实践
|
||||
const scrollToThumbnail = (index) => {
|
||||
if (!thumbnailContainerRef.current) return;
|
||||
|
||||
const container = thumbnailContainerRef.current;
|
||||
const containerWidth = container.clientWidth;
|
||||
const thumbnailWidth = 64; // 64px 缩略图宽度
|
||||
const gap = 16; // 16px 间距
|
||||
const totalThumbnailWidth = thumbnailWidth + gap;
|
||||
|
||||
// 计算一次能显示多少个缩略图(类似Swiper的slidesPerView)
|
||||
const visibleCount = Math.floor(containerWidth / totalThumbnailWidth);
|
||||
|
||||
// 计算目标滚动位置
|
||||
let scrollLeft;
|
||||
|
||||
if (index < visibleCount / 2) {
|
||||
// 如果是前几个,滚动到开头
|
||||
scrollLeft = 0;
|
||||
} else if (index >= data.attachments.length - visibleCount / 2) {
|
||||
// 如果是后几个,滚动到末尾
|
||||
scrollLeft = container.scrollWidth - containerWidth;
|
||||
} else {
|
||||
// 居中显示
|
||||
scrollLeft = (index - Math.floor(visibleCount / 2)) * totalThumbnailWidth;
|
||||
}
|
||||
|
||||
// 确保滚动位置在有效范围内
|
||||
scrollLeft = Math.max(0, Math.min(scrollLeft, container.scrollWidth - containerWidth));
|
||||
|
||||
container.scrollTo({
|
||||
left: scrollLeft,
|
||||
behavior: 'smooth'
|
||||
});
|
||||
};
|
||||
// 缩略图导航控制 - 移除这些函数,不再需要
|
||||
// const goToPrevThumbs = () => {
|
||||
// if (thumbsSwiperRef.current && thumbsSwiperRef.current.swiper) {
|
||||
// thumbsSwiperRef.current.swiper.slidePrev();
|
||||
// }
|
||||
// };
|
||||
|
||||
// const goToNextThumbs = () => {
|
||||
// if (thumbsSwiperRef.current && thumbsSwiperRef.current.swiper) {
|
||||
// if (thumbsSwiperRef.current && thumbsSwiperRef.current.swiper) {
|
||||
// thumbsSwiperRef.current.swiper.slideNext();
|
||||
// }
|
||||
// }
|
||||
// };
|
||||
|
||||
return (
|
||||
<div className="w-full md:w-120 flex justify-center md:justify-start mb-4 md:mb-0 max-w-full">
|
||||
<div className="relative w-full max-w-[320px] md:max-w-full">
|
||||
{/* 主图显示区域 */}
|
||||
<div className="relative overflow-hidden rounded-xl shadow-lg mb-6">
|
||||
<div className="relative overflow-hidden rounded-xl shadow-lg mb-6 group/main-image">
|
||||
<img
|
||||
src={currentImage}
|
||||
alt={data?.title || 'Product Image'}
|
||||
className="w-full max-w-[320px] md:max-w-full object-contain"
|
||||
loading="lazy"
|
||||
/>
|
||||
|
||||
{/* 导航箭头 - 简约大气设计 */}
|
||||
{/* 图片计数器 */}
|
||||
{data.attachments.length > 1 && (
|
||||
<div className="absolute bottom-3 right-3 bg-black/60 text-white text-xs px-2 py-1 rounded-full backdrop-blur-sm">
|
||||
{currentImageIndex + 1} / {data.attachments.length}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 导航箭头 - 悬浮时显示 */}
|
||||
{data.attachments.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
className="absolute left-3 top-1/2 transform -translate-y-1/2 w-10 h-10 bg-white/90 backdrop-blur-sm rounded-full border border-gray-100 flex items-center justify-center shadow-lg hover:bg-white hover:shadow-xl transition-all duration-300 z-10 group"
|
||||
className="absolute left-2 top-1/2 transform -translate-y-1/2 w-12 h-12 bg-white/40 backdrop-blur-md rounded-full border-0 flex items-center justify-center shadow-[0_8px_32px_rgba(0,0,0,0.08)] hover:bg-white/60 hover:shadow-[0_12px_40px_rgba(0,0,0,0.12)] transition-all duration-500 ease-out z-10 focus:outline-none focus:ring-2 focus:ring-white/30 focus:ring-offset-2 focus:ring-offset-transparent opacity-0 group-hover/main-image:opacity-100 scale-90 group-hover/main-image:scale-100"
|
||||
onClick={() => changeMainImage('prev')}
|
||||
aria-label="Previous image"
|
||||
aria-label={`上一张图片 (${currentImageIndex === 0 ? data.attachments.length : currentImageIndex} / ${data.attachments.length})`}
|
||||
title="上一张图片 (←)"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-600 group-hover:text-gray-800 transition-colors duration-200"
|
||||
className="w-5 h-5 text-gray-700 hover:text-gray-900 transition-all duration-500 ease-out hover:scale-110"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={2}
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
|
||||
<path d="M15 18l-6-6 6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="absolute right-3 top-1/2 transform -translate-y-1/2 w-10 h-10 bg-white/90 backdrop-blur-sm rounded-full border border-gray-100 flex items-center justify-center shadow-lg hover:bg-white hover:shadow-xl transition-all duration-300 z-10 group"
|
||||
className="absolute right-2 top-1/2 transform -translate-y-1/2 w-12 h-12 bg-white/40 backdrop-blur-md rounded-full border-0 flex items-center justify-center shadow-[0_8px_32px_rgba(0,0,0,0.08)] hover:bg-white/60 hover:shadow-[0_12px_40px_rgba(0,0,0,0.12)] transition-all duration-500 ease-out z-10 focus:outline-none focus:ring-2 focus:ring-white/30 focus:ring-offset-2 focus:ring-offset-transparent opacity-0 group-hover/main-image:opacity-100 scale-90 group-hover/main-image:scale-100"
|
||||
onClick={() => changeMainImage('next')}
|
||||
aria-label="Next image"
|
||||
aria-label={`下一张图片 (${currentImageIndex === data.attachments.length - 1 ? 1 : currentImageIndex + 2} / ${data.attachments.length})`}
|
||||
title="下一张图片 (→)"
|
||||
>
|
||||
<svg
|
||||
className="w-4 h-4 text-gray-600 group-hover:text-gray-800 transition-colors duration-200"
|
||||
className="w-5 h-5 text-gray-700 hover:text-gray-900 transition-all duration-500 ease-out hover:scale-110"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={2}
|
||||
strokeWidth={1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
||||
<path d="M9 18l6-6-6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 缩略图列表 - 业内最佳实践,确保完整显示 */}
|
||||
<div className="relative">
|
||||
<div
|
||||
ref={thumbnailContainerRef}
|
||||
className="flex gap-4 overflow-x-auto pb-2"
|
||||
style={{
|
||||
scrollbarWidth: 'none',
|
||||
msOverflowStyle: 'none',
|
||||
WebkitOverflowScrolling: 'touch'
|
||||
}}
|
||||
>
|
||||
{data.attachments.map((attachment, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`flex-shrink-0 w-16 h-16 rounded-lg overflow-hidden cursor-pointer transition-all duration-300 hover:scale-105 ${
|
||||
index === currentImageIndex
|
||||
? 'border-2 border-blue-500'
|
||||
: 'border border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
onClick={() => {
|
||||
changeMainImageByIndex(index);
|
||||
scrollToThumbnail(index);
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={attachment.file_url}
|
||||
alt={`${data?.title || 'Product'} - ${index + 1}`}
|
||||
className="w-full h-full object-cover"
|
||||
onError={(e) => {
|
||||
console.error('Image load error:', attachment.file_url);
|
||||
e.target.style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{/* 缩略图列表 - 使用Swiper实现最佳滚动效果 */}
|
||||
{data.attachments.length > 1 && (
|
||||
<div className="relative">
|
||||
<Swiper
|
||||
ref={thumbsSwiperRef}
|
||||
onSwiper={setThumbsSwiper}
|
||||
spaceBetween={16}
|
||||
slidesPerView="auto"
|
||||
freeMode={true}
|
||||
watchSlidesProgress={true}
|
||||
modules={[FreeMode, Navigation, Pagination]}
|
||||
className="thumbs-swiper"
|
||||
grabCursor={true}
|
||||
resistance={true}
|
||||
resistanceRatio={0.85}
|
||||
breakpoints={{
|
||||
0: { slidesPerView: 3, spaceBetween: 12 },
|
||||
480: { slidesPerView: 4, spaceBetween: 16 },
|
||||
768: { slidesPerView: 5, spaceBetween: 16 },
|
||||
1024: { slidesPerView: 6, spaceBetween: 16 }
|
||||
}}
|
||||
>
|
||||
{data.attachments.map((attachment, index) => (
|
||||
<SwiperSlide
|
||||
key={index}
|
||||
className="!w-16 !h-16"
|
||||
onClick={() => changeMainImageByIndex(index)}
|
||||
>
|
||||
<div
|
||||
className={`w-full h-full rounded-lg overflow-hidden cursor-pointer transition-all duration-300 hover:scale-105 focus-within:ring-2 focus-within:ring-blue-500 focus-within:ring-offset-2 ${
|
||||
index === currentImageIndex
|
||||
? 'border-2 border-blue-500 ring-2 ring-blue-200'
|
||||
: 'border border-gray-200 hover:border-gray-300'
|
||||
}`}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
aria-label={`查看图片 ${index + 1} / ${data.attachments.length}`}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
changeMainImageByIndex(index);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={attachment.file_url}
|
||||
alt={`${data?.title || 'Product'} - ${index + 1}`}
|
||||
className="w-full h-full object-cover"
|
||||
loading="lazy"
|
||||
onError={(e) => {
|
||||
console.error('Image load error:', attachment.file_url);
|
||||
e.target.style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
{/* 缩略图加载状态指示 */}
|
||||
<div className="absolute inset-0 bg-gray-100 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
||||
<div className="w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin"></div>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
))}
|
||||
</Swiper>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 内联样式 */}
|
||||
<style jsx>{`
|
||||
/* 隐藏滚动条 */
|
||||
.overflow-x-auto::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.overflow-x-auto {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
/* 缩略图悬停效果 */
|
||||
.flex-shrink-0 {
|
||||
.thumbs-swiper .swiper-slide {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.flex-shrink-0:hover {
|
||||
.thumbs-swiper .swiper-slide:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
@ -193,6 +290,10 @@ const ProductImageGallery = ({ data }) => {
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.ring-2.ring-blue-200 {
|
||||
box-shadow: 0 0 0 2px rgba(191, 219, 254, 0.5);
|
||||
}
|
||||
|
||||
/* 导航按钮样式增强 */
|
||||
.backdrop-blur-sm {
|
||||
backdrop-filter: blur(8px);
|
||||
@ -206,9 +307,94 @@ const ProductImageGallery = ({ data }) => {
|
||||
|
||||
/* 响应式优化 */
|
||||
@media (max-width: 768px) {
|
||||
.w-16.h-16 {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
.thumbs-swiper .swiper-slide {
|
||||
width: 4rem !important;
|
||||
height: 4rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* 隐藏Swiper默认滚动条 */
|
||||
.thumbs-swiper .swiper-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* 优化Swiper滑动体验 */
|
||||
.thumbs-swiper {
|
||||
touch-action: pan-y pinch-zoom;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.thumbs-swiper .swiper-slide {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* 加载状态动画 */
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* 焦点状态优化 */
|
||||
.focus-within\\:ring-2:focus-within {
|
||||
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
|
||||
}
|
||||
|
||||
.focus-within\\:ring-blue-500:focus-within {
|
||||
--tw-ring-opacity: 1;
|
||||
--tw-ring-color: rgb(59 130 246 / var(--tw-ring-opacity));
|
||||
}
|
||||
|
||||
.focus-within\\:ring-offset-2:focus-within {
|
||||
--tw-ring-offset-width: 2px;
|
||||
}
|
||||
|
||||
/* 触摸设备优化 */
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.thumbs-swiper .swiper-slide:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 强制导航箭头为圆形 */
|
||||
.group\/main-image button {
|
||||
border-radius: 50% !important;
|
||||
-webkit-border-radius: 50% !important;
|
||||
}
|
||||
|
||||
/* 高对比度模式支持 */
|
||||
@media (prefers-contrast: high) {
|
||||
.border-gray-200 {
|
||||
border-color: #000;
|
||||
}
|
||||
|
||||
.border-blue-500 {
|
||||
border-color: #0066cc;
|
||||
}
|
||||
|
||||
.text-gray-600 {
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
|
||||
/* 减少动画偏好 */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.thumbs-swiper .swiper-slide,
|
||||
.backdrop-blur-sm {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.animate-spin {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user