'use client' import { useEffect, useState } from 'react' import { useSession } from 'next-auth/react' import { useRouter } from 'next/navigation' import Image from 'next/image' import { format } from 'date-fns' import UploadModal from '@/components/UploadModal' import Header from '@/components/Header' import RankingPanel from '@/components/RankingPanel' import AIChatWidget from '@/components/AIChatWidget' import ChangePasswordModal from '@/components/ChangePasswordModal' interface Material { id: string userId: 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 renderTime?: string | null imageResolution?: string | null colorMode?: string | null authorName: string zipFileName: string zipFileSize: string | number downloadCount: number likeCount: number isLiked: boolean createdAt: string user: { username: string } } export default function Home() { const { data: session, status } = useSession() const router = useRouter() const [materials, setMaterials] = useState([]) const [loading, setLoading] = useState(true) const [showUploadModal, setShowUploadModal] = useState(false) const [searchKeyword, setSearchKeyword] = useState('') const [selectedCategory, setSelectedCategory] = useState('') const [selectedScene, setSelectedScene] = useState('') const [selectedMaterialType, setSelectedMaterialType] = useState('') // 素材类型筛选 const [selectedPlatform, setSelectedPlatform] = useState('') // 平台分类筛选 const [categories, setCategories] = useState>([]) const [scenes, setScenes] = useState>([]) const [platforms, setPlatforms] = useState>([]) const [materialTypes] = useState([ { value: '3DMAX', label: '3DMAX' }, { value: 'PSD', label: 'PSD' }, { value: 'AI', label: 'AI' }, { value: 'CAD', label: 'CAD' }, { value: 'C4D', label: 'C4D' }, { value: '视频', label: '视频' }, { value: '材质', label: '材质' }, { value: '模特', label: '模特' }, { value: '标准饰品', label: '标准饰品' }, { value: '其他', label: '其他' } ]) const [downloadingId, setDownloadingId] = useState(null) const [downloadProgress, setDownloadProgress] = useState(0) const [selectedMonth, setSelectedMonth] = useState('') const [months, setMonths] = useState>([]) const [sortBy, setSortBy] = useState('createdAt') // createdAt | downloadCount const [previewImage, setPreviewImage] = useState<{ url: string; title: string } | null>(null) const [totalMaterialsCount, setTotalMaterialsCount] = useState(0) const [currentPage, setCurrentPage] = useState(1) const [totalPages, setTotalPages] = useState(1) const [shareUrl, setShareUrl] = useState(null) const [showShareModal, setShowShareModal] = useState(false) const [showShareNotification, setShowShareNotification] = useState(false) const [jumpPage, setJumpPage] = useState('') const [themeColor, setThemeColor] = useState('#1f2937') // 主题颜色 const [showIntranetTip, setShowIntranetTip] = useState(false) // 内网切换提示 const [showFilterModal, setShowFilterModal] = useState(false) // 筛选弹窗 const [showChangePasswordModal, setShowChangePasswordModal] = useState(false) // 修改密码弹窗 const [isDefaultPassword, setIsDefaultPassword] = useState(false) // 是否使用默认密码 const pageSize = 20 useEffect(() => { if (status === 'unauthenticated') { router.push('/login') } }, [status, router]) // 系统初始化检查和默认密码检查 useEffect(() => { const checkSystemInit = async () => { if (status !== 'authenticated') return try { // 1. 检查系统是否已初始化 const initResponse = await fetch('/api/system/init') const initData = await initResponse.json() if (!initData.initialized) { // 系统未初始化,执行初始化 const initResult = await fetch('/api/system/init', { method: 'POST' }) const initResultData = await initResult.json() if (initResultData.success) { console.log('系统初始化成功') // 如果是管理员且使用默认密码,显示修改密码弹窗 if ((session?.user as any)?.role === 'ADMIN') { const passwordCheck = await fetch('/api/user/check-default-password') const passwordData = await passwordCheck.json() if (passwordData.isDefaultPassword) { setIsDefaultPassword(true) setShowChangePasswordModal(true) } } } } else { // 系统已初始化,检查是否使用默认密码 const passwordCheck = await fetch('/api/user/check-default-password') const passwordData = await passwordCheck.json() if (passwordData.isDefaultPassword) { setIsDefaultPassword(true) setShowChangePasswordModal(true) } } } catch (error) { console.error('系统初始化检查失败:', error) } } checkSystemInit() }, [status, session]) // 检查是否有未完成的上传(断点续传) useEffect(() => { if (status !== 'authenticated') return const checkUnfinishedUpload = () => { try { const savedState = localStorage.getItem('uploadState') if (savedState) { const data = JSON.parse(savedState) // 检查是否在24小时内(避免过期数据) const oneDayAgo = Date.now() - 24 * 60 * 60 * 1000 if (data.timestamp && data.timestamp > oneDayAgo) { // 如果有未完成的上传,自动打开上传模态框 // UploadModal 组件内部会检查并显示恢复对话框 setShowUploadModal(true) } else { // 超过24小时,清除过期数据 localStorage.removeItem('uploadState') } } } catch (e) { console.error('Failed to check unfinished upload:', e) localStorage.removeItem('uploadState') } } checkUnfinishedUpload() }, [status]) // 检查是否需要显示内网切换提示(使用后端判断) useEffect(() => { const checkNetworkStatus = async () => { if (status !== 'authenticated') return const tipDismissed = sessionStorage.getItem('intranet_tip_dismissed') if (tipDismissed === 'true') return try { // 调用后端API获取准确的网络状态 const response = await fetch('/api/network-status') const data = await response.json() if (data.success) { console.log(`[Network Status] isIntranet: ${data.isIntranet}, isPublicDomain: ${data.isPublicDomain}`) // 如果用户在内网但访问的是公网域名,显示切换提示 if (data.isIntranet && data.isPublicDomain) { console.log('💡 后端检测到用户在内网访问公网,显示切换提示') setShowIntranetTip(true) } } } catch (error) { console.error('网络状态检测失败:', error) } } checkNetworkStatus() }, [status]) const handleDismissIntranetTip = () => { sessionStorage.setItem('intranet_tip_dismissed', 'true') setShowIntranetTip(false) } const handleSwitchToIntranet = () => { const url = process.env.NEXT_PUBLIC_INTRANET_URL if (url) { window.location.href = url } else { alert('未配置内网地址,请联系管理员设置 NEXT_PUBLIC_INTRANET_URL') } } useEffect(() => { fetchMaterials() fetchCategories() fetchScenes() fetchPlatforms() fetchThemeColor() }, []) // 获取主题颜色 const fetchThemeColor = async () => { try { const response = await fetch('/api/system-settings') if (response.ok) { const data = await response.json() setThemeColor(data.headerBgColor || '#1f2937') } } catch (error) { console.error('获取主题颜色失败:', error) } } // 当选择店铺变化时,重新获取场景数量(父子关系联动) useEffect(() => { fetchScenes(selectedCategory || undefined) }, [selectedCategory]) useEffect(() => { setCurrentPage(1) // 重置到第一页 }, [searchKeyword, selectedCategory, selectedScene, selectedMaterialType, selectedPlatform, selectedMonth, sortBy]) useEffect(() => { if (status === 'authenticated') { fetchMaterials() } // eslint-disable-next-line react-hooks/exhaustive-deps }, [currentPage, searchKeyword, selectedCategory, selectedScene, selectedMaterialType, selectedPlatform, selectedMonth, sortBy]) const fetchCategories = async () => { try { const response = await fetch('/api/categories') if (response.ok) { const data = await response.json() setCategories(data) } } catch (error) { console.error('Error fetching categories:', error) } } const fetchScenes = async (categoryName?: string) => { try { const url = categoryName ? `/api/scenes?category=${encodeURIComponent(categoryName)}` : '/api/scenes' const response = await fetch(url) if (response.ok) { const data = await response.json() setScenes(data) } } catch (error) { console.error('Error fetching scenes:', error) } } const fetchPlatforms = async () => { try { const response = await fetch('/api/platforms') if (response.ok) { const data = await response.json() setPlatforms(data) } } catch (error) { console.error('Error fetching platforms:', error) } } const fetchMaterials = async () => { try { const params = new URLSearchParams() if (searchKeyword) params.append('search', searchKeyword) if (selectedCategory) params.append('category', selectedCategory) if (selectedScene) params.append('scene', selectedScene) if (selectedMaterialType) params.append('materialType', selectedMaterialType) if (selectedPlatform) params.append('platform', selectedPlatform) if (selectedMonth) params.append('month', selectedMonth) if (sortBy) params.append('sortBy', sortBy) params.append('page', currentPage.toString()) params.append('pageSize', pageSize.toString()) const url = `/api/materials${params.toString() ? '?' + params.toString() : ''}` const response = await fetch(url) if (response.ok) { const data = await response.json() setMaterials(data.materials || data) setTotalPages(data.totalPages || 1) // 获取月份统计和总数 fetchMonthsAndTotal() } } catch (error) { console.error('Error fetching materials:', error) } finally { setLoading(false) } } const fetchMonthsAndTotal = async () => { try { const response = await fetch('/api/materials/stats') if (response.ok) { const data = await response.json() setMonths(data.months || []) setTotalMaterialsCount(data.total || 0) } } catch (error) { console.error('Error fetching months:', error) } } // 格式化文件大小 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 handleShare = async (materialId: string) => { const base = process.env.NEXT_PUBLIC_PORTAL_BASE_URL || window.location.origin const url = `${base}/share/${materialId}` setShareUrl(url) try { await navigator.clipboard.writeText(url) setShowShareNotification(true) setTimeout(() => setShowShareNotification(false), 2000) } catch (error) { setShowShareModal(true) } } const copyShareUrl = async () => { if (shareUrl) { try { await navigator.clipboard.writeText(shareUrl) alert('复制成功!') setShowShareModal(false) } catch (error) { alert('复制失败,请手动复制') } } } const handleDownload = (id: string) => { setDownloadingId(id) 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) // 刷新列表以更新下载次数 fetchMaterials() } catch (error) { console.error('下载处理失败:', error) alert('下载失败,请稍后重试') } } else { try { const errorText = xhr.responseText const error = JSON.parse(errorText) alert(error.error || '下载失败') } catch { alert('下载失败') } } setDownloadingId(null) setDownloadProgress(0) }) xhr.addEventListener('error', () => { alert('下载失败,请稍后重试') setDownloadingId(null) setDownloadProgress(0) }) xhr.open('GET', `/api/materials/download?id=${id}`) xhr.responseType = 'arraybuffer' xhr.send() } const handleLike = async (id: string) => { try { const response = await fetch('/api/materials/like', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ materialId: id }), }) if (response.ok) { // 刷新列表以更新点赞状态 fetchMaterials() } else { const error = await response.json() alert(error.error || '操作失败') } } catch (error) { console.error('Like error:', error) alert('操作失败,请稍后重试') } } // 编辑素材 const handleEdit = (material: Material) => { router.push(`/edit/${material.id}`) } if (status === 'loading' || loading) { return (
加载中...
) } if (!session) { return null } return (
{/* 内网切换提示 */} {showIntranetTip && (

🚀 检测到您正在内网环境访问公网!

切换到内网地址 ({(process.env.NEXT_PUBLIC_INTRANET_URL || '').replace(/^https?:\/\//, '')}) 可获得 10倍以上 的速度提升,上传下载更快

)}
{/* Header Section */}
{/* Search Bar and Filter Button */}
setSearchKeyword(e.target.value)} placeholder="搜索SPU、店铺、场景类别、作者..." className="w-full pl-12 pr-4 py-3 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white focus:border-gray-900 dark:focus:border-gray-400 focus:ring-2 focus:ring-gray-200 dark:focus:ring-gray-600 outline-none transition-all" /> {searchKeyword && ( )}
{/* 筛选按钮 */}
{/* 筛选选项已整合到筛选按钮中,这里隐藏 */} {false && ( <>

类型分页

{selectedMaterialType && ( {materials.length} 个素材 )}
{materialTypes.map((type) => ( ))}
{/* Category Filter (使用店铺) */}

使用店铺

{selectedCategory && ( {materials.length} 个素材 )}
{categories.map((category) => ( ))}
{/* Scene Filter (场景类别) */}

场景类别

{selectedScene && ( {materials.length} 个素材 )}
{scenes.map((scene) => ( ))}
{/* Platform Filter (平台分类) - 条件显示,仅在有平台时显示 */} {platforms.length > 0 && (

平台分类

{selectedPlatform && ( {materials.length} 个素材 )}
{platforms.map((platform) => ( ))}
)} {/* Month Filter (上传月份) */}

上传月份

{months.map((month) => ( ))}
)} {/* Sort Options (排序方式) - 保留排序选项 */}

排序方式

{/* Active Filters Display */} {(searchKeyword || selectedCategory || selectedScene || selectedMaterialType || selectedPlatform || selectedMonth) && (
当前筛选: {searchKeyword && ( 搜索: {searchKeyword} )} {selectedCategory && ( 店铺: {selectedCategory} )} {selectedScene && ( 场景类别: {selectedScene} )} {selectedMaterialType && ( 素材类型: {selectedMaterialType} )} {selectedPlatform && ( 平台分类: {selectedPlatform} )} {selectedMonth && ( 月份: {months.find(m => m.value === selectedMonth)?.label || selectedMonth} )}
💡 提示:可以组合使用多个筛选条件,找到更精确的素材
)}
{/* Materials Grid */} {loading ? (

加载中...

) : materials.length === 0 ? (

{searchKeyword || selectedCategory || selectedScene || selectedMaterialType || selectedPlatform ? '没有找到匹配的素材' : '还没有素材,快来上传第一个吧!'}

) : ( <>
{materials.map((material) => (
{/* Preview Image */}
{ // PSD、模特和视频类型直接跳转到详情页,其他类型打开预览弹窗 if (material.materialType === 'PSD' || material.materialType === '模特' || material.materialType === '视频') { router.push(`/share/${material.id}`) } else { setPreviewImage({ url: material.previewUrl, title: material.productModel }) } }} > {material.sceneName} {/* SPU标签 */}
{material.productModel}
{/* 放大提示 */}
{/* Info */}
{/* 店铺、场景、素材类型和渲染器 */}
{material.categoryName} {material.sceneName} {material.materialType} {material.materialType === '3DMAX' && ( {material.renderer} )}
{/* 作者和时间 */}

作者: {material.authorName}

文件: {material.zipFileName}

大小: {formatFileSize(material.zipFileSize)}

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

{/* 统计数据 */}
{material.downloadCount} {material.likeCount}
{/* Action Buttons */}
{/* 编辑按钮:仅素材上传者和管理员可见 */} {session?.user && ( material.userId === (session.user as any).id || (session.user as any).role === 'ADMIN' ) && ( )}
))}
{/* 分页组件 */} {totalPages > 1 && (
{/* 首页 */} {/* 上一页 */} {/* 页码 */}
{Array.from({ length: Math.min(totalPages, 7) }, (_, i) => { let pageNum if (totalPages <= 7) { pageNum = i + 1 } else if (currentPage <= 4) { pageNum = i + 1 } else if (currentPage >= totalPages - 3) { pageNum = totalPages - 6 + i } else { pageNum = currentPage - 3 + i } return ( ) })}
{/* 下一页 */} {/* 末页 */}
{/* 页码跳转和信息 */}
第 {currentPage} / {totalPages} 页,共 {totalMaterialsCount} 条
跳至 setJumpPage(e.target.value)} onKeyPress={(e) => { if (e.key === 'Enter') { const page = parseInt(jumpPage) if (page >= 1 && page <= totalPages) { setCurrentPage(page) setJumpPage('') window.scrollTo({ top: 0, behavior: 'smooth' }) } } }} className="w-16 px-2 py-1 text-center rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none" placeholder="" /> 页
)} )}
{/* Upload Modal */} {showUploadModal && ( setShowUploadModal(false)} onSuccess={() => { setShowUploadModal(false) fetchMaterials() fetchCategories() fetchScenes() }} /> )} {/* Image Preview Modal */} {previewImage && (
setPreviewImage(null)} >
{previewImage.title} e.stopPropagation()} />

{previewImage.title}

)} {/* Share Notification */} {showShareNotification && (
分享链接已复制到剪贴板!
)} {/* 筛选弹窗 */} {showFilterModal && (
setShowFilterModal(false)} >
e.stopPropagation()} >

筛选选项

{/* 类型分页 */}

类型分页

{materialTypes.map((type) => ( ))}
{/* 使用店铺 */}

使用店铺

{categories.map((category) => ( ))}
{/* 场景类别 */}

场景类别

{scenes.map((scene) => ( ))}
{/* 平台分类 */} {platforms.length > 0 && (

平台分类

{platforms.map((platform) => ( ))}
)} {/* 上传月份 */}

上传月份

{months.map((month) => ( ))}
)} {/* Share Modal */} {showShareModal && shareUrl && (
setShowShareModal(false)} >
e.stopPropagation()} >

分享链接

{shareUrl}
)} {/* AI聊天助手 */} {/* 修改密码弹窗 */} { if (!isDefaultPassword) { setShowChangePasswordModal(false) } }} isRequired={isDefaultPassword} onSuccess={() => { setIsDefaultPassword(false) setShowChangePasswordModal(false) // 刷新页面以更新session window.location.reload() }} />
) }