优化产品详情页缩略图效果
This commit is contained in:
parent
74699a172b
commit
bf0958db93
@ -1,9 +1,10 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
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) {
|
||||
@ -43,64 +44,174 @@ const ProductImageGallery = ({ data }) => {
|
||||
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="flex-shrink-0 w-full md:w-120 flex justify-center md:justify-start mb-4 md:mb-0 max-w-full">
|
||||
<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-4">
|
||||
<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-2 top-1/2 transform -translate-y-1/2 w-8 h-8 bg-white rounded-full border border-gray-300 flex items-center justify-center shadow-md hover:bg-gray-50 transition-colors z-10"
|
||||
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" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
||||
<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-2 top-1/2 transform -translate-y-1/2 w-8 h-8 bg-white rounded-full border border-gray-300 flex items-center justify-center shadow-md hover:bg-gray-50 transition-colors z-10"
|
||||
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" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
<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>
|
||||
|
||||
{/* 缩略图列表 - 直接显示attachments */}
|
||||
<div className="flex flex-wrap gap-1 justify-center md:justify-start">
|
||||
{data.attachments.map((attachment, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`w-16 h-16 border-1 rounded-lg overflow-hidden cursor-pointer transition-all hover:border-blue-500 ${
|
||||
index === currentImageIndex ? 'border-blue-500' : 'border-gray-200'
|
||||
}`}
|
||||
onClick={() => changeMainImageByIndex(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 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);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
>
|
||||
<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>
|
||||
);
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user