优化产品详情页缩略图效果
This commit is contained in:
parent
74699a172b
commit
bf0958db93
@ -1,9 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
const ProductImageGallery = ({ data }) => {
|
const ProductImageGallery = ({ data }) => {
|
||||||
const [currentImageIndex, setCurrentImageIndex] = useState(0);
|
const [currentImageIndex, setCurrentImageIndex] = useState(0);
|
||||||
|
const thumbnailContainerRef = useRef(null);
|
||||||
|
|
||||||
// 如果没有attachments,不显示组件
|
// 如果没有attachments,不显示组件
|
||||||
if (!data?.attachments || data.attachments.length === 0) {
|
if (!data?.attachments || data.attachments.length === 0) {
|
||||||
@ -43,64 +44,174 @@ const ProductImageGallery = ({ data }) => {
|
|||||||
setCurrentImageIndex(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 (
|
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 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
|
<img
|
||||||
src={currentImage}
|
src={currentImage}
|
||||||
alt={data?.title || 'Product Image'}
|
alt={data?.title || 'Product Image'}
|
||||||
className="w-full max-w-[320px] md:max-w-full object-contain"
|
className="w-full max-w-[320px] md:max-w-full object-contain"
|
||||||
/>
|
/>
|
||||||
{/* 导航箭头 */}
|
|
||||||
|
{/* 导航箭头 - 简约大气设计 */}
|
||||||
{data.attachments.length > 1 && (
|
{data.attachments.length > 1 && (
|
||||||
<>
|
<>
|
||||||
<button
|
<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')}
|
onClick={() => changeMainImage('prev')}
|
||||||
aria-label="Previous image"
|
aria-label="Previous image"
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
|
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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<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')}
|
onClick={() => changeMainImage('next')}
|
||||||
aria-label="Next image"
|
aria-label="Next image"
|
||||||
>
|
>
|
||||||
<svg className="w-4 h-4 text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 缩略图列表 - 直接显示attachments */}
|
{/* 缩略图列表 - 业内最佳实践,确保完整显示 */}
|
||||||
<div className="flex flex-wrap gap-1 justify-center md:justify-start">
|
<div className="relative">
|
||||||
{data.attachments.map((attachment, index) => (
|
<div
|
||||||
<div
|
ref={thumbnailContainerRef}
|
||||||
key={index}
|
className="flex gap-4 overflow-x-auto pb-2"
|
||||||
className={`w-16 h-16 border-1 rounded-lg overflow-hidden cursor-pointer transition-all hover:border-blue-500 ${
|
style={{
|
||||||
index === currentImageIndex ? 'border-blue-500' : 'border-gray-200'
|
scrollbarWidth: 'none',
|
||||||
}`}
|
msOverflowStyle: 'none',
|
||||||
onClick={() => changeMainImageByIndex(index)}
|
WebkitOverflowScrolling: 'touch'
|
||||||
>
|
}}
|
||||||
<img
|
>
|
||||||
src={attachment.file_url}
|
{data.attachments.map((attachment, index) => (
|
||||||
alt={`${data?.title || 'Product'} - ${index + 1}`}
|
<div
|
||||||
className="w-full h-full object-cover"
|
key={index}
|
||||||
onError={(e) => {
|
className={`flex-shrink-0 w-16 h-16 rounded-lg overflow-hidden cursor-pointer transition-all duration-300 hover:scale-105 ${
|
||||||
console.error('Image load error:', attachment.file_url);
|
index === currentImageIndex
|
||||||
e.target.style.display = 'none';
|
? '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>
|
||||||
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user