This commit is contained in:
root
2025-07-12 21:05:36 +08:00
parent cda87edd6a
commit 97ac04a74a
9 changed files with 1241 additions and 974 deletions

View File

@@ -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环境确保用户体验的一致性和简便性

View File

@@ -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应用中
记住:先让简单版本工作,再逐步增加复杂性。

View File

@@ -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就绪存储灵活*

View File

@@ -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

View File

@@ -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'

View 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
}
}

View 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

View 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)
}
}

View File

@@ -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: []
}
}
}
}