Some checks failed
CI Check / check (push) Has been cancelled
- 替换所有浏览器原生弹窗(confirm/alert/prompt) - 创建 ConfirmDialog、AlertDialog、PromptDialog 组件 - 创建 DialogProvider 和 useDialog Hook - 替换所有 admin 组件、首页、分享页面中的原生弹窗 - 素材预览面板统一 - 创建 MaterialPreviewModal 组件 - 使用统一弹窗组件,统一退出逻辑 - 首页UI统一到三层架构 - 迁移主要按钮和输入框样式 - 迁移筛选弹窗中的所有按钮样式 - 使用 useThemeStyles() Hook - 更新文档 - 更新优化记录、优化计划、后续修改、更新日志
222 lines
8.7 KiB
TypeScript
222 lines
8.7 KiB
TypeScript
'use client'
|
||
|
||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
||
import Image from 'next/image'
|
||
import EditMaterialModal from '@/components/EditMaterialModal'
|
||
import { useThemeStyles, useTheme, cn } from '@/lib/ui-styles'
|
||
import { useDialog } from '@/components/ui/DialogProvider'
|
||
import type { Material, ToastFn } from './types'
|
||
import LoadingState from './ui/LoadingState'
|
||
|
||
interface MaterialsTabProps {
|
||
showToast: ToastFn
|
||
}
|
||
|
||
export default function MaterialsTab({ showToast }: MaterialsTabProps) {
|
||
const styles = useThemeStyles()
|
||
const { confirm } = useDialog()
|
||
const theme = useTheme()
|
||
const [materials, setMaterials] = useState<Material[]>([])
|
||
const [loading, setLoading] = useState(true)
|
||
const [editingMaterial, setEditingMaterial] = useState<Material | null>(null)
|
||
const [materialSearchTerm, setMaterialSearchTerm] = useState('')
|
||
|
||
const fetchMaterials = useCallback(async () => {
|
||
try {
|
||
setLoading(true)
|
||
const response = await fetch('/api/materials?pageSize=1000')
|
||
if (response.ok) {
|
||
const data = await response.json()
|
||
setMaterials(data.materials || data)
|
||
} else {
|
||
showToast('获取素材列表失败', 'error')
|
||
}
|
||
} catch (error) {
|
||
console.error('Error fetching materials:', error)
|
||
showToast('获取素材列表失败', 'error')
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
}, [showToast])
|
||
|
||
useEffect(() => {
|
||
fetchMaterials()
|
||
}, [fetchMaterials])
|
||
|
||
const handleDeleteMaterial = async (id: string) => {
|
||
const confirmed = await confirm({
|
||
title: '确认删除',
|
||
message: '确定要删除这个素材吗?',
|
||
variant: 'danger',
|
||
})
|
||
if (!confirmed) return
|
||
|
||
try {
|
||
const response = await fetch(`/api/materials?id=${id}`, {
|
||
method: 'DELETE',
|
||
})
|
||
|
||
if (response.ok) {
|
||
setMaterials(prevMaterials => prevMaterials.filter(m => m.id !== id))
|
||
showToast('删除成功')
|
||
} else {
|
||
showToast('删除失败', 'error')
|
||
}
|
||
} catch (error) {
|
||
console.error('Error deleting material:', error)
|
||
showToast('删除失败', 'error')
|
||
}
|
||
}
|
||
|
||
const filteredMaterials = useMemo(() => {
|
||
if (!materialSearchTerm.trim()) return materials
|
||
const term = materialSearchTerm.toLowerCase()
|
||
return materials.filter((material) => {
|
||
return (
|
||
material.productModel?.toLowerCase().includes(term) ||
|
||
material.brandName?.toLowerCase().includes(term) ||
|
||
material.categoryName?.toLowerCase().includes(term) ||
|
||
material.sceneName?.toLowerCase().includes(term) ||
|
||
material.materialType?.toLowerCase().includes(term) ||
|
||
material.user.username?.toLowerCase().includes(term)
|
||
)
|
||
})
|
||
}, [materials, materialSearchTerm])
|
||
|
||
if (loading) {
|
||
return <LoadingState message="加载素材列表..." />
|
||
}
|
||
|
||
// 根据主题选择颜色类名
|
||
const borderColor = theme === 'dark' ? 'border-slate-800' : 'border-slate-200'
|
||
const textTitle = theme === 'dark' ? 'text-white' : 'text-slate-900'
|
||
const textDesc = theme === 'dark' ? 'text-slate-400' : 'text-slate-500'
|
||
const textIcon = theme === 'dark' ? 'text-slate-500' : 'text-slate-400'
|
||
const textIconHover = theme === 'dark' ? 'hover:text-slate-200' : 'hover:text-slate-700'
|
||
const bgImage = theme === 'dark' ? 'bg-slate-800' : 'bg-slate-100'
|
||
const textPlaceholder = theme === 'dark' ? 'text-slate-500' : 'text-slate-400'
|
||
const textItem = theme === 'dark' ? 'text-white' : 'text-slate-900'
|
||
const textItemDesc = theme === 'dark' ? 'text-slate-400' : 'text-slate-500'
|
||
|
||
return (
|
||
<>
|
||
<div className={cn(styles.cardStyles.base)}>
|
||
<div className={cn('px-6 py-4 border-b', borderColor, 'flex items-center justify-between')}>
|
||
<div>
|
||
<h2 className={cn('text-xl font-semibold', textTitle)}>素材管理</h2>
|
||
<p className={cn('text-sm mt-1', textDesc)}>
|
||
管理所有上传的素材,支持搜索、编辑和删除
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div className="p-6">
|
||
{/* 搜索框 */}
|
||
<div className="mb-6">
|
||
<div className="relative">
|
||
<input
|
||
type="text"
|
||
placeholder="搜索素材(型号、品牌、分类、场景、类型、上传者)..."
|
||
value={materialSearchTerm}
|
||
onChange={(e) => setMaterialSearchTerm(e.target.value)}
|
||
className={cn(styles.inputStyles.base, 'pl-11 pr-10')}
|
||
/>
|
||
<svg className={cn('absolute left-4 top-3 w-5 h-5', textIcon)} 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>
|
||
{materialSearchTerm && (
|
||
<button
|
||
onClick={() => setMaterialSearchTerm('')}
|
||
className={cn(
|
||
styles.buttonStyles.ghost,
|
||
styles.buttonModifiers.icon,
|
||
'absolute right-2 top-2',
|
||
textIcon,
|
||
textIconHover,
|
||
)}
|
||
aria-label="清空搜索"
|
||
>
|
||
<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>
|
||
|
||
{/* 素材列表 */}
|
||
{filteredMaterials.length === 0 ? (
|
||
<div className={cn('text-center py-12', textDesc)}>
|
||
{materialSearchTerm ? '暂无匹配的素材' : '暂无素材'}
|
||
</div>
|
||
) : (
|
||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
|
||
{filteredMaterials.map((material) => (
|
||
<div
|
||
key={material.id}
|
||
className={cn(styles.cardStyles.hover, 'overflow-hidden')}
|
||
>
|
||
<div className={cn('aspect-square relative overflow-hidden', bgImage)}>
|
||
{material.previewUrl ? (
|
||
<Image
|
||
src={material.previewUrl}
|
||
alt={material.productModel || '素材预览图'}
|
||
fill
|
||
className="object-cover"
|
||
sizes="(max-width: 768px) 100vw, (max-width: 1280px) 33vw, 25vw"
|
||
/>
|
||
) : (
|
||
<div className={cn('w-full h-full flex items-center justify-center', textPlaceholder)}>
|
||
<svg className="w-12 h-12" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||
</svg>
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div className="p-4">
|
||
<h3 className={cn('font-semibold truncate mb-1', textItem)}>
|
||
{material.productModel}
|
||
</h3>
|
||
<div className={cn('text-xs space-y-1 mb-3', textItemDesc)}>
|
||
<div>类型: {material.materialType}</div>
|
||
<div>分类: {material.categoryName}</div>
|
||
<div>上传者: {material.user.username}</div>
|
||
</div>
|
||
<div className="flex gap-2">
|
||
<button
|
||
onClick={() => setEditingMaterial(material)}
|
||
className={cn(styles.buttonStyles.secondary, styles.buttonModifiers.compactXs, 'flex-1')}
|
||
>
|
||
编辑
|
||
</button>
|
||
<button
|
||
onClick={() => handleDeleteMaterial(material.id)}
|
||
className={cn(styles.buttonStyles.danger, styles.buttonModifiers.compactXs, 'flex-1')}
|
||
>
|
||
删除
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* Edit Material Modal */}
|
||
{editingMaterial && (
|
||
<EditMaterialModal
|
||
material={editingMaterial}
|
||
onClose={() => setEditingMaterial(null)}
|
||
onSuccess={async () => {
|
||
setEditingMaterial(null)
|
||
await fetchMaterials()
|
||
showToast('修改成功')
|
||
}}
|
||
/>
|
||
)}
|
||
</>
|
||
)
|
||
}
|
||
|