fix: add template track
This commit is contained in:
349
docs/progress_system_usage.md
Normal file
349
docs/progress_system_usage.md
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
# 🚀 通用进度系统使用指南
|
||||||
|
|
||||||
|
一个完全重构的通用进度监听系统,从特定的模板导入功能抽离出来,现在可以用于任何需要进度监控的操作。
|
||||||
|
|
||||||
|
## 🎯 系统架构
|
||||||
|
|
||||||
|
```
|
||||||
|
Frontend (React)
|
||||||
|
├── useProgressCommand Hook # React Hook for progress management
|
||||||
|
├── ProgressListener Class # Generic progress listener utility
|
||||||
|
└── Service Classes # Specialized service classes
|
||||||
|
|
||||||
|
Backend (Rust)
|
||||||
|
├── execute_python_action_with_progress # Generic Python executor
|
||||||
|
├── PythonCommandBuilder # Command construction helper
|
||||||
|
└── Progress Callbacks # Flexible callback system
|
||||||
|
|
||||||
|
Python
|
||||||
|
└── JSON-RPC Protocol # Standardized progress reporting
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 使用方式
|
||||||
|
|
||||||
|
### 1. 使用React Hook(推荐)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useTemplateProgress } from '../hooks/useProgressCommand'
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const {
|
||||||
|
isExecuting,
|
||||||
|
progress,
|
||||||
|
result,
|
||||||
|
error,
|
||||||
|
logs,
|
||||||
|
batchImport
|
||||||
|
} = useTemplateProgress({
|
||||||
|
onSuccess: (result) => {
|
||||||
|
console.log('Import completed:', result)
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
console.error('Import failed:', error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleImport = async () => {
|
||||||
|
try {
|
||||||
|
await batchImport('/path/to/templates')
|
||||||
|
} catch (error) {
|
||||||
|
// Error is already handled by the hook
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{isExecuting && (
|
||||||
|
<div>
|
||||||
|
<div>Progress: {progress?.progress}%</div>
|
||||||
|
<div>Message: {progress?.message}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button onClick={handleImport} disabled={isExecuting}>
|
||||||
|
{isExecuting ? 'Importing...' : 'Import Templates'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* Real-time logs */}
|
||||||
|
<div>
|
||||||
|
{logs.map((log, index) => (
|
||||||
|
<div key={index}>{log}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 使用通用Hook
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useProgressCommand } from '../hooks/useProgressCommand'
|
||||||
|
|
||||||
|
function DataProcessingComponent() {
|
||||||
|
const { execute, isExecuting, progress, logs } = useProgressCommand()
|
||||||
|
|
||||||
|
const processData = async () => {
|
||||||
|
await execute(
|
||||||
|
'process_data_with_progress',
|
||||||
|
{
|
||||||
|
request: {
|
||||||
|
input_file: '/input.csv',
|
||||||
|
output_file: '/output.json',
|
||||||
|
processing_type: 'transform'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'data-processing-progress'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button onClick={processData} disabled={isExecuting}>
|
||||||
|
Process Data
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{progress && (
|
||||||
|
<div>
|
||||||
|
<div>Step: {progress.step}</div>
|
||||||
|
<div>Progress: {progress.progress}%</div>
|
||||||
|
<div>Message: {progress.message}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 使用Service类
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { AIVideoProgressService, DataProcessingService } from '../services/tauri'
|
||||||
|
|
||||||
|
// AI Video Generation
|
||||||
|
const generateVideo = async () => {
|
||||||
|
const onProgress = (progress) => {
|
||||||
|
console.log(`${progress.step}: ${progress.message}`)
|
||||||
|
updateProgressBar(progress.progress)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await AIVideoProgressService.generateVideoWithProgress(
|
||||||
|
'/path/to/image.jpg',
|
||||||
|
'A beautiful sunset',
|
||||||
|
'/path/to/output.mp4',
|
||||||
|
onProgress
|
||||||
|
)
|
||||||
|
console.log('Video generated:', result)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Generation failed:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data Processing
|
||||||
|
const processFiles = async () => {
|
||||||
|
const result = await DataProcessingService.batchConvertFilesWithProgress(
|
||||||
|
['/file1.txt', '/file2.txt'],
|
||||||
|
'pdf',
|
||||||
|
(progress) => console.log(progress.message)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 使用底层ProgressListener
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { ProgressListener } from '../services/tauri'
|
||||||
|
|
||||||
|
const customOperation = async () => {
|
||||||
|
const listener = new ProgressListener()
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await listener.execute(
|
||||||
|
'my_custom_command',
|
||||||
|
{ param1: 'value1' },
|
||||||
|
'my-progress-event',
|
||||||
|
(progress) => {
|
||||||
|
// Custom progress handling
|
||||||
|
console.log(progress)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
listener.cleanup()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔧 添加新的进度命令
|
||||||
|
|
||||||
|
### 1. Rust端(后端)
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn my_new_command_with_progress(
|
||||||
|
app: AppHandle,
|
||||||
|
my_param: String
|
||||||
|
) -> Result<String, String> {
|
||||||
|
let params = &[("--my_param", my_param.as_str())];
|
||||||
|
|
||||||
|
execute_python_action_with_progress(
|
||||||
|
app,
|
||||||
|
"python_core.my_module",
|
||||||
|
"my_action",
|
||||||
|
params,
|
||||||
|
"my-command-progress",
|
||||||
|
None,
|
||||||
|
).await
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Python端
|
||||||
|
|
||||||
|
```python
|
||||||
|
from python_core.utils.jsonrpc import create_response_handler, create_progress_reporter
|
||||||
|
|
||||||
|
def my_action():
|
||||||
|
rpc = create_response_handler("my_action")
|
||||||
|
progress = create_progress_reporter()
|
||||||
|
|
||||||
|
try:
|
||||||
|
progress.step("start", "开始处理...")
|
||||||
|
# ... processing ...
|
||||||
|
|
||||||
|
progress.report("process", 50.0, "处理中...", {"detail": "info"})
|
||||||
|
# ... more processing ...
|
||||||
|
|
||||||
|
progress.complete("处理完成!")
|
||||||
|
|
||||||
|
result = {"status": True, "data": "result"}
|
||||||
|
rpc.success(result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
progress.error(f"处理失败: {str(e)}")
|
||||||
|
rpc.error(-32603, "处理失败", str(e))
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 前端Service
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
export class MyService {
|
||||||
|
static async myOperationWithProgress(
|
||||||
|
param: string,
|
||||||
|
onProgress?: (progress: any) => void
|
||||||
|
): Promise<any> {
|
||||||
|
return executeCommandWithProgress(
|
||||||
|
'my_new_command_with_progress',
|
||||||
|
{ myParam: param },
|
||||||
|
'my-command-progress',
|
||||||
|
onProgress
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. 专用Hook
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
export function useMyOperationProgress(options = {}) {
|
||||||
|
const hook = useProgressCommand(options)
|
||||||
|
|
||||||
|
const executeOperation = useCallback(async (param: string) => {
|
||||||
|
return hook.execute(
|
||||||
|
'my_new_command_with_progress',
|
||||||
|
{ myParam: param },
|
||||||
|
'my-command-progress'
|
||||||
|
)
|
||||||
|
}, [hook])
|
||||||
|
|
||||||
|
return {
|
||||||
|
...hook,
|
||||||
|
executeOperation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 UI组件示例
|
||||||
|
|
||||||
|
### 进度模态框
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
function ProgressModal({ isOpen, onClose, progress, logs, isExecuting }) {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onClose={!isExecuting ? onClose : undefined}>
|
||||||
|
<div className="p-6">
|
||||||
|
<h2 className="text-xl font-bold mb-4">操作进度</h2>
|
||||||
|
|
||||||
|
{/* Progress Bar */}
|
||||||
|
{progress && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<div className="flex justify-between mb-2">
|
||||||
|
<span>{progress.step}</span>
|
||||||
|
<span>{progress.progress >= 0 ? `${progress.progress}%` : '处理中...'}</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className="bg-blue-600 h-2 rounded-full transition-all"
|
||||||
|
style={{ width: `${Math.max(0, progress.progress)}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-gray-600 mt-2">{progress.message}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Logs */}
|
||||||
|
<div className="bg-gray-50 rounded p-4 h-64 overflow-y-auto">
|
||||||
|
{logs.map((log, index) => (
|
||||||
|
<div key={index} className="text-xs text-gray-600 font-mono">
|
||||||
|
{log}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Actions */}
|
||||||
|
<div className="flex justify-end mt-4">
|
||||||
|
{!isExecuting && (
|
||||||
|
<button onClick={onClose} className="px-4 py-2 bg-gray-200 rounded">
|
||||||
|
关闭
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔄 迁移指南
|
||||||
|
|
||||||
|
### 从旧的模板导入方式迁移
|
||||||
|
|
||||||
|
**旧方式**:
|
||||||
|
```tsx
|
||||||
|
const [importing, setImporting] = useState(false)
|
||||||
|
const [progress, setProgress] = useState(null)
|
||||||
|
const [logs, setLogs] = useState([])
|
||||||
|
|
||||||
|
const handleImport = async () => {
|
||||||
|
setImporting(true)
|
||||||
|
// ... manual progress handling
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**新方式**:
|
||||||
|
```tsx
|
||||||
|
const { isExecuting, progress, logs, batchImport } = useTemplateProgress()
|
||||||
|
|
||||||
|
const handleImport = async () => {
|
||||||
|
await batchImport(folderPath)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎯 最佳实践
|
||||||
|
|
||||||
|
1. **优先使用Hook**:React Hook提供最佳的状态管理和生命周期处理
|
||||||
|
2. **合理的进度粒度**:不要过于频繁地报告进度,影响性能
|
||||||
|
3. **错误处理**:始终处理可能的错误情况
|
||||||
|
4. **用户反馈**:提供清晰的进度信息和状态反馈
|
||||||
|
5. **资源清理**:确保在组件卸载时清理监听器
|
||||||
|
|
||||||
|
这个通用进度系统现在可以轻松扩展到任何需要进度监控的操作!🚀
|
||||||
@@ -102,6 +102,20 @@ pub async fn delete_template(app: tauri::AppHandle, template_id: String) -> Resu
|
|||||||
execute_python_command(app, &args, None).await
|
execute_python_command(app, &args, None).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_template_detail(app: tauri::AppHandle, template_id: String) -> Result<String, String> {
|
||||||
|
let args = vec![
|
||||||
|
"-m".to_string(),
|
||||||
|
"python_core.services.template_manager".to_string(),
|
||||||
|
"--action".to_string(),
|
||||||
|
"get_template_detail".to_string(),
|
||||||
|
"--template_id".to_string(),
|
||||||
|
template_id,
|
||||||
|
];
|
||||||
|
|
||||||
|
execute_python_command(app, &args, None).await
|
||||||
|
}
|
||||||
|
|
||||||
// Example: AI Video generation with progress
|
// Example: AI Video generation with progress
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ pub fn run() {
|
|||||||
commands::batch_import_templates_with_progress,
|
commands::batch_import_templates_with_progress,
|
||||||
commands::get_templates,
|
commands::get_templates,
|
||||||
commands::get_template,
|
commands::get_template,
|
||||||
|
commands::get_template_detail,
|
||||||
commands::delete_template
|
commands::delete_template
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
|
|||||||
@@ -20,6 +20,39 @@ interface TemplateInfo {
|
|||||||
tags: string[]
|
tags: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 轨道和片段的数据结构
|
||||||
|
interface TrackSegment {
|
||||||
|
id: string
|
||||||
|
type: 'video' | 'audio' | 'image' | 'text' | 'effect'
|
||||||
|
name: string
|
||||||
|
start_time: number
|
||||||
|
end_time: number
|
||||||
|
duration: number
|
||||||
|
resource_path?: string
|
||||||
|
properties?: any
|
||||||
|
effects?: any[]
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Track {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
type: 'video' | 'audio' | 'subtitle'
|
||||||
|
index: number
|
||||||
|
segments: TrackSegment[]
|
||||||
|
properties?: any
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TemplateDetail {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
canvas_config: any
|
||||||
|
tracks: Track[]
|
||||||
|
duration: number
|
||||||
|
fps: number
|
||||||
|
sample_rate?: number
|
||||||
|
}
|
||||||
|
|
||||||
// Import the progress interface from the hook
|
// Import the progress interface from the hook
|
||||||
import type { ProgressState } from '../hooks/useProgressCommand'
|
import type { ProgressState } from '../hooks/useProgressCommand'
|
||||||
|
|
||||||
@@ -41,6 +74,8 @@ const TemplateManagePage: React.FC = () => {
|
|||||||
const [searchTerm, setSearchTerm] = useState('')
|
const [searchTerm, setSearchTerm] = useState('')
|
||||||
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
|
const [viewMode, setViewMode] = useState<'grid' | 'list'>('grid')
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState<TemplateInfo | null>(null)
|
const [selectedTemplate, setSelectedTemplate] = useState<TemplateInfo | null>(null)
|
||||||
|
const [templateDetail, setTemplateDetail] = useState<TemplateDetail | null>(null)
|
||||||
|
const [loadingDetail, setLoadingDetail] = useState(false)
|
||||||
const [showImportModal, setShowImportModal] = useState(false)
|
const [showImportModal, setShowImportModal] = useState(false)
|
||||||
|
|
||||||
// Use the progress hook for template operations
|
// Use the progress hook for template operations
|
||||||
@@ -82,6 +117,24 @@ const TemplateManagePage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 加载模板详情(包含轨道和片段信息)
|
||||||
|
const loadTemplateDetail = async (template: TemplateInfo) => {
|
||||||
|
try {
|
||||||
|
setLoadingDetail(true)
|
||||||
|
setSelectedTemplate(template)
|
||||||
|
|
||||||
|
// 调用后端API获取模板详情
|
||||||
|
const detail = await TemplateService.getTemplateDetail(template.id)
|
||||||
|
setTemplateDetail(detail)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load template detail:', error)
|
||||||
|
// 如果加载详情失败,至少显示基本信息
|
||||||
|
setTemplateDetail(null)
|
||||||
|
} finally {
|
||||||
|
setLoadingDetail(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleBatchImport = async () => {
|
const handleBatchImport = async () => {
|
||||||
try {
|
try {
|
||||||
// Select folder using Tauri dialog
|
// Select folder using Tauri dialog
|
||||||
@@ -127,6 +180,13 @@ const TemplateManagePage: React.FC = () => {
|
|||||||
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`
|
return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to format time (for segments, in seconds)
|
||||||
|
const formatTime = (seconds: number): string => {
|
||||||
|
const minutes = Math.floor(seconds / 60)
|
||||||
|
const secs = (seconds % 60).toFixed(2)
|
||||||
|
return `${minutes}:${secs.padStart(5, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
const formatDate = (dateString: string) => {
|
const formatDate = (dateString: string) => {
|
||||||
return new Date(dateString).toLocaleDateString('zh-CN')
|
return new Date(dateString).toLocaleDateString('zh-CN')
|
||||||
}
|
}
|
||||||
@@ -300,7 +360,7 @@ const TemplateManagePage: React.FC = () => {
|
|||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
<div className="flex items-center justify-between mt-3 pt-3 border-t border-gray-100">
|
<div className="flex items-center justify-between mt-3 pt-3 border-t border-gray-100">
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedTemplate(template)}
|
onClick={() => loadTemplateDetail(template)}
|
||||||
className="flex items-center gap-1 text-blue-600 hover:text-blue-700 text-sm"
|
className="flex items-center gap-1 text-blue-600 hover:text-blue-700 text-sm"
|
||||||
>
|
>
|
||||||
<Eye size={14} />
|
<Eye size={14} />
|
||||||
@@ -333,7 +393,7 @@ const TemplateManagePage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedTemplate(template)}
|
onClick={() => loadTemplateDetail(template)}
|
||||||
className="p-2 text-blue-600 hover:text-blue-700"
|
className="p-2 text-blue-600 hover:text-blue-700"
|
||||||
>
|
>
|
||||||
<Eye size={16} />
|
<Eye size={16} />
|
||||||
@@ -527,6 +587,95 @@ const TemplateManagePage: React.FC = () => {
|
|||||||
<p className="text-gray-900">{new Date(selectedTemplate.updated_at).toLocaleString('zh-CN')}</p>
|
<p className="text-gray-900">{new Date(selectedTemplate.updated_at).toLocaleString('zh-CN')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 轨道和片段信息 */}
|
||||||
|
<div className="border-t border-gray-200 pt-4">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-3">轨道和片段信息</label>
|
||||||
|
|
||||||
|
{loadingDetail ? (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||||
|
<span className="ml-2 text-gray-600">加载详情中...</span>
|
||||||
|
</div>
|
||||||
|
) : templateDetail ? (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 画布信息 */}
|
||||||
|
<div className="bg-gray-50 rounded-lg p-3">
|
||||||
|
<h4 className="font-medium text-gray-900 mb-2">画布配置</h4>
|
||||||
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||||
|
<div>尺寸: {templateDetail.canvas_config?.width || 0} × {templateDetail.canvas_config?.height || 0}</div>
|
||||||
|
<div>帧率: {templateDetail.fps || 30} FPS</div>
|
||||||
|
<div>时长: {formatDuration(templateDetail.duration)}</div>
|
||||||
|
{templateDetail.sample_rate && (
|
||||||
|
<div>采样率: {templateDetail.sample_rate} Hz</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 轨道列表 */}
|
||||||
|
<div className="space-y-3">
|
||||||
|
{templateDetail.tracks.map((track, trackIndex) => (
|
||||||
|
<div key={track.id} className="border border-gray-200 rounded-lg p-3">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h4 className="font-medium text-gray-900">
|
||||||
|
轨道 {track.index + 1}: {track.name}
|
||||||
|
</h4>
|
||||||
|
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||||
|
track.type === 'video' ? 'bg-blue-100 text-blue-800' :
|
||||||
|
track.type === 'audio' ? 'bg-green-100 text-green-800' :
|
||||||
|
'bg-purple-100 text-purple-800'
|
||||||
|
}`}>
|
||||||
|
{track.type === 'video' ? '视频' : track.type === 'audio' ? '音频' : '字幕'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 片段列表 */}
|
||||||
|
<div className="space-y-2">
|
||||||
|
{track.segments.length > 0 ? (
|
||||||
|
track.segments.map((segment, segmentIndex) => (
|
||||||
|
<div key={segment.id} className="bg-gray-50 rounded p-2 text-sm">
|
||||||
|
<div className="flex items-center justify-between mb-1">
|
||||||
|
<span className="font-medium">{segment.name}</span>
|
||||||
|
<span className={`px-1.5 py-0.5 rounded text-xs ${
|
||||||
|
segment.type === 'video' ? 'bg-blue-100 text-blue-700' :
|
||||||
|
segment.type === 'audio' ? 'bg-green-100 text-green-700' :
|
||||||
|
segment.type === 'image' ? 'bg-yellow-100 text-yellow-700' :
|
||||||
|
segment.type === 'text' ? 'bg-purple-100 text-purple-700' :
|
||||||
|
'bg-gray-100 text-gray-700'
|
||||||
|
}`}>
|
||||||
|
{segment.type === 'video' ? '视频' :
|
||||||
|
segment.type === 'audio' ? '音频' :
|
||||||
|
segment.type === 'image' ? '图片' :
|
||||||
|
segment.type === 'text' ? '文本' : '特效'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-2 text-xs text-gray-600">
|
||||||
|
<div>开始: {formatTime(segment.start_time)}</div>
|
||||||
|
<div>结束: {formatTime(segment.end_time)}</div>
|
||||||
|
<div>时长: {formatTime(segment.duration)}</div>
|
||||||
|
</div>
|
||||||
|
{segment.resource_path && (
|
||||||
|
<div className="text-xs text-gray-500 mt-1 truncate">
|
||||||
|
资源: {segment.resource_path}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
<div className="text-sm text-gray-500 italic">该轨道暂无片段</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
<p>无法加载模板详情</p>
|
||||||
|
<p className="text-sm">请检查模板文件是否完整</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-end space-x-3 pt-4 border-t border-gray-200">
|
<div className="flex justify-end space-x-3 pt-4 border-t border-gray-200">
|
||||||
<button
|
<button
|
||||||
onClick={() => setSelectedTemplate(null)}
|
onClick={() => setSelectedTemplate(null)}
|
||||||
|
|||||||
@@ -679,6 +679,19 @@ export class TemplateService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get template detail with tracks and segments
|
||||||
|
*/
|
||||||
|
static async getTemplateDetail(templateId: string): Promise<any> {
|
||||||
|
try {
|
||||||
|
const result = await invoke('get_template_detail', { templateId })
|
||||||
|
return JSON.parse(result as string)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to get template detail:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a specific template
|
* Get a specific template
|
||||||
*/
|
*/
|
||||||
|
|||||||
Reference in New Issue
Block a user