flnxi-Wu/app/admin/_components/MaterialsTab.tsx
WuYongJie 7dc8d2a528
Some checks failed
CI Check / check (push) Has been cancelled
feat: 统一弹窗系统 & 首页UI优化
- 替换所有浏览器原生弹窗(confirm/alert/prompt)
  - 创建 ConfirmDialog、AlertDialog、PromptDialog 组件
  - 创建 DialogProvider 和 useDialog Hook
  - 替换所有 admin 组件、首页、分享页面中的原生弹窗

- 素材预览面板统一
  - 创建 MaterialPreviewModal 组件
  - 使用统一弹窗组件,统一退出逻辑

- 首页UI统一到三层架构
  - 迁移主要按钮和输入框样式
  - 迁移筛选弹窗中的所有按钮样式
  - 使用 useThemeStyles() Hook

- 更新文档
  - 更新优化记录、优化计划、后续修改、更新日志
2025-11-22 16:20:05 +08:00

222 lines
8.7 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 { 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('修改成功')
}}
/>
)}
</>
)
}