产品详情页实现多图展示,processDataItem函数增加支持下载附件到本地服务器
@ -5,6 +5,7 @@ import { notFound } from 'next/navigation';
|
||||
import DynamicListPage from "@/components/common/DynamicListPage";
|
||||
import { Suspense } from 'react';
|
||||
import { getPageData } from "@/utils/data";
|
||||
import ProductImageGallery from "@/components/products/ProductImageGallery";
|
||||
|
||||
const baseSlug = 'products';
|
||||
|
||||
@ -123,15 +124,9 @@ export default async function Page({ params, searchParams }) {
|
||||
{/* 图片和附加信息并排显示,响应式优化 */}
|
||||
{(data.image || data.subtitle) && (
|
||||
<div className="flex flex-col md:flex-row gap-4 md:gap-8 mb-6 md:mb-8 items-center md:items-start w-full max-w-full">
|
||||
{data.image && (
|
||||
<div className="flex-shrink-0 w-full md:w-72 flex justify-center md:justify-start mb-4 md:mb-0 max-w-full">
|
||||
<img
|
||||
src={data.image}
|
||||
alt={data.title}
|
||||
className="w-full max-w-[320px] md:max-w-full max-h-60 md:max-h-72 rounded-xl shadow-lg object-contain"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* 图片轮播区块 */}
|
||||
<ProductImageGallery data={data} />
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* 产品标题 */}
|
||||
{data.title && (
|
||||
@ -141,7 +136,7 @@ export default async function Page({ params, searchParams }) {
|
||||
)}
|
||||
{/* 产品副标题 */}
|
||||
{data.subtitle && (
|
||||
<div className="bg-gray-50 whitespace-pre-line text-gray-700 w-full max-w-full prose prose-sm">
|
||||
<div className="bg-gray-50 whitespace-pre-line text-gray-700 w-full max-w-full prose prose-sm p-4 rounded-lg">
|
||||
{data.subtitle}
|
||||
</div>
|
||||
)}
|
||||
@ -186,4 +181,4 @@ export default async function Page({ params, searchParams }) {
|
||||
} else {
|
||||
notFound();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
108
components/products/ProductImageGallery.jsx
Normal file
@ -0,0 +1,108 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
const ProductImageGallery = ({ data }) => {
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0);
|
||||
|
||||
// 如果没有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]);
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex-shrink-0 w-full md:w-72 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">
|
||||
<img
|
||||
src={currentImage}
|
||||
alt={data?.title || 'Product Image'}
|
||||
className="w-full max-w-[320px] md:max-w-full max-h-60 md:max-h-72 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"
|
||||
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>
|
||||
</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"
|
||||
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>
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 缩略图列表 - 直接显示attachments */}
|
||||
<div className="flex flex-wrap gap-2 justify-center md:justify-start">
|
||||
{data.attachments.map((attachment, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={`w-16 h-16 border-2 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>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProductImageGallery;
|
||||
BIN
public/files/a47e76hv1j_476c0c45.png
Normal file
|
After Width: | Height: | Size: 856 KiB |
BIN
public/files/h8ibigsafe_53103f0c.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
public/files/lpd8ov2ak7_03a99731.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
public/files/lpd8ov2ak7_092f5df0.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
public/files/lpd8ov2ak7_323cebbe.png
Normal file
|
After Width: | Height: | Size: 741 KiB |
BIN
public/files/lpd8ov2ak7_3632b8d5.png
Normal file
|
After Width: | Height: | Size: 799 KiB |
BIN
public/files/lpd8ov2ak7_4fe7ffd4.png
Normal file
|
After Width: | Height: | Size: 971 KiB |
BIN
public/files/lpd8ov2ak7_503fe3e4.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
public/files/lpd8ov2ak7_85e5a79e.png
Normal file
|
After Width: | Height: | Size: 760 KiB |
BIN
public/files/lpd8ov2ak7_91a359f1.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
BIN
public/files/lpd8ov2ak7_9aca70f9.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
public/files/lpd8ov2ak7_9ade59cf.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
public/files/lpd8ov2ak7_a2e6192a.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
public/files/lpd8ov2ak7_edcf6679.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
@ -82,6 +82,15 @@ export async function processDataItem(item, downloadFiles) {
|
||||
item.file_src = await downloadToLocal(item.file_src);
|
||||
}
|
||||
|
||||
// 处理attachments字段
|
||||
if (item.attachments && Array.isArray(item.attachments)) {
|
||||
for (const attachment of item.attachments) {
|
||||
if (attachment.file_url) {
|
||||
attachment.file_url = await downloadToLocal(attachment.file_url);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (item.items && Array.isArray(item.items)) {
|
||||
for (const subItem of item.items) {
|
||||
if (subItem.item_image) {
|
||||
|
||||