flnxi-Wu/app/share/[id]/page.tsx
WuYongJie f646b71813
Some checks failed
CI Check / check (push) Has been cancelled
chore: 初始化本地仓库并同步最近改动
2025-11-18 17:50:16 +08:00

586 lines
25 KiB
TypeScript
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 { useEffect, useState } 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'
interface Material {
id: string
previewUrl: string
productModel: string
brandName?: string | null
seriesName?: string | null
skuCode?: string | null
materialTexture?: string | null
categoryName: string
sceneName: string
materialType: string
renderer?: string | null
renderTime?: string | null
imageResolution?: string | null
colorMode?: string | null
authorName: string
zipFileName: string
zipFileSize: string | number
downloadCount: number
likeCount: number
displayImages: string[] // PSD展示图片URLs
createdAt: string
}
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<Material | null>(null)
const [loading, setLoading] = useState(true)
const [downloading, setDownloading] = useState(false)
const [downloadProgress, setDownloadProgress] = useState(0)
const [showBackToTop, setShowBackToTop] = useState(false)
const [aiEditEnabled, setAiEditEnabled] = useState(
(process.env.NEXT_PUBLIC_AI_EDIT_ENABLED || '').toLowerCase() === 'true'
)
useEffect(() => {
// 分享页面允许未登录访问
if (status !== 'loading') {
fetchMaterial()
}
}, [status, materialId])
useEffect(() => {
const ensureFeatureFlags = async () => {
if (aiEditEnabled) return
try {
const response = await fetch('/api/system-settings')
if (response.ok) {
const data = await response.json()
if (data?.enableAiImageEdit) {
setAiEditEnabled(true)
}
}
} catch (err) {
console.warn('加载系统设置失败(AI改图判断):', err)
}
}
ensureFeatureFlags()
}, [aiEditEnabled])
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)
} else {
alert('素材不存在或已被删除')
router.push('/')
}
} catch (error) {
console.error('Error fetching material:', error)
alert('加载失败')
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 renderAdditionalInfo = (variant: 'compact' | 'full' = 'compact') => {
if (!material) return null
const entries = [
{ label: '品牌', value: material.brandName },
{ label: '系列', value: material.seriesName },
{ label: 'SKU', value: material.skuCode },
{ label: '材质 / 工艺', value: material.materialTexture },
{ label: '渲染耗时', value: material.renderTime },
{ label: '输出分辨率', value: material.imageResolution },
{ label: '颜色模式', value: material.colorMode },
].filter(entry => entry.value)
if (entries.length === 0) return null
const labelClass = variant === 'compact'
? 'text-xs text-gray-500 dark:text-gray-400 mb-1'
: 'text-sm text-gray-500 dark:text-gray-400 mb-1'
const valueClass = variant === 'compact'
? 'text-sm font-medium text-gray-900 dark:text-white break-all'
: 'text-base font-medium text-gray-900 dark:text-white break-all'
return (
<div className={`grid grid-cols-1 ${variant === 'compact' ? 'gap-3 mt-4 pt-4 border-t border-gray-100 dark:border-gray-700' : 'gap-4 mt-6 pt-6 border-t border-gray-200 dark:border-gray-700'}`}>
{entries.map((entry, index) => (
<div key={`${entry.label}-${index}`}>
<p className={labelClass}>{entry.label}</p>
<p className={valueClass}>{entry.value}</p>
</div>
))}
</div>
)
}
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)
alert('下载失败,请稍后重试')
}
} else {
alert('下载失败')
}
setDownloading(false)
setDownloadProgress(0)
})
xhr.addEventListener('error', () => {
alert('下载失败,请稍后重试')
setDownloading(false)
setDownloadProgress(0)
})
xhr.open('GET', `/api/materials/download?id=${material.id}`)
xhr.responseType = 'arraybuffer'
xhr.send()
}
if (loading) {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<main className="container mx-auto px-4 py-8">
<div className="text-center py-16">
<p className="text-gray-500 dark:text-gray-400 text-lg">加载中...</p>
</div>
</main>
</div>
)
}
if (!material) {
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<Header session={session} />
<main className="container mx-auto px-4 py-8">
<div className="text-center py-16">
<p className="text-gray-500 dark:text-gray-400 text-lg">素材不存在</p>
</div>
</main>
</div>
)
}
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<Header session={session} />
<main className="container mx-auto px-4 py-8">
{(material.materialType === 'PSD' || material.materialType === '模特') && material.displayImages && material.displayImages.length > 0 ? (
/* PSD/模特类型:左右布局 */
<div className="flex gap-8 max-w-7xl mx-auto">
{/* 左侧:展示图片(固定790px宽度) */}
<div className="flex-shrink-0" style={{ width: '790px' }}>
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg overflow-hidden">
<div className="w-full bg-gray-200 dark:bg-gray-700 flex flex-col items-center">
{material.displayImages.map((imageUrl, index) => (
<div key={index} className="relative" style={{ maxWidth: '790px', width: '100%' }}>
<Image
src={imageUrl}
alt={`${material.productModel} - 展示图 ${index + 1}`}
width={790}
height={1500}
unoptimized
className="w-full h-auto"
style={{ display: 'block', maxWidth: '790px' }}
/>
</div>
))}
</div>
</div>
</div>
{/* 右侧:素材信息(固定) */}
<div className="w-80 flex-shrink-0">
<div className="sticky top-24 bg-white dark:bg-gray-800 rounded-2xl shadow-lg p-6 space-y-6">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-3">
{material.productModel}
</h1>
<div className="flex flex-wrap gap-2">
<span className="inline-flex items-center px-3 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded-full text-xs font-medium">
{material.categoryName}
</span>
<span className="inline-flex items-center px-3 py-1 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded-full text-xs font-medium">
{material.sceneName}
</span>
<span className="inline-flex items-center px-3 py-1 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded-full text-xs font-medium">
{material.materialType}
</span>
</div>
</div>
<div className="space-y-4">
<div>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">作者</p>
<p className="text-sm font-medium text-gray-900 dark:text-white">{material.authorName}</p>
</div>
<div>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">文件大小</p>
<p className="text-sm font-medium text-blue-600 dark:text-blue-400">{formatFileSize(material.zipFileSize)}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">下载</p>
<p className="text-sm font-medium text-gray-900 dark:text-white">{material.downloadCount}</p>
</div>
<div>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">点赞</p>
<p className="text-sm font-medium text-gray-900 dark:text-white">{material.likeCount}</p>
</div>
</div>
<div>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">文件名</p>
<p className="text-xs text-gray-700 dark:text-gray-300 break-all">{material.zipFileName}</p>
</div>
<div>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">上传时间</p>
<p className="text-xs text-gray-700 dark:text-gray-300">
{format(new Date(material.createdAt), 'yyyy-MM-dd HH:mm')}
</p>
</div>
{renderAdditionalInfo('compact')}
</div>
{/* Download Button */}
<button
onClick={handleDownload}
disabled={downloading}
className="w-full btn-primary py-3 text-sm font-medium relative overflow-hidden disabled:opacity-75"
>
{downloading ? (
<>
<div
className="absolute left-0 top-0 bottom-0 bg-blue-600 transition-all duration-300"
style={{ width: `${downloadProgress}%` }}
/>
<span className="relative z-10 flex items-center justify-center gap-2">
<svg className="w-5 h-5 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
下载中 {downloadProgress}%
</span>
</>
) : (
<span className="flex items-center justify-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
立即下载
</span>
)}
</button>
{/* AI改图(占位,默认隐藏。后续由开关控制显示) */}
{aiEditEnabled && (
<button
onClick={() => alert('AI改图功能待开放')}
className="w-full mt-3 py-2.5 bg-purple-600 text-white rounded-xl text-sm font-medium hover:bg-purple-700 transition-colors"
>
AI改图(预留)
</button>
)}
<button
onClick={() => router.push('/')}
className="w-full py-2.5 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-xl text-sm font-medium hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
返回首页
</button>
</div>
</div>
</div>
) : material.materialType === '视频' ? (
/* 视频类型:视频播放布局 */
<div className="max-w-6xl mx-auto">
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg overflow-hidden">
{/* 视频播放器 */}
<div className="relative w-full bg-black">
<video
controls
className="w-full h-auto max-h-[800px]"
poster={material.previewUrl}
preload="metadata"
>
<source src={`/api/materials/stream?id=${material.id}`} type="video/mp4" />
您的浏览器不支持视频播放。
</video>
</div>
{/* Material Info */}
<div className="p-8">
<div className="mb-6">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
{material.productModel}
</h1>
<div className="flex items-center gap-3 mb-4">
<span className="inline-flex items-center px-3 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded-full text-sm font-medium">
{material.categoryName}
</span>
<span className="inline-flex items-center px-3 py-1 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded-full text-sm font-medium">
{material.sceneName}
</span>
<span className="inline-flex items-center px-3 py-1 bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-full text-sm font-medium">
视频
</span>
</div>
</div>
<div className="grid grid-cols-2 gap-6 mb-6">
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">作者</p>
<p className="text-lg font-medium text-gray-900 dark:text-white">{material.authorName}</p>
</div>
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">文件大小</p>
<p className="text-lg font-medium text-blue-600 dark:text-blue-400">{formatFileSize(material.zipFileSize)}</p>
</div>
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">下载次数</p>
<p className="text-lg font-medium text-gray-900 dark:text-white">{material.downloadCount}</p>
</div>
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">点赞数</p>
<p className="text-lg font-medium text-gray-900 dark:text-white">{material.likeCount}</p>
</div>
</div>
<div className="mb-6">
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">文件名</p>
<p className="text-sm text-gray-700 dark:text-gray-300 break-all">{material.zipFileName}</p>
</div>
<div className="mb-6">
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">上传时间</p>
<p className="text-sm text-gray-700 dark:text-gray-300">
{format(new Date(material.createdAt), 'yyyy-MM-dd HH:mm')}
</p>
</div>
{/* Download Button */}
<button
onClick={handleDownload}
disabled={downloading}
className="w-full btn-primary py-4 text-lg font-medium relative overflow-hidden disabled:opacity-75"
>
{downloading ? (
<>
<div
className="absolute left-0 top-0 bottom-0 bg-blue-600 transition-all duration-300"
style={{ width: `${downloadProgress}%` }}
/>
<span className="relative z-10 flex items-center justify-center gap-2">
<svg className="w-6 h-6 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
下载中 {downloadProgress}%
</span>
</>
) : (
<span className="flex items-center justify-center gap-2">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
立即下载
</span>
)}
</button>
<button
onClick={() => router.push('/')}
className="w-full mt-4 py-3 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-xl font-medium hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
返回首页
</button>
</div>
</div>
</div>
) : (
/* 非PSD/非视频类型:原有布局 */
<div className="max-w-4xl mx-auto">
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-lg overflow-hidden">
{/* 普通预览图 */}
<div className="relative w-full h-96 bg-gray-200 dark:bg-gray-700">
<Image
src={material.previewUrl}
alt={material.productModel}
fill
unoptimized
className="object-contain"
sizes="(max-width: 1024px) 100vw, 1024px"
/>
</div>
{/* Material Info */}
<div className="p-8">
<div className="mb-6">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-2">
{material.productModel}
</h1>
<div className="flex items-center gap-3 mb-4">
<span className="inline-flex items-center px-3 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded-full text-sm font-medium">
{material.categoryName}
</span>
<span className="inline-flex items-center px-3 py-1 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded-full text-sm font-medium">
{material.sceneName}
</span>
</div>
</div>
<div className="grid grid-cols-2 gap-6 mb-6">
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">作者</p>
<p className="text-lg font-medium text-gray-900 dark:text-white">{material.authorName}</p>
</div>
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">文件大小</p>
<p className="text-lg font-medium text-blue-600 dark:text-blue-400">{formatFileSize(material.zipFileSize)}</p>
</div>
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">下载次数</p>
<p className="text-lg font-medium text-gray-900 dark:text-white">{material.downloadCount}</p>
</div>
<div>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">点赞数</p>
<p className="text-lg font-medium text-gray-900 dark:text-white">{material.likeCount}</p>
</div>
</div>
<div className="mb-6">
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">文件名</p>
<p className="text-sm text-gray-700 dark:text-gray-300 break-all">{material.zipFileName}</p>
</div>
<div className="mb-6">
<p className="text-sm text-gray-500 dark:text-gray-400 mb-1">上传时间</p>
<p className="text-sm text-gray-700 dark:text-gray-300">
{format(new Date(material.createdAt), 'yyyy-MM-dd HH:mm')}
</p>
</div>
{renderAdditionalInfo('full')}
{/* Download Button */}
<button
onClick={handleDownload}
disabled={downloading}
className="w-full btn-primary py-4 text-lg font-medium relative overflow-hidden disabled:opacity-75"
>
{downloading ? (
<>
<div
className="absolute left-0 top-0 bottom-0 bg-blue-600 transition-all duration-300"
style={{ width: `${downloadProgress}%` }}
/>
<span className="relative z-10 flex items-center justify-center gap-2">
<svg className="w-6 h-6 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
下载中 {downloadProgress}%
</span>
</>
) : (
<span className="flex items-center justify-center gap-2">
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
立即下载
</span>
)}
</button>
<button
onClick={() => router.push('/')}
className="w-full mt-4 py-3 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-xl font-medium hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
>
返回首页
</button>
</div>
</div>
</div>
)}
</main>
{/* 返回顶部按钮 */}
{showBackToTop && (
<button
onClick={scrollToTop}
className="fixed bottom-8 right-8 w-12 h-12 bg-blue-600 hover:bg-blue-700 text-white rounded-full shadow-lg flex items-center justify-center transition-all z-50"
title="返回顶部"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 10l7-7m0 0l7 7m-7-7v18" />
</svg>
</button>
)}
</div>
)
}