From 010080f61e4cd616b1809d9980b6ed5311054d96 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 11 Jul 2025 00:29:09 +0800 Subject: [PATCH] =?UTF-8?q?=E7=B4=A0=E6=9D=90=E5=88=86=E7=B1=BB=E7=AE=A1?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../services/resource_category_manager.py | 281 +++++++++++++++ src-tauri/src/commands/mod.rs | 2 + src-tauri/src/commands/resource_category.rs | 98 ++++++ src-tauri/src/lib.rs | 8 +- src/App.tsx | 2 + src/components/Sidebar.tsx | 3 +- src/pages/ResourceCategoryPage.tsx | 331 ++++++++++++++++++ src/services/resourceCategoryService.ts | 130 +++++++ 8 files changed, 853 insertions(+), 2 deletions(-) create mode 100644 python_core/services/resource_category_manager.py create mode 100644 src-tauri/src/commands/resource_category.rs create mode 100644 src/pages/ResourceCategoryPage.tsx create mode 100644 src/services/resourceCategoryService.ts diff --git a/python_core/services/resource_category_manager.py b/python_core/services/resource_category_manager.py new file mode 100644 index 0000000..80df735 --- /dev/null +++ b/python_core/services/resource_category_manager.py @@ -0,0 +1,281 @@ +""" +素材分类管理服务 +管理素材分类信息,包括分类标题、AI识别提示词和展示颜色 +""" + +import json +import uuid +from pathlib import Path +from typing import List, Dict, Optional +from dataclasses import dataclass, asdict +from datetime import datetime + +from python_core.config import settings +from python_core.utils.logger import logger + + +@dataclass +class ResourceCategory: + """素材分类数据结构""" + id: str + title: str # 分类标题 + ai_prompt: str # AI识别提示词 + color: str # 展示颜色 (hex格式,如 #FF5733) + created_at: str + updated_at: str + is_active: bool = True + + +class ResourceCategoryManager: + """素材分类管理器""" + + def __init__(self): + self.cache_dir = settings.temp_dir / "cache" + self.cache_dir.mkdir(parents=True, exist_ok=True) + + # 分类数据文件 + self.categories_file = self.cache_dir / "resource_category.json" + self.categories = self._load_categories() + + def _load_categories(self) -> List[ResourceCategory]: + """加载分类数据""" + if self.categories_file.exists(): + try: + with open(self.categories_file, 'r', encoding='utf-8') as f: + data = json.load(f) + return [ResourceCategory(**item) for item in data] + except Exception as e: + logger.error(f"Failed to load categories: {e}") + return self._create_default_categories() + else: + return self._create_default_categories() + + def _create_default_categories(self) -> List[ResourceCategory]: + """创建默认分类""" + now = datetime.now().isoformat() + default_categories = [ + ResourceCategory( + id=str(uuid.uuid4()), + title="人物视频", + ai_prompt="包含人物、人脸、人体的视频素材", + color="#FF6B6B", + created_at=now, + updated_at=now + ), + ResourceCategory( + id=str(uuid.uuid4()), + title="风景视频", + ai_prompt="自然风景、城市景观、建筑物的视频素材", + color="#4ECDC4", + created_at=now, + updated_at=now + ), + ResourceCategory( + id=str(uuid.uuid4()), + title="动物视频", + ai_prompt="动物、宠物、野生动物的视频素材", + color="#45B7D1", + created_at=now, + updated_at=now + ), + ResourceCategory( + id=str(uuid.uuid4()), + title="音乐音效", + ai_prompt="背景音乐、音效、声音素材", + color="#96CEB4", + created_at=now, + updated_at=now + ), + ResourceCategory( + id=str(uuid.uuid4()), + title="文字图片", + ai_prompt="文字、标题、图标、图形素材", + color="#FFEAA7", + created_at=now, + updated_at=now + ), + ResourceCategory( + id=str(uuid.uuid4()), + title="特效素材", + ai_prompt="转场、特效、动画、滤镜素材", + color="#DDA0DD", + created_at=now, + updated_at=now + ) + ] + self._save_categories(default_categories) + return default_categories + + def _save_categories(self, categories: List[ResourceCategory] = None): + """保存分类数据""" + if categories is None: + categories = self.categories + + try: + data = [asdict(category) for category in categories] + with open(self.categories_file, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + logger.info(f"Categories saved to {self.categories_file}") + except Exception as e: + logger.error(f"Failed to save categories: {e}") + raise + + def get_all_categories(self) -> List[Dict]: + """获取所有分类""" + return [asdict(category) for category in self.categories if category.is_active] + + def get_category_by_id(self, category_id: str) -> Optional[Dict]: + """根据ID获取分类""" + for category in self.categories: + if category.id == category_id and category.is_active: + return asdict(category) + return None + + def create_category(self, title: str, ai_prompt: str, color: str) -> Dict: + """创建新分类""" + now = datetime.now().isoformat() + new_category = ResourceCategory( + id=str(uuid.uuid4()), + title=title, + ai_prompt=ai_prompt, + color=color, + created_at=now, + updated_at=now + ) + + self.categories.append(new_category) + self._save_categories() + + logger.info(f"Created new category: {title}") + return asdict(new_category) + + def update_category(self, category_id: str, title: str = None, + ai_prompt: str = None, color: str = None) -> Optional[Dict]: + """更新分类""" + for i, category in enumerate(self.categories): + if category.id == category_id and category.is_active: + if title is not None: + category.title = title + if ai_prompt is not None: + category.ai_prompt = ai_prompt + if color is not None: + category.color = color + category.updated_at = datetime.now().isoformat() + + self.categories[i] = category + self._save_categories() + + logger.info(f"Updated category: {category_id}") + return asdict(category) + return None + + def delete_category(self, category_id: str) -> bool: + """删除分类(软删除)""" + for i, category in enumerate(self.categories): + if category.id == category_id and category.is_active: + category.is_active = False + category.updated_at = datetime.now().isoformat() + + self.categories[i] = category + self._save_categories() + + logger.info(f"Deleted category: {category_id}") + return True + return False + + def search_categories(self, keyword: str) -> List[Dict]: + """搜索分类""" + keyword = keyword.lower() + results = [] + + for category in self.categories: + if (category.is_active and + (keyword in category.title.lower() or + keyword in category.ai_prompt.lower())): + results.append(asdict(category)) + + return results + + +# 全局实例 +resource_category_manager = ResourceCategoryManager() + + +def main(): + """命令行接口""" + import sys + import json + + if len(sys.argv) < 2: + print(json.dumps({"status": False, "msg": "No command specified"})) + return + + command = sys.argv[1] + + try: + if command == "get_all_categories": + categories = resource_category_manager.get_all_categories() + print(json.dumps({"status": True, "data": categories})) + + elif command == "get_category_by_id": + if len(sys.argv) < 3: + print(json.dumps({"status": False, "msg": "Category ID required"})) + return + category_id = sys.argv[2] + category = resource_category_manager.get_category_by_id(category_id) + if category: + print(json.dumps({"status": True, "data": category})) + else: + print(json.dumps({"status": False, "msg": "Category not found"})) + + elif command == "create_category": + if len(sys.argv) < 5: + print(json.dumps({"status": False, "msg": "Title, prompt and color required"})) + return + title, ai_prompt, color = sys.argv[2], sys.argv[3], sys.argv[4] + result = resource_category_manager.create_category(title, ai_prompt, color) + print(json.dumps({"status": True, "data": result})) + + elif command == "update_category": + if len(sys.argv) < 4: + print(json.dumps({"status": False, "msg": "Category ID and update data required"})) + return + category_id = sys.argv[2] + update_data = json.loads(sys.argv[3]) + result = resource_category_manager.update_category( + category_id, + update_data.get('title'), + update_data.get('ai_prompt'), + update_data.get('color') + ) + if result: + print(json.dumps({"status": True, "data": result})) + else: + print(json.dumps({"status": False, "msg": "Category not found"})) + + elif command == "delete_category": + if len(sys.argv) < 3: + print(json.dumps({"status": False, "msg": "Category ID required"})) + return + category_id = sys.argv[2] + success = resource_category_manager.delete_category(category_id) + print(json.dumps({"status": True, "data": success})) + + elif command == "search_categories": + if len(sys.argv) < 3: + print(json.dumps({"status": False, "msg": "Search keyword required"})) + return + keyword = sys.argv[2] + results = resource_category_manager.search_categories(keyword) + print(json.dumps({"status": True, "data": results})) + + else: + print(json.dumps({"status": False, "msg": f"Unknown command: {command}"})) + + except Exception as e: + logger.error(f"Command execution failed: {e}") + print(json.dumps({"status": False, "msg": str(e)})) + + +if __name__ == "__main__": + main() diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index a671415..be7d297 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -5,6 +5,7 @@ pub mod ai_video; pub mod file_system; pub mod project; pub mod template; +pub mod resource_category; // Re-export all commands for easy access pub use basic::*; @@ -13,3 +14,4 @@ pub use ai_video::*; pub use file_system::*; pub use project::*; pub use template::*; +pub use resource_category::*; diff --git a/src-tauri/src/commands/resource_category.rs b/src-tauri/src/commands/resource_category.rs new file mode 100644 index 0000000..dc2a909 --- /dev/null +++ b/src-tauri/src/commands/resource_category.rs @@ -0,0 +1,98 @@ +use serde::{Deserialize, Serialize}; +use tauri::{command, AppHandle}; +use crate::python_executor::execute_python_command; + +#[derive(Debug, Serialize, Deserialize)] +pub struct CreateCategoryRequest { + pub title: String, + pub ai_prompt: String, + pub color: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct UpdateCategoryRequest { + pub title: Option, + pub ai_prompt: Option, + pub color: Option, +} + +/// 获取所有资源分类 +#[command] +pub async fn get_all_resource_categories(app: AppHandle) -> Result { + let args = vec![ + "python_core.services.resource_category_manager".to_string(), + "get_all_categories".to_string(), + ]; + + execute_python_command(app, &args, None).await +} + +/// 根据ID获取资源分类 +#[command] +pub async fn get_resource_category_by_id(app: AppHandle, category_id: String) -> Result { + let args = vec![ + "python_core.services.resource_category_manager".to_string(), + "get_category_by_id".to_string(), + category_id, + ]; + + execute_python_command(app, &args, None).await +} + +/// 创建新的资源分类 +#[command] +pub async fn create_resource_category(app: AppHandle, request: CreateCategoryRequest) -> Result { + let args = vec![ + "python_core.services.resource_category_manager".to_string(), + "create_category".to_string(), + request.title, + request.ai_prompt, + request.color, + ]; + + execute_python_command(app, &args, None).await +} + +/// 更新资源分类 +#[command] +pub async fn update_resource_category( + app: AppHandle, + category_id: String, + request: UpdateCategoryRequest, +) -> Result { + let request_json = serde_json::to_string(&request) + .map_err(|e| format!("Failed to serialize request: {}", e))?; + + let args = vec![ + "python_core.services.resource_category_manager".to_string(), + "update_category".to_string(), + category_id, + request_json, + ]; + + execute_python_command(app, &args, None).await +} + +/// 删除资源分类 +#[command] +pub async fn delete_resource_category(app: AppHandle, category_id: String) -> Result { + let args = vec![ + "python_core.services.resource_category_manager".to_string(), + "delete_category".to_string(), + category_id, + ]; + + execute_python_command(app, &args, None).await +} + +/// 搜索资源分类 +#[command] +pub async fn search_resource_categories(app: AppHandle, keyword: String) -> Result { + let args = vec![ + "python_core.services.resource_category_manager".to_string(), + "search_categories".to_string(), + keyword, + ]; + + execute_python_command(app, &args, None).await +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 56a3e8c..de567c4 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -42,7 +42,13 @@ pub fn run() { commands::get_templates, commands::get_template, commands::get_template_detail, - commands::delete_template + commands::delete_template, + commands::get_all_resource_categories, + commands::get_resource_category_by_id, + commands::create_resource_category, + commands::update_resource_category, + commands::delete_resource_category, + commands::search_resource_categories ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/App.tsx b/src/App.tsx index ace8ede..9509446 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,6 +7,7 @@ import AIVideoPage from './pages/AIVideoPage' import SettingsPage from './pages/SettingsPage' import TemplateManagePage from './pages/TemplateManagePage' import TemplateDetailPage from './pages/TemplateDetailPage' +import ResourceCategoryPage from './pages/ResourceCategoryPage' import KVTestPage from './pages/KVTestPage' function App() { @@ -18,6 +19,7 @@ function App() { } /> } /> } /> + } /> } /> } /> diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 84bd6f1..67fc6b3 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react' import { Link, useLocation } from 'react-router-dom' -import { Home, Video, Settings, FolderOpen, Music, Image, Sparkles, Layout, Clock, CheckCircle, XCircle, List } from 'lucide-react' +import { Home, Video, Settings, FolderOpen, Music, Image, Sparkles, Layout, Clock, CheckCircle, XCircle, List, Tags } from 'lucide-react' import { clsx } from 'clsx' const Sidebar: React.FC = () => { @@ -23,6 +23,7 @@ const Sidebar: React.FC = () => { { path: '/editor', icon: Video, label: '编辑器' }, { path: '/ai-video', icon: Sparkles, label: 'AI 视频' }, { path: '/templates', icon: Layout, label: '模板管理' }, + { path: '/resource-categories', icon: Tags, label: '分类管理' }, { path: '/projects', icon: FolderOpen, label: '项目' }, { path: '/media', icon: Image, label: '媒体库' }, { path: '/audio', icon: Music, label: '音频' }, diff --git a/src/pages/ResourceCategoryPage.tsx b/src/pages/ResourceCategoryPage.tsx new file mode 100644 index 0000000..83b4b4b --- /dev/null +++ b/src/pages/ResourceCategoryPage.tsx @@ -0,0 +1,331 @@ +import React, { useState, useEffect } from 'react' +import { Plus, Edit, Trash2, Search, Save, X, Palette } from 'lucide-react' +import { ResourceCategoryService, ResourceCategory } from '../services/resourceCategoryService' + +const ResourceCategoryPage: React.FC = () => { + const [categories, setCategories] = useState([]) + const [loading, setLoading] = useState(true) + const [searchTerm, setSearchTerm] = useState('') + const [editingCategory, setEditingCategory] = useState(null) + const [showCreateForm, setShowCreateForm] = useState(false) + const [formData, setFormData] = useState({ + title: '', + ai_prompt: '', + color: '#FF6B6B' + }) + + // 预设颜色选项 + const presetColors = [ + '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7', '#DDA0DD', + '#FF7675', '#74B9FF', '#00B894', '#FDCB6E', '#E17055', '#A29BFE', + '#FD79A8', '#6C5CE7', '#00CEC9', '#55A3FF', '#FF9F43', '#26DE81' + ] + + useEffect(() => { + loadCategories() + }, []) + + const loadCategories = async () => { + try { + setLoading(true) + const response = await ResourceCategoryService.getAllCategories() + if (response.status && response.data) { + setCategories(response.data) + } else { + console.error('Failed to load categories:', response.msg) + } + } catch (error) { + console.error('Failed to load categories:', error) + } finally { + setLoading(false) + } + } + + const handleCreateCategory = async () => { + try { + const response = await ResourceCategoryService.createCategory(formData) + if (response.status && response.data) { + setCategories([...categories, response.data]) + setShowCreateForm(false) + setFormData({ title: '', ai_prompt: '', color: '#FF6B6B' }) + } else { + alert('创建失败: ' + (response.msg || '未知错误')) + } + } catch (error) { + console.error('Failed to create category:', error) + alert('创建失败: ' + (error instanceof Error ? error.message : '未知错误')) + } + } + + const handleUpdateCategory = async () => { + if (!editingCategory) return + + try { + const response = await ResourceCategoryService.updateCategory(editingCategory.id, formData) + if (response.status && response.data) { + const updatedCategories = categories.map(cat => + cat.id === editingCategory.id ? response.data! : cat + ) + setCategories(updatedCategories) + setEditingCategory(null) + setFormData({ title: '', ai_prompt: '', color: '#FF6B6B' }) + } else { + alert('更新失败: ' + (response.msg || '未知错误')) + } + } catch (error) { + console.error('Failed to update category:', error) + alert('更新失败: ' + (error instanceof Error ? error.message : '未知错误')) + } + } + + const handleDeleteCategory = async (categoryId: string) => { + if (!confirm('确定要删除这个分类吗?')) return + + try { + const response = await ResourceCategoryService.deleteCategory(categoryId) + if (response.status) { + setCategories(categories.filter(cat => cat.id !== categoryId)) + } else { + alert('删除失败: ' + (response.msg || '未知错误')) + } + } catch (error) { + console.error('Failed to delete category:', error) + alert('删除失败: ' + (error instanceof Error ? error.message : '未知错误')) + } + } + + const startEdit = (category: ResourceCategory) => { + setEditingCategory(category) + setFormData({ + title: category.title, + ai_prompt: category.ai_prompt, + color: category.color + }) + setShowCreateForm(false) + } + + const cancelEdit = () => { + setEditingCategory(null) + setShowCreateForm(false) + setFormData({ title: '', ai_prompt: '', color: '#FF6B6B' }) + } + + const filteredCategories = categories.filter(category => + category.title.toLowerCase().includes(searchTerm.toLowerCase()) || + category.ai_prompt.toLowerCase().includes(searchTerm.toLowerCase()) + ) + + if (loading) { + return ( +
+
加载中...
+
+ ) + } + + return ( +
+ {/* 页面标题 */} +
+

素材分类管理

+

管理素材分类,设置AI识别提示词和展示颜色

+
+ + {/* 搜索和创建按钮 */} +
+
+ + setSearchTerm(e.target.value)} + 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" + /> +
+ + +
+ + {/* 分类列表 */} +
+ {filteredCategories.map((category) => ( +
+ {/* 分类标题和颜色 */} +
+
+
+

{category.title}

+
+ +
+ + +
+
+ + {/* AI提示词 */} +
+

{category.ai_prompt}

+
+ + {/* 创建时间 */} +
+ 创建于 {new Date(category.created_at).toLocaleDateString()} +
+
+ ))} +
+ + {/* 空状态 */} + {filteredCategories.length === 0 && ( +
+
+ {searchTerm ? '没有找到匹配的分类' : '暂无分类'} +
+ {!searchTerm && ( + + )} +
+ )} + + {/* 创建/编辑表单弹窗 */} + {(showCreateForm || editingCategory) && ( +
+
+ {/* 表单标题 */} +
+

+ {editingCategory ? '编辑分类' : '新建分类'} +

+ +
+ + {/* 表单内容 */} +
+ {/* 分类标题 */} +
+ + 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" + /> +
+ + {/* AI识别提示词 */} +
+ +