mxivideo/src/pages/ResourceCategoryPageV2.tsx

694 lines
25 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.

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'
import {
useCategoriesData,
useCategoryActions,
useCategoryStore
} from '../stores/useCategoryStore'
const ResourceCategoryPageV2: React.FC = () => {
// 使用 Zustand store 管理分类数据
const { categories, loading } = useCategoriesData()
const {
loadCategories,
refreshCategories,
addCategory,
updateCategory,
removeCategory
} = useCategoryActions()
// 从 store 获取搜索功能
const { searchCategories } = useCategoryStore()
// 本地 UI 状态
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 [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(() => {
// 使用 store 的 loadCategories 方法
loadCategories()
}, [loadCategories])
// 获取过滤后的分类列表
const getFilteredCategories = () => {
let filtered = categories
// 根据搜索词过滤
if (searchTerm.trim()) {
filtered = searchCategories(searchTerm.trim())
}
// 根据云端/本地过滤
if (!includeCloud) {
filtered = filtered.filter(cat => !cat.is_cloud)
}
// 根据启用/禁用状态过滤
if (!showDisabled) {
filtered = filtered.filter(cat => cat.is_active)
}
// 根据颜色过滤
if (selectedColor) {
filtered = filtered.filter(cat => cat.color === selectedColor)
}
return filtered
}
const filteredCategories = getFilteredCategories()
// 刷新分类数据
const handleRefresh = async () => {
await refreshCategories()
}
const handleCreateCategory = async () => {
try {
const newCategory = await ResourceCategoryServiceV2.createCategory(
formData.title,
formData.ai_prompt,
formData.color,
formData.is_cloud
)
if (newCategory) {
// 使用 store 的 addCategory 方法
addCategory(newCategory)
setShowCreateForm(false)
setFormData({ title: '', ai_prompt: '', color: '#FF6B6B', is_cloud: false })
}
} 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) {
// 使用 store 的 updateCategory 方法
updateCategory(editingCategory.id, updatedCategory)
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)
// 使用 store 的 removeCategory 方法
removeCategory(categoryId)
} 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)
}
// 使用 store 的 updateCategory 方法
updateCategory(categoryId, { is_active: isActive })
} 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 refreshCategories()
} catch (error) {
console.error('Failed to batch import:', error)
alert('批量导入失败: ' + (error instanceof Error ? error.message : '数据格式错误'))
}
}
const handleSearchByColor = (color: string) => {
if (color === selectedColor) {
// 取消颜色筛选
setSelectedColor('')
} else {
// 按颜色筛选
setSelectedColor(color)
}
// 过滤逻辑已经在 getFilteredCategories 中处理
}
const handleSearch = () => {
// 搜索逻辑已经在 getFilteredCategories 中处理
// 这里只需要触发重新渲染React 会自动调用 getFilteredCategories
}
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 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)}
onKeyDown={(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