gongxiangp2/components/admin/CategoryManagement.tsx
chenpei 0ce59be34e 备份:组织架构设计方案讨论前的代码快照
- 包含所有当前修改和新功能
- 准备开始组织架构优化实施方案
2025-12-30 13:20:26 +08:00

671 lines
22 KiB
TypeScript

/**
* 分类管理组件
* 提取自 app/admin/page.tsx 的分类管理功能
* 包含:店铺分类、场景分类、平台分类、品类管理
*/
'use client'
import { useState } from 'react'
import type { Category, Scene, Platform, ProductCategory } from '@/hooks/useAdminState'
interface CategoryManagementProps {
categories: Category[]
setCategories: React.Dispatch<React.SetStateAction<Category[]>>
scenes: Scene[]
setScenes: React.Dispatch<React.SetStateAction<Scene[]>>
platforms: Platform[]
setPlatforms: React.Dispatch<React.SetStateAction<Platform[]>>
productCategories: ProductCategory[]
setProductCategories: React.Dispatch<React.SetStateAction<ProductCategory[]>>
showToast: (message: string, type?: 'success' | 'error') => void
}
export default function CategoryManagement({
categories,
setCategories,
scenes,
setScenes,
platforms,
setPlatforms,
productCategories,
setProductCategories,
showToast,
}: CategoryManagementProps) {
// 编辑状态
const [editingCategory, setEditingCategory] = useState<Category | null>(null)
const [editingScene, setEditingScene] = useState<Scene | null>(null)
const [editingPlatform, setEditingPlatform] = useState<Platform | null>(null)
const [editingProductCategory, setEditingProductCategory] = useState<ProductCategory | null>(null)
// 添加弹窗状态
const [showAddCategoryModal, setShowAddCategoryModal] = useState(false)
const [showAddSceneModal, setShowAddSceneModal] = useState(false)
const [showAddPlatformModal, setShowAddPlatformModal] = useState(false)
const [showAddProductCategoryModal, setShowAddProductCategoryModal] = useState(false)
// 新建输入状态
const [newCategoryName, setNewCategoryName] = useState('')
const [newSceneName, setNewSceneName] = useState('')
const [newPlatformName, setNewPlatformName] = useState('')
const [newProductCategoryName, setNewProductCategoryName] = useState('')
// ========== 店铺分类操作 ==========
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(prev => [...prev, { 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 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(prev =>
prev.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 handleDeleteCategory = async (id: string) => {
if (!confirm('确定要删除这个使用店铺分类吗?')) return
try {
const response = await fetch(`/api/categories?id=${id}`, { method: 'DELETE' })
if (response.ok) {
setCategories(prev => prev.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 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(prev => [...prev, { 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')
}
}
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(prev =>
prev.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 handleDeleteScene = async (id: string) => {
if (!confirm('确定要删除这个场景类别吗?')) return
try {
const response = await fetch(`/api/scenes?id=${id}`, { method: 'DELETE' })
if (response.ok) {
setScenes(prev => prev.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 handleAddPlatform = async () => {
if (!newPlatformName.trim()) {
showToast('平台名称不能为空', 'error')
return
}
try {
const response = await fetch('/api/platforms', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newPlatformName.trim() }),
})
if (response.ok) {
const data = await response.json()
setPlatforms(prev => [...prev, { id: data.id, name: newPlatformName.trim(), count: 0 }])
setShowAddPlatformModal(false)
setNewPlatformName('')
showToast('添加成功')
} else {
const data = await response.json()
showToast(data.error || '添加失败', 'error')
}
} catch (error) {
console.error('Error adding platform:', error)
showToast('添加失败', 'error')
}
}
const handleUpdatePlatform = async (id: string, newName: string) => {
if (!newName.trim()) {
showToast('平台名称不能为空', 'error')
return
}
try {
const response = await fetch('/api/platforms', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, name: newName.trim() }),
})
if (response.ok) {
setPlatforms(prev =>
prev.map(platform => platform.id === id ? { ...platform, name: newName.trim() } : platform)
)
setEditingPlatform(null)
showToast('修改成功')
} else {
const data = await response.json()
showToast(data.error || '修改失败', 'error')
}
} catch (error) {
console.error('Error updating platform:', error)
showToast('修改失败', 'error')
}
}
const handleDeletePlatform = async (id: string) => {
if (!confirm('确定要删除这个平台分类吗?')) return
try {
const response = await fetch(`/api/platforms?id=${id}`, { method: 'DELETE' })
if (response.ok) {
setPlatforms(prev => prev.filter(platform => platform.id !== id))
showToast('删除成功')
} else {
const data = await response.json()
showToast(data.error || '删除失败', 'error')
}
} catch (error) {
console.error('Error deleting platform:', error)
showToast('删除失败', 'error')
}
}
// ========== 品类操作 ==========
const handleAddProductCategory = async () => {
if (!newProductCategoryName.trim()) {
showToast('请输入品类名称', 'error')
return
}
try {
const response = await fetch('/api/product-categories', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: newProductCategoryName.trim() }),
})
if (response.ok) {
const data = await response.json()
setProductCategories(prev => [...prev, { id: data.id, name: newProductCategoryName.trim(), count: 0 }])
setShowAddProductCategoryModal(false)
setNewProductCategoryName('')
showToast('添加成功')
} else {
const data = await response.json()
showToast(data.error || '添加失败', 'error')
}
} catch (error) {
console.error('Error adding product category:', error)
showToast('添加失败', 'error')
}
}
const handleUpdateProductCategory = async (id: string, newName: string) => {
if (!newName.trim()) {
showToast('品类名称不能为空', 'error')
return
}
try {
const response = await fetch('/api/product-categories', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, name: newName.trim() }),
})
if (response.ok) {
setProductCategories(prev =>
prev.map(cat => cat.id === id ? { ...cat, name: newName.trim() } : cat)
)
setEditingProductCategory(null)
showToast('修改成功')
} else {
const data = await response.json()
showToast(data.error || '修改失败', 'error')
}
} catch (error) {
console.error('Error updating product category:', error)
showToast('修改失败', 'error')
}
}
const handleDeleteProductCategory = async (id: string) => {
if (!confirm('确定要删除这个品类吗?')) return
try {
const response = await fetch(`/api/product-categories?id=${id}`, { method: 'DELETE' })
if (response.ok) {
setProductCategories(prev => prev.filter(cat => cat.id !== id))
showToast('删除成功')
} else {
const data = await response.json()
showToast(data.error || '删除失败', 'error')
}
} catch (error) {
console.error('Error deleting product category:', error)
showToast('删除失败', 'error')
}
}
return (
<div className="space-y-8">
{/* 使用店铺分类 */}
<CategorySection
title="使用店铺分类"
description="管理所有使用店铺分类名称"
addButtonText="新增店铺"
items={categories}
editingItem={editingCategory}
onAdd={() => setShowAddCategoryModal(true)}
onEdit={setEditingCategory}
onUpdate={handleUpdateCategory}
onDelete={handleDeleteCategory}
onCancelEdit={() => setEditingCategory(null)}
/>
{/* 场景类别 */}
<CategorySection
title="场景类别"
description="管理所有场景类别名称"
addButtonText="新增场景"
items={scenes}
editingItem={editingScene}
onAdd={() => setShowAddSceneModal(true)}
onEdit={setEditingScene}
onUpdate={handleUpdateScene}
onDelete={handleDeleteScene}
onCancelEdit={() => setEditingScene(null)}
/>
{/* 平台分类 */}
<CategorySection
title="平台分类"
description="管理所有平台分类名称"
addButtonText="新增平台"
items={platforms}
editingItem={editingPlatform}
onAdd={() => setShowAddPlatformModal(true)}
onEdit={setEditingPlatform}
onUpdate={handleUpdatePlatform}
onDelete={handleDeletePlatform}
onCancelEdit={() => setEditingPlatform(null)}
/>
{/* 品类管理 */}
<CategorySection
title="品类管理"
description="管理所有品类名称"
addButtonText="新增品类"
items={productCategories}
editingItem={editingProductCategory}
onAdd={() => setShowAddProductCategoryModal(true)}
onEdit={setEditingProductCategory}
onUpdate={handleUpdateProductCategory}
onDelete={handleDeleteProductCategory}
onCancelEdit={() => setEditingProductCategory(null)}
/>
{/* 添加弹窗 */}
<AddModal
show={showAddCategoryModal}
title="新增店铺分类"
placeholder="请输入店铺名称"
value={newCategoryName}
onChange={setNewCategoryName}
onConfirm={handleAddCategory}
onCancel={() => {
setShowAddCategoryModal(false)
setNewCategoryName('')
}}
/>
<AddModal
show={showAddSceneModal}
title="新增场景类别"
placeholder="请输入场景名称"
value={newSceneName}
onChange={setNewSceneName}
onConfirm={handleAddScene}
onCancel={() => {
setShowAddSceneModal(false)
setNewSceneName('')
}}
/>
<AddModal
show={showAddPlatformModal}
title="新增平台分类"
placeholder="请输入平台名称"
value={newPlatformName}
onChange={setNewPlatformName}
onConfirm={handleAddPlatform}
onCancel={() => {
setShowAddPlatformModal(false)
setNewPlatformName('')
}}
/>
<AddModal
show={showAddProductCategoryModal}
title="新增品类"
placeholder="请输入品类名称"
value={newProductCategoryName}
onChange={setNewProductCategoryName}
onConfirm={handleAddProductCategory}
onCancel={() => {
setShowAddProductCategoryModal(false)
setNewProductCategoryName('')
}}
/>
</div>
)
}
// 分类区块组件
interface CategoryItem {
id: string
name: string
count: number
}
interface CategorySectionProps {
title: string
description: string
addButtonText: string
items: CategoryItem[]
editingItem: CategoryItem | null
onAdd: () => void
onEdit: (item: CategoryItem) => void
onUpdate: (id: string, newName: string) => void
onDelete: (id: string) => void
onCancelEdit: () => void
}
function CategorySection({
title,
description,
addButtonText,
items,
editingItem,
onAdd,
onEdit,
onUpdate,
onDelete,
onCancelEdit,
}: CategorySectionProps) {
const [editValue, setEditValue] = useState('')
const handleStartEdit = (item: CategoryItem) => {
onEdit(item)
setEditValue(item.name)
}
const handleConfirmEdit = (id: string) => {
onUpdate(id, editValue)
setEditValue('')
}
const handleCancelEdit = () => {
onCancelEdit()
setEditValue('')
}
return (
<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">{title}</h2>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">{description}</p>
</div>
<button
onClick={onAdd}
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>
{addButtonText}
</button>
</div>
<div className="p-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{items.map((item) => (
<div
key={item.id}
className="border border-gray-200 dark:border-gray-600 rounded-lg p-4 hover:shadow-md transition-shadow"
>
{editingItem?.id === item.id ? (
<div className="space-y-3">
<input
type="text"
value={editValue}
onChange={(e) => setEditValue(e.target.value)}
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 dark:bg-gray-700 dark:text-white"
autoFocus
/>
<div className="flex gap-2">
<button
onClick={() => handleConfirmEdit(item.id)}
className="flex-1 px-3 py-1.5 bg-gray-900 dark:bg-gray-600 text-white text-sm rounded-lg hover:bg-gray-800 dark:hover:bg-gray-500 transition-colors"
>
保存
</button>
<button
onClick={handleCancelEdit}
className="flex-1 px-3 py-1.5 border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 text-sm rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
>
取消
</button>
</div>
</div>
) : (
<div className="flex items-center justify-between">
<div>
<h3 className="font-medium text-gray-900 dark:text-white">{item.name}</h3>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">{item.count} 个素材</p>
</div>
<div className="flex gap-2">
<button
onClick={() => handleStartEdit(item)}
className="p-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
<svg className="w-4 h-4" 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={() => onDelete(item.id)}
className="p-2 text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-lg transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
</div>
</div>
)}
</div>
))}
</div>
{items.length === 0 && (
<div className="text-center py-8 text-gray-500 dark:text-gray-400">
暂无数据
</div>
)}
</div>
</div>
)
}
// 添加弹窗组件
interface AddModalProps {
show: boolean
title: string
placeholder: string
value: string
onChange: (value: string) => void
onConfirm: () => void
onCancel: () => void
}
function AddModal({
show,
title,
placeholder,
value,
onChange,
onConfirm,
onCancel,
}: AddModalProps) {
if (!show) return null
return (
<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 p-6 w-96 max-w-[90vw]">
<h3 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">{title}</h3>
<input
type="text"
placeholder={placeholder}
value={value}
onChange={(e) => onChange(e.target.value)}
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 dark:bg-gray-700 dark:text-white mb-4"
autoFocus
onKeyDown={(e) => {
if (e.key === 'Enter') onConfirm()
if (e.key === 'Escape') onCancel()
}}
/>
<div className="flex justify-end gap-3">
<button
onClick={onCancel}
className="px-4 py-2 text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
>
取消
</button>
<button
onClick={onConfirm}
className="px-4 py-2 bg-gray-900 dark:bg-gray-600 text-white rounded-lg hover:bg-gray-800 dark:hover:bg-gray-500 transition-colors"
>
确定
</button>
</div>
</div>
</div>
)
}