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

1585 lines
69 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 } 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<Material[]>([])
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<Array<{ id: string; name: string; count: number }>>([])
const [scenes, setScenes] = useState<Array<{ id: string; name: string; count: number }>>([])
const [platforms, setPlatforms] = useState<Array<{ id: string; name: string; count: number }>>([])
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<string | null>(null)
const [downloadProgress, setDownloadProgress] = useState(0)
const [selectedMonth, setSelectedMonth] = useState('')
const [months, setMonths] = useState<Array<{ value: string; label: string; count: number }>>([])
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<string | null>(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 (
<div className="min-h-screen flex items-center justify-center dark:bg-gray-900">
<div className="text-xl text-gray-600 dark:text-gray-400">加载中...</div>
</div>
)
}
if (!session) {
return null
}
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
<Header session={session} />
{/* 内网切换提示 */}
{showIntranetTip && (
<div className="bg-gradient-to-r from-blue-500 to-blue-600 text-white shadow-lg">
<div className="container mx-auto px-4 py-3 max-w-7xl">
<div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-3 flex-1">
<svg className="w-6 h-6 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="flex-1">
<p className="font-medium text-sm sm:text-base">
🚀 检测到您正在内网环境访问公网!
</p>
<p className="text-xs sm:text-sm opacity-90 mt-0.5">
切换到内网地址 ({(process.env.NEXT_PUBLIC_INTRANET_URL || '').replace(/^https?:\/\//, '')}) 可获得 <strong>10倍以上</strong> 的速度提升,上传下载更快
</p>
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<button
onClick={handleSwitchToIntranet}
className="px-4 py-2 bg-white text-blue-600 rounded-lg hover:bg-blue-50 transition-colors font-medium text-sm whitespace-nowrap"
>
立即切换
</button>
<button
onClick={handleDismissIntranetTip}
className="px-3 py-2 hover:bg-blue-700 rounded-lg transition-colors"
title="关闭提示"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
</div>
)}
<RankingPanel />
<main className="container mx-auto px-4 py-8 max-w-7xl">
{/* Header Section */}
<div className="mb-8">
<div className="flex justify-end items-center mb-6">
<button
onClick={() => setShowUploadModal(true)}
className="px-6 py-3 rounded-apple text-white font-medium transition-all hover:opacity-90 shadow-apple"
style={{ backgroundColor: themeColor }}
>
<span className="mr-2">+</span>
上传素材
</button>
</div>
{/* Search Bar and Filter Button */}
<div className="mb-6 flex gap-3">
<div className="relative flex-1">
<input
type="text"
value={searchKeyword}
onChange={(e) => 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"
/>
<svg
className="absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
{searchKeyword && (
<button
onClick={() => setSearchKeyword('')}
className="absolute right-4 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
)}
</div>
{/* 筛选按钮 */}
<button
onClick={() => setShowFilterModal(true)}
className="px-6 py-3 rounded-apple border border-gray-300 dark:border-gray-600 dark:bg-gray-800 dark:text-white hover:bg-gray-50 dark:hover:bg-gray-700 transition-all flex items-center gap-2 relative"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
</svg>
筛选
{/* 显示已选筛选数量 */}
{(selectedCategory || selectedScene || selectedMaterialType || selectedPlatform || selectedMonth) && (
<span className="absolute -top-1 -right-1 w-5 h-5 bg-red-500 text-white text-xs rounded-full flex items-center justify-center">
{[selectedCategory, selectedScene, selectedMaterialType, selectedPlatform, selectedMonth].filter(Boolean).length}
</span>
)}
</button>
</div>
{/* 筛选选项已整合到筛选按钮中,这里隐藏 */}
{false && (
<>
<div className="mb-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-medium text-gray-700 dark:text-gray-300">类型分页</h2>
{selectedMaterialType && (
<span className="text-xs text-gray-500 dark:text-gray-400">
{materials.length} 个素材
</span>
)}
</div>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedMaterialType('')}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedMaterialType === ''
? 'text-white'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
style={selectedMaterialType === '' ? { backgroundColor: themeColor } : {}}
>
全部
</button>
{materialTypes.map((type) => (
<button
key={type.value}
onClick={() => setSelectedMaterialType(type.value === selectedMaterialType ? '' : type.value)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedMaterialType === type.value
? 'text-white'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
style={selectedMaterialType === type.value ? { backgroundColor: themeColor } : {}}
>
{type.label}
</button>
))}
</div>
</div>
{/* Category Filter (使用店铺) */}
<div className="mb-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-medium text-gray-700 dark:text-gray-300">使用店铺</h2>
{selectedCategory && (
<span className="text-xs text-gray-500 dark:text-gray-400">
{materials.length} 个素材
</span>
)}
</div>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedCategory('')}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedCategory === ''
? 'text-white'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
style={selectedCategory === '' ? { backgroundColor: themeColor } : {}}
>
全部
<span className={`ml-1.5 text-xs ${
selectedCategory === ''
? 'text-gray-300'
: 'text-gray-500 dark:text-gray-400'
}`}>
({totalMaterialsCount})
</span>
</button>
{categories.map((category) => (
<button
key={category.id}
onClick={() => setSelectedCategory(category.name === selectedCategory ? '' : category.name)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedCategory === category.name
? 'text-white'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
style={selectedCategory === category.name ? { backgroundColor: themeColor } : {}}
>
{category.name}
<span className={`ml-1.5 text-xs ${
selectedCategory === category.name
? 'text-gray-300'
: 'text-gray-500 dark:text-gray-400'
}`}>
({category.count || 0})
</span>
</button>
))}
</div>
</div>
{/* Scene Filter (场景类别) */}
<div className="mb-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-medium text-gray-700 dark:text-gray-300">场景类别</h2>
{selectedScene && (
<span className="text-xs text-gray-500 dark:text-gray-400">
{materials.length} 个素材
</span>
)}
</div>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedScene('')}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedScene === ''
? 'text-white'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
style={selectedScene === '' ? { backgroundColor: themeColor } : {}}
>
全部
<span className={`ml-1.5 text-xs ${
selectedScene === ''
? 'text-gray-300'
: 'text-gray-500 dark:text-gray-400'
}`}>
({totalMaterialsCount})
</span>
</button>
{scenes.map((scene) => (
<button
key={scene.id}
onClick={() => setSelectedScene(scene.name === selectedScene ? '' : scene.name)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedScene === scene.name
? 'text-white'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
style={selectedScene === scene.name ? { backgroundColor: themeColor } : {}}
>
{scene.name}
<span className={`ml-1.5 text-xs ${
selectedScene === scene.name
? 'text-gray-300'
: 'text-gray-500 dark:text-gray-400'
}`}>
({scene.count || 0})
</span>
</button>
))}
</div>
</div>
{/* Platform Filter (平台分类) - 条件显示,仅在有平台时显示 */}
{platforms.length > 0 && (
<div className="mb-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-medium text-gray-700 dark:text-gray-300">平台分类</h2>
{selectedPlatform && (
<span className="text-xs text-gray-500 dark:text-gray-400">
{materials.length} 个素材
</span>
)}
</div>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedPlatform('')}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedPlatform === ''
? 'text-white'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
style={selectedPlatform === '' ? { backgroundColor: themeColor } : {}}
>
全部
<span className={`ml-1.5 text-xs ${
selectedPlatform === ''
? 'text-gray-300'
: 'text-gray-500 dark:text-gray-400'
}`}>
({totalMaterialsCount})
</span>
</button>
{platforms.map((platform) => (
<button
key={platform.id}
onClick={() => setSelectedPlatform(platform.name === selectedPlatform ? '' : platform.name)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedPlatform === platform.name
? 'text-white'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
style={selectedPlatform === platform.name ? { backgroundColor: themeColor } : {}}
>
{platform.name}
<span className={`ml-1.5 text-xs ${
selectedPlatform === platform.name
? 'text-gray-300'
: 'text-gray-500 dark:text-gray-400'
}`}>
({platform.count || 0})
</span>
</button>
))}
</div>
</div>
)}
{/* Month Filter (上传月份) */}
<div className="mb-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-medium text-gray-700 dark:text-gray-300">上传月份</h2>
</div>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedMonth('')}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedMonth === ''
? 'text-white'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
style={selectedMonth === '' ? { backgroundColor: themeColor } : {}}
>
全部
<span className={`ml-1.5 text-xs ${
selectedMonth === ''
? 'text-gray-300'
: 'text-gray-500 dark:text-gray-400'
}`}>
({totalMaterialsCount})
</span>
</button>
{months.map((month) => (
<button
key={month.value}
onClick={() => setSelectedMonth(month.value === selectedMonth ? '' : month.value)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedMonth === month.value
? 'text-white'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
style={selectedMonth === month.value ? { backgroundColor: themeColor } : {}}
>
{month.label}
<span className={`ml-1.5 text-xs ${
selectedMonth === month.value
? 'text-gray-300'
: 'text-gray-500 dark:text-gray-400'
}`}>
({month.count})
</span>
</button>
))}
</div>
</div>
</>
)}
{/* Sort Options (排序方式) - 保留排序选项 */}
<div className="mb-4">
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-medium text-gray-700 dark:text-gray-300">排序方式</h2>
</div>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSortBy('createdAt')}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
sortBy === 'createdAt'
? 'text-white'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
style={sortBy === 'createdAt' ? { backgroundColor: themeColor } : {}}
>
最新上传
</button>
<button
onClick={() => setSortBy('downloadCount')}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
sortBy === 'downloadCount'
? 'text-white'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
style={sortBy === 'downloadCount' ? { backgroundColor: themeColor } : {}}
>
下载最多
</button>
</div>
</div>
{/* Active Filters Display */}
{(searchKeyword || selectedCategory || selectedScene || selectedMaterialType || selectedPlatform || selectedMonth) && (
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-apple p-3 mb-4">
<div className="flex items-center gap-2 text-sm text-gray-600 dark:text-gray-400 flex-wrap">
<span className="font-medium text-blue-600 dark:text-blue-400">当前筛选:</span>
{searchKeyword && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 dark:bg-gray-800 rounded-full">
搜索: {searchKeyword}
<button
onClick={() => setSearchKeyword('')}
className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
>
×
</button>
</span>
)}
{selectedCategory && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 dark:bg-gray-800 rounded-full">
店铺: {selectedCategory}
<button
onClick={() => setSelectedCategory('')}
className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
>
×
</button>
</span>
)}
{selectedScene && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 dark:bg-gray-800 rounded-full">
场景类别: {selectedScene}
<button
onClick={() => setSelectedScene('')}
className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
>
×
</button>
</span>
)}
{selectedMaterialType && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 dark:bg-gray-800 rounded-full">
素材类型: {selectedMaterialType}
<button
onClick={() => setSelectedMaterialType('')}
className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
>
×
</button>
</span>
)}
{selectedPlatform && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 dark:bg-gray-800 rounded-full">
平台分类: {selectedPlatform}
<button
onClick={() => setSelectedPlatform('')}
className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
>
×
</button>
</span>
)}
{selectedMonth && (
<span className="inline-flex items-center gap-1 px-3 py-1 bg-gray-100 dark:bg-gray-800 rounded-full">
月份: {months.find(m => m.value === selectedMonth)?.label || selectedMonth}
<button
onClick={() => setSelectedMonth('')}
className="text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
>
×
</button>
</span>
)}
<button
onClick={() => {
setSearchKeyword('')
setSelectedCategory('')
setSelectedScene('')
setSelectedMaterialType('')
setSelectedPlatform('')
setSelectedMonth('')
}}
className="text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 underline ml-2 font-medium"
>
清除所有筛选
</button>
</div>
<div className="text-xs text-gray-500 dark:text-gray-400 mt-2">
💡 提示:可以组合使用多个筛选条件,找到更精确的素材
</div>
</div>
)}
</div>
{/* Materials Grid */}
{loading ? (
<div className="text-center py-16">
<p className="text-gray-500 dark:text-gray-400 text-lg">加载中...</p>
</div>
) : materials.length === 0 ? (
<div className="text-center py-16">
<p className="text-gray-500 dark:text-gray-400 text-lg">
{searchKeyword || selectedCategory || selectedScene || selectedMaterialType || selectedPlatform ? '没有找到匹配的素材' : '还没有素材,快来上传第一个吧!'}
</p>
</div>
) : (
<>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
{materials.map((material) => (
<div key={material.id} className="card-apple overflow-hidden dark:bg-gray-800 dark:border-gray-700">
{/* Preview Image */}
<div
className="relative w-full h-[400px] bg-gray-200 dark:bg-gray-700 cursor-pointer group overflow-hidden"
onClick={() => {
// PSD、模特和视频类型直接跳转到详情页,其他类型打开预览弹窗
if (material.materialType === 'PSD' || material.materialType === '模特' || material.materialType === '视频') {
router.push(`/share/${material.id}`)
} else {
setPreviewImage({ url: material.previewUrl, title: material.productModel })
}
}}
>
<Image
src={material.previewUrl}
alt={material.sceneName}
fill
unoptimized
className="object-cover transition-transform group-hover:scale-105"
sizes="(max-width: 768px) 100vw, 300px"
/>
{/* SPU标签 */}
<div className="absolute top-2 left-2 bg-black/70 text-white px-2 py-1 rounded text-xs font-medium">
{material.productModel}
</div>
{/* 放大提示 */}
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/30 transition-all flex items-center justify-center">
<svg className="w-12 h-12 text-white opacity-0 group-hover:opacity-100 transition-opacity" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v6m3-3H7" />
</svg>
</div>
</div>
{/* Info */}
<div className="p-4">
{/* 店铺、场景、素材类型和渲染器 */}
<div className="flex items-center gap-2 mb-3 flex-wrap">
<span className="inline-flex items-center px-2 py-1 bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded text-xs font-medium">
{material.categoryName}
</span>
<span className="inline-flex items-center px-2 py-1 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300 rounded text-xs font-medium">
{material.sceneName}
</span>
<span className="inline-flex items-center px-2 py-1 bg-orange-100 dark:bg-orange-900/30 text-orange-700 dark:text-orange-300 rounded text-xs font-medium">
{material.materialType}
</span>
{material.materialType === '3DMAX' && (
<span className="inline-flex items-center px-2 py-1 bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded text-xs font-medium">
{material.renderer}
</span>
)}
</div>
{/* 作者和时间 */}
<div className="space-y-1 text-sm text-gray-600 dark:text-gray-400 mb-3">
<p className="truncate">
<span className="font-medium">作者:</span>
{material.authorName}
</p>
<p className="truncate text-xs">
<span className="font-medium">文件:</span>
{material.zipFileName}
</p>
<p className="text-xs">
<span className="font-medium">大小:</span>
<span className="text-blue-600 dark:text-blue-400">{formatFileSize(material.zipFileSize)}</span>
</p>
<p className="text-xs text-gray-500 dark:text-gray-500">
{format(new Date(material.createdAt), 'yyyy-MM-dd HH:mm')}
</p>
</div>
{/* 统计数据 */}
<div className="flex items-center justify-between text-xs text-gray-500 dark:text-gray-400 mb-3 pb-3 border-b border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-3">
<span className="flex items-center gap-1">
<svg className="w-4 h-4" 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>
{material.downloadCount}
</span>
<span className="flex items-center gap-1">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
</svg>
{material.likeCount}
</span>
</div>
</div>
{/* Action Buttons */}
<div className="flex gap-2">
{/* 编辑按钮:仅素材上传者和管理员可见 */}
{session?.user && (
material.userId === (session.user as any).id || (session.user as any).role === 'ADMIN'
) && (
<button
onClick={() => handleEdit(material)}
className="flex-none px-4 py-2 rounded-apple font-medium text-sm transition-all bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300 hover:bg-blue-200 dark:hover:bg-blue-800"
title="编辑"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
</svg>
</button>
)}
<button
onClick={() => handleShare(material.id)}
className="flex-none px-4 py-2 rounded-apple font-medium text-sm transition-all bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600"
title="分享"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8.684 13.342C8.886 12.938 9 12.482 9 12c0-.482-.114-.938-.316-1.342m0 2.684a3 3 0 110-2.684m0 2.684l6.632 3.316m-6.632-6l6.632-3.316m0 0a3 3 0 105.367-2.684 3 3 0 00-5.367 2.684zm0 9.316a3 3 0 105.368 2.684 3 3 0 00-5.368-2.684z" />
</svg>
</button>
<button
onClick={() => handleDownload(material.id)}
disabled={downloadingId === material.id}
className="flex-1 btn-primary text-sm flex items-center justify-center gap-2 relative overflow-hidden disabled:opacity-75"
title="下载"
>
{downloadingId === material.id ? (
<>
{/* 进度条背景 */}
<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 gap-2">
<svg className="w-4 h-4 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>
</>
) : (
<>
<svg className="w-4 h-4" 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>
下载
</>
)}
</button>
</div>
</div>
</div>
))}
</div>
{/* 分页组件 */}
{totalPages > 1 && (
<div className="mt-8 flex flex-col items-center gap-4">
<div className="flex items-center justify-center gap-2">
{/* 首页 */}
<button
onClick={() => {
setCurrentPage(1)
window.scrollTo({ top: 0, behavior: 'smooth' })
}}
disabled={currentPage === 1}
className="px-4 py-2 rounded-lg bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
首页
</button>
{/* 上一页 */}
<button
onClick={() => {
setCurrentPage(prev => Math.max(1, prev - 1))
window.scrollTo({ top: 0, behavior: 'smooth' })
}}
disabled={currentPage === 1}
className="px-4 py-2 rounded-lg bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
<svg className="w-5 h-5" 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>
{/* 页码 */}
<div className="flex items-center gap-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 (
<button
key={pageNum}
onClick={() => {
setCurrentPage(pageNum)
window.scrollTo({ top: 0, behavior: 'smooth' })
}}
className={`px-4 py-2 rounded-lg transition-all ${
currentPage === pageNum
? 'text-white font-medium'
: 'bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700'
}`}
style={currentPage === pageNum ? { backgroundColor: themeColor } : {}}
>
{pageNum}
</button>
)
})}
</div>
{/* 下一页 */}
<button
onClick={() => {
setCurrentPage(prev => Math.min(totalPages, prev + 1))
window.scrollTo({ top: 0, behavior: 'smooth' })
}}
disabled={currentPage === totalPages}
className="px-4 py-2 rounded-lg bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
{/* 末页 */}
<button
onClick={() => {
setCurrentPage(totalPages)
window.scrollTo({ top: 0, behavior: 'smooth' })
}}
disabled={currentPage === totalPages}
className="px-4 py-2 rounded-lg bg-white dark:bg-gray-800 text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-50 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
>
末页
</button>
</div>
{/* 页码跳转和信息 */}
<div className="flex items-center gap-4">
<span className="text-sm text-gray-600 dark:text-gray-400">
第 {currentPage} / {totalPages} 页,共 {totalMaterialsCount} 条
</span>
<div className="flex items-center gap-2">
<span className="text-sm text-gray-600 dark:text-gray-400">跳至</span>
<input
type="number"
min="1"
max={totalPages}
value={jumpPage}
onChange={(e) => 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=""
/>
<span className="text-sm text-gray-600 dark:text-gray-400">页</span>
<button
onClick={() => {
const page = parseInt(jumpPage)
if (page >= 1 && page <= totalPages) {
setCurrentPage(page)
setJumpPage('')
window.scrollTo({ top: 0, behavior: 'smooth' })
}
}}
className="px-3 py-1 text-sm rounded text-white hover:opacity-90 transition-all"
style={{ backgroundColor: themeColor }}
>
跳转
</button>
</div>
</div>
</div>
)}
</>
)}
</main>
{/* Upload Modal */}
{showUploadModal && (
<UploadModal
onClose={() => setShowUploadModal(false)}
onSuccess={() => {
setShowUploadModal(false)
fetchMaterials()
fetchCategories()
fetchScenes()
}}
/>
)}
{/* Image Preview Modal */}
{previewImage && (
<div
className="fixed inset-0 z-[100] bg-black/90 flex items-center justify-center p-4"
onClick={() => setPreviewImage(null)}
>
<div className="relative max-w-7xl max-h-[90vh] w-full h-full flex items-center justify-center">
<button
onClick={() => setPreviewImage(null)}
className="absolute top-4 right-4 z-10 bg-white/10 hover:bg-white/20 text-white rounded-full p-2 transition-all"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
<div className="text-center">
<img
src={previewImage.url}
alt={previewImage.title}
className="max-w-full max-h-[80vh] object-contain mx-auto"
onClick={(e) => e.stopPropagation()}
/>
<p className="text-white mt-4 text-lg font-medium">{previewImage.title}</p>
</div>
</div>
</div>
)}
{/* Share Notification */}
{showShareNotification && (
<div className="fixed top-20 left-1/2 transform -translate-x-1/2 bg-green-500 text-white px-6 py-3 rounded-lg shadow-lg z-[200] flex items-center gap-2 animate-fade-in">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
<span>分享链接已复制到剪贴板!</span>
</div>
)}
{/* 筛选弹窗 */}
{showFilterModal && (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center z-[200] p-4"
onClick={() => setShowFilterModal(false)}
>
<div
className="bg-white dark:bg-gray-800 rounded-xl p-6 max-w-4xl w-full max-h-[90vh] overflow-y-auto shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between mb-6">
<h3 className="text-xl font-semibold text-gray-900 dark:text-white">筛选选项</h3>
<button
onClick={() => setShowFilterModal(false)}
className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div className="space-y-6">
{/* 类型分页 */}
<div>
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">类型分页</h4>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedMaterialType('')}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedMaterialType === ''
? 'text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
style={selectedMaterialType === '' ? { backgroundColor: themeColor } : {}}
>
全部
</button>
{materialTypes.map((type) => (
<button
key={type.value}
onClick={() => setSelectedMaterialType(type.value === selectedMaterialType ? '' : type.value)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedMaterialType === type.value
? 'text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
style={selectedMaterialType === type.value ? { backgroundColor: themeColor } : {}}
>
{type.label}
</button>
))}
</div>
</div>
{/* 使用店铺 */}
<div>
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">使用店铺</h4>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedCategory('')}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedCategory === ''
? 'text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
style={selectedCategory === '' ? { backgroundColor: themeColor } : {}}
>
全部 ({totalMaterialsCount})
</button>
{categories.map((category) => (
<button
key={category.id}
onClick={() => setSelectedCategory(category.name === selectedCategory ? '' : category.name)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedCategory === category.name
? 'text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
style={selectedCategory === category.name ? { backgroundColor: themeColor } : {}}
>
{category.name} ({category.count || 0})
</button>
))}
</div>
</div>
{/* 场景类别 */}
<div>
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">场景类别</h4>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedScene('')}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedScene === ''
? 'text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
style={selectedScene === '' ? { backgroundColor: themeColor } : {}}
>
全部 ({totalMaterialsCount})
</button>
{scenes.map((scene) => (
<button
key={scene.id}
onClick={() => setSelectedScene(scene.name === selectedScene ? '' : scene.name)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedScene === scene.name
? 'text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
style={selectedScene === scene.name ? { backgroundColor: themeColor } : {}}
>
{scene.name} ({scene.count || 0})
</button>
))}
</div>
</div>
{/* 平台分类 */}
{platforms.length > 0 && (
<div>
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">平台分类</h4>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedPlatform('')}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedPlatform === ''
? 'text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
style={selectedPlatform === '' ? { backgroundColor: themeColor } : {}}
>
全部 ({totalMaterialsCount})
</button>
{platforms.map((platform) => (
<button
key={platform.id}
onClick={() => setSelectedPlatform(platform.name === selectedPlatform ? '' : platform.name)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedPlatform === platform.name
? 'text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
style={selectedPlatform === platform.name ? { backgroundColor: themeColor } : {}}
>
{platform.name} ({platform.count || 0})
</button>
))}
</div>
</div>
)}
{/* 上传月份 */}
<div>
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">上传月份</h4>
<div className="flex flex-wrap gap-2">
<button
onClick={() => setSelectedMonth('')}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedMonth === ''
? 'text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
style={selectedMonth === '' ? { backgroundColor: themeColor } : {}}
>
全部 ({totalMaterialsCount})
</button>
{months.map((month) => (
<button
key={month.value}
onClick={() => setSelectedMonth(month.value === selectedMonth ? '' : month.value)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-all ${
selectedMonth === month.value
? 'text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
style={selectedMonth === month.value ? { backgroundColor: themeColor } : {}}
>
{month.label} ({month.count})
</button>
))}
</div>
</div>
</div>
<div className="mt-6 flex gap-3 justify-end">
<button
onClick={() => {
setSelectedCategory('')
setSelectedScene('')
setSelectedMaterialType('')
setSelectedPlatform('')
setSelectedMonth('')
}}
className="px-4 py-2 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
清除全部
</button>
<button
onClick={() => setShowFilterModal(false)}
className="px-4 py-2 text-white rounded-lg transition-all hover:opacity-90"
style={{ backgroundColor: themeColor }}
>
确定
</button>
</div>
</div>
</div>
)}
{/* Share Modal */}
{showShareModal && shareUrl && (
<div
className="fixed inset-0 bg-black/50 flex items-center justify-center z-[200]"
onClick={() => setShowShareModal(false)}
>
<div
className="bg-white dark:bg-gray-800 rounded-xl p-6 max-w-lg w-full mx-4 shadow-2xl"
onClick={(e) => e.stopPropagation()}
>
<h3 className="text-lg font-semibold mb-4 text-gray-900 dark:text-white">分享链接</h3>
<div className="bg-gray-100 dark:bg-gray-700 p-3 rounded-lg mb-4 break-all text-sm text-gray-800 dark:text-gray-200">
{shareUrl}
</div>
<div className="flex gap-2">
<button
onClick={copyShareUrl}
className="flex-1 text-white px-4 py-2 rounded-lg transition-all hover:opacity-90"
style={{ backgroundColor: themeColor }}
>
复制链接
</button>
<button
onClick={() => setShowShareModal(false)}
className="flex-1 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 text-gray-800 dark:text-white px-4 py-2 rounded-lg transition-colors"
>
关闭
</button>
</div>
</div>
</div>
)}
{/* AI聊天助手 */}
<AIChatWidget />
{/* 修改密码弹窗 */}
<ChangePasswordModal
isOpen={showChangePasswordModal}
onClose={() => {
if (!isDefaultPassword) {
setShowChangePasswordModal(false)
}
}}
isRequired={isDefaultPassword}
onSuccess={() => {
setIsDefaultPassword(false)
setShowChangePasswordModal(false)
// 刷新页面以更新session
window.location.reload()
}}
/>
</div>
)
}