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;
// 初始化当前索引为主图索引
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);
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;
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);
// 智能滚动缩略图到可见区域
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 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;