t10015/components/products/ProductImageGallery.jsx

406 lines
14 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';
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 [thumbsSwiper, setThumbsSwiper] = useState(null);
const thumbsSwiperRef = 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;
// 专门的缩略图滚动函数
const scrollThumbnailToIndex = (index) => {
console.log('滚动缩略图到索引:', index); // 调试信息
if (thumbsSwiperRef.current && thumbsSwiperRef.current.swiper) {
const swiper = thumbsSwiperRef.current.swiper;
console.log('Swiper实例存在开始滚动'); // 调试信息
// 使用更简单的方法:直接滚动到指定索引
try {
swiper.slideTo(index, 300);
console.log('滚动成功,目标索引:', index);
} catch (error) {
console.error('滚动失败:', error);
}
} else {
console.log('Swiper实例不存在thumbsSwiperRef:', thumbsSwiperRef.current);
}
};
const changeMainImage = (direction) => {
console.log('changeMainImage 被调用,方向:', direction); // 调试信息
if (data.attachments.length <= 1) return;
let newIndex;
if (direction === 'next') {
newIndex = (currentImageIndex + 1) % data.attachments.length;
} else {
newIndex = (currentImageIndex - 1 + data.attachments.length) % data.attachments.length;
}
console.log('新的索引:', newIndex, '当前索引:', currentImageIndex); // 调试信息
setCurrentImageIndex(newIndex);
// 自动滚动缩略图到对应位置
setTimeout(() => {
console.log('开始执行滚动,索引:', newIndex); // 调试信息
scrollThumbnailToIndex(newIndex);
}, 100);
};
const changeMainImageByIndex = (index) => {
setCurrentImageIndex(index);
// 智能滚动缩略图到可见区域
setTimeout(() => {
scrollThumbnailToIndex(index);
}, 100);
};
// 初始化当前索引为主图索引
useEffect(() => {
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);
setTimeout(() => scrollThumbnailToIndex(0), 100);
break;
case 'End':
event.preventDefault();
setCurrentImageIndex(data.attachments.length - 1);
setTimeout(() => scrollThumbnailToIndex(data.attachments.length - 1), 100);
break;
default:
break;
}
};
// 添加键盘事件监听器
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [data.attachments.length]);
// 缩略图导航控制 - 移除这些函数,不再需要
// 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 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-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={`上一张图片 (${currentImageIndex === 0 ? data.attachments.length : currentImageIndex} / ${data.attachments.length})`}
title="上一张图片 (←)"
>
<svg
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={1.5}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M15 18l-6-6 6-6" />
</svg>
</button>
<button
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={`下一张图片 (${currentImageIndex === data.attachments.length - 1 ? 1 : currentImageIndex + 2} / ${data.attachments.length})`}
title="下一张图片 (→)"
>
<svg
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={1.5}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M9 18l6-6-6-6" />
</svg>
</button>
</>
)}
</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>
{/* 内联样式 */}
<style jsx>{`
/* 缩略图悬停效果 */
.thumbs-swiper .swiper-slide {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.thumbs-swiper .swiper-slide:hover {
transform: scale(1.05);
}
/* 当前选中的缩略图样式 */
.border-2.border-blue-500 {
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);
-webkit-backdrop-filter: blur(8px);
}
.group:hover .backdrop-blur-sm {
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
}
/* 响应式优化 */
@media (max-width: 768px) {
.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>
</div>
);
};
export default ProductImageGallery;