fix
This commit is contained in:
700
src/pages/ResourceCategoryPageV2.tsx
Normal file
700
src/pages/ResourceCategoryPageV2.tsx
Normal file
@@ -0,0 +1,700 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Plus, Edit, Trash2, Search, Save, X, Cloud, Users, Palette, BarChart3, Download, Upload } from 'lucide-react'
|
||||
import {
|
||||
ResourceCategoryServiceV2,
|
||||
ResourceCategoryV2,
|
||||
CategoryBatchItem
|
||||
} from '../services/resourceCategoryServiceV2'
|
||||
|
||||
const ResourceCategoryPageV2: React.FC = () => {
|
||||
const [categories, setCategories] = useState<ResourceCategoryV2[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [editingCategory, setEditingCategory] = useState<ResourceCategoryV2 | null>(null)
|
||||
const [showCreateForm, setShowCreateForm] = useState(false)
|
||||
const [showBatchImport, setShowBatchImport] = useState(false)
|
||||
const [includeCloud, setIncludeCloud] = useState(true)
|
||||
const [showDisabled, setShowDisabled] = useState(true)
|
||||
const [selectedColor, setSelectedColor] = useState<string>('')
|
||||
const [categoryCount, setCategoryCount] = useState(0)
|
||||
const [formData, setFormData] = useState({
|
||||
title: '',
|
||||
ai_prompt: '',
|
||||
color: '#FF6B6B',
|
||||
is_cloud: false
|
||||
})
|
||||
const [batchData, setBatchData] = useState('')
|
||||
|
||||
// 预设颜色选项(扩展版)
|
||||
const presetColors = [
|
||||
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD',
|
||||
'#FF7675', '#74B9FF', '#00B894', '#FDCB6E', '#E17055', '#A29BFE',
|
||||
'#FD79A8', '#6C5CE7', '#00CEC9', '#55A3FF', '#FF9F43', '#26DE81',
|
||||
'#FF3838', '#FF9500', '#FFDD00', '#48CAE4', '#9B59B6', '#E74C3C',
|
||||
'#F39C12', '#27AE60', '#3498DB', '#8E44AD', '#34495E', '#95A5A6'
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
loadCategories()
|
||||
loadCategoryCount()
|
||||
}, [includeCloud])
|
||||
|
||||
const loadCategories = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const result = await ResourceCategoryServiceV2.getAllCategories({
|
||||
include_cloud: includeCloud,
|
||||
limit: 200
|
||||
})
|
||||
setCategories(result)
|
||||
} catch (error) {
|
||||
console.error('Failed to load categories:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const loadCategoryCount = async () => {
|
||||
try {
|
||||
const count = await ResourceCategoryServiceV2.getCategoryCount(includeCloud)
|
||||
setCategoryCount(count)
|
||||
} catch (error) {
|
||||
console.error('Failed to load category count:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreateCategory = async () => {
|
||||
try {
|
||||
const newCategory = await ResourceCategoryServiceV2.createCategory(
|
||||
formData.title,
|
||||
formData.ai_prompt,
|
||||
formData.color,
|
||||
formData.is_cloud
|
||||
)
|
||||
|
||||
if (newCategory) {
|
||||
setCategories([...categories, newCategory])
|
||||
setShowCreateForm(false)
|
||||
setFormData({ title: '', ai_prompt: '', color: '#FF6B6B', is_cloud: false })
|
||||
await loadCategoryCount()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to create category:', error)
|
||||
alert('创建分类失败: ' + (error instanceof Error ? error.message : '未知错误'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdateCategory = async () => {
|
||||
if (!editingCategory) return
|
||||
|
||||
try {
|
||||
const updatedCategory = await ResourceCategoryServiceV2.updateCategory(
|
||||
editingCategory.id,
|
||||
{
|
||||
title: formData.title,
|
||||
ai_prompt: formData.ai_prompt,
|
||||
color: formData.color
|
||||
}
|
||||
)
|
||||
|
||||
if (updatedCategory) {
|
||||
const updatedCategories = categories.map(cat =>
|
||||
cat.id === editingCategory.id ? updatedCategory : cat
|
||||
)
|
||||
setCategories(updatedCategories)
|
||||
setEditingCategory(null)
|
||||
setFormData({ title: '', ai_prompt: '', color: '#FF6B6B', is_cloud: false })
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update category:', error)
|
||||
alert('更新分类失败: ' + (error instanceof Error ? error.message : '未知错误'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeleteCategory = async (categoryId: string) => {
|
||||
if (!confirm('确定要删除这个分类吗?此操作不可恢复。')) return
|
||||
|
||||
try {
|
||||
await ResourceCategoryServiceV2.deleteCategory(categoryId, true)
|
||||
setCategories(categories.filter(cat => cat.id !== categoryId))
|
||||
await loadCategoryCount()
|
||||
} catch (error) {
|
||||
console.error('Failed to delete category:', error)
|
||||
alert('删除分类失败: ' + (error instanceof Error ? error.message : '未知错误'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleToggleCategory = async (categoryId: string, isActive: boolean) => {
|
||||
try {
|
||||
if (isActive) {
|
||||
await ResourceCategoryServiceV2.activateCategory(categoryId)
|
||||
} else {
|
||||
await ResourceCategoryServiceV2.deactivateCategory(categoryId)
|
||||
}
|
||||
|
||||
const updatedCategories = categories.map(cat =>
|
||||
cat.id === categoryId ? { ...cat, is_active: isActive } : cat
|
||||
)
|
||||
setCategories(updatedCategories)
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle category:', error)
|
||||
alert('切换分类状态失败: ' + (error instanceof Error ? error.message : '未知错误'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleBatchImport = async () => {
|
||||
try {
|
||||
const batchItems: CategoryBatchItem[] = JSON.parse(batchData)
|
||||
const result = await ResourceCategoryServiceV2.batchCreateCategories(batchItems)
|
||||
|
||||
alert(`批量导入完成: 成功 ${result.success} 个,失败 ${result.failed} 个`)
|
||||
setShowBatchImport(false)
|
||||
setBatchData('')
|
||||
await loadCategories()
|
||||
await loadCategoryCount()
|
||||
} catch (error) {
|
||||
console.error('Failed to batch import:', error)
|
||||
alert('批量导入失败: ' + (error instanceof Error ? error.message : '数据格式错误'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearchByColor = async (color: string) => {
|
||||
if (color === selectedColor) {
|
||||
// 取消颜色筛选
|
||||
setSelectedColor('')
|
||||
await loadCategories()
|
||||
} else {
|
||||
// 按颜色筛选
|
||||
setSelectedColor(color)
|
||||
try {
|
||||
const result = await ResourceCategoryServiceV2.getCategoriesByColor(color, {
|
||||
include_cloud: includeCloud
|
||||
})
|
||||
setCategories(result)
|
||||
} catch (error) {
|
||||
console.error('Failed to search by color:', error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = async () => {
|
||||
if (!searchTerm.trim()) {
|
||||
await loadCategories()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await ResourceCategoryServiceV2.searchCategories(searchTerm, {
|
||||
include_cloud: includeCloud
|
||||
})
|
||||
setCategories(result)
|
||||
} catch (error) {
|
||||
console.error('Failed to search categories:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const startEdit = (category: ResourceCategoryV2) => {
|
||||
setEditingCategory(category)
|
||||
setFormData({
|
||||
title: category.title,
|
||||
ai_prompt: category.ai_prompt,
|
||||
color: category.color,
|
||||
is_cloud: category.is_cloud
|
||||
})
|
||||
setShowCreateForm(false)
|
||||
}
|
||||
|
||||
const cancelEdit = () => {
|
||||
setEditingCategory(null)
|
||||
setShowCreateForm(false)
|
||||
setShowBatchImport(false)
|
||||
setFormData({ title: '', ai_prompt: '', color: '#FF6B6B', is_cloud: false })
|
||||
setBatchData('')
|
||||
}
|
||||
|
||||
const exportCategories = () => {
|
||||
const exportData = categories.map(cat => ({
|
||||
title: cat.title,
|
||||
ai_prompt: cat.ai_prompt,
|
||||
color: cat.color,
|
||||
is_cloud: cat.is_cloud
|
||||
}))
|
||||
|
||||
const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = 'resource_categories.json'
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
const filteredCategories = categories.filter(category => {
|
||||
// 搜索过滤
|
||||
const matchesSearch = !searchTerm ||
|
||||
category.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
category.ai_prompt.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
|
||||
// 状态过滤
|
||||
const matchesStatus = showDisabled || category.is_active
|
||||
|
||||
return matchesSearch && matchesStatus
|
||||
})
|
||||
|
||||
// 统计信息
|
||||
const stats = {
|
||||
total: categories.length,
|
||||
active: categories.filter(c => c.is_active).length,
|
||||
cloud: categories.filter(c => c.is_cloud).length,
|
||||
user: categories.filter(c => !c.is_cloud).length
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="text-gray-500">加载中...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
{/* 页面标题和统计 */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900">素材分类管理 V2</h1>
|
||||
<p className="text-gray-600 mt-2">基于最新API的增强版分类管理,支持云端分类、批量操作等高级功能</p>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<div className="flex space-x-4">
|
||||
<div className="bg-blue-50 rounded-lg p-3 text-center min-w-[80px]">
|
||||
<div className="text-2xl font-bold text-blue-600">{stats.total}</div>
|
||||
<div className="text-xs text-blue-500">总数</div>
|
||||
</div>
|
||||
<div className="bg-green-50 rounded-lg p-3 text-center min-w-[80px]">
|
||||
<div className="text-2xl font-bold text-green-600">{stats.active}</div>
|
||||
<div className="text-xs text-green-500">启用</div>
|
||||
</div>
|
||||
<div className="bg-purple-50 rounded-lg p-3 text-center min-w-[80px]">
|
||||
<div className="text-2xl font-bold text-purple-600">{stats.cloud}</div>
|
||||
<div className="text-xs text-purple-500">云端</div>
|
||||
</div>
|
||||
<div className="bg-orange-50 rounded-lg p-3 text-center min-w-[80px]">
|
||||
<div className="text-2xl font-bold text-orange-600">{stats.user}</div>
|
||||
<div className="text-xs text-orange-500">用户</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 搜索和操作栏 */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div className="flex items-center space-x-4 flex-1">
|
||||
{/* 搜索框 */}
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400" size={20} />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="搜索分类..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onKeyPress={(e) => e.key === 'Enter' && handleSearch()}
|
||||
className="pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 筛选选项 */}
|
||||
<div className="flex items-center space-x-3">
|
||||
<label className="flex items-center text-sm text-gray-600">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includeCloud}
|
||||
onChange={(e) => setIncludeCloud(e.target.checked)}
|
||||
className="mr-2"
|
||||
/>
|
||||
<Cloud size={16} className="mr-1" />
|
||||
包含云端
|
||||
</label>
|
||||
|
||||
<label className="flex items-center text-sm text-gray-600">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showDisabled}
|
||||
onChange={(e) => setShowDisabled(e.target.checked)}
|
||||
className="mr-2"
|
||||
/>
|
||||
显示禁用
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex items-center space-x-3">
|
||||
<button
|
||||
onClick={exportCategories}
|
||||
className="flex items-center px-3 py-2 text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
title="导出分类"
|
||||
>
|
||||
<Download size={16} className="mr-2" />
|
||||
导出
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => setShowBatchImport(true)}
|
||||
className="flex items-center px-3 py-2 text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
title="批量导入"
|
||||
>
|
||||
<Upload size={16} className="mr-2" />
|
||||
批量导入
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setShowCreateForm(true)
|
||||
setEditingCategory(null)
|
||||
setFormData({ title: '', ai_prompt: '', color: '#FF6B6B', is_cloud: false })
|
||||
}}
|
||||
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||
>
|
||||
<Plus size={20} className="mr-2" />
|
||||
新建分类
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 颜色筛选栏 */}
|
||||
<div className="mb-6">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Palette size={16} className="text-gray-500" />
|
||||
<span className="text-sm text-gray-600">按颜色筛选:</span>
|
||||
<div className="flex space-x-2">
|
||||
{presetColors.slice(0, 12).map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
onClick={() => handleSearchByColor(color)}
|
||||
className={`w-6 h-6 rounded-full border-2 transition-all ${
|
||||
selectedColor === color
|
||||
? 'border-gray-800 scale-110'
|
||||
: 'border-gray-300 hover:border-gray-400'
|
||||
}`}
|
||||
style={{ backgroundColor: color }}
|
||||
title={`筛选颜色: ${color}`}
|
||||
/>
|
||||
))}
|
||||
{selectedColor && (
|
||||
<button
|
||||
onClick={() => handleSearchByColor('')}
|
||||
className="px-2 py-1 text-xs bg-gray-100 text-gray-600 rounded hover:bg-gray-200"
|
||||
>
|
||||
清除
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 分类列表 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{filteredCategories.map((category) => (
|
||||
<div key={category.id} className={`bg-white rounded-lg shadow-sm border p-6 transition-all ${
|
||||
category.is_active
|
||||
? 'border-gray-200'
|
||||
: 'border-gray-300 bg-gray-50 opacity-75'
|
||||
}`}>
|
||||
{/* 分类标题和标识 */}
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="w-4 h-4 rounded-full mr-3"
|
||||
style={{ backgroundColor: category.color }}
|
||||
/>
|
||||
<h3 className={`text-lg font-semibold ${category.is_active ? 'text-gray-900' : 'text-gray-400'}`}>
|
||||
{category.title}
|
||||
</h3>
|
||||
|
||||
{/* 状态标识 */}
|
||||
<div className="flex items-center ml-2 space-x-1">
|
||||
{category.is_cloud && (
|
||||
<span className="px-2 py-1 text-xs bg-purple-100 text-purple-600 rounded flex items-center">
|
||||
<Cloud size={12} className="mr-1" />
|
||||
云端
|
||||
</span>
|
||||
)}
|
||||
{!category.is_active && (
|
||||
<span className="px-2 py-1 text-xs bg-gray-100 text-gray-500 rounded">
|
||||
已禁用
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
{/* 启用/禁用开关 */}
|
||||
<label className="relative inline-flex items-center cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={category.is_active}
|
||||
onChange={(e) => handleToggleCategory(category.id, e.target.checked)}
|
||||
className="sr-only peer"
|
||||
/>
|
||||
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
||||
</label>
|
||||
|
||||
<button
|
||||
onClick={() => startEdit(category)}
|
||||
className="p-2 text-gray-400 hover:text-blue-600 transition-colors"
|
||||
title="编辑"
|
||||
>
|
||||
<Edit size={16} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteCategory(category.id)}
|
||||
className="p-2 text-gray-400 hover:text-red-600 transition-colors"
|
||||
title="删除"
|
||||
disabled={category.is_cloud}
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* AI提示词 */}
|
||||
<div className="mb-4">
|
||||
<p className="text-sm text-gray-600 leading-relaxed line-clamp-3">{category.ai_prompt}</p>
|
||||
</div>
|
||||
|
||||
{/* 分类信息 */}
|
||||
<div className="flex items-center justify-between text-xs text-gray-400">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Users size={12} />
|
||||
<span>{category.user_id}</span>
|
||||
</div>
|
||||
<div>
|
||||
创建于 {new Date(category.created_at).toLocaleDateString()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 空状态 */}
|
||||
{filteredCategories.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-400 mb-4">
|
||||
{searchTerm || selectedColor ? '没有找到匹配的分类' : '暂无分类'}
|
||||
</div>
|
||||
{!searchTerm && !selectedColor && (
|
||||
<button
|
||||
onClick={() => setShowCreateForm(true)}
|
||||
className="text-blue-600 hover:text-blue-700"
|
||||
>
|
||||
创建第一个分类
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 创建/编辑表单弹窗 */}
|
||||
{(showCreateForm || editingCategory) && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-md mx-4">
|
||||
{/* 表单标题 */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||
<h2 className="text-lg font-semibold text-gray-900">
|
||||
{editingCategory ? '编辑分类' : '新建分类'}
|
||||
</h2>
|
||||
<button
|
||||
onClick={cancelEdit}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 表单内容 */}
|
||||
<div className="p-6 space-y-4">
|
||||
{/* 分类标题 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
分类标题 *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.title}
|
||||
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
|
||||
placeholder="请输入分类标题"
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* AI识别提示词 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
AI识别提示词
|
||||
</label>
|
||||
<textarea
|
||||
value={formData.ai_prompt}
|
||||
onChange={(e) => setFormData({ ...formData, ai_prompt: e.target.value })}
|
||||
placeholder="描述这类素材的特征,用于AI自动分类"
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 展示颜色 */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
展示颜色
|
||||
</label>
|
||||
<div className="flex items-center space-x-3 mb-3">
|
||||
<div
|
||||
className="w-8 h-8 rounded-lg border-2 border-gray-300"
|
||||
style={{ backgroundColor: formData.color }}
|
||||
/>
|
||||
<input
|
||||
type="color"
|
||||
value={formData.color}
|
||||
onChange={(e) => setFormData({ ...formData, color: e.target.value })}
|
||||
className="w-16 h-8 border border-gray-300 rounded cursor-pointer"
|
||||
/>
|
||||
<span className="text-sm text-gray-600">{formData.color}</span>
|
||||
</div>
|
||||
|
||||
{/* 预设颜色 */}
|
||||
<div className="grid grid-cols-8 gap-2">
|
||||
{presetColors.map((color) => (
|
||||
<button
|
||||
key={color}
|
||||
onClick={() => setFormData({ ...formData, color })}
|
||||
className={`w-8 h-8 rounded-lg border-2 transition-all ${
|
||||
formData.color === color
|
||||
? 'border-gray-800 scale-110'
|
||||
: 'border-gray-300 hover:border-gray-400'
|
||||
}`}
|
||||
style={{ backgroundColor: color }}
|
||||
title={color}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 云端分类选项 */}
|
||||
{!editingCategory && (
|
||||
<div>
|
||||
<label className="flex items-center text-sm text-gray-700">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.is_cloud}
|
||||
onChange={(e) => setFormData({ ...formData, is_cloud: e.target.checked })}
|
||||
className="mr-2"
|
||||
/>
|
||||
<Cloud size={16} className="mr-1" />
|
||||
设为云端公共分类
|
||||
</label>
|
||||
<p className="text-xs text-gray-500 mt-1">云端分类对所有用户可见</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 表单按钮 */}
|
||||
<div className="flex items-center justify-end space-x-3 p-6 border-t border-gray-200">
|
||||
<button
|
||||
onClick={cancelEdit}
|
||||
className="px-4 py-2 text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={editingCategory ? handleUpdateCategory : handleCreateCategory}
|
||||
disabled={!formData.title.trim()}
|
||||
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<Save size={16} className="mr-2" />
|
||||
{editingCategory ? '保存' : '创建'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 批量导入弹窗 */}
|
||||
{showBatchImport && (
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg shadow-xl w-full max-w-2xl mx-4">
|
||||
{/* 弹窗标题 */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||
<h2 className="text-lg font-semibold text-gray-900">批量导入分类</h2>
|
||||
<button
|
||||
onClick={cancelEdit}
|
||||
className="text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<X size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 导入说明 */}
|
||||
<div className="p-6">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-sm font-medium text-gray-700 mb-2">数据格式说明</h3>
|
||||
<div className="bg-gray-50 rounded-lg p-4 text-sm">
|
||||
<p className="mb-2">请输入JSON格式的分类数据,格式如下:</p>
|
||||
<pre className="text-xs text-gray-600 bg-white p-2 rounded border overflow-x-auto">
|
||||
{`[
|
||||
{
|
||||
"title": "视频素材",
|
||||
"ai_prompt": "用于识别视频文件",
|
||||
"color": "#FF6B6B",
|
||||
"is_cloud": false
|
||||
},
|
||||
{
|
||||
"title": "音频素材",
|
||||
"ai_prompt": "用于识别音频文件",
|
||||
"color": "#4ECDC4"
|
||||
}
|
||||
]`}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 数据输入 */}
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||||
分类数据 (JSON格式)
|
||||
</label>
|
||||
<textarea
|
||||
value={batchData}
|
||||
onChange={(e) => setBatchData(e.target.value)}
|
||||
placeholder="请输入JSON格式的分类数据..."
|
||||
rows={10}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<div className="flex items-center justify-end space-x-3">
|
||||
<button
|
||||
onClick={cancelEdit}
|
||||
className="px-4 py-2 text-gray-600 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={handleBatchImport}
|
||||
disabled={!batchData.trim()}
|
||||
className="flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
<Upload size={16} className="mr-2" />
|
||||
导入
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResourceCategoryPageV2
|
||||
467
src/services/resourceCategoryServiceV2.ts
Normal file
467
src/services/resourceCategoryServiceV2.ts
Normal file
@@ -0,0 +1,467 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
// 扩展的资源分类接口,包含新字段
|
||||
export interface ResourceCategoryV2 {
|
||||
id: string
|
||||
title: string
|
||||
ai_prompt: string
|
||||
color: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
is_active: boolean
|
||||
is_cloud: boolean
|
||||
user_id: string
|
||||
}
|
||||
|
||||
// 新的响应格式
|
||||
export interface ResourceCategoryResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
data?: any
|
||||
output?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
// 请求接口
|
||||
export interface CategoryListRequest {
|
||||
user_id?: string
|
||||
include_cloud?: boolean
|
||||
limit?: number
|
||||
verbose?: boolean
|
||||
json_output?: boolean
|
||||
}
|
||||
|
||||
export interface CategoryGetRequest {
|
||||
category_id: string
|
||||
user_id?: string
|
||||
verbose?: boolean
|
||||
json_output?: boolean
|
||||
}
|
||||
|
||||
export interface CategoryCreateRequest {
|
||||
title: string
|
||||
ai_prompt?: string
|
||||
color?: string
|
||||
is_cloud?: boolean
|
||||
user_id?: string
|
||||
verbose?: boolean
|
||||
json_output?: boolean
|
||||
}
|
||||
|
||||
export interface CategoryUpdateRequest {
|
||||
category_id: string
|
||||
title?: string
|
||||
ai_prompt?: string
|
||||
color?: string
|
||||
is_active?: boolean
|
||||
user_id?: string
|
||||
verbose?: boolean
|
||||
json_output?: boolean
|
||||
}
|
||||
|
||||
export interface CategoryDeleteRequest {
|
||||
category_id: string
|
||||
hard_delete?: boolean
|
||||
user_id?: string
|
||||
verbose?: boolean
|
||||
json_output?: boolean
|
||||
}
|
||||
|
||||
export interface CategorySearchRequest {
|
||||
query: string
|
||||
user_id?: string
|
||||
include_cloud?: boolean
|
||||
limit?: number
|
||||
verbose?: boolean
|
||||
json_output?: boolean
|
||||
}
|
||||
|
||||
export interface CategoryColorRequest {
|
||||
color: string
|
||||
user_id?: string
|
||||
include_cloud?: boolean
|
||||
limit?: number
|
||||
verbose?: boolean
|
||||
json_output?: boolean
|
||||
}
|
||||
|
||||
export interface CategoryBatchItem {
|
||||
title: string
|
||||
ai_prompt?: string
|
||||
color?: string
|
||||
is_cloud?: boolean
|
||||
}
|
||||
|
||||
export interface CategoryBatchCreateRequest {
|
||||
categories: CategoryBatchItem[]
|
||||
user_id?: string
|
||||
verbose?: boolean
|
||||
json_output?: boolean
|
||||
}
|
||||
|
||||
export interface BatchCreateResult {
|
||||
total: number
|
||||
success: number
|
||||
failed: number
|
||||
error?: string
|
||||
}
|
||||
|
||||
export class ResourceCategoryServiceV2 {
|
||||
private static defaultUserId = 'default'
|
||||
|
||||
/**
|
||||
* 获取所有分类(新版本)
|
||||
*/
|
||||
static async getAllCategories(request?: Partial<CategoryListRequest>): Promise<ResourceCategoryV2[]> {
|
||||
try {
|
||||
const params: CategoryListRequest = {
|
||||
user_id: this.defaultUserId,
|
||||
include_cloud: true,
|
||||
limit: 100,
|
||||
verbose: false,
|
||||
json_output: true,
|
||||
...request
|
||||
}
|
||||
|
||||
const response = await invoke<ResourceCategoryResponse>('get_all_resource_categories_cli', { request: params })
|
||||
|
||||
if (response.success && response.data) {
|
||||
return Array.isArray(response.data) ? response.data : []
|
||||
} else {
|
||||
console.error('Failed to get categories:', response.error || response.message)
|
||||
return []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get all categories:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取分类
|
||||
*/
|
||||
static async getCategoryById(categoryId: string, userId?: string): Promise<ResourceCategoryV2 | null> {
|
||||
try {
|
||||
const params: CategoryGetRequest = {
|
||||
category_id: categoryId,
|
||||
user_id: userId || this.defaultUserId,
|
||||
verbose: false,
|
||||
json_output: true
|
||||
}
|
||||
|
||||
const response = await invoke<ResourceCategoryResponse>('get_resource_category_by_id_cli', { request: params })
|
||||
|
||||
if (response.success && response.data) {
|
||||
return response.data
|
||||
} else {
|
||||
console.error('Failed to get category:', response.error || response.message)
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get category by id:', error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新分类
|
||||
*/
|
||||
static async createCategory(
|
||||
title: string,
|
||||
aiPrompt?: string,
|
||||
color?: string,
|
||||
isCloud?: boolean,
|
||||
userId?: string
|
||||
): Promise<ResourceCategoryV2 | null> {
|
||||
try {
|
||||
const params: CategoryCreateRequest = {
|
||||
title,
|
||||
ai_prompt: aiPrompt || '',
|
||||
color: color || '#FF5733',
|
||||
is_cloud: isCloud || false,
|
||||
user_id: userId || this.defaultUserId,
|
||||
verbose: false,
|
||||
json_output: true
|
||||
}
|
||||
|
||||
const response = await invoke<ResourceCategoryResponse>('create_resource_category_cli', { request: params })
|
||||
|
||||
if (response.success && response.data) {
|
||||
return response.data
|
||||
} else {
|
||||
console.error('Failed to create category:', response.error || response.message)
|
||||
throw new Error(response.error || response.message || 'Failed to create category')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to create category:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新分类
|
||||
*/
|
||||
static async updateCategory(
|
||||
categoryId: string,
|
||||
updates: {
|
||||
title?: string
|
||||
ai_prompt?: string
|
||||
color?: string
|
||||
is_active?: boolean
|
||||
},
|
||||
userId?: string
|
||||
): Promise<ResourceCategoryV2 | null> {
|
||||
try {
|
||||
const params: CategoryUpdateRequest = {
|
||||
category_id: categoryId,
|
||||
title: updates.title,
|
||||
ai_prompt: updates.ai_prompt,
|
||||
color: updates.color,
|
||||
is_active: updates.is_active,
|
||||
user_id: userId || this.defaultUserId,
|
||||
verbose: false,
|
||||
json_output: true
|
||||
}
|
||||
|
||||
const response = await invoke<ResourceCategoryResponse>('update_resource_category_cli', { request: params })
|
||||
|
||||
if (response.success && response.data) {
|
||||
return response.data
|
||||
} else {
|
||||
console.error('Failed to update category:', response.error || response.message)
|
||||
throw new Error(response.error || response.message || 'Failed to update category')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update category:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除分类
|
||||
*/
|
||||
static async deleteCategory(categoryId: string, hardDelete = true, userId?: string): Promise<boolean> {
|
||||
try {
|
||||
const params: CategoryDeleteRequest = {
|
||||
category_id: categoryId,
|
||||
hard_delete: hardDelete,
|
||||
user_id: userId || this.defaultUserId,
|
||||
verbose: false,
|
||||
json_output: true
|
||||
}
|
||||
|
||||
const response = await invoke<ResourceCategoryResponse>('delete_resource_category_cli', { request: params })
|
||||
|
||||
if (response.success) {
|
||||
return true
|
||||
} else {
|
||||
console.error('Failed to delete category:', response.error || response.message)
|
||||
throw new Error(response.error || response.message || 'Failed to delete category')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete category:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索分类
|
||||
*/
|
||||
static async searchCategories(query: string, request?: Partial<CategorySearchRequest>): Promise<ResourceCategoryV2[]> {
|
||||
try {
|
||||
const params: CategorySearchRequest = {
|
||||
query,
|
||||
user_id: this.defaultUserId,
|
||||
include_cloud: true,
|
||||
limit: 50,
|
||||
verbose: false,
|
||||
json_output: true,
|
||||
...request
|
||||
}
|
||||
|
||||
const response = await invoke<ResourceCategoryResponse>('search_resource_categories_cli', { request: params })
|
||||
|
||||
if (response.success && response.data) {
|
||||
return Array.isArray(response.data) ? response.data : []
|
||||
} else {
|
||||
console.error('Failed to search categories:', response.error || response.message)
|
||||
return []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to search categories:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按颜色获取分类
|
||||
*/
|
||||
static async getCategoriesByColor(color: string, request?: Partial<CategoryColorRequest>): Promise<ResourceCategoryV2[]> {
|
||||
try {
|
||||
const params: CategoryColorRequest = {
|
||||
color,
|
||||
user_id: this.defaultUserId,
|
||||
include_cloud: true,
|
||||
limit: 50,
|
||||
verbose: false,
|
||||
json_output: true,
|
||||
...request
|
||||
}
|
||||
|
||||
const response = await invoke<ResourceCategoryResponse>('get_resource_categories_by_color_cli', { request: params })
|
||||
|
||||
if (response.success && response.data) {
|
||||
return Array.isArray(response.data) ? response.data : []
|
||||
} else {
|
||||
console.error('Failed to get categories by color:', response.error || response.message)
|
||||
return []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get categories by color:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取云端分类
|
||||
*/
|
||||
static async getCloudCategories(limit = 100): Promise<ResourceCategoryV2[]> {
|
||||
try {
|
||||
const params: CategoryListRequest = {
|
||||
limit,
|
||||
verbose: false,
|
||||
json_output: true
|
||||
}
|
||||
|
||||
const response = await invoke<ResourceCategoryResponse>('get_cloud_resource_categories_cli', { request: params })
|
||||
|
||||
if (response.success && response.data) {
|
||||
return Array.isArray(response.data) ? response.data : []
|
||||
} else {
|
||||
console.error('Failed to get cloud categories:', response.error || response.message)
|
||||
return []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get cloud categories:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量创建分类
|
||||
*/
|
||||
static async batchCreateCategories(categories: CategoryBatchItem[], userId?: string): Promise<BatchCreateResult> {
|
||||
try {
|
||||
const params: CategoryBatchCreateRequest = {
|
||||
categories,
|
||||
user_id: userId || this.defaultUserId,
|
||||
verbose: false,
|
||||
json_output: true
|
||||
}
|
||||
|
||||
const response = await invoke<ResourceCategoryResponse>('batch_create_resource_categories_cli', { request: params })
|
||||
|
||||
if (response.success && response.data) {
|
||||
return response.data
|
||||
} else {
|
||||
console.error('Failed to batch create categories:', response.error || response.message)
|
||||
throw new Error(response.error || response.message || 'Failed to batch create categories')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to batch create categories:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分类数量
|
||||
*/
|
||||
static async getCategoryCount(includeCloud = true, userId?: string): Promise<number> {
|
||||
try {
|
||||
const params: CategoryListRequest = {
|
||||
user_id: userId || this.defaultUserId,
|
||||
include_cloud: includeCloud,
|
||||
verbose: false,
|
||||
json_output: true
|
||||
}
|
||||
|
||||
const response = await invoke<ResourceCategoryResponse>('get_resource_category_count_cli', { request: params })
|
||||
|
||||
if (response.success && response.data && typeof response.data.count === 'number') {
|
||||
return response.data.count
|
||||
} else {
|
||||
console.error('Failed to get category count:', response.error || response.message)
|
||||
return 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get category count:', error)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 激活分类
|
||||
*/
|
||||
static async activateCategory(categoryId: string, userId?: string): Promise<boolean> {
|
||||
try {
|
||||
const params: CategoryGetRequest = {
|
||||
category_id: categoryId,
|
||||
user_id: userId || this.defaultUserId,
|
||||
verbose: false,
|
||||
json_output: true
|
||||
}
|
||||
|
||||
const response = await invoke<ResourceCategoryResponse>('activate_resource_category_cli', { request: params })
|
||||
|
||||
if (response.success) {
|
||||
return true
|
||||
} else {
|
||||
console.error('Failed to activate category:', response.error || response.message)
|
||||
throw new Error(response.error || response.message || 'Failed to activate category')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to activate category:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 停用分类
|
||||
*/
|
||||
static async deactivateCategory(categoryId: string, userId?: string): Promise<boolean> {
|
||||
try {
|
||||
const params: CategoryGetRequest = {
|
||||
category_id: categoryId,
|
||||
user_id: userId || this.defaultUserId,
|
||||
verbose: false,
|
||||
json_output: true
|
||||
}
|
||||
|
||||
const response = await invoke<ResourceCategoryResponse>('deactivate_resource_category_cli', { request: params })
|
||||
|
||||
if (response.success) {
|
||||
return true
|
||||
} else {
|
||||
console.error('Failed to deactivate category:', response.error || response.message)
|
||||
throw new Error(response.error || response.message || 'Failed to deactivate category')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to deactivate category:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置默认用户ID
|
||||
*/
|
||||
static setDefaultUserId(userId: string) {
|
||||
this.defaultUserId = userId
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认用户ID
|
||||
*/
|
||||
static getDefaultUserId(): string {
|
||||
return this.defaultUserId
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user