gongxiangp2/app/admin/page.tsx
2025-10-26 09:11:04 +08:00

1081 lines
42 KiB
TypeScript

'use client'
import { useEffect, useState, useCallback } from 'react'
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import Header from '@/components/Header'
import EditMaterialModal from '@/components/EditMaterialModal'
import { format } from 'date-fns'
interface User {
id: string
email: string
username: string
role: string
isActive: boolean
canDownload: boolean
createdAt: string
_count: {
materials: number
}
}
interface Material {
id: string
previewUrl: string
productModel: string
categoryName: string
sceneName: string
renderer: string
authorName: string
createdAt: string
user: {
username: string
}
}
interface Category {
id: string
name: string
count: number
}
interface Scene {
id: string
name: string
count: number
}
export default function AdminPage() {
const { data: session, status } = useSession()
const router = useRouter()
const [activeTab, setActiveTab] = useState<'materials' | 'users' | 'categories'>('materials')
const [materials, setMaterials] = useState<Material[]>([])
const [users, setUsers] = useState<User[]>([])
const [categories, setCategories] = useState<Category[]>([])
const [scenes, setScenes] = useState<Scene[]>([])
const [loading, setLoading] = useState(true)
const [editingMaterial, setEditingMaterial] = useState<Material | null>(null)
const [editingCategory, setEditingCategory] = useState<Category | null>(null)
const [editingScene, setEditingScene] = useState<Scene | null>(null)
const [showAddCategoryModal, setShowAddCategoryModal] = useState(false)
const [showAddSceneModal, setShowAddSceneModal] = useState(false)
const [newCategoryName, setNewCategoryName] = useState('')
const [newSceneName, setNewSceneName] = useState('')
const [toast, setToast] = useState<{message: string, type: 'success' | 'error'} | null>(null)
// Toast自动隐藏
useEffect(() => {
if (toast) {
const timer = setTimeout(() => setToast(null), 3000)
return () => clearTimeout(timer)
}
}, [toast])
const showToast = (message: string, type: 'success' | 'error' = 'success') => {
setToast({ message, type })
}
useEffect(() => {
if (status === 'unauthenticated') {
router.push('/login')
return
}
if (status === 'authenticated' && (session.user as any)?.role !== 'ADMIN') {
router.push('/')
return
}
}, [status, session, router])
useEffect(() => {
if (status === 'authenticated') {
fetchData()
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [status, activeTab])
const fetchData = async () => {
setLoading(true)
try {
if (activeTab === 'materials') {
const response = await fetch('/api/materials?pageSize=1000')
if (response.ok) {
const data = await response.json()
// 处理新的分页API响应格式
setMaterials(data.materials || data)
}
} else if (activeTab === 'users') {
const response = await fetch('/api/admin/users')
if (response.ok) {
const data = await response.json()
setUsers(data)
}
} else if (activeTab === 'categories') {
const [categoriesRes, scenesRes] = await Promise.all([
fetch('/api/categories'),
fetch('/api/scenes')
])
if (categoriesRes.ok) {
const data = await categoriesRes.json()
setCategories(data)
}
if (scenesRes.ok) {
const data = await scenesRes.json()
setScenes(data)
}
}
} catch (error) {
console.error('Error fetching data:', error)
} finally {
setLoading(false)
}
}
const handleDeleteMaterial = async (id: string) => {
if (!confirm('确定要删除这个素材吗?')) 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 handleDeleteUser = async (id: string) => {
if (!confirm('确定要删除这个用户吗?该用户的所有素材也会被删除。')) return
try {
const response = await fetch(`/api/admin/users?id=${id}`, {
method: 'DELETE',
})
if (response.ok) {
// 直接从本地状态移除
setUsers(prevUsers => prevUsers.filter(u => u.id !== id))
showToast('删除成功')
} else {
const data = await response.json()
showToast(data.error || '删除失败', 'error')
}
} catch (error) {
console.error('Error deleting user:', error)
showToast('删除失败', 'error')
}
}
const handleToggleDownload = async (userId: string, currentCanDownload: boolean) => {
const action = currentCanDownload ? '关闭' : '开启'
if (!confirm(`确定要${action}该用户的下载权限吗?`)) return
try {
const response = await fetch('/api/admin/users/toggle-download', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ userId }),
})
if (response.ok) {
// 直接更新本地状态,避免重新加载整个列表
setUsers(prevUsers =>
prevUsers.map(user =>
user.id === userId
? { ...user, canDownload: !currentCanDownload }
: user
)
)
showToast(`下载权限已${action}`)
} else {
const data = await response.json()
showToast(data.error || '操作失败', 'error')
}
} catch (error) {
console.error('Error toggling download permission:', error)
showToast('操作失败', 'error')
}
}
const handleToggleActive = async (userId: string, currentStatus: boolean) => {
const action = currentStatus ? '禁用' : '激活'
if (!confirm(`确定要${action}这个用户吗?`)) return
try {
const response = await fetch('/api/admin/users/toggle-active', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
userId,
isActive: !currentStatus,
}),
})
if (response.ok) {
// 直接更新本地状态
setUsers(prevUsers =>
prevUsers.map(user =>
user.id === userId
? { ...user, isActive: !currentStatus }
: user
)
)
showToast(`${action}成功`)
} else {
const data = await response.json()
showToast(data.error || `${action}失败`, 'error')
}
} catch (error) {
console.error('Toggle active error:', error)
showToast(`${action}失败`, 'error')
}
}
const handleUpdateCategory = async (id: string, newName: string) => {
if (!newName.trim()) {
showToast('分类名称不能为空', 'error')
return
}
try {
const response = await fetch('/api/categories', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ id, name: newName.trim() }),
})
if (response.ok) {
// 直接更新本地状态
setCategories(prevCategories =>
prevCategories.map(cat =>
cat.id === id ? { ...cat, name: newName.trim() } : cat
)
)
setEditingCategory(null)
showToast('修改成功')
} else {
const data = await response.json()
showToast(data.error || '修改失败', 'error')
}
} catch (error) {
console.error('Error updating category:', error)
showToast('修改失败', 'error')
}
}
const handleUpdateScene = async (id: string, newName: string) => {
if (!newName.trim()) {
showToast('场景名称不能为空', 'error')
return
}
try {
const response = await fetch('/api/scenes', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ id, name: newName.trim() }),
})
if (response.ok) {
// 直接更新本地状态
setScenes(prevScenes =>
prevScenes.map(scene =>
scene.id === id ? { ...scene, name: newName.trim() } : scene
)
)
setEditingScene(null)
showToast('修改成功')
} else {
const data = await response.json()
showToast(data.error || '修改失败', 'error')
}
} catch (error) {
console.error('Error updating scene:', error)
showToast('修改失败', 'error')
}
}
const handleDeleteCategory = async (id: string) => {
if (!confirm('确定要删除这个使用店铺分类吗?')) return
try {
const response = await fetch(`/api/categories?id=${id}`, {
method: 'DELETE',
})
if (response.ok) {
// 直接从本地状态移除
setCategories(prevCategories => prevCategories.filter(cat => cat.id !== id))
showToast('删除成功')
} else {
const data = await response.json()
showToast(data.error || '删除失败', 'error')
}
} catch (error) {
console.error('Error deleting category:', error)
showToast('删除失败', 'error')
}
}
const handleDeleteScene = async (id: string) => {
if (!confirm('确定要删除这个场景类别吗?')) return
try {
const response = await fetch(`/api/scenes?id=${id}`, {
method: 'DELETE',
})
if (response.ok) {
// 直接从本地状态移除
setScenes(prevScenes => prevScenes.filter(scene => scene.id !== id))
showToast('删除成功')
} else {
const data = await response.json()
showToast(data.error || '删除失败', 'error')
}
} catch (error) {
console.error('Error deleting scene:', error)
showToast('删除失败', 'error')
}
}
const handleSetRole = async (userId: string, currentRole: string) => {
const newRole = currentRole === 'ADMIN' ? 'USER' : 'ADMIN'
const action = newRole === 'ADMIN' ? '设置为管理员' : '取消管理员权限'
if (!confirm(`确定要${action}吗?`)) return
try {
const response = await fetch('/api/admin/users/set-role', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
userId,
role: newRole,
}),
})
if (response.ok) {
// 直接更新本地状态
setUsers(prevUsers =>
prevUsers.map(user =>
user.id === userId
? { ...user, role: newRole }
: user
)
)
showToast(`${action}成功`)
} else {
const data = await response.json()
showToast(data.error || `${action}失败`, 'error')
}
} catch (error) {
console.error('Set role error:', error)
showToast(`${action}失败`, 'error')
}
}
const handleAddCategory = async () => {
if (!newCategoryName.trim()) {
showToast('分类名称不能为空', 'error')
return
}
try {
const response = await fetch('/api/categories', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: newCategoryName.trim() }),
})
if (response.ok) {
const data = await response.json()
// 添加到本地状态
setCategories(prevCategories => [...prevCategories, { id: data.id, name: newCategoryName.trim(), count: 0 }])
setShowAddCategoryModal(false)
setNewCategoryName('')
showToast('添加成功')
} else {
const data = await response.json()
showToast(data.error || '添加失败', 'error')
}
} catch (error) {
console.error('Error adding category:', error)
showToast('添加失败', 'error')
}
}
const handleAddScene = async () => {
if (!newSceneName.trim()) {
showToast('场景名称不能为空', 'error')
return
}
try {
const response = await fetch('/api/scenes', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: newSceneName.trim() }),
})
if (response.ok) {
const data = await response.json()
// 添加到本地状态
setScenes(prevScenes => [...prevScenes, { id: data.id, name: newSceneName.trim(), count: 0 }])
setShowAddSceneModal(false)
setNewSceneName('')
showToast('添加成功')
} else {
const data = await response.json()
showToast(data.error || '添加失败', 'error')
}
} catch (error) {
console.error('Error adding scene:', error)
showToast('添加失败', 'error')
}
}
if (status === 'loading' || loading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-xl text-gray-600">加载中...</div>
</div>
)
}
if (!session || (session.user as any)?.role !== 'ADMIN') {
return null
}
return (
<div className="min-h-screen bg-gray-50">
<Header session={session} />
{/* Toast通知 */}
{toast && (
<div className={`fixed top-20 right-4 z-50 px-6 py-3 rounded-lg shadow-lg ${
toast.type === 'success'
? 'bg-green-500 text-white'
: 'bg-red-500 text-white'
} animate-fade-in`}>
{toast.message}
</div>
)}
<main className="container mx-auto px-4 py-8 max-w-7xl">
<h1 className="text-3xl font-bold text-gray-900 mb-8">系统设置</h1>
{/* Tabs */}
<div className="flex space-x-4 border-b border-gray-200 mb-8">
<button
type="button"
onClick={() => setActiveTab('materials')}
className={`pb-4 px-4 font-medium transition-colors ${
activeTab === 'materials'
? 'text-gray-900 border-b-2 border-gray-900'
: 'text-gray-600 hover:text-gray-900'
}`}
>
素材管理
</button>
<button
type="button"
onClick={() => setActiveTab('users')}
className={`pb-4 px-4 font-medium transition-colors ${
activeTab === 'users'
? 'text-gray-900 border-b-2 border-gray-900'
: 'text-gray-600 hover:text-gray-900'
}`}
>
用户管理
</button>
<button
type="button"
onClick={() => setActiveTab('categories')}
className={`pb-4 px-4 font-medium transition-colors ${
activeTab === 'categories'
? 'text-gray-900 border-b-2 border-gray-900'
: 'text-gray-600 hover:text-gray-900'
}`}
>
分类管理
</button>
</div>
{/* Materials Table */}
{activeTab === 'materials' && (
<div className="card-apple overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
预览图
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
商品型号
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
场景名称
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
作者
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
上传时间
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
操作
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{materials.map((material) => (
<tr key={material.id}>
<td className="px-6 py-4 whitespace-nowrap">
<img
src={material.previewUrl}
alt={material.sceneName}
className="w-16 h-16 object-cover rounded-lg"
/>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{material.productModel}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
{material.sceneName}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">
{material.authorName}
</div>
<div className="text-xs text-gray-500">
@{material.user.username}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{format(new Date(material.createdAt), 'yyyy-MM-dd HH:mm')}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<div className="flex items-center space-x-3">
<button
onClick={() => setEditingMaterial(material)}
className="text-gray-700 hover:text-gray-900 font-medium"
>
编辑
</button>
<button
onClick={() => handleDeleteMaterial(material.id)}
className="text-gray-700 hover:text-gray-900 font-medium"
>
删除
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{materials.length === 0 && (
<div className="text-center py-8 text-gray-500">
暂无素材
</div>
)}
</div>
)}
{/* Users Table */}
{activeTab === 'users' && (
<div className="card-apple overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-gray-50 border-b border-gray-200">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
用户名
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
邮箱
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
角色
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
状态
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
下载权限
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
素材数量
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
注册时间
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
操作
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{users.map((user) => (
<tr key={user.id}>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{user.username}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm text-gray-900">{user.email}</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
user.role === 'ADMIN'
? 'bg-purple-100 text-purple-800'
: 'bg-green-100 text-green-800'
}`}
>
{user.role === 'ADMIN' ? '管理员' : '用户'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
user.isActive
? 'bg-green-100 text-green-800'
: 'bg-red-100 text-red-800'
}`}
>
{user.isActive ? '已激活' : '未激活'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span
className={`px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
user.canDownload
? 'bg-blue-100 text-blue-800'
: 'bg-gray-100 text-gray-800'
}`}
>
{user.canDownload ? '允许下载' : '禁止下载'}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{user._count.materials}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{format(new Date(user.createdAt), 'yyyy-MM-dd')}
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm">
<div className="flex items-center space-x-3">
{user.email !== session?.user?.email && (
<>
<button
onClick={() => handleSetRole(user.id, user.role)}
className={`font-medium ${
user.role === 'ADMIN'
? 'text-orange-600 hover:text-orange-800 dark:text-orange-400'
: 'text-purple-600 hover:text-purple-800 dark:text-purple-400'
}`}
>
{user.role === 'ADMIN' ? '取消管理员' : '设为管理员'}
</button>
<button
onClick={() => handleToggleActive(user.id, user.isActive)}
className="text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-gray-100 font-medium"
>
{user.isActive ? '禁用' : '激活'}
</button>
{user.role !== 'ADMIN' && (
<button
onClick={() => handleToggleDownload(user.id, user.canDownload)}
className={`font-medium ${
user.canDownload
? 'text-yellow-600 hover:text-yellow-800 dark:text-yellow-400'
: 'text-blue-600 hover:text-blue-800 dark:text-blue-400'
}`}
>
{user.canDownload ? '关闭下载' : '开启下载'}
</button>
)}
<button
onClick={() => handleDeleteUser(user.id)}
className="text-red-600 hover:text-red-800 dark:text-red-400 font-medium"
>
删除
</button>
</>
)}
{user.email === session?.user?.email && (
<span className="text-xs text-gray-500 dark:text-gray-400 italic">
当前账号
</span>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{users.length === 0 && (
<div className="text-center py-8 text-gray-500">
暂无用户
</div>
)}
</div>
)}
{/* Categories Management */}
{activeTab === 'categories' && (
<div className="space-y-8">
{/* 使用店铺分类 */}
<div className="card-apple">
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">使用店铺分类</h2>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">管理所有使用店铺分类名称</p>
</div>
<button
onClick={() => setShowAddCategoryModal(true)}
className="px-4 py-2 bg-gray-900 dark:bg-gray-700 text-white text-sm rounded-lg hover:bg-gray-800 dark:hover:bg-gray-600 transition-colors flex items-center gap-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
新增店铺
</button>
</div>
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{categories.map((category) => (
<div
key={category.id}
className="border border-gray-200 dark:border-gray-600 rounded-lg p-4 hover:shadow-md transition-shadow"
>
{editingCategory?.id === category.id ? (
<div className="space-y-3">
<input
type="text"
defaultValue={category.name}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-gray-400 outline-none"
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleUpdateCategory(category.id, e.currentTarget.value)
} else if (e.key === 'Escape') {
setEditingCategory(null)
}
}}
autoFocus
/>
<div className="flex gap-2">
<button
onClick={(e) => {
const input = e.currentTarget.parentElement?.previousElementSibling as HTMLInputElement
handleUpdateCategory(category.id, input.value)
}}
className="flex-1 px-3 py-1.5 bg-gray-900 text-white text-sm rounded-lg hover:bg-gray-800 transition-colors"
>
保存
</button>
<button
onClick={() => setEditingCategory(null)}
className="flex-1 px-3 py-1.5 border border-gray-300 text-gray-700 text-sm rounded-lg hover:bg-gray-50 transition-colors"
>
取消
</button>
</div>
</div>
) : (
<div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="font-medium text-gray-900 dark:text-white">
{category.name}
</h3>
<span className="text-xs text-gray-500 bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded-full">
{category.count} 个素材
</span>
</div>
<div className="flex gap-2">
<button
onClick={() => setEditingCategory(category)}
className="flex-1 px-3 py-1.5 text-gray-700 dark:text-gray-300 text-sm border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
编辑
</button>
<button
onClick={() => handleDeleteCategory(category.id)}
className="flex-1 px-3 py-1.5 text-red-600 dark:text-red-400 text-sm border border-red-300 dark:border-red-600 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
>
删除
</button>
</div>
</div>
)}
</div>
))}
</div>
{categories.length === 0 && (
<div className="text-center py-8 text-gray-500">
暂无使用店铺分类
</div>
)}
</div>
</div>
{/* 场景类别 */}
<div className="card-apple">
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">场景类别</h2>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">管理所有场景类别名称</p>
</div>
<button
onClick={() => setShowAddSceneModal(true)}
className="px-4 py-2 bg-gray-900 dark:bg-gray-700 text-white text-sm rounded-lg hover:bg-gray-800 dark:hover:bg-gray-600 transition-colors flex items-center gap-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
新增场景
</button>
</div>
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{scenes.map((scene) => (
<div
key={scene.id}
className="border border-gray-200 dark:border-gray-600 rounded-lg p-4 hover:shadow-md transition-shadow"
>
{editingScene?.id === scene.id ? (
<div className="space-y-3">
<input
type="text"
defaultValue={scene.name}
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-gray-400 outline-none"
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleUpdateScene(scene.id, e.currentTarget.value)
} else if (e.key === 'Escape') {
setEditingScene(null)
}
}}
autoFocus
/>
<div className="flex gap-2">
<button
onClick={(e) => {
const input = e.currentTarget.parentElement?.previousElementSibling as HTMLInputElement
handleUpdateScene(scene.id, input.value)
}}
className="flex-1 px-3 py-1.5 bg-gray-900 text-white text-sm rounded-lg hover:bg-gray-800 transition-colors"
>
保存
</button>
<button
onClick={() => setEditingScene(null)}
className="flex-1 px-3 py-1.5 border border-gray-300 text-gray-700 text-sm rounded-lg hover:bg-gray-50 transition-colors"
>
取消
</button>
</div>
</div>
) : (
<div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="font-medium text-gray-900 dark:text-white">
{scene.name}
</h3>
<span className="text-xs text-gray-500 bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded-full">
{scene.count} 个素材
</span>
</div>
<div className="flex gap-2">
<button
onClick={() => setEditingScene(scene)}
className="flex-1 px-3 py-1.5 text-gray-700 dark:text-gray-300 text-sm border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
>
编辑
</button>
<button
onClick={() => handleDeleteScene(scene.id)}
className="flex-1 px-3 py-1.5 text-red-600 dark:text-red-400 text-sm border border-red-300 dark:border-red-600 rounded-lg hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
>
删除
</button>
</div>
</div>
)}
</div>
))}
</div>
{scenes.length === 0 && (
<div className="text-center py-8 text-gray-500">
暂无场景类别
</div>
)}
</div>
</div>
</div>
)}
</main>
{/* Edit Material Modal */}
{editingMaterial && (
<EditMaterialModal
material={editingMaterial}
onClose={() => setEditingMaterial(null)}
onSuccess={async () => {
setEditingMaterial(null)
// 只重新获取素材列表,不刷新其他数据
if (activeTab === 'materials') {
try {
const response = await fetch('/api/materials?pageSize=1000')
if (response.ok) {
const data = await response.json()
setMaterials(data.materials || data)
}
} catch (error) {
console.error('Error fetching materials:', error)
}
}
showToast('修改成功')
}}
/>
)}
{/* Add Category Modal */}
{showAddCategoryModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
新增使用店铺分类
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
分类名称
</label>
<input
type="text"
value={newCategoryName}
onChange={(e) => setNewCategoryName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleAddCategory()
} else if (e.key === 'Escape') {
setShowAddCategoryModal(false)
setNewCategoryName('')
}
}}
placeholder="请输入店铺名称"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded-lg focus:ring-2 focus:ring-gray-400 outline-none"
autoFocus
/>
</div>
<div className="flex gap-3 justify-end">
<button
onClick={() => {
setShowAddCategoryModal(false)
setNewCategoryName('')
}}
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={handleAddCategory}
className="px-4 py-2 bg-gray-900 dark:bg-gray-700 text-white rounded-lg hover:bg-gray-800 dark:hover:bg-gray-600 transition-colors"
>
确定添加
</button>
</div>
</div>
</div>
</div>
)}
{/* Add Scene Modal */}
{showAddSceneModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md p-6">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
新增场景类别
</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
类别名称
</label>
<input
type="text"
value={newSceneName}
onChange={(e) => setNewSceneName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleAddScene()
} else if (e.key === 'Escape') {
setShowAddSceneModal(false)
setNewSceneName('')
}
}}
placeholder="请输入场景类别名称"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded-lg focus:ring-2 focus:ring-gray-400 outline-none"
autoFocus
/>
</div>
<div className="flex gap-3 justify-end">
<button
onClick={() => {
setShowAddSceneModal(false)
setNewSceneName('')
}}
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={handleAddScene}
className="px-4 py-2 bg-gray-900 dark:bg-gray-700 text-white rounded-lg hover:bg-gray-800 dark:hover:bg-gray-600 transition-colors"
>
确定添加
</button>
</div>
</div>
</div>
</div>
)}
</div>
)
}