1. 排行榜悬浮窗关闭后小按钮消失问题 - 修复面板居中模式关闭时按钮位置错误 - 保存并恢复按钮的居中状态 2. 首页tag按钮按素材数量排序 - 添加按素材数量降序排序逻辑 3. 上传弹窗输入框浅色模式文字不可见 - 添加 text-slate-900 确保浅色模式下文字可见 - 添加 placeholder 样式确保占位符正确显示
660 lines
28 KiB
TypeScript
660 lines
28 KiB
TypeScript
/**
|
||
* SharePage 组件
|
||
*
|
||
* 素材分享页面
|
||
* 显示素材详情、预览图、下载等功能
|
||
* 支持PSD/模特类型的多图展示
|
||
* 使用三层架构样式系统,支持深浅色主题
|
||
*
|
||
* @created 2025-01-XX
|
||
* @component
|
||
*/
|
||
|
||
'use client'
|
||
|
||
import { useEffect, useState, 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 { useDialog } from '@/components/ui/DialogProvider'
|
||
import { useThemeStyles, useTheme, cn } from '@/lib/ui-styles'
|
||
|
||
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 styles = useThemeStyles()
|
||
const theme = useTheme()
|
||
const { data: session, status } = useSession()
|
||
const router = useRouter()
|
||
const { alert } = useDialog()
|
||
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'
|
||
)
|
||
|
||
const fetchMaterial = useCallback(async () => {
|
||
try {
|
||
const response = await fetch(`/api/materials/share?id=${materialId}`)
|
||
if (response.ok) {
|
||
const data = await response.json()
|
||
setMaterial(data)
|
||
// 记录浏览历史(仅登录用户)
|
||
if (session?.user) {
|
||
fetch('/api/user/history', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ materialId }),
|
||
}).catch((err) => console.warn('记录浏览历史失败:', err))
|
||
}
|
||
} else {
|
||
alert({
|
||
title: '错误',
|
||
message: '素材不存在或已被删除',
|
||
variant: 'error',
|
||
})
|
||
router.push('/')
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching material:', error)
|
||
alert({
|
||
title: '加载失败',
|
||
message: '加载失败',
|
||
variant: 'error',
|
||
})
|
||
router.push('/')
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}, [materialId, router, session])
|
||
|
||
useEffect(() => {
|
||
if (status !== 'loading') {
|
||
fetchMaterial()
|
||
}
|
||
}, [status, fetchMaterial])
|
||
|
||
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 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-slate-500 dark:text-slate-400 mb-1'
|
||
: 'text-sm text-slate-500 dark:text-slate-400 mb-1'
|
||
const valueClass = variant === 'compact'
|
||
? 'text-sm font-medium text-slate-900 dark:text-white break-all'
|
||
: 'text-base font-medium text-slate-900 dark:text-white break-all'
|
||
|
||
return (
|
||
<div className={cn(
|
||
'grid grid-cols-1',
|
||
variant === 'compact'
|
||
? 'gap-3 mt-4 pt-4 border-t border-slate-100 dark:border-slate-700'
|
||
: 'gap-4 mt-6 pt-6 border-t border-slate-200 dark:border-slate-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({
|
||
title: '下载失败',
|
||
message: '下载失败,请稍后重试',
|
||
variant: 'error',
|
||
})
|
||
}
|
||
} else {
|
||
alert({
|
||
title: '下载失败',
|
||
message: '下载失败',
|
||
variant: 'error',
|
||
})
|
||
}
|
||
setDownloading(false)
|
||
setDownloadProgress(0)
|
||
})
|
||
|
||
xhr.addEventListener('error', () => {
|
||
alert({
|
||
title: '下载失败',
|
||
message: '下载失败,请稍后重试',
|
||
variant: 'error',
|
||
})
|
||
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-slate-50 dark:bg-slate-900">
|
||
<main className="container mx-auto px-4 py-8">
|
||
<div className="text-center py-16">
|
||
<p className="text-slate-500 dark:text-slate-400 text-lg">加载中...</p>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
if (!material) {
|
||
return (
|
||
<div className="min-h-screen bg-slate-50 dark:bg-slate-900">
|
||
<Header session={session} />
|
||
<main className="container mx-auto px-4 py-8">
|
||
<div className="text-center py-16">
|
||
<p className="text-slate-500 dark:text-slate-400 text-lg">素材不存在</p>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="min-h-screen bg-slate-50 dark:bg-slate-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={cn('rounded-2xl shadow-lg overflow-hidden', styles.cardStyles.base)}>
|
||
<div className="w-full bg-slate-200 dark:bg-slate-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={cn('sticky top-24 rounded-2xl shadow-lg p-6 space-y-6', styles.cardStyles.base)}>
|
||
<div>
|
||
<h1 className={cn('text-2xl font-bold mb-3', theme === 'dark' ? 'text-white' : 'text-slate-900')}>
|
||
{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={cn('text-xs mb-1', theme === 'dark' ? 'text-slate-400' : 'text-slate-500')}>作者</p>
|
||
<p className={cn('text-sm font-medium', theme === 'dark' ? 'text-white' : 'text-slate-900')}>{material.authorName}</p>
|
||
</div>
|
||
<div>
|
||
<p className={cn('text-xs mb-1', theme === 'dark' ? 'text-slate-400' : 'text-slate-500')}>文件大小</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={cn('text-xs mb-1', theme === 'dark' ? 'text-slate-400' : 'text-slate-500')}>下载</p>
|
||
<p className={cn('text-sm font-medium', theme === 'dark' ? 'text-white' : 'text-slate-900')}>{material.downloadCount}</p>
|
||
</div>
|
||
<div>
|
||
<p className={cn('text-xs mb-1', theme === 'dark' ? 'text-slate-400' : 'text-slate-500')}>点赞</p>
|
||
<p className={cn('text-sm font-medium', theme === 'dark' ? 'text-white' : 'text-slate-900')}>{material.likeCount}</p>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<p className={cn('text-xs mb-1', theme === 'dark' ? 'text-slate-400' : 'text-slate-500')}>文件名</p>
|
||
<p className={cn('text-xs break-all', theme === 'dark' ? 'text-slate-300' : 'text-slate-700')}>{material.zipFileName}</p>
|
||
</div>
|
||
<div>
|
||
<p className={cn('text-xs mb-1', theme === 'dark' ? 'text-slate-400' : 'text-slate-500')}>上传时间</p>
|
||
<p className={cn('text-xs', theme === 'dark' ? 'text-slate-300' : 'text-slate-700')}>
|
||
{format(new Date(material.createdAt), 'yyyy-MM-dd HH:mm')}
|
||
</p>
|
||
</div>
|
||
{renderAdditionalInfo('compact')}
|
||
</div>
|
||
|
||
{/* Download Button */}
|
||
<button
|
||
onClick={handleDownload}
|
||
disabled={downloading}
|
||
className={cn(
|
||
'w-full py-3 text-sm font-medium relative overflow-hidden disabled:opacity-75 rounded-xl',
|
||
'bg-blue-600 hover:bg-blue-700 text-white transition-colors',
|
||
styles.buttonStyles.primary
|
||
)}
|
||
>
|
||
{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({
|
||
title: '提示',
|
||
message: 'AI改图功能待开放',
|
||
variant: 'warning',
|
||
})}
|
||
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.back()}
|
||
className={cn(
|
||
'w-full py-2.5 rounded-xl text-sm font-medium transition-colors',
|
||
styles.buttonStyles.secondary
|
||
)}
|
||
>
|
||
返回上一页
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : material.materialType === '视频' ? (
|
||
/* 视频类型:视频播放布局 */
|
||
<div className="max-w-6xl mx-auto">
|
||
<div className={cn('rounded-2xl shadow-lg overflow-hidden', styles.cardStyles.base)}>
|
||
{/* 视频播放器 */}
|
||
<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={cn('text-3xl font-bold mb-2', theme === 'dark' ? 'text-white' : 'text-slate-900')}>
|
||
{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={cn('text-sm mb-1', theme === 'dark' ? 'text-slate-400' : 'text-slate-500')}>作者</p>
|
||
<p className={cn('text-lg font-medium', theme === 'dark' ? 'text-white' : 'text-slate-900')}>{material.authorName}</p>
|
||
</div>
|
||
<div>
|
||
<p className={cn('text-sm mb-1', theme === 'dark' ? 'text-slate-400' : 'text-slate-500')}>文件大小</p>
|
||
<p className="text-lg font-medium text-blue-600 dark:text-blue-400">{formatFileSize(material.zipFileSize)}</p>
|
||
</div>
|
||
<div>
|
||
<p className={cn('text-sm mb-1', theme === 'dark' ? 'text-slate-400' : 'text-slate-500')}>下载次数</p>
|
||
<p className={cn('text-lg font-medium', theme === 'dark' ? 'text-white' : 'text-slate-900')}>{material.downloadCount}</p>
|
||
</div>
|
||
<div>
|
||
<p className={cn('text-sm mb-1', theme === 'dark' ? 'text-slate-400' : 'text-slate-500')}>点赞数</p>
|
||
<p className={cn('text-lg font-medium', theme === 'dark' ? 'text-white' : 'text-slate-900')}>{material.likeCount}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mb-6">
|
||
<p className={cn('text-sm mb-1', theme === 'dark' ? 'text-slate-400' : 'text-slate-500')}>文件名</p>
|
||
<p className={cn('text-sm break-all', theme === 'dark' ? 'text-slate-300' : 'text-slate-700')}>{material.zipFileName}</p>
|
||
</div>
|
||
|
||
<div className="mb-6">
|
||
<p className={cn('text-sm mb-1', theme === 'dark' ? 'text-slate-400' : 'text-slate-500')}>上传时间</p>
|
||
<p className={cn('text-sm', theme === 'dark' ? 'text-slate-300' : 'text-slate-700')}>
|
||
{format(new Date(material.createdAt), 'yyyy-MM-dd HH:mm')}
|
||
</p>
|
||
</div>
|
||
|
||
{/* Download Button */}
|
||
<button
|
||
onClick={handleDownload}
|
||
disabled={downloading}
|
||
className={cn(
|
||
'w-full py-4 text-lg font-medium relative overflow-hidden disabled:opacity-75 rounded-xl',
|
||
'bg-blue-600 hover:bg-blue-700 text-white transition-colors',
|
||
styles.buttonStyles.primary
|
||
)}
|
||
>
|
||
{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={cn(
|
||
'w-full mt-4 py-3 rounded-xl font-medium transition-colors',
|
||
styles.buttonStyles.secondary
|
||
)}
|
||
>
|
||
返回首页
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
) : (
|
||
/* 非PSD/非视频类型:统一布局 */
|
||
<div className="max-w-4xl mx-auto">
|
||
<div className={cn('rounded-2xl shadow-lg overflow-hidden', styles.cardStyles.base)}>
|
||
{/* 普通预览图 */}
|
||
<div className={cn('relative w-full h-96', theme === 'dark' ? 'bg-slate-800' : 'bg-slate-200')}>
|
||
<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={cn('text-3xl font-bold mb-2', theme === 'dark' ? 'text-white' : 'text-slate-900')}>
|
||
{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={cn('text-sm mb-1', theme === 'dark' ? 'text-slate-400' : 'text-slate-500')}>作者</p>
|
||
<p className={cn('text-lg font-medium', theme === 'dark' ? 'text-white' : 'text-slate-900')}>{material.authorName}</p>
|
||
</div>
|
||
<div>
|
||
<p className={cn('text-sm mb-1', theme === 'dark' ? 'text-slate-400' : 'text-slate-500')}>文件大小</p>
|
||
<p className="text-lg font-medium text-blue-600 dark:text-blue-400">{formatFileSize(material.zipFileSize)}</p>
|
||
</div>
|
||
<div>
|
||
<p className={cn('text-sm mb-1', theme === 'dark' ? 'text-slate-400' : 'text-slate-500')}>下载次数</p>
|
||
<p className={cn('text-lg font-medium', theme === 'dark' ? 'text-white' : 'text-slate-900')}>{material.downloadCount}</p>
|
||
</div>
|
||
<div>
|
||
<p className={cn('text-sm mb-1', theme === 'dark' ? 'text-slate-400' : 'text-slate-500')}>点赞数</p>
|
||
<p className={cn('text-lg font-medium', theme === 'dark' ? 'text-white' : 'text-slate-900')}>{material.likeCount}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="mb-6">
|
||
<p className={cn('text-sm mb-1', theme === 'dark' ? 'text-slate-400' : 'text-slate-500')}>文件名</p>
|
||
<p className={cn('text-sm break-all', theme === 'dark' ? 'text-slate-300' : 'text-slate-700')}>{material.zipFileName}</p>
|
||
</div>
|
||
|
||
<div className="mb-6">
|
||
<p className={cn('text-sm mb-1', theme === 'dark' ? 'text-slate-400' : 'text-slate-500')}>上传时间</p>
|
||
<p className={cn('text-sm', theme === 'dark' ? 'text-slate-300' : 'text-slate-700')}>
|
||
{format(new Date(material.createdAt), 'yyyy-MM-dd HH:mm')}
|
||
</p>
|
||
</div>
|
||
|
||
{renderAdditionalInfo('full')}
|
||
|
||
{/* Download Button */}
|
||
<button
|
||
onClick={handleDownload}
|
||
disabled={downloading}
|
||
className={cn(
|
||
'w-full py-4 text-lg font-medium relative overflow-hidden disabled:opacity-75 rounded-xl',
|
||
'bg-blue-600 hover:bg-blue-700 text-white transition-colors',
|
||
styles.buttonStyles.primary
|
||
)}
|
||
>
|
||
{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={cn(
|
||
'w-full mt-4 py-3 rounded-xl font-medium transition-colors',
|
||
styles.buttonStyles.secondary
|
||
)}
|
||
>
|
||
返回首页
|
||
</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>
|
||
)
|
||
}
|
||
|