t10015/components/products/ProductImageGallery.jsx

220 lines
7.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client';
import { useState, useEffect, useRef } from 'react';
const ProductImageGallery = ({ data }) => {
const [currentImageIndex, setCurrentImageIndex] = useState(0);
const thumbnailContainerRef = useRef(null);
// 如果没有attachments不显示组件
if (!data?.attachments || data.attachments.length === 0) {
return null;
}
// 找到主图在attachments中的索引
const getMainImageIndex = () => {
if (!data.image) return 0;
const mainImageIndex = data.attachments.findIndex(
attachment => attachment.file_url === data.image
);
return mainImageIndex >= 0 ? mainImageIndex : 0;
};
const mainImageIndex = getMainImageIndex();
const currentImage = data.attachments[currentImageIndex]?.file_url || data.attachments[0]?.file_url;
// 初始化当前索引为主图索引
useEffect(() => {
setCurrentImageIndex(mainImageIndex);
}, [mainImageIndex]);
const changeMainImage = (direction) => {
if (data.attachments.length <= 1) return;
if (direction === 'next') {
setCurrentImageIndex((prev) => (prev + 1) % data.attachments.length);
} else {
setCurrentImageIndex((prev) => (prev - 1 + data.attachments.length) % data.attachments.length);
}
};
const changeMainImageByIndex = (index) => {
setCurrentImageIndex(index);
};
// 智能滚动到缩略图 - 业内最佳实践
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'
});
};
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">
<img
src={currentImage}
alt={data?.title || 'Product Image'}
className="w-full max-w-[320px] md:max-w-full object-contain"
/>
{/* 导航箭头 - 简约大气设计 */}
{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"
onClick={() => changeMainImage('prev')}
aria-label="Previous image"
>
<svg
className="w-4 h-4 text-gray-600 group-hover:text-gray-800 transition-colors duration-200"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M15 19l-7-7 7-7" />
</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"
onClick={() => changeMainImage('next')}
aria-label="Next image"
>
<svg
className="w-4 h-4 text-gray-600 group-hover:text-gray-800 transition-colors duration-200"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
strokeWidth={2}
>
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
</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>
))}
</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 {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.flex-shrink-0:hover {
transform: scale(1.05);
}
/* 当前选中的缩略图样式 */
.border-2.border-blue-500 {
border-color: #3b82f6;
}
/* 导航按钮样式增强 */
.backdrop-blur-sm {
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.group:hover .backdrop-blur-sm {
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
/* 响应式优化 */
@media (max-width: 768px) {
.w-16.h-16 {
width: 4rem;
height: 4rem;
}
}
`}</style>
</div>
);
};
export default ProductImageGallery;