t10015/components/products/ProductImageGallery.jsx

385 lines
13 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, useCallback, useMemo } 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;
}
// 使用useMemo优化主图索引计算
const mainImageIndex = useMemo(() => {
if (!data.image) return 0;
const mainImageIndex = data.attachments.findIndex(
attachment => attachment.file_url === data.image
);
return mainImageIndex >= 0 ? mainImageIndex : 0;
}, [data.image, data.attachments]);
// 使用useMemo优化当前图片URL
const currentImage = useMemo(() => {
return data.attachments[currentImageIndex]?.file_url || data.attachments[0]?.file_url;
}, [data.attachments, currentImageIndex]);
// 使用useCallback优化缩略图滚动函数
const scrollThumbnailToIndex = useCallback((index) => {
if (thumbsSwiperRef.current?.swiper) {
try {
const swiper = thumbsSwiperRef.current.swiper;
if (index === 0) {
// 第一张图片,滚动到最左侧
swiper.scrollTo(0, 300);
} else {
// 其他图片使用原有的slideTo方法保持正常滚动
swiper.slideTo(index, 300);
}
} catch (error) {
// 静默处理错误
}
}
}, []);
// 使用useCallback优化主图切换函数
const changeMainImage = useCallback((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;
}
setCurrentImageIndex(newIndex);
// 自动滚动缩略图到对应位置
setTimeout(() => {
scrollThumbnailToIndex(newIndex);
}, 100);
}, [currentImageIndex, data.attachments.length, scrollThumbnailToIndex]);
// 使用useCallback优化按索引切换主图函数
const changeMainImageByIndex = useCallback((index) => {
setCurrentImageIndex(index);
// 智能滚动缩略图到可见区域
setTimeout(() => {
scrollThumbnailToIndex(index);
}, 100);
}, [scrollThumbnailToIndex]);
// 初始化当前索引为主图索引
useEffect(() => {
setCurrentImageIndex(mainImageIndex);
}, [mainImageIndex]);
// 键盘导航支持 - 使用useCallback优化
const handleKeyDown = useCallback((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;
}
}, [data.attachments.length, changeMainImage, scrollThumbnailToIndex]);
useEffect(() => {
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [handleKeyDown]);
// 使用useMemo优化缩略图渲染
const thumbnailSlides = useMemo(() => {
return 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 border-[#eee] hover:border-gray-300'
}`}
style={index === currentImageIndex ? { borderColor: '#1A1A1A', borderWidth: '2px' } : {}}
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) => {
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>
));
}, [data.attachments, currentImageIndex, data?.title, changeMainImageByIndex]);
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, Thumbs]}
className="thumbs-swiper"
grabCursor={true}
resistance={true}
resistanceRatio={0.85}
>
{thumbnailSlides}
</Swiper>
</div>
)}
</div>
{/* 内联样式 */}
<style jsx>{`
/* 缩略图悬停效果 */
.thumbs-swiper .swiper-slide {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
width: 64px !important;
margin-right: 16px !important;
}
.thumbs-swiper .swiper-slide:hover {
transform: scale(1.05);
}
.thumbs-swiper .swiper-slide:last-child {
margin-right: 0 !important;
}
.thumbs-swiper .swiper-slide .border-2 {
}
/* 导航按钮样式增强 */
.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;