1585 lines
69 KiB
TypeScript
1585 lines
69 KiB
TypeScript
'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>
|
||
)
|
||
}
|