fix
This commit is contained in:
@@ -1,224 +0,0 @@
|
||||
# 嵌入式Python方案指南
|
||||
|
||||
## 🎯 方案概述
|
||||
|
||||
这个方案将Python环境直接打包到你的Tauri应用中,用户无需安装Python即可使用所有功能。
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 设置嵌入式Python环境
|
||||
|
||||
```cmd
|
||||
# 运行设置脚本
|
||||
scripts\setup_embedded_python.bat
|
||||
```
|
||||
|
||||
这将:
|
||||
- 下载Python 3.11嵌入式版本
|
||||
- 安装必要的依赖(requests, Pillow等)
|
||||
- 复制你的python_core模块
|
||||
- 创建启动脚本
|
||||
- 更新Tauri配置
|
||||
|
||||
### 2. 测试嵌入式Python
|
||||
|
||||
```cmd
|
||||
# 测试Python版本
|
||||
src-tauri\python-embed\python.exe --version
|
||||
|
||||
# 测试依赖
|
||||
src-tauri\python-embed\python.exe -c "import requests, PIL; print('Dependencies OK')"
|
||||
|
||||
# 测试python_core模块
|
||||
src-tauri\python-embed\python.exe -c "import python_core; print('python_core OK')"
|
||||
```
|
||||
|
||||
### 3. 启动应用测试
|
||||
|
||||
```cmd
|
||||
cargo tauri dev
|
||||
```
|
||||
|
||||
现在应用会自动使用嵌入式Python,无需系统Python环境。
|
||||
|
||||
## 📁 目录结构
|
||||
|
||||
设置完成后,你的项目结构如下:
|
||||
|
||||
```
|
||||
your-project/
|
||||
├── src-tauri/
|
||||
│ ├── python-embed/ # 嵌入式Python环境
|
||||
│ │ ├── python.exe # Python解释器
|
||||
│ │ ├── python311.dll # Python库
|
||||
│ │ ├── Lib/ # 标准库
|
||||
│ │ ├── Scripts/ # pip等工具
|
||||
│ │ ├── python_core/ # 你的Python模块
|
||||
│ │ └── python-core.bat # 启动脚本
|
||||
│ └── tauri.conf.json # 已更新配置
|
||||
├── python_core/ # 原始Python代码
|
||||
└── scripts/
|
||||
├── setup_embedded_python.py
|
||||
└── setup_embedded_python.bat
|
||||
```
|
||||
|
||||
## 🔧 工作原理
|
||||
|
||||
### 执行优先级
|
||||
|
||||
Rust代码会按以下优先级选择Python环境:
|
||||
|
||||
1. **嵌入式Python** - 如果 `src-tauri/python-embed/python.exe` 存在
|
||||
2. **Sidecar** - 如果 `mixvideo-python-core.exe` 存在
|
||||
3. **系统Python** - 作为最后的回退选项
|
||||
|
||||
### 环境变量控制
|
||||
|
||||
你可以通过环境变量强制使用特定的Python:
|
||||
|
||||
```cmd
|
||||
# 强制使用系统Python
|
||||
set MIXVIDEO_FORCE_SYSTEM_PYTHON=1
|
||||
cargo tauri dev
|
||||
```
|
||||
|
||||
## 📦 构建和分发
|
||||
|
||||
### 开发模式
|
||||
|
||||
在开发模式下,嵌入式Python位于 `src-tauri/python-embed/`
|
||||
|
||||
### 生产构建
|
||||
|
||||
```cmd
|
||||
cargo tauri build
|
||||
```
|
||||
|
||||
构建时,Tauri会自动将 `python-embed` 目录打包到最终的可执行文件中。
|
||||
|
||||
### 分发
|
||||
|
||||
最终的应用包含:
|
||||
- 你的Tauri应用
|
||||
- 完整的Python环境
|
||||
- 所有Python依赖
|
||||
- 你的python_core模块
|
||||
|
||||
用户无需安装任何额外软件即可运行。
|
||||
|
||||
## 🧪 测试和调试
|
||||
|
||||
### 测试嵌入式Python
|
||||
|
||||
```cmd
|
||||
# 直接测试
|
||||
src-tauri\python-embed\python.exe -m python_core.main --module test --action hello
|
||||
|
||||
# 测试特定服务
|
||||
src-tauri\python-embed\python.exe -m python_core.services.template_manager --action get_templates
|
||||
```
|
||||
|
||||
### 在应用中测试
|
||||
|
||||
1. 启动应用:`cargo tauri dev`
|
||||
2. 访问Python测试页面
|
||||
3. 运行测试 - 应该显示"Using embedded Python for execution"
|
||||
|
||||
### 调试问题
|
||||
|
||||
如果遇到问题:
|
||||
|
||||
1. **检查Python路径**:
|
||||
```cmd
|
||||
dir src-tauri\python-embed\python.exe
|
||||
```
|
||||
|
||||
2. **检查模块导入**:
|
||||
```cmd
|
||||
src-tauri\python-embed\python.exe -c "import sys; print(sys.path)"
|
||||
```
|
||||
|
||||
3. **查看日志**:
|
||||
- 控制台会显示使用的Python类型
|
||||
- 检查错误信息
|
||||
|
||||
## ⚙️ 自定义配置
|
||||
|
||||
### 添加更多依赖
|
||||
|
||||
编辑 `scripts/setup_embedded_python.py`:
|
||||
|
||||
```python
|
||||
dependencies = [
|
||||
"requests",
|
||||
"Pillow",
|
||||
"numpy", # 添加新依赖
|
||||
"opencv-python", # 添加新依赖
|
||||
]
|
||||
```
|
||||
|
||||
然后重新运行设置脚本。
|
||||
|
||||
### 更新Python版本
|
||||
|
||||
修改 `setup_embedded_python.py` 中的下载URL:
|
||||
|
||||
```python
|
||||
python_url = "https://www.python.org/ftp/python/3.12.0/python-3.12.0-embed-amd64.zip"
|
||||
```
|
||||
|
||||
### 自定义Python路径
|
||||
|
||||
如果需要自定义路径,修改 `get_embedded_python_path()` 函数。
|
||||
|
||||
## 🎉 优势
|
||||
|
||||
### 对比其他方案
|
||||
|
||||
| 方案 | 优势 | 劣势 |
|
||||
|------|------|------|
|
||||
| **嵌入式Python** | ✅ 无需用户安装<br>✅ 版本一致<br>✅ 完全控制 | ❌ 包体积较大<br>❌ 更新需要重新分发 |
|
||||
| Sidecar | ✅ 包体积小<br>✅ 独立更新 | ❌ 构建复杂<br>❌ 依赖管理困难 |
|
||||
| 系统Python | ✅ 包体积最小 | ❌ 需要用户安装<br>❌ 版本不一致 |
|
||||
|
||||
### 适用场景
|
||||
|
||||
嵌入式Python特别适合:
|
||||
- 面向普通用户的应用
|
||||
- 需要特定Python版本的应用
|
||||
- 希望简化部署的企业应用
|
||||
- 不希望依赖外部环境的应用
|
||||
|
||||
## 🔄 维护和更新
|
||||
|
||||
### 更新Python代码
|
||||
|
||||
1. 修改 `python_core/` 中的代码
|
||||
2. 重新运行设置脚本:`scripts\setup_embedded_python.bat`
|
||||
3. 测试更改:`cargo tauri dev`
|
||||
|
||||
### 更新依赖
|
||||
|
||||
1. 修改 `setup_embedded_python.py` 中的依赖列表
|
||||
2. 重新运行设置脚本
|
||||
3. 测试新依赖
|
||||
|
||||
### 版本管理
|
||||
|
||||
建议在版本控制中:
|
||||
- ✅ 包含 `scripts/setup_embedded_python.py`
|
||||
- ✅ 包含 `python_core/` 源代码
|
||||
- ❌ 排除 `src-tauri/python-embed/` (太大)
|
||||
- ✅ 在CI/CD中自动运行设置脚本
|
||||
|
||||
## 📋 最佳实践
|
||||
|
||||
1. **定期更新Python版本** - 保持安全性
|
||||
2. **最小化依赖** - 减少包体积
|
||||
3. **测试多平台** - 确保跨平台兼容性
|
||||
4. **监控包大小** - 避免过度膨胀
|
||||
5. **文档化依赖** - 便于维护
|
||||
|
||||
---
|
||||
|
||||
这个方案为你提供了一个完全自包含的Python环境,确保用户体验的一致性和简便性!
|
||||
@@ -1,152 +0,0 @@
|
||||
# 快速修复指南 - Python Core 构建问题
|
||||
|
||||
## 问题分析
|
||||
|
||||
构建失败的原因是 `build.spec` 文件中使用了 `__file__` 变量,但在PyInstaller的spec文件中这个变量不可用。
|
||||
|
||||
## 快速解决方案
|
||||
|
||||
### 方案1:使用命令行直接构建(推荐)
|
||||
|
||||
在 `python_core` 目录下运行:
|
||||
|
||||
```cmd
|
||||
# Windows
|
||||
python -m PyInstaller --onefile --console --name mixvideo-python-core main_simple.py
|
||||
|
||||
# 或者如果要包含完整功能
|
||||
python -m PyInstaller --onefile --console --name mixvideo-python-core ^
|
||||
--hidden-import python_core ^
|
||||
--hidden-import python_core.utils ^
|
||||
--hidden-import python_core.services ^
|
||||
main.py
|
||||
```
|
||||
|
||||
### 方案2:使用测试批处理文件
|
||||
|
||||
直接运行:
|
||||
```cmd
|
||||
cd python_core
|
||||
build_test.bat
|
||||
```
|
||||
|
||||
### 方案3:修复spec文件
|
||||
|
||||
如果要使用spec文件,请使用以下简化版本:
|
||||
|
||||
```python
|
||||
# build_fixed.spec
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
block_cipher = None
|
||||
|
||||
a = Analysis(
|
||||
['main.py'],
|
||||
pathex=['.'],
|
||||
binaries=[],
|
||||
datas=[],
|
||||
hiddenimports=[
|
||||
'python_core',
|
||||
'python_core.utils',
|
||||
'python_core.utils.logger',
|
||||
'python_core.utils.jsonrpc',
|
||||
'python_core.services',
|
||||
'python_core.services.template_manager',
|
||||
# 添加其他需要的模块
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='mixvideo-python-core',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
```
|
||||
|
||||
## 验证步骤
|
||||
|
||||
1. **构建成功后**,在 `python_core/dist/` 目录下会生成 `mixvideo-python-core.exe`
|
||||
|
||||
2. **测试可执行文件**:
|
||||
```cmd
|
||||
dist\mixvideo-python-core.exe --version
|
||||
dist\mixvideo-python-core.exe --module test --action hello
|
||||
```
|
||||
|
||||
3. **复制到Tauri目录**:
|
||||
```cmd
|
||||
mkdir ..\src-tauri\binaries
|
||||
copy dist\mixvideo-python-core.exe ..\src-tauri\binaries\
|
||||
```
|
||||
|
||||
4. **更新Tauri配置**:
|
||||
在 `src-tauri/tauri.conf.json` 中添加:
|
||||
```json
|
||||
{
|
||||
"bundle": {
|
||||
"externalBin": [
|
||||
{
|
||||
"name": "mixvideo-python-core",
|
||||
"src": "binaries/mixvideo-python-core",
|
||||
"targets": ["all"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 如果仍然遇到问题
|
||||
|
||||
### 检查Python环境
|
||||
```cmd
|
||||
python --version
|
||||
python -m pip list | findstr pyinstaller
|
||||
```
|
||||
|
||||
### 安装必要依赖
|
||||
```cmd
|
||||
python -m pip install pyinstaller requests Pillow
|
||||
```
|
||||
|
||||
### 使用最简单的构建
|
||||
```cmd
|
||||
cd python_core
|
||||
python -m PyInstaller --onefile main_simple.py
|
||||
```
|
||||
|
||||
这将创建一个基本的可执行文件,可以用来测试整个流程。
|
||||
|
||||
## 下一步
|
||||
|
||||
一旦基本构建成功,就可以:
|
||||
1. 逐步添加更多的隐藏导入
|
||||
2. 测试完整的功能
|
||||
3. 集成到Tauri应用中
|
||||
|
||||
记住:先让简单版本工作,再逐步增加复杂性。
|
||||
@@ -1,319 +0,0 @@
|
||||
# Python Core 架构设计总结
|
||||
|
||||
## 🎯 架构验证结果
|
||||
|
||||
```
|
||||
🎉 架构设计验证通过!
|
||||
|
||||
✅ 硬性要求满足:
|
||||
1. ✅ 命令行集成 - 所有功能都通过CLI触发
|
||||
2. ✅ 进度反馈 - JSON RPC Progress 统一进度条
|
||||
3. ✅ API就绪 - 服务具备API化基础
|
||||
4. ✅ 存储抽象 - 支持多种存储方式切换
|
||||
|
||||
通过测试: 6/6
|
||||
```
|
||||
|
||||
## 🏗️ 架构设计概览
|
||||
|
||||
### **分层架构**
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ CLI Layer (命令行层) │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ MediaManager │ │ SceneDetection │ │ TemplateManager │ │
|
||||
│ │ Commander │ │ Commander │ │ Commander │ │
|
||||
│ │ (进度条) │ │ (进度条) │ │ (进度条) │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Service Layer (服务层) │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ MediaManager │ │ SceneDetection │ │ TemplateManager │ │
|
||||
│ │ Service │ │ Service │ │ Service │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Storage Layer (存储层) │
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ JSON Storage │ │ Database │ │ MongoDB │ │
|
||||
│ │ (当前) │ │ Storage │ │ Storage │ │
|
||||
│ │ │ │ (未来) │ │ (未来) │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 🔧 核心组件实现
|
||||
|
||||
### **1. 进度命令基类**
|
||||
```python
|
||||
class ProgressJSONRPCCommander(ABC):
|
||||
"""带进度的JSON-RPC命令基类"""
|
||||
|
||||
def _is_progressive_command(self, command: str) -> bool:
|
||||
"""判断是否需要进度报告"""
|
||||
return command in ["batch_upload", "batch_detect", "compare"]
|
||||
|
||||
def create_task(self, name: str, total: int):
|
||||
"""创建进度任务"""
|
||||
return ProgressTask(name, total, self.progress_reporter)
|
||||
```
|
||||
|
||||
**实现状态**: ✅ 已实现并验证
|
||||
- 媒体管理器: `upload`, `batch_upload` 支持进度
|
||||
- 场景检测: `batch_detect`, `compare` 支持进度
|
||||
- JSON-RPC 2.0 进度协议标准化
|
||||
|
||||
### **2. 存储抽象层**
|
||||
```python
|
||||
class StorageInterface(ABC):
|
||||
"""存储接口基类"""
|
||||
|
||||
@abstractmethod
|
||||
def save(self, collection: str, key: str, data: Any) -> bool:
|
||||
"""保存数据"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def load(self, collection: str, key: str) -> Any:
|
||||
"""加载数据"""
|
||||
pass
|
||||
```
|
||||
|
||||
**实现状态**: ✅ 已实现并验证
|
||||
- JSON存储: 完整实现,支持批量操作
|
||||
- 存储工厂: 支持多种存储类型注册
|
||||
- 无缝切换: 配置驱动的存储选择
|
||||
|
||||
### **3. 服务基类**
|
||||
```python
|
||||
class ServiceBase(ABC):
|
||||
"""服务基类"""
|
||||
|
||||
def __init__(self, storage: Optional[StorageInterface] = None):
|
||||
self.storage = storage or get_storage(self.get_service_name())
|
||||
|
||||
@abstractmethod
|
||||
def get_service_name(self) -> str:
|
||||
"""获取服务名称"""
|
||||
pass
|
||||
```
|
||||
|
||||
**实现状态**: ✅ 已实现并验证
|
||||
- 统一存储接口: 所有服务使用相同的存储抽象
|
||||
- 进度支持: `ProgressServiceBase` 提供进度回调
|
||||
- 集合管理: 自动命名空间隔离
|
||||
|
||||
## 📊 硬性要求满足情况
|
||||
|
||||
### **1. 命令行集成 ✅**
|
||||
```bash
|
||||
# 媒体管理
|
||||
python -m python_core.services.media_manager upload video.mp4
|
||||
python -m python_core.services.media_manager batch_upload /videos
|
||||
|
||||
# 场景检测
|
||||
python -m python_core.services.scene_detection detect video.mp4
|
||||
python -m python_core.services.scene_detection batch_detect /videos
|
||||
|
||||
# 模板管理 (未来)
|
||||
python -m python_core.services.template_manager import /templates
|
||||
```
|
||||
|
||||
**验证结果**:
|
||||
- ✅ 所有功能都通过CLI触发
|
||||
- ✅ 统一的命令行接口
|
||||
- ✅ 标准化的参数处理
|
||||
|
||||
### **2. 进度反馈 ✅**
|
||||
```json
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"method": "progress",
|
||||
"params": {
|
||||
"step": "media_manager",
|
||||
"progress": 0.65,
|
||||
"message": "处理文件: video.mp4 (3/5)",
|
||||
"details": {
|
||||
"current": 3,
|
||||
"total": 5,
|
||||
"elapsed_time": 2.5,
|
||||
"estimated_remaining": 1.2
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**验证结果**:
|
||||
- ✅ JSON-RPC 2.0 进度协议
|
||||
- ✅ 实时进度反馈
|
||||
- ✅ 智能进度命令识别
|
||||
|
||||
### **3. API就绪 ✅**
|
||||
```python
|
||||
# 当前CLI接口
|
||||
result = commander.execute_command("upload", {"video_path": "video.mp4"})
|
||||
|
||||
# 未来API接口 (无缝迁移)
|
||||
@app.post("/api/v1/media_manager/upload")
|
||||
async def api_upload(args: dict):
|
||||
return commander.execute_command("upload", args)
|
||||
```
|
||||
|
||||
**验证结果**:
|
||||
- ✅ 标准化的命令执行接口
|
||||
- ✅ JSON格式的输入输出
|
||||
- ✅ 统一的参数处理
|
||||
- ✅ 进度回调机制
|
||||
|
||||
### **4. 存储抽象 ✅**
|
||||
```python
|
||||
# 当前JSON存储
|
||||
config = StorageConfig(storage_type=StorageType.JSON)
|
||||
storage = StorageFactory.create_storage(config)
|
||||
|
||||
# 未来数据库存储 (无缝切换)
|
||||
config = StorageConfig(storage_type=StorageType.DATABASE,
|
||||
connection_string="postgresql://...")
|
||||
storage = StorageFactory.create_storage(config)
|
||||
```
|
||||
|
||||
**验证结果**:
|
||||
- ✅ 统一的存储接口
|
||||
- ✅ 多种存储实现支持
|
||||
- ✅ 配置驱动的存储选择
|
||||
- ✅ 无缝切换能力
|
||||
|
||||
## 🚀 迁移路径设计
|
||||
|
||||
### **阶段1: 当前实现 (已完成)**
|
||||
- ✅ JSON文件存储
|
||||
- ✅ 进度命令行接口
|
||||
- ✅ 模块化服务架构
|
||||
|
||||
### **阶段2: 存储扩展 (规划中)**
|
||||
```python
|
||||
# 数据库存储
|
||||
class DatabaseStorage(StorageInterface):
|
||||
def __init__(self, config: StorageConfig):
|
||||
self.engine = create_engine(config.connection_string)
|
||||
|
||||
# MongoDB存储
|
||||
class MongoDBStorage(StorageInterface):
|
||||
def __init__(self, config: StorageConfig):
|
||||
self.client = MongoClient(config.connection_string)
|
||||
```
|
||||
|
||||
### **阶段3: API化 (规划中)**
|
||||
```python
|
||||
# REST API
|
||||
@app.post("/api/v1/{service}/{command}")
|
||||
async def execute_command(service: str, command: str, args: dict):
|
||||
commander = get_commander(service)
|
||||
return commander.execute_command(command, args)
|
||||
|
||||
# WebSocket进度
|
||||
@app.websocket("/ws/progress")
|
||||
async def websocket_progress(websocket: WebSocket):
|
||||
# 实时进度推送
|
||||
pass
|
||||
```
|
||||
|
||||
### **阶段4: 微服务化 (规划中)**
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
media-manager:
|
||||
image: mixvideo/media-manager
|
||||
environment:
|
||||
- STORAGE_TYPE=database
|
||||
- DATABASE_URL=postgresql://...
|
||||
|
||||
scene-detection:
|
||||
image: mixvideo/scene-detection
|
||||
environment:
|
||||
- STORAGE_TYPE=mongodb
|
||||
- MONGODB_URL=mongodb://...
|
||||
```
|
||||
|
||||
## 🎯 架构优势
|
||||
|
||||
### **1. 统一性**
|
||||
- 🎯 **命令行优先**: 所有功能都可通过CLI访问
|
||||
- 📊 **进度统一**: 统一的JSON-RPC进度协议
|
||||
- 🔧 **接口标准**: 标准化的服务接口
|
||||
|
||||
### **2. 可扩展性**
|
||||
- 🔌 **插件化**: 易于添加新服务和存储
|
||||
- 💾 **存储灵活**: 支持多种存储后端
|
||||
- 🌐 **API就绪**: 无痛迁移到API
|
||||
|
||||
### **3. 可维护性**
|
||||
- 🧩 **模块化**: 清晰的模块分离
|
||||
- 📝 **文档完整**: 详细的架构文档
|
||||
- 🧪 **测试友好**: 独立测试各层组件
|
||||
|
||||
### **4. 用户友好**
|
||||
- 📊 **进度可视**: 实时进度反馈
|
||||
- 🔧 **配置灵活**: 丰富的配置选项
|
||||
- 📄 **输出多样**: 多种输出格式
|
||||
|
||||
## 📈 实际验证数据
|
||||
|
||||
### **存储层测试**
|
||||
```
|
||||
✅ 数据保存: 成功
|
||||
✅ 数据存在检查: 存在
|
||||
✅ 数据加载: 成功
|
||||
✅ 键列表: ['test_key']
|
||||
✅ 批量保存: 成功 (3/3)
|
||||
✅ 集合统计: 4 个文件
|
||||
✅ 清空集合: 成功
|
||||
```
|
||||
|
||||
### **服务层测试**
|
||||
```
|
||||
✅ 基础服务创建成功: test_service
|
||||
✅ 服务数据保存: 成功
|
||||
✅ 服务数据加载: 成功
|
||||
✅ 进度服务创建成功: test_progress_service
|
||||
✅ 收到进度消息: 3 条
|
||||
```
|
||||
|
||||
### **CLI集成测试**
|
||||
```
|
||||
✅ 媒体管理器使用进度Commander
|
||||
✅ 场景检测使用进度Commander
|
||||
✅ 进度命令识别正确
|
||||
✅ API风格命令执行成功: 找到 3 个片段
|
||||
```
|
||||
|
||||
## 🎉 总结
|
||||
|
||||
### **架构成果**
|
||||
- ✅ **完全满足硬性要求** - 4个核心要求全部实现
|
||||
- ✅ **验证通过率100%** - 6/6测试全部通过
|
||||
- ✅ **实际可用** - 现有服务完全兼容
|
||||
- ✅ **未来就绪** - 具备完整的迁移路径
|
||||
|
||||
### **技术亮点**
|
||||
- 🎯 **分层清晰** - CLI/Service/Storage三层架构
|
||||
- 🔄 **接口统一** - 标准化的组件接口
|
||||
- 📊 **进度标准** - JSON-RPC 2.0进度协议
|
||||
- 🔌 **插件化** - 支持存储和服务扩展
|
||||
|
||||
### **实用价值**
|
||||
- 💡 **开发效率** - 统一的开发模式
|
||||
- 🚀 **部署灵活** - 多种部署方式支持
|
||||
- 📈 **扩展性强** - 易于添加新功能
|
||||
- 🔧 **维护简单** - 清晰的模块边界
|
||||
|
||||
这个架构设计完全满足您的硬性要求,提供了从当前JSON存储到未来微服务化的完整演进路径,是一个经过验证的、可持续发展的架构方案!
|
||||
|
||||
---
|
||||
|
||||
*Python Core 架构 - 命令行优先,进度可视,API就绪,存储灵活!*
|
||||
@@ -6,7 +6,6 @@ from pathlib import Path
|
||||
from typing import Optional, List
|
||||
from dataclasses import asdict
|
||||
import typer
|
||||
import json
|
||||
from python_core.utils.jsonrpc_enhanced import create_response_handler, create_progress_reporter
|
||||
from python_core.services.template_manager_cloud import TemplateManagerCloud, TemplateInfo
|
||||
from python_core.utils.logger import logger
|
||||
|
||||
@@ -7,7 +7,7 @@ import HomePage from './pages/HomePage'
|
||||
import EditorPage from './pages/EditorPage'
|
||||
import AIVideoPage from './pages/AIVideoPage'
|
||||
import SettingsPage from './pages/SettingsPage'
|
||||
import TemplateManagePage from './pages/TemplateManagePage'
|
||||
import TemplateManagePage from './pages/TemplateManagePageV2'
|
||||
import TemplateDetailPage from './pages/TemplateDetailPage'
|
||||
import ResourceCategoryPage from './pages/ResourceCategoryPage'
|
||||
import ProjectManagePage from './pages/ProjectManagePage'
|
||||
|
||||
297
src/hooks/useTemplateProgressV2.ts
Normal file
297
src/hooks/useTemplateProgressV2.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react'
|
||||
import { listen, UnlistenFn } from '@tauri-apps/api/event'
|
||||
import { TemplateServiceV2, type BatchImportResult } from '../services/TemplateServiceV2'
|
||||
|
||||
// 进度信息接口
|
||||
export interface ProgressInfo {
|
||||
current: number
|
||||
total: number
|
||||
message: string
|
||||
percentage: number
|
||||
}
|
||||
|
||||
// Hook选项
|
||||
export interface UseTemplateProgressOptions {
|
||||
onSuccess?: (result: BatchImportResult) => void
|
||||
onError?: (error: Error) => void
|
||||
onProgress?: (progress: ProgressInfo) => void
|
||||
}
|
||||
|
||||
// Hook返回值
|
||||
export interface UseTemplateProgressReturn {
|
||||
isExecuting: boolean
|
||||
progress: ProgressInfo | null
|
||||
result: BatchImportResult | null
|
||||
error: Error | null
|
||||
logs: string[]
|
||||
batchImport: (sourceFolder: string, userId?: string) => Promise<void>
|
||||
reset: () => void
|
||||
}
|
||||
|
||||
export const useTemplateProgressV2 = (options: UseTemplateProgressOptions = {}): UseTemplateProgressReturn => {
|
||||
const [isExecuting, setIsExecuting] = useState(false)
|
||||
const [progress, setProgress] = useState<ProgressInfo | null>(null)
|
||||
const [result, setResult] = useState<BatchImportResult | null>(null)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
const [logs, setLogs] = useState<string[]>([])
|
||||
|
||||
const unlistenRef = useRef<UnlistenFn | null>(null)
|
||||
const { onSuccess, onError, onProgress } = options
|
||||
|
||||
// 清理监听器
|
||||
const cleanup = useCallback(async () => {
|
||||
if (unlistenRef.current) {
|
||||
await unlistenRef.current()
|
||||
unlistenRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 重置状态
|
||||
const reset = useCallback(() => {
|
||||
setIsExecuting(false)
|
||||
setProgress(null)
|
||||
setResult(null)
|
||||
setError(null)
|
||||
setLogs([])
|
||||
cleanup()
|
||||
}, [cleanup])
|
||||
|
||||
// 添加日志
|
||||
const addLog = useCallback((message: string) => {
|
||||
const timestamp = new Date().toLocaleTimeString()
|
||||
setLogs(prev => [...prev, `[${timestamp}] ${message}`])
|
||||
}, [])
|
||||
|
||||
// 更新进度
|
||||
const updateProgress = useCallback((current: number, total: number, message: string) => {
|
||||
const percentage = total > 0 ? Math.round((current / total) * 100) : 0
|
||||
const progressInfo: ProgressInfo = {
|
||||
current,
|
||||
total,
|
||||
message,
|
||||
percentage
|
||||
}
|
||||
|
||||
setProgress(progressInfo)
|
||||
addLog(`进度: ${current}/${total} (${percentage}%) - ${message}`)
|
||||
|
||||
if (onProgress) {
|
||||
onProgress(progressInfo)
|
||||
}
|
||||
}, [addLog, onProgress])
|
||||
|
||||
// 批量导入模板
|
||||
const batchImport = useCallback(async (sourceFolder: string, userId?: string) => {
|
||||
try {
|
||||
setIsExecuting(true)
|
||||
setError(null)
|
||||
setResult(null)
|
||||
setProgress(null)
|
||||
setLogs([])
|
||||
|
||||
addLog(`开始批量导入模板: ${sourceFolder}`)
|
||||
|
||||
// 监听进度事件
|
||||
unlistenRef.current = await listen('template-import-progress', (event: any) => {
|
||||
const { current, total, message } = event.payload
|
||||
updateProgress(current, total, message)
|
||||
})
|
||||
|
||||
// 执行导入
|
||||
const importResult = await TemplateServiceV2.batchImportTemplates(sourceFolder, userId)
|
||||
|
||||
setResult(importResult)
|
||||
addLog(`导入完成: 成功 ${importResult.imported_count} 个,失败 ${importResult.failed_count} 个`)
|
||||
|
||||
// 显示详细结果
|
||||
if (importResult.imported_templates.length > 0) {
|
||||
addLog('成功导入的模板:')
|
||||
importResult.imported_templates.forEach(template => {
|
||||
addLog(` ✓ ${template.name} (${template.id})`)
|
||||
})
|
||||
}
|
||||
|
||||
if (importResult.failed_templates.length > 0) {
|
||||
addLog('导入失败的模板:')
|
||||
importResult.failed_templates.forEach(failed => {
|
||||
addLog(` ✗ ${failed.name}: ${failed.error}`)
|
||||
})
|
||||
}
|
||||
|
||||
if (onSuccess) {
|
||||
onSuccess(importResult)
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error(String(err))
|
||||
setError(error)
|
||||
addLog(`导入失败: ${error.message}`)
|
||||
|
||||
if (onError) {
|
||||
onError(error)
|
||||
}
|
||||
} finally {
|
||||
setIsExecuting(false)
|
||||
cleanup()
|
||||
}
|
||||
}, [addLog, updateProgress, onSuccess, onError, cleanup])
|
||||
|
||||
// 组件卸载时清理
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
cleanup()
|
||||
}
|
||||
}, [cleanup])
|
||||
|
||||
return {
|
||||
isExecuting,
|
||||
progress,
|
||||
result,
|
||||
error,
|
||||
logs,
|
||||
batchImport,
|
||||
reset
|
||||
}
|
||||
}
|
||||
|
||||
// 简化版本的进度Hook,用于其他操作
|
||||
export const useTemplateOperationV2 = () => {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
const execute = useCallback(async <T>(operation: () => Promise<T>): Promise<T | null> => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
const result = await operation()
|
||||
return result
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error(String(err))
|
||||
setError(error)
|
||||
return null
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const reset = useCallback(() => {
|
||||
setLoading(false)
|
||||
setError(null)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
loading,
|
||||
error,
|
||||
execute,
|
||||
reset
|
||||
}
|
||||
}
|
||||
|
||||
// 模板搜索Hook
|
||||
export const useTemplateSearchV2 = () => {
|
||||
const [searchResults, setSearchResults] = useState<any[]>([])
|
||||
const [searching, setSearching] = useState(false)
|
||||
const [searchError, setSearchError] = useState<Error | null>(null)
|
||||
|
||||
const search = useCallback(async (
|
||||
query: string,
|
||||
includeCloud: boolean = true,
|
||||
limit: number = 50,
|
||||
userId?: string
|
||||
) => {
|
||||
try {
|
||||
setSearching(true)
|
||||
setSearchError(null)
|
||||
|
||||
if (!query.trim()) {
|
||||
setSearchResults([])
|
||||
return
|
||||
}
|
||||
|
||||
const results = await TemplateServiceV2.searchTemplates(query, includeCloud, limit, userId)
|
||||
setSearchResults(results)
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error(String(err))
|
||||
setSearchError(error)
|
||||
setSearchResults([])
|
||||
} finally {
|
||||
setSearching(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const searchByTag = useCallback(async (
|
||||
tag: string,
|
||||
includeCloud: boolean = true,
|
||||
limit: number = 50,
|
||||
userId?: string
|
||||
) => {
|
||||
try {
|
||||
setSearching(true)
|
||||
setSearchError(null)
|
||||
|
||||
const results = await TemplateServiceV2.getTemplatesByTag(tag, includeCloud, limit, userId)
|
||||
setSearchResults(results)
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error(String(err))
|
||||
setSearchError(error)
|
||||
setSearchResults([])
|
||||
} finally {
|
||||
setSearching(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const clearSearch = useCallback(() => {
|
||||
setSearchResults([])
|
||||
setSearchError(null)
|
||||
}, [])
|
||||
|
||||
return {
|
||||
searchResults,
|
||||
searching,
|
||||
searchError,
|
||||
search,
|
||||
searchByTag,
|
||||
clearSearch
|
||||
}
|
||||
}
|
||||
|
||||
// 模板统计Hook
|
||||
export const useTemplateStatsV2 = () => {
|
||||
const [stats, setStats] = useState<any>(null)
|
||||
const [popularTags, setPopularTags] = useState<any[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
const loadStats = useCallback(async (userId?: string) => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
const [statsResult, tagsResult] = await Promise.all([
|
||||
TemplateServiceV2.getTemplateStats(userId),
|
||||
TemplateServiceV2.getPopularTags(20, userId)
|
||||
])
|
||||
|
||||
setStats(statsResult)
|
||||
setPopularTags(tagsResult)
|
||||
} catch (err) {
|
||||
const error = err instanceof Error ? err : new Error(String(err))
|
||||
setError(error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const refresh = useCallback((userId?: string) => {
|
||||
loadStats(userId)
|
||||
}, [loadStats])
|
||||
|
||||
return {
|
||||
stats,
|
||||
popularTags,
|
||||
loading,
|
||||
error,
|
||||
loadStats,
|
||||
refresh
|
||||
}
|
||||
}
|
||||
564
src/pages/TemplateManagePageV2.tsx
Normal file
564
src/pages/TemplateManagePageV2.tsx
Normal file
@@ -0,0 +1,564 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { TemplateServiceV2, type TemplateInfo, type TemplateStats, type TagInfo } from '../services/TemplateServiceV2'
|
||||
import {
|
||||
Search,
|
||||
Grid,
|
||||
List,
|
||||
Upload,
|
||||
Filter,
|
||||
Tag,
|
||||
BarChart3,
|
||||
RefreshCw,
|
||||
Download,
|
||||
Eye,
|
||||
Trash2
|
||||
} from 'lucide-react'
|
||||
|
||||
const TemplateManagePageV2: React.FC = () => {
|
||||
const navigate = useNavigate()
|
||||
|
||||
// State
|
||||
const [templates, setTemplates] = useState<TemplateInfo[]>([])
|
||||
const [filteredTemplates, setFilteredTemplates] = useState<TemplateInfo[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [importing, setImporting] = useState(false)
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [selectedTag, setSelectedTag] = useState<string>('')
|
||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
|
||||
const [includeCloud, setIncludeCloud] = useState(true)
|
||||
const [showStats, setShowStats] = useState(false)
|
||||
|
||||
// Stats and tags
|
||||
const [stats, setStats] = useState<TemplateStats | null>(null)
|
||||
const [popularTags, setPopularTags] = useState<TagInfo[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
loadTemplates()
|
||||
loadStats()
|
||||
loadPopularTags()
|
||||
}, [includeCloud])
|
||||
|
||||
useEffect(() => {
|
||||
filterTemplates()
|
||||
}, [templates, searchTerm, selectedTag])
|
||||
|
||||
// 过滤模板
|
||||
const filterTemplates = () => {
|
||||
let filtered = templates
|
||||
|
||||
// 搜索过滤
|
||||
if (searchTerm) {
|
||||
filtered = filtered.filter(template =>
|
||||
template.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
template.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
template.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||
)
|
||||
}
|
||||
|
||||
// 标签过滤
|
||||
if (selectedTag) {
|
||||
filtered = filtered.filter(template =>
|
||||
template.tags.includes(selectedTag)
|
||||
)
|
||||
}
|
||||
|
||||
setFilteredTemplates(filtered)
|
||||
}
|
||||
|
||||
// 加载模板列表
|
||||
const loadTemplates = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const templateList = await TemplateServiceV2.getTemplates(includeCloud, 200)
|
||||
setTemplates(templateList)
|
||||
} catch (error) {
|
||||
console.error('Error loading templates:', error)
|
||||
setTemplates([])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载统计信息
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const templateStats = await TemplateServiceV2.getTemplateStats()
|
||||
setStats(templateStats)
|
||||
} catch (error) {
|
||||
console.error('Error loading stats:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载热门标签
|
||||
const loadPopularTags = async () => {
|
||||
try {
|
||||
const tags = await TemplateServiceV2.getPopularTags(15)
|
||||
setPopularTags(tags)
|
||||
} catch (error) {
|
||||
console.error('Error loading popular tags:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 批量导入
|
||||
const handleBatchImport = async () => {
|
||||
try {
|
||||
const folderResult = await invoke<string>('select_folder')
|
||||
if (!folderResult) return
|
||||
|
||||
setImporting(true)
|
||||
const result = await TemplateServiceV2.batchImportTemplates(folderResult)
|
||||
|
||||
if (result.imported_count > 0) {
|
||||
await loadTemplates() // 重新加载模板列表
|
||||
await loadStats() // 重新加载统计信息
|
||||
alert(`导入成功!成功导入 ${result.imported_count} 个模板,失败 ${result.failed_count} 个`)
|
||||
} else {
|
||||
alert('导入失败:' + result.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Import failed:', error)
|
||||
alert('导入失败: ' + (error instanceof Error ? error.message : 'Unknown error'))
|
||||
} finally {
|
||||
setImporting(false)
|
||||
}
|
||||
}
|
||||
|
||||
// 删除模板
|
||||
const handleDeleteTemplate = async (templateId: string) => {
|
||||
if (!confirm('确定要删除这个模板吗?此操作不可撤销。')) return
|
||||
|
||||
try {
|
||||
const success = await TemplateServiceV2.deleteTemplate(templateId)
|
||||
if (success) {
|
||||
setTemplates(templates.filter(t => t.id !== templateId))
|
||||
await loadStats() // 重新加载统计信息
|
||||
} else {
|
||||
alert('删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Delete failed:', error)
|
||||
alert('删除失败: ' + (error instanceof Error ? error.message : 'Unknown error'))
|
||||
}
|
||||
}
|
||||
|
||||
// 查看模板详情
|
||||
const viewTemplateDetail = (template: TemplateInfo) => {
|
||||
navigate(`/templates/${template.id}`)
|
||||
}
|
||||
|
||||
// 按标签搜索
|
||||
const handleTagClick = async (tag: string) => {
|
||||
if (selectedTag === tag) {
|
||||
setSelectedTag('')
|
||||
return
|
||||
}
|
||||
|
||||
setSelectedTag(tag)
|
||||
try {
|
||||
const templatesByTag = await TemplateServiceV2.getTemplatesByTag(tag, includeCloud, 100)
|
||||
setTemplates(templatesByTag)
|
||||
} catch (error) {
|
||||
console.error('Error loading templates by tag:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索模板
|
||||
const handleSearch = async () => {
|
||||
if (!searchTerm.trim()) {
|
||||
await loadTemplates()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
const searchResults = await TemplateServiceV2.searchTemplates(searchTerm, includeCloud, 100)
|
||||
setTemplates(searchResults)
|
||||
} catch (error) {
|
||||
console.error('Search failed:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900">模板管理 V2</h1>
|
||||
<p className="text-gray-600">管理和导入视频编辑模板 - 新版本</p>
|
||||
</div>
|
||||
<div className="flex items-center space-x-4">
|
||||
<button
|
||||
onClick={() => setShowStats(!showStats)}
|
||||
className="flex items-center space-x-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
<BarChart3 size={20} />
|
||||
<span>统计</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={loadTemplates}
|
||||
disabled={loading}
|
||||
className="flex items-center space-x-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw size={20} className={loading ? 'animate-spin' : ''} />
|
||||
<span>刷新</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Panel */}
|
||||
{showStats && stats && (
|
||||
<div className="bg-white rounded-lg shadow-sm p-6 mb-6">
|
||||
<h2 className="text-xl font-semibold mb-4">模板统计</h2>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-blue-600">{stats.total_templates}</div>
|
||||
<div className="text-sm text-gray-600">总模板数</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-green-600">{stats.user_templates}</div>
|
||||
<div className="text-sm text-gray-600">用户模板</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-purple-600">{stats.cloud_templates}</div>
|
||||
<div className="text-sm text-gray-600">云端模板</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-orange-600">{stats.total_materials}</div>
|
||||
<div className="text-sm text-gray-600">总素材数</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Search and Filters */}
|
||||
<div className="bg-white rounded-lg shadow-sm p-4 mb-6">
|
||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between space-y-4 lg:space-y-0">
|
||||
{/* Search */}
|
||||
<div className="flex items-center space-x-4">
|
||||
<div className="relative flex-1 lg:w-80">
|
||||
<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="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleSearch}
|
||||
disabled={loading}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50"
|
||||
>
|
||||
搜索
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* Cloud Toggle */}
|
||||
<label className="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={includeCloud}
|
||||
onChange={(e) => setIncludeCloud(e.target.checked)}
|
||||
className="rounded"
|
||||
/>
|
||||
<span className="text-sm">包含云端模板</span>
|
||||
</label>
|
||||
|
||||
{/* View Mode */}
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => setViewMode('grid')}
|
||||
className={`p-2 rounded ${viewMode === 'grid' ? 'bg-blue-100 text-blue-600' : 'text-gray-400'}`}
|
||||
>
|
||||
<Grid size={20} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('list')}
|
||||
className={`p-2 rounded ${viewMode === 'list' ? 'bg-blue-100 text-blue-600' : 'text-gray-400'}`}
|
||||
>
|
||||
<List size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Import Button */}
|
||||
<button
|
||||
onClick={handleBatchImport}
|
||||
disabled={importing}
|
||||
className="flex items-center space-x-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50"
|
||||
>
|
||||
<Upload size={20} />
|
||||
<span>{importing ? '导入中...' : '批量导入'}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Popular Tags */}
|
||||
{popularTags.length > 0 && (
|
||||
<div className="bg-white rounded-lg shadow-sm p-4 mb-6">
|
||||
<h3 className="text-lg font-medium mb-3">热门标签</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{popularTags.map((tagInfo) => (
|
||||
<button
|
||||
key={tagInfo.tag}
|
||||
onClick={() => handleTagClick(tagInfo.tag)}
|
||||
className={`flex items-center space-x-1 px-3 py-1 rounded-full text-sm ${
|
||||
selectedTag === tagInfo.tag
|
||||
? 'bg-blue-100 text-blue-700 border border-blue-300'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
<Tag size={14} />
|
||||
<span>{tagInfo.tag}</span>
|
||||
<span className="text-xs opacity-75">({tagInfo.count})</span>
|
||||
</button>
|
||||
))}
|
||||
{selectedTag && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedTag('')
|
||||
loadTemplates()
|
||||
}}
|
||||
className="px-3 py-1 bg-red-100 text-red-700 rounded-full text-sm hover:bg-red-200"
|
||||
>
|
||||
清除筛选
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Template Grid/List */}
|
||||
<div className="bg-white rounded-lg shadow-sm p-6">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<RefreshCw className="animate-spin mr-2" size={24} />
|
||||
<span>加载中...</span>
|
||||
</div>
|
||||
) : filteredTemplates.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="text-gray-400 mb-4">
|
||||
<Grid size={48} className="mx-auto" />
|
||||
</div>
|
||||
<h3 className="text-lg font-medium text-gray-900 mb-2">暂无模板</h3>
|
||||
<p className="text-gray-600">点击"批量导入"按钮开始导入模板</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-medium">
|
||||
模板列表 ({filteredTemplates.length})
|
||||
</h2>
|
||||
{selectedTag && (
|
||||
<div className="text-sm text-gray-600">
|
||||
筛选标签: <span className="font-medium">{selectedTag}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{viewMode === 'grid' ? (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
|
||||
{filteredTemplates.map((template) => (
|
||||
<TemplateCardV2
|
||||
key={template.id}
|
||||
template={template}
|
||||
onView={viewTemplateDetail}
|
||||
onDelete={handleDeleteTemplate}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{filteredTemplates.map((template) => (
|
||||
<TemplateListItemV2
|
||||
key={template.id}
|
||||
template={template}
|
||||
onView={viewTemplateDetail}
|
||||
onDelete={handleDeleteTemplate}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 模板卡片组件
|
||||
const TemplateCardV2: React.FC<{
|
||||
template: TemplateInfo
|
||||
onView: (template: TemplateInfo) => void
|
||||
onDelete: (templateId: string) => void
|
||||
}> = ({ template, onView, onDelete }) => {
|
||||
const thumbnailUrl = TemplateServiceV2.getThumbnailUrl(template)
|
||||
const isCloud = TemplateServiceV2.isCloudTemplate(template)
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition-shadow">
|
||||
{/* Thumbnail */}
|
||||
<div className="aspect-video bg-gray-100 flex items-center justify-center relative">
|
||||
{thumbnailUrl ? (
|
||||
<img
|
||||
src={thumbnailUrl}
|
||||
alt={template.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="text-gray-400">
|
||||
<Eye size={32} />
|
||||
</div>
|
||||
)}
|
||||
{isCloud && (
|
||||
<div className="absolute top-2 right-2 bg-blue-500 text-white px-2 py-1 rounded text-xs">
|
||||
云端
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="p-4">
|
||||
<h3 className="font-medium text-gray-900 mb-1 truncate">{template.name}</h3>
|
||||
<p className="text-sm text-gray-600 mb-3 line-clamp-2">{template.description}</p>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="flex items-center justify-between text-xs text-gray-500 mb-3">
|
||||
<span>{TemplateServiceV2.formatDuration(template.duration)}</span>
|
||||
<span>{template.material_count} 素材</span>
|
||||
<span>{template.track_count} 轨道</span>
|
||||
</div>
|
||||
|
||||
{/* Tags */}
|
||||
{template.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mb-3">
|
||||
{template.tags.slice(0, 3).map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-2 py-1 bg-gray-100 text-gray-600 rounded text-xs"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
{template.tags.length > 3 && (
|
||||
<span className="px-2 py-1 bg-gray-100 text-gray-600 rounded text-xs">
|
||||
+{template.tags.length - 3}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-gray-500">
|
||||
{TemplateServiceV2.formatDate(template.created_at)}
|
||||
</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => onView(template)}
|
||||
className="p-1 text-blue-600 hover:text-blue-700"
|
||||
>
|
||||
<Eye size={16} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onDelete(template.id)}
|
||||
className="p-1 text-red-600 hover:text-red-700"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// 模板列表项组件
|
||||
const TemplateListItemV2: React.FC<{
|
||||
template: TemplateInfo
|
||||
onView: (template: TemplateInfo) => void
|
||||
onDelete: (templateId: string) => void
|
||||
}> = ({ template, onView, onDelete }) => {
|
||||
const thumbnailUrl = TemplateServiceV2.getThumbnailUrl(template)
|
||||
const isCloud = TemplateServiceV2.isCloudTemplate(template)
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
{/* Thumbnail */}
|
||||
<div className="w-16 h-12 bg-gray-100 rounded flex items-center justify-center flex-shrink-0 relative">
|
||||
{thumbnailUrl ? (
|
||||
<img
|
||||
src={thumbnailUrl}
|
||||
alt={template.name}
|
||||
className="w-full h-full object-cover rounded"
|
||||
/>
|
||||
) : (
|
||||
<Eye size={16} className="text-gray-400" />
|
||||
)}
|
||||
{isCloud && (
|
||||
<div className="absolute -top-1 -right-1 bg-blue-500 text-white px-1 rounded text-xs">
|
||||
云
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="flex-1">
|
||||
<h3 className="font-medium text-gray-900">{template.name}</h3>
|
||||
<p className="text-sm text-gray-600 mb-1">{template.description}</p>
|
||||
{template.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{template.tags.slice(0, 5).map((tag) => (
|
||||
<span
|
||||
key={tag}
|
||||
className="px-2 py-0.5 bg-gray-100 text-gray-600 rounded text-xs"
|
||||
>
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-6 text-sm text-gray-500">
|
||||
<span>{TemplateServiceV2.formatDuration(template.duration)}</span>
|
||||
<span>{template.material_count} 素材</span>
|
||||
<span>{template.track_count} 轨道</span>
|
||||
<span>{TemplateServiceV2.formatDate(template.created_at)}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<button
|
||||
onClick={() => onView(template)}
|
||||
className="p-2 text-blue-600 hover:text-blue-700"
|
||||
>
|
||||
<Eye size={16} />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onDelete(template.id)}
|
||||
className="p-2 text-red-600 hover:text-red-700"
|
||||
>
|
||||
<Trash2 size={16} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TemplateManagePageV2
|
||||
379
src/services/TemplateServiceV2.ts
Normal file
379
src/services/TemplateServiceV2.ts
Normal file
@@ -0,0 +1,379 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
// 统一的响应格式
|
||||
export interface TemplateResponse {
|
||||
success: boolean
|
||||
message: string
|
||||
data?: any
|
||||
output?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
// 模板信息接口
|
||||
export interface TemplateInfo {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
thumbnail_path: string
|
||||
draft_content_path: string
|
||||
resources_path: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
canvas_config: any
|
||||
duration: number
|
||||
material_count: number
|
||||
track_count: number
|
||||
tags: string[]
|
||||
is_cloud: boolean
|
||||
user_id: string
|
||||
}
|
||||
|
||||
// 请求接口
|
||||
export interface BatchImportRequest {
|
||||
source_folder: string
|
||||
user_id?: string
|
||||
verbose?: boolean
|
||||
json_output?: boolean
|
||||
}
|
||||
|
||||
export interface TemplateListRequest {
|
||||
user_id?: string
|
||||
include_cloud?: boolean
|
||||
limit?: number
|
||||
verbose?: boolean
|
||||
json_output?: boolean
|
||||
}
|
||||
|
||||
export interface TemplateGetRequest {
|
||||
template_id: string
|
||||
user_id?: string
|
||||
verbose?: boolean
|
||||
json_output?: boolean
|
||||
}
|
||||
|
||||
export interface TemplateDeleteRequest {
|
||||
template_id: string
|
||||
user_id?: string
|
||||
verbose?: boolean
|
||||
json_output?: boolean
|
||||
}
|
||||
|
||||
export interface TemplateSearchRequest {
|
||||
query: string
|
||||
user_id?: string
|
||||
include_cloud?: boolean
|
||||
limit?: number
|
||||
verbose?: boolean
|
||||
json_output?: boolean
|
||||
}
|
||||
|
||||
// 批量导入结果
|
||||
export interface BatchImportResult {
|
||||
imported_count: number
|
||||
failed_count: number
|
||||
imported_templates: Array<{
|
||||
id: string
|
||||
name: string
|
||||
path: string
|
||||
}>
|
||||
failed_templates: Array<{
|
||||
name: string
|
||||
path: string
|
||||
error: string
|
||||
}>
|
||||
message: string
|
||||
}
|
||||
|
||||
// 统计信息
|
||||
export interface TemplateStats {
|
||||
total_templates: number
|
||||
user_templates: number
|
||||
cloud_templates: number
|
||||
total_materials: number
|
||||
total_tracks: number
|
||||
total_duration: number
|
||||
average_duration: number
|
||||
}
|
||||
|
||||
// 标签信息
|
||||
export interface TagInfo {
|
||||
tag: string
|
||||
count: number
|
||||
}
|
||||
|
||||
export class TemplateServiceV2 {
|
||||
private static currentUserId: string = 'default'
|
||||
|
||||
/**
|
||||
* 设置当前用户ID
|
||||
*/
|
||||
static setUserId(userId: string) {
|
||||
this.currentUserId = userId
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量导入模板 (新版本)
|
||||
*/
|
||||
static async batchImportTemplates(sourceFolder: string, userId?: string): Promise<BatchImportResult> {
|
||||
try {
|
||||
const request: BatchImportRequest = {
|
||||
source_folder: sourceFolder,
|
||||
user_id: userId || this.currentUserId,
|
||||
verbose: false,
|
||||
json_output: true
|
||||
}
|
||||
|
||||
const response = await invoke<TemplateResponse>('batch_import_templates_cli', { request })
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error(response.error || response.message || 'Import failed')
|
||||
}
|
||||
|
||||
return response.data as BatchImportResult
|
||||
} catch (error) {
|
||||
console.error('Batch import templates failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模板列表 (新版本)
|
||||
*/
|
||||
static async getTemplates(includeCloud: boolean = true, limit: number = 100, userId?: string): Promise<TemplateInfo[]> {
|
||||
try {
|
||||
const request: TemplateListRequest = {
|
||||
user_id: userId || this.currentUserId,
|
||||
include_cloud: includeCloud,
|
||||
limit: limit,
|
||||
verbose: false,
|
||||
json_output: true
|
||||
}
|
||||
|
||||
const response = await invoke<TemplateResponse>('get_templates_cli', { request })
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error(response.error || response.message || 'Failed to get templates')
|
||||
}
|
||||
|
||||
return response.data?.templates || []
|
||||
} catch (error) {
|
||||
console.error('Get templates failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个模板 (新版本)
|
||||
*/
|
||||
static async getTemplate(templateId: string, userId?: string): Promise<TemplateInfo> {
|
||||
try {
|
||||
const request: TemplateGetRequest = {
|
||||
template_id: templateId,
|
||||
user_id: userId || this.currentUserId,
|
||||
verbose: false,
|
||||
json_output: true
|
||||
}
|
||||
|
||||
const response = await invoke<TemplateResponse>('get_template_cli', { request })
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error(response.error || response.message || 'Failed to get template')
|
||||
}
|
||||
|
||||
return response.data as TemplateInfo
|
||||
} catch (error) {
|
||||
console.error('Get template failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除模板 (新版本)
|
||||
*/
|
||||
static async deleteTemplate(templateId: string, userId?: string): Promise<boolean> {
|
||||
try {
|
||||
const request: TemplateDeleteRequest = {
|
||||
template_id: templateId,
|
||||
user_id: userId || this.currentUserId,
|
||||
verbose: false,
|
||||
json_output: true
|
||||
}
|
||||
|
||||
const response = await invoke<TemplateResponse>('delete_template_cli', { request })
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error(response.error || response.message || 'Failed to delete template')
|
||||
}
|
||||
|
||||
return response.data?.deleted || false
|
||||
} catch (error) {
|
||||
console.error('Delete template failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索模板 (新功能)
|
||||
*/
|
||||
static async searchTemplates(
|
||||
query: string,
|
||||
includeCloud: boolean = true,
|
||||
limit: number = 50,
|
||||
userId?: string
|
||||
): Promise<TemplateInfo[]> {
|
||||
try {
|
||||
const request: TemplateSearchRequest = {
|
||||
query: query,
|
||||
user_id: userId || this.currentUserId,
|
||||
include_cloud: includeCloud,
|
||||
limit: limit,
|
||||
verbose: false,
|
||||
json_output: true
|
||||
}
|
||||
|
||||
const response = await invoke<TemplateResponse>('search_templates_cli', { request })
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error(response.error || response.message || 'Failed to search templates')
|
||||
}
|
||||
|
||||
return response.data?.templates || []
|
||||
} catch (error) {
|
||||
console.error('Search templates failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模板统计信息 (新功能)
|
||||
*/
|
||||
static async getTemplateStats(userId?: string): Promise<TemplateStats> {
|
||||
try {
|
||||
const response = await invoke<TemplateResponse>('get_template_stats_cli', {
|
||||
user_id: userId || this.currentUserId,
|
||||
verbose: false
|
||||
})
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error(response.error || response.message || 'Failed to get template stats')
|
||||
}
|
||||
|
||||
return response.data as TemplateStats
|
||||
} catch (error) {
|
||||
console.error('Get template stats failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取热门标签 (新功能)
|
||||
*/
|
||||
static async getPopularTags(limit: number = 20, userId?: string): Promise<TagInfo[]> {
|
||||
try {
|
||||
const response = await invoke<TemplateResponse>('get_popular_tags_cli', {
|
||||
user_id: userId || this.currentUserId,
|
||||
limit: limit,
|
||||
verbose: false
|
||||
})
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error(response.error || response.message || 'Failed to get popular tags')
|
||||
}
|
||||
|
||||
return response.data?.tags || []
|
||||
} catch (error) {
|
||||
console.error('Get popular tags failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据标签获取模板 (新功能)
|
||||
*/
|
||||
static async getTemplatesByTag(
|
||||
tag: string,
|
||||
includeCloud: boolean = true,
|
||||
limit: number = 50,
|
||||
userId?: string
|
||||
): Promise<TemplateInfo[]> {
|
||||
try {
|
||||
const response = await invoke<TemplateResponse>('get_templates_by_tag_cli', {
|
||||
tag: tag,
|
||||
user_id: userId || this.currentUserId,
|
||||
include_cloud: includeCloud,
|
||||
limit: limit,
|
||||
verbose: false
|
||||
})
|
||||
|
||||
if (!response.success) {
|
||||
throw new Error(response.error || response.message || 'Failed to get templates by tag')
|
||||
}
|
||||
|
||||
return response.data?.templates || []
|
||||
} catch (error) {
|
||||
console.error('Get templates by tag failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证模板文件夹结构
|
||||
*/
|
||||
static validateTemplateFolder(folderPath: string): boolean {
|
||||
// 这个方法保持不变,依赖后端验证
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化持续时间
|
||||
*/
|
||||
static formatDuration(seconds: number): string {
|
||||
const minutes = Math.floor(seconds / 60)
|
||||
const remainingSeconds = seconds % 60
|
||||
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
*/
|
||||
static formatDate(dateString: string): string {
|
||||
try {
|
||||
const date = new Date(dateString)
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
} catch {
|
||||
return dateString
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模板缩略图URL
|
||||
*/
|
||||
static getThumbnailUrl(template: TemplateInfo): string {
|
||||
if (template.thumbnail_path) {
|
||||
// 如果是相对路径,转换为绝对路径
|
||||
if (!template.thumbnail_path.startsWith('http') && !template.thumbnail_path.startsWith('/')) {
|
||||
return `file://${template.resources_path}/${template.thumbnail_path}`
|
||||
}
|
||||
return template.thumbnail_path
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查模板是否为云端模板
|
||||
*/
|
||||
static isCloudTemplate(template: TemplateInfo): boolean {
|
||||
return template.is_cloud || false
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查模板是否属于当前用户
|
||||
*/
|
||||
static isUserTemplate(template: TemplateInfo, userId?: string): boolean {
|
||||
return template.user_id === (userId || this.currentUserId)
|
||||
}
|
||||
}
|
||||
@@ -1,277 +0,0 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
export interface TemplateInfo {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
thumbnail_path: string
|
||||
draft_content_path: string
|
||||
resources_path: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
canvas_config: any
|
||||
duration: number
|
||||
material_count: number
|
||||
track_count: number
|
||||
tags: string[]
|
||||
}
|
||||
|
||||
export interface BatchImportRequest {
|
||||
source_folder: string
|
||||
}
|
||||
|
||||
export interface BatchImportResult {
|
||||
status: boolean
|
||||
msg: string
|
||||
imported_count: number
|
||||
failed_count: number
|
||||
imported_templates: TemplateInfo[]
|
||||
failed_templates: Array<{
|
||||
name: string
|
||||
error: string
|
||||
}>
|
||||
}
|
||||
|
||||
export interface TemplateResult {
|
||||
status: boolean
|
||||
msg?: string
|
||||
template?: TemplateInfo
|
||||
templates?: TemplateInfo[]
|
||||
}
|
||||
|
||||
export class TemplateService {
|
||||
/**
|
||||
* Batch import templates from a folder
|
||||
*/
|
||||
static async batchImportTemplates(sourceFolder: string): Promise<BatchImportResult> {
|
||||
try {
|
||||
const request: BatchImportRequest = {
|
||||
source_folder: sourceFolder
|
||||
}
|
||||
|
||||
const response = await invoke<string>('batch_import_templates', { request })
|
||||
const result = JSON.parse(response)
|
||||
|
||||
if (!result.status) {
|
||||
throw new Error(result.error || result.msg || 'Import failed')
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Batch import templates failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all templates
|
||||
*/
|
||||
static async getTemplates(): Promise<TemplateResult> {
|
||||
try {
|
||||
const response = await invoke<string>('get_templates')
|
||||
const result = JSON.parse(response)
|
||||
|
||||
if (!result.status) {
|
||||
throw new Error(result.error || result.msg || 'Failed to get templates')
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Get templates failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific template
|
||||
*/
|
||||
static async getTemplate(templateId: string): Promise<TemplateResult> {
|
||||
try {
|
||||
const response = await invoke<string>('get_template', { templateId })
|
||||
const result = JSON.parse(response)
|
||||
|
||||
if (!result.status) {
|
||||
throw new Error(result.error || result.msg || 'Failed to get template')
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Get template failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a template
|
||||
*/
|
||||
static async deleteTemplate(templateId: string): Promise<TemplateResult> {
|
||||
try {
|
||||
const response = await invoke<string>('delete_template', { templateId })
|
||||
const result = JSON.parse(response)
|
||||
|
||||
if (!result.status) {
|
||||
throw new Error(result.error || result.msg || 'Failed to delete template')
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Delete template failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate template folder structure
|
||||
*/
|
||||
static validateTemplateFolder(folderPath: string): boolean {
|
||||
// This would be implemented to check if the folder contains valid template structure
|
||||
// For now, we'll rely on the backend validation
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate template thumbnail
|
||||
*/
|
||||
static async generateThumbnail(templateId: string): Promise<string> {
|
||||
// This would be implemented to generate a thumbnail from the template
|
||||
// For now, return empty string
|
||||
return ''
|
||||
}
|
||||
|
||||
/**
|
||||
* Export template
|
||||
*/
|
||||
static async exportTemplate(templateId: string, targetPath: string): Promise<boolean> {
|
||||
try {
|
||||
// This would be implemented to export a template to a specific location
|
||||
// For now, just return true
|
||||
return true
|
||||
} catch (error) {
|
||||
console.error('Export template failed:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Duplicate template
|
||||
*/
|
||||
static async duplicateTemplate(templateId: string, newName: string): Promise<TemplateResult> {
|
||||
try {
|
||||
// This would be implemented to create a copy of an existing template
|
||||
// For now, just return a placeholder result
|
||||
return {
|
||||
status: false,
|
||||
msg: 'Duplicate functionality not implemented yet'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Duplicate template failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update template metadata
|
||||
*/
|
||||
static async updateTemplate(templateId: string, updates: Partial<TemplateInfo>): Promise<TemplateResult> {
|
||||
try {
|
||||
// This would be implemented to update template metadata
|
||||
// For now, just return a placeholder result
|
||||
return {
|
||||
status: false,
|
||||
msg: 'Update functionality not implemented yet'
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Update template failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search templates
|
||||
*/
|
||||
static async searchTemplates(query: string, filters?: {
|
||||
tags?: string[]
|
||||
duration_min?: number
|
||||
duration_max?: number
|
||||
material_count_min?: number
|
||||
material_count_max?: number
|
||||
}): Promise<TemplateResult> {
|
||||
try {
|
||||
// This would be implemented to search templates with filters
|
||||
// For now, just get all templates and filter on frontend
|
||||
const result = await this.getTemplates()
|
||||
|
||||
if (result.status && result.templates) {
|
||||
const filteredTemplates = result.templates.filter(template =>
|
||||
template.name.toLowerCase().includes(query.toLowerCase()) ||
|
||||
template.description.toLowerCase().includes(query.toLowerCase())
|
||||
)
|
||||
|
||||
return {
|
||||
status: true,
|
||||
templates: filteredTemplates
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error('Search templates failed:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get template statistics
|
||||
*/
|
||||
static async getTemplateStats(): Promise<{
|
||||
total_templates: number
|
||||
total_materials: number
|
||||
total_size: number
|
||||
most_used_tags: string[]
|
||||
}> {
|
||||
try {
|
||||
const result = await this.getTemplates()
|
||||
|
||||
if (result.status && result.templates) {
|
||||
const templates = result.templates
|
||||
const totalTemplates = templates.length
|
||||
const totalMaterials = templates.reduce((sum, t) => sum + t.material_count, 0)
|
||||
|
||||
// Extract all tags and count frequency
|
||||
const tagCounts: { [key: string]: number } = {}
|
||||
templates.forEach(template => {
|
||||
template.tags.forEach(tag => {
|
||||
tagCounts[tag] = (tagCounts[tag] || 0) + 1
|
||||
})
|
||||
})
|
||||
|
||||
const mostUsedTags = Object.entries(tagCounts)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.slice(0, 10)
|
||||
.map(([tag]) => tag)
|
||||
|
||||
return {
|
||||
total_templates: totalTemplates,
|
||||
total_materials: totalMaterials,
|
||||
total_size: 0, // Would need to calculate from file sizes
|
||||
most_used_tags: mostUsedTags
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
total_templates: 0,
|
||||
total_materials: 0,
|
||||
total_size: 0,
|
||||
most_used_tags: []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Get template stats failed:', error)
|
||||
return {
|
||||
total_templates: 0,
|
||||
total_materials: 0,
|
||||
total_size: 0,
|
||||
most_used_tags: []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user