'use client' import { useEffect, useState, useRef, useCallback } from 'react' import { useSession } from 'next-auth/react' import { useRouter, useParams } from 'next/navigation' import Image from 'next/image' import { format } from 'date-fns' import Header from '@/components/Header' import VideoPlayerModal from '@/components/VideoPlayerModal' import SkuList from '@/components/SkuList' import SkuEditModal from '@/components/SkuEditModal' import ToastBubble from '@/components/ToastBubble' import { useToast } from '@/hooks/useToast' import AudioVisualizer from '@/components/AudioVisualizer' interface Material { id: string userId: string previewUrl: string productModel: string categoryName: string sceneName: string platformName?: string // 平台分类 productCategoryName?: string // 品类分类 materialType: string authorName: string zipFileName: string zipFileSize: string | number downloadCount: number likeCount: number displayImages: string[] // PSD展示图片URLs createdAt: string videoSourceUrl?: string | null // 原始视频URL videoPreview540Url?: string | null // 720p预览视频URL(字段名保持540Url以兼容数据库) videoPreviewClipUrl?: string | null // 视频片段URL(10秒,无音频) videoCoverUrl?: string | null // 视频封面URL videoDuration?: number | null // 视频时长(秒) videoProcessingStatus?: string | null // 视频处理状态 isLiked?: boolean // 是否已点赞 isFavorited?: boolean // 是否已收藏 skus?: Array<{ id: string skuName: string filePath: string fileSize: string sortOrder: number }> } export default function SharePage() { const { data: session, status } = useSession() const router = useRouter() const params = useParams() const materialId = params?.id as string const [material, setMaterial] = useState(null) const [loading, setLoading] = useState(true) const [downloading, setDownloading] = useState(false) const [downloadProgress, setDownloadProgress] = useState(0) const [showBackToTop, setShowBackToTop] = useState(false) const [showVideoModal, setShowVideoModal] = useState(false) const [videoSrc, setVideoSrc] = useState(null) const [showSkuEditModal, setShowSkuEditModal] = useState(false) const { toast, showToast } = useToast() // 统一的Toast提示 // 音频播放状态 const audioRef = useRef(null) const [playingAudio, setPlayingAudio] = useState(false) const [audioProgress, setAudioProgress] = useState(0) // 0-100 useEffect(() => { // 分享页面允许未登录访问 if (status !== 'loading') { fetchMaterial() } }, [status, materialId]) useEffect(() => { // 监听滚动,控制返回顶部按钮显示 const handleScroll = () => { setShowBackToTop(window.scrollY > 400) } window.addEventListener('scroll', handleScroll) return () => window.removeEventListener('scroll', handleScroll) }, []) const fetchMaterial = async () => { try { const response = await fetch(`/api/materials/share?id=${materialId}`) if (response.ok) { const data = await response.json() setMaterial(data) // 如果是标准模型类型,获取SKU列表 if (data.materialType === '标准模型') { const skuResponse = await fetch(`/api/materials/${materialId}/skus`) if (skuResponse.ok) { const skuData = await skuResponse.json() setMaterial(prev => prev ? { ...prev, skus: skuData.skus } : null) } } } else { showToast('素材不存在或已被删除', { variant: 'error' }) router.push('/') } } catch (error) { console.error('Error fetching material:', error) showToast('加载失败', { variant: 'error' }) router.push('/') } finally { setLoading(false) } } const formatFileSize = (bytes: string | number): string => { const size = typeof bytes === 'string' ? parseInt(bytes) : bytes if (size === 0) return '未知' const units = ['B', 'KB', 'MB', 'GB'] const i = Math.floor(Math.log(size) / Math.log(1024)) return `${(size / Math.pow(1024, i)).toFixed(2)} ${units[i]}` } const scrollToTop = () => { window.scrollTo({ top: 0, behavior: 'smooth' }) } // 音频播放控制 const handleAudioPlay = useCallback(() => { if (!material || material.materialType !== '音乐') { console.warn('[Share] handleAudioPlay: material is not music type', material?.materialType) return } // 如果正在播放,则暂停 if (playingAudio) { if (audioRef.current) { audioRef.current.pause() } setPlayingAudio(false) setAudioProgress(0) return } // 如果音频已经加载,直接播放 if (audioRef.current && audioRef.current.readyState >= 2) { audioRef.current.play().then(() => { setPlayingAudio(true) }).catch((error) => { console.error('[Share] Audio play failed:', error) showToast(`播放失败: ${error.message || '未知错误'}`, { variant: 'error' }) }) return } // 停止之前的音频 if (audioRef.current) { audioRef.current.pause() audioRef.current = null } // 创建新的音频元素 // 如果zipFileName是HTTP URL(老素材),直接使用;否则加上/uploads/前缀(新素材) let audioUrl = '' if (material.zipFileName) { const zipFileName = material.zipFileName if (zipFileName.startsWith('http://') || zipFileName.startsWith('https://')) { // 老素材:zipFileName是完整的S3 URL audioUrl = zipFileName } else { // 新素材:zipFileName只是文件名,需要加上/uploads/前缀 audioUrl = `/uploads/${zipFileName}` } } console.log('[Share] Audio URL:', audioUrl, 'Material:', { id: material.id, zipFileName: material.zipFileName }) if (!audioUrl) { console.error('[Share] No audio URL found for material:', material.id) showToast('音频文件不存在', { variant: 'error' }) return } const audio = new Audio(audioUrl) audio.preload = 'auto' // 预加载音频 audioRef.current = audio // 监听加载错误 audio.addEventListener('error', (e) => { const error = audio.error console.error('[Share] Audio error:', { code: error?.code, message: error?.message, audioUrl, materialId: material.id }) showToast(`音频加载失败: ${error?.message || '未知错误'}`, { variant: 'error' }) setPlayingAudio(false) setAudioProgress(0) audioRef.current = null }) // 监听可以播放(使用canplay事件,有足够数据就开始播放,减少延迟) const handleCanPlay = () => { console.log('[Share] Audio can play:', audioUrl) audio.play().then(() => { console.log('[Share] Audio playback started:', audioUrl) setPlayingAudio(true) }).catch((error) => { console.error('[Share] Audio play failed:', error, 'URL:', audioUrl) showToast(`播放失败: ${error.message || '未知错误'}`, { variant: 'error' }) setPlayingAudio(false) audioRef.current = null }) audio.removeEventListener('canplay', handleCanPlay) } audio.addEventListener('canplay', handleCanPlay) // 如果音频已经可以播放,立即播放 if (audio.readyState >= 2) { handleCanPlay() } // 监听播放进度 audio.addEventListener('timeupdate', () => { if (audio.duration) { setAudioProgress((audio.currentTime / audio.duration) * 100) } }) // 监听播放结束 audio.addEventListener('ended', () => { setPlayingAudio(false) setAudioProgress(0) }) // 开始加载音频 audio.load() }, [material, playingAudio, showToast]) // 组件卸载时清理音频 useEffect(() => { return () => { if (audioRef.current) { audioRef.current.pause() audioRef.current = null } } }, []) const handleDownload = () => { if (!material) return setDownloading(true) setDownloadProgress(0) const xhr = new XMLHttpRequest() xhr.addEventListener('progress', (e) => { if (e.lengthComputable) { const percentComplete = Math.round((e.loaded / e.total) * 100) setDownloadProgress(percentComplete) } }) xhr.addEventListener('load', () => { if (xhr.status === 200) { try { const blob = new Blob([xhr.response], { type: 'application/zip' }) const contentDisposition = xhr.getResponseHeader('Content-Disposition') let filename = 'material.zip' if (contentDisposition) { const filenameMatch = contentDisposition.match(/filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/) if (filenameMatch && filenameMatch[1]) { filename = decodeURIComponent(filenameMatch[1].replace(/['"]/g, '')) } } const url = window.URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = filename document.body.appendChild(a) a.click() a.remove() window.URL.revokeObjectURL(url) } catch (error) { console.error('下载处理失败:', error) showToast('下载失败,请稍后重试', { variant: 'error' }) } } else { showToast('下载失败', { variant: 'error' }) } setDownloading(false) setDownloadProgress(0) }) xhr.addEventListener('error', () => { showToast('下载失败,请稍后重试', { variant: 'error' }) setDownloading(false) setDownloadProgress(0) }) xhr.open('GET', `/api/materials/download?id=${material.id}`) xhr.responseType = 'arraybuffer' xhr.send() } if (loading) { return (

加载中...

) } if (!material) { return (

素材不存在

) } return (
{/* 所有素材类型统一使用左右布局 */}
{/* 左侧:预览图(固定790px宽度) */}
{(() => { // 判断是否为视频类型 const isVideoType = material.materialType === '主图视频' || material.materialType === '安装视频' || material.materialType === '种草视频' const videoUrl = material.videoPreviewClipUrl || material.videoPreview540Url || material.videoSourceUrl const posterUrl = material.videoCoverUrl || material.previewUrl if ((material.materialType === '详情页' || material.materialType === '模特') && material.displayImages && material.displayImages.length > 0) { /* 详情页/模特类型:显示多张展示图 */ return (
{material.displayImages.map((imageUrl, index) => (
{`${material.productModel}
))}
) } else if (isVideoType && videoUrl) { /* 视频类型:显示视频预览(可点击播放完整视频) */ return (
{ // 优先使用720p预览,其次使用10秒片段,最后使用源视频 const playUrl = material.videoPreview540Url || material.videoPreviewClipUrl || material.videoSourceUrl if (playUrl) { setVideoSrc(playUrl) setShowVideoModal(true) } }} >
) } else if (material.materialType === '音乐') { /* 音频类型:显示音频播放器和可视化 */ return (
{/* 圆形频谱可视化背景 - 只在播放时显示 */} {playingAudio && (
)} {/* 播放进度条(底部) - 可点击和拖动 */} {playingAudio && (
{ e.stopPropagation() if (audioRef.current) { const rect = e.currentTarget.getBoundingClientRect() const percent = (e.clientX - rect.left) / rect.width audioRef.current.currentTime = audioRef.current.duration * percent } }} onMouseDown={(e) => { e.stopPropagation() const handleMouseMove = (moveEvent: MouseEvent) => { if (audioRef.current) { const rect = e.currentTarget.getBoundingClientRect() const percent = Math.max(0, Math.min(1, (moveEvent.clientX - rect.left) / rect.width)) audioRef.current.currentTime = audioRef.current.duration * percent } } const handleMouseUp = () => { document.removeEventListener('mousemove', handleMouseMove) document.removeEventListener('mouseup', handleMouseUp) } document.addEventListener('mousemove', handleMouseMove) document.addEventListener('mouseup', handleMouseUp) }} >
{/* 拖动手柄 */}
)} {/* 信息覆盖层 */}
{/* 封面图(如果有,且未播放时显示) */} {material.previewUrl && !playingAudio && (
{material.productModel}
)} {/* 播放/暂停按钮 */} {/* 音频名称 */}

{material.productModel}

) } else { /* 其他类型:显示单张预览图(自适应高度) */ return (
{material.productModel}
) } })()}
{/* 标准模型类型:显示SKU列表 */} {material.materialType === '标准模型' && (
setShowSkuEditModal(true)} />
)}
{/* 右侧:素材信息(固定) */}
{/* 标题和标签 - 统一间距 */}

{material.productModel}

{material.categoryName} {material.sceneName} {material.materialType && ( {material.materialType} )}
{/* 素材信息 - 统一字体和间距 */}

作者

{material.authorName}

文件大小

{formatFileSize(material.zipFileSize)}

下载次数

{material.downloadCount}

点赞数

{material.likeCount}

文件名

{material.zipFileName}

上传时间

{format(new Date(material.createdAt), 'yyyy-MM-dd HH:mm')}

{/* 操作按钮 - 统一样式 */}
{/* 返回顶部按钮 */} {showBackToTop && ( )} {/* Video Player Modal */} {showVideoModal && videoSrc && ( { setShowVideoModal(false) setVideoSrc(null) }} /> )} {/* SKU编辑模态框 */} {showSkuEditModal && material && ( { setShowSkuEditModal(false) }} onSaved={() => { setShowSkuEditModal(false) // 重新获取SKU列表 if (material.materialType === '标准模型') { fetch(`/api/materials/${materialId}/skus`) .then(res => res.json()) .then(data => { setMaterial(prev => prev ? { ...prev, skus: data.skus } : null) }) .catch(console.error) } }} /> )} {/* 统一的Toast提示 */} {toast && toast.show && ( )}
) }