fix: 完善前端组件和类型定义
- 添加素材导入对话框组件 - 实现项目详情页面完整功能 - 添加素材状态管理store - 完善TypeScript类型定义 - 更新项目列表路由导航
This commit is contained in:
@@ -1,6 +1,8 @@
|
|||||||
|
|
||||||
|
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
|
||||||
import { ProjectList } from './components/ProjectList';
|
import { ProjectList } from './components/ProjectList';
|
||||||
import { ProjectForm } from './components/ProjectForm';
|
import { ProjectForm } from './components/ProjectForm';
|
||||||
|
import { ProjectDetails } from './pages/ProjectDetails';
|
||||||
import { useProjectStore } from './store/projectStore';
|
import { useProjectStore } from './store/projectStore';
|
||||||
import { useUIStore } from './store/uiStore';
|
import { useUIStore } from './store/uiStore';
|
||||||
import { CreateProjectRequest, UpdateProjectRequest } from './types/project';
|
import { CreateProjectRequest, UpdateProjectRequest } from './types/project';
|
||||||
@@ -45,9 +47,13 @@ function App() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<Router>
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50">
|
||||||
<main className="container mx-auto px-6 py-8 max-w-7xl">
|
<main className="container mx-auto px-6 py-8 max-w-7xl">
|
||||||
<ProjectList />
|
<Routes>
|
||||||
|
<Route path="/" element={<ProjectList />} />
|
||||||
|
<Route path="/project/:id" element={<ProjectDetails />} />
|
||||||
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* 创建项目模态框 */}
|
{/* 创建项目模态框 */}
|
||||||
@@ -68,6 +74,7 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</Router>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
315
apps/desktop/src/components/MaterialImportDialog.tsx
Normal file
315
apps/desktop/src/components/MaterialImportDialog.tsx
Normal file
@@ -0,0 +1,315 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { X, Upload, FileText, AlertCircle, CheckCircle, Loader2 } from 'lucide-react';
|
||||||
|
import { useMaterialStore } from '../store/materialStore';
|
||||||
|
import { CreateMaterialRequest, MaterialImportResult } from '../types/material';
|
||||||
|
|
||||||
|
interface MaterialImportDialogProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
projectId: string;
|
||||||
|
onClose: () => void;
|
||||||
|
onImportComplete: (result: MaterialImportResult) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 素材导入对话框组件
|
||||||
|
* 遵循 Tauri 开发规范的组件设计模式
|
||||||
|
*/
|
||||||
|
export const MaterialImportDialog: React.FC<MaterialImportDialogProps> = ({
|
||||||
|
isOpen,
|
||||||
|
projectId,
|
||||||
|
onClose,
|
||||||
|
onImportComplete
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
isImporting,
|
||||||
|
importProgress,
|
||||||
|
error,
|
||||||
|
importMaterials,
|
||||||
|
selectMaterialFiles,
|
||||||
|
validateMaterialFiles,
|
||||||
|
checkFFmpegAvailable,
|
||||||
|
clearError
|
||||||
|
} = useMaterialStore();
|
||||||
|
|
||||||
|
const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
|
||||||
|
const [autoProcess, setAutoProcess] = useState(true);
|
||||||
|
const [maxSegmentDuration, setMaxSegmentDuration] = useState(300); // 5分钟
|
||||||
|
const [ffmpegAvailable, setFFmpegAvailable] = useState(false);
|
||||||
|
const [step, setStep] = useState<'select' | 'configure' | 'importing' | 'complete'>('select');
|
||||||
|
|
||||||
|
// 检查 FFmpeg 可用性
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
checkFFmpegAvailable().then(setFFmpegAvailable);
|
||||||
|
}
|
||||||
|
}, [isOpen, checkFFmpegAvailable]);
|
||||||
|
|
||||||
|
// 重置状态
|
||||||
|
useEffect(() => {
|
||||||
|
if (isOpen) {
|
||||||
|
setSelectedFiles([]);
|
||||||
|
setStep('select');
|
||||||
|
clearError();
|
||||||
|
}
|
||||||
|
}, [isOpen, clearError]);
|
||||||
|
|
||||||
|
// 选择文件
|
||||||
|
const handleSelectFiles = async () => {
|
||||||
|
try {
|
||||||
|
const filePaths = await selectMaterialFiles();
|
||||||
|
if (filePaths.length > 0) {
|
||||||
|
const validFiles = await validateMaterialFiles(filePaths);
|
||||||
|
setSelectedFiles(validFiles);
|
||||||
|
if (validFiles.length > 0) {
|
||||||
|
setStep('configure');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('选择文件失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 开始导入
|
||||||
|
const handleStartImport = async () => {
|
||||||
|
if (selectedFiles.length === 0) return;
|
||||||
|
|
||||||
|
setStep('importing');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const request: CreateMaterialRequest = {
|
||||||
|
project_id: projectId,
|
||||||
|
file_paths: selectedFiles,
|
||||||
|
auto_process: autoProcess,
|
||||||
|
max_segment_duration: maxSegmentDuration,
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await importMaterials(request);
|
||||||
|
setStep('complete');
|
||||||
|
onImportComplete(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('导入失败:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 格式化文件大小
|
||||||
|
const formatFileSize = (bytes: number) => {
|
||||||
|
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||||
|
if (bytes === 0) return '0 B';
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
|
return Math.round(bytes / Math.pow(1024, i) * 100) / 100 + ' ' + sizes[i];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取文件名
|
||||||
|
const getFileName = (path: string) => {
|
||||||
|
return path.split(/[/\\]/).pop() || path;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isOpen) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||||
|
<div className="bg-white rounded-lg shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-hidden">
|
||||||
|
{/* 头部 */}
|
||||||
|
<div className="flex items-center justify-between p-6 border-b border-gray-200">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900">导入素材</h2>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 内容 */}
|
||||||
|
<div className="p-6 overflow-y-auto max-h-[calc(90vh-140px)]">
|
||||||
|
{/* FFmpeg 状态检查 */}
|
||||||
|
{!ffmpegAvailable && (
|
||||||
|
<div className="mb-6 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<AlertCircle className="w-5 h-5 text-yellow-600 mr-2" />
|
||||||
|
<span className="text-yellow-800">
|
||||||
|
FFmpeg 不可用,视频处理功能将受限。请确保已安装 FFmpeg。
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 错误信息 */}
|
||||||
|
{error && (
|
||||||
|
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<AlertCircle className="w-5 h-5 text-red-600 mr-2" />
|
||||||
|
<span className="text-red-800">{error}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 步骤 1: 选择文件 */}
|
||||||
|
{step === 'select' && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<Upload className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-2">选择素材文件</h3>
|
||||||
|
<p className="text-gray-600 mb-6">
|
||||||
|
支持视频、音频、图片等多种格式
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={handleSelectFiles}
|
||||||
|
className="inline-flex items-center px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
<Upload className="w-5 h-5 mr-2" />
|
||||||
|
选择文件
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 步骤 2: 配置导入选项 */}
|
||||||
|
{step === 'configure' && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-4">已选择的文件</h3>
|
||||||
|
<div className="max-h-40 overflow-y-auto border border-gray-200 rounded-lg">
|
||||||
|
{selectedFiles.map((file, index) => (
|
||||||
|
<div key={index} className="flex items-center p-3 border-b border-gray-100 last:border-b-0">
|
||||||
|
<FileText className="w-5 h-5 text-gray-400 mr-3" />
|
||||||
|
<span className="text-sm text-gray-900 truncate">{getFileName(file)}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<h3 className="text-lg font-medium text-gray-900">导入配置</h3>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="autoProcess"
|
||||||
|
checked={autoProcess}
|
||||||
|
onChange={(e) => setAutoProcess(e.target.checked)}
|
||||||
|
className="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
||||||
|
/>
|
||||||
|
<label htmlFor="autoProcess" className="ml-2 text-sm text-gray-900">
|
||||||
|
自动处理素材(提取元数据、场景检测等)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{autoProcess && (
|
||||||
|
<div>
|
||||||
|
<label htmlFor="maxDuration" className="block text-sm font-medium text-gray-700 mb-2">
|
||||||
|
最大片段时长(秒)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="maxDuration"
|
||||||
|
value={maxSegmentDuration}
|
||||||
|
onChange={(e) => setMaxSegmentDuration(Number(e.target.value))}
|
||||||
|
min="60"
|
||||||
|
max="3600"
|
||||||
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-gray-500">
|
||||||
|
超过此时长的视频将被自动切分
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 步骤 3: 导入进度 */}
|
||||||
|
{step === 'importing' && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<Loader2 className="w-16 h-16 text-blue-600 mx-auto mb-4 animate-spin" />
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-2">正在导入素材</h3>
|
||||||
|
|
||||||
|
{importProgress && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
{importProgress.current_status}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||||
|
style={{
|
||||||
|
width: `${(importProgress.processed_count / importProgress.total_count) * 100}%`
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
{importProgress.processed_count} / {importProgress.total_count} 文件已处理
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{importProgress.current_file && (
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
当前文件: {getFileName(importProgress.current_file)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{importProgress.errors.length > 0 && (
|
||||||
|
<div className="text-left">
|
||||||
|
<h4 className="text-sm font-medium text-red-600 mb-2">错误信息:</h4>
|
||||||
|
<div className="max-h-20 overflow-y-auto text-xs text-red-600 space-y-1">
|
||||||
|
{importProgress.errors.map((error, index) => (
|
||||||
|
<div key={index}>{error}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 步骤 4: 完成 */}
|
||||||
|
{step === 'complete' && (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="text-center">
|
||||||
|
<CheckCircle className="w-16 h-16 text-green-600 mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-2">导入完成</h3>
|
||||||
|
<p className="text-gray-600">
|
||||||
|
素材已成功导入到项目中
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 底部按钮 */}
|
||||||
|
<div className="flex items-center justify-end space-x-3 p-6 border-t border-gray-200">
|
||||||
|
{step === 'configure' && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => setStep('select')}
|
||||||
|
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
|
||||||
|
>
|
||||||
|
返回
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleStartImport}
|
||||||
|
disabled={selectedFiles.length === 0}
|
||||||
|
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||||
|
>
|
||||||
|
开始导入
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(step === 'select' || step === 'complete') && (
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-6 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
|
||||||
|
>
|
||||||
|
{step === 'complete' ? '完成' : '取消'}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { useProjectStore } from '../store/projectStore';
|
import { useProjectStore } from '../store/projectStore';
|
||||||
import { useUIStore } from '../store/uiStore';
|
import { useUIStore } from '../store/uiStore';
|
||||||
@@ -13,6 +14,7 @@ import { Plus, Trash2 } from 'lucide-react';
|
|||||||
* 遵循 Tauri 开发规范的组件设计模式
|
* 遵循 Tauri 开发规范的组件设计模式
|
||||||
*/
|
*/
|
||||||
export const ProjectList: React.FC = () => {
|
export const ProjectList: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
const {
|
const {
|
||||||
projects,
|
projects,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -54,8 +56,8 @@ export const ProjectList: React.FC = () => {
|
|||||||
// 处理项目打开
|
// 处理项目打开
|
||||||
const handleProjectOpen = (project: any) => {
|
const handleProjectOpen = (project: any) => {
|
||||||
setCurrentProject(project);
|
setCurrentProject(project);
|
||||||
// TODO: 导航到项目详情页面
|
// 导航到项目详情页面
|
||||||
console.log('打开项目:', project);
|
navigate(`/project/${project.id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理项目编辑
|
// 处理项目编辑
|
||||||
|
|||||||
289
apps/desktop/src/pages/ProjectDetails.tsx
Normal file
289
apps/desktop/src/pages/ProjectDetails.tsx
Normal file
@@ -0,0 +1,289 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
|
import { ArrowLeft, FolderOpen, Calendar, Settings, Upload } from 'lucide-react';
|
||||||
|
import { useProjectStore } from '../store/projectStore';
|
||||||
|
import { useMaterialStore } from '../store/materialStore';
|
||||||
|
import { Project } from '../types/project';
|
||||||
|
import { MaterialImportResult } from '../types/material';
|
||||||
|
import { LoadingSpinner } from '../components/LoadingSpinner';
|
||||||
|
import { ErrorMessage } from '../components/ErrorMessage';
|
||||||
|
import { MaterialImportDialog } from '../components/MaterialImportDialog';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 项目详情页面组件
|
||||||
|
* 遵循 Tauri 开发规范的页面组件设计模式
|
||||||
|
*/
|
||||||
|
export const ProjectDetails: React.FC = () => {
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { projects, isLoading, error, loadProjects } = useProjectStore();
|
||||||
|
const {
|
||||||
|
materials,
|
||||||
|
stats,
|
||||||
|
loadMaterials,
|
||||||
|
loadMaterialStats,
|
||||||
|
isLoading: materialsLoading
|
||||||
|
} = useMaterialStore();
|
||||||
|
const [project, setProject] = useState<Project | null>(null);
|
||||||
|
const [showImportDialog, setShowImportDialog] = useState(false);
|
||||||
|
|
||||||
|
// 加载项目详情
|
||||||
|
useEffect(() => {
|
||||||
|
if (!projects.length) {
|
||||||
|
loadProjects();
|
||||||
|
}
|
||||||
|
}, [projects.length, loadProjects]);
|
||||||
|
|
||||||
|
// 根据ID查找项目
|
||||||
|
useEffect(() => {
|
||||||
|
if (id && projects.length > 0) {
|
||||||
|
const foundProject = projects.find(p => p.id === id);
|
||||||
|
setProject(foundProject || null);
|
||||||
|
|
||||||
|
// 加载项目素材
|
||||||
|
if (foundProject) {
|
||||||
|
loadMaterials(foundProject.id);
|
||||||
|
loadMaterialStats(foundProject.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [id, projects, loadMaterials, loadMaterialStats]);
|
||||||
|
|
||||||
|
// 返回项目列表
|
||||||
|
const handleBack = () => {
|
||||||
|
navigate('/');
|
||||||
|
};
|
||||||
|
|
||||||
|
// 打开项目文件夹
|
||||||
|
const handleOpenFolder = async () => {
|
||||||
|
if (project) {
|
||||||
|
try {
|
||||||
|
const { openPath } = await import('@tauri-apps/plugin-opener');
|
||||||
|
await openPath(project.path);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('打开文件夹失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 素材导入处理
|
||||||
|
const handleMaterialImport = () => {
|
||||||
|
setShowImportDialog(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 导入完成处理
|
||||||
|
const handleImportComplete = (result: MaterialImportResult) => {
|
||||||
|
setShowImportDialog(false);
|
||||||
|
console.log('导入完成:', result);
|
||||||
|
// 重新加载素材列表
|
||||||
|
if (project) {
|
||||||
|
loadMaterials(project.id);
|
||||||
|
loadMaterialStats(project.id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-[400px]">
|
||||||
|
<LoadingSpinner />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <ErrorMessage message={error} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!project) {
|
||||||
|
return (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<h2 className="text-2xl font-bold text-gray-900 mb-4">项目未找到</h2>
|
||||||
|
<p className="text-gray-600 mb-6">请检查项目ID是否正确</p>
|
||||||
|
<button
|
||||||
|
onClick={handleBack}
|
||||||
|
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||||
|
返回项目列表
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
{/* 页面头部 */}
|
||||||
|
<div className="mb-8">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<button
|
||||||
|
onClick={handleBack}
|
||||||
|
className="inline-flex items-center text-gray-600 hover:text-gray-900 transition-colors"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="w-5 h-5 mr-2" />
|
||||||
|
返回项目列表
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<button
|
||||||
|
onClick={handleOpenFolder}
|
||||||
|
className="inline-flex items-center px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors"
|
||||||
|
>
|
||||||
|
<FolderOpen className="w-4 h-4 mr-2" />
|
||||||
|
打开文件夹
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleMaterialImport}
|
||||||
|
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
<Upload className="w-4 h-4 mr-2" />
|
||||||
|
导入素材
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button className="inline-flex items-center px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors">
|
||||||
|
<Settings className="w-4 h-4 mr-2" />
|
||||||
|
项目设置
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 项目基本信息 */}
|
||||||
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
|
<div className="flex items-start justify-between">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold text-gray-900 mb-2">{project.name}</h1>
|
||||||
|
{project.description && (
|
||||||
|
<p className="text-gray-600 mb-4">{project.description}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-6 text-sm text-gray-500">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<FolderOpen className="w-4 h-4 mr-1" />
|
||||||
|
<span className="font-mono">{project.path}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
|
<Calendar className="w-4 h-4 mr-1" />
|
||||||
|
<span>创建于 {new Date(project.created_at).toLocaleDateString('zh-CN')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={`px-3 py-1 rounded-full text-xs font-medium ${
|
||||||
|
project.is_active
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: 'bg-gray-100 text-gray-800'
|
||||||
|
}`}>
|
||||||
|
{project.is_active ? '活跃' : '非活跃'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 项目内容区域 */}
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
{/* 素材管理 */}
|
||||||
|
<div className="lg:col-span-2">
|
||||||
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-900 mb-4">素材管理</h2>
|
||||||
|
|
||||||
|
{/* 素材导入区域 */}
|
||||||
|
<div className="border-2 border-dashed border-gray-300 rounded-lg p-8 text-center mb-6">
|
||||||
|
<Upload className="w-12 h-12 text-gray-400 mx-auto mb-4" />
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-2">导入素材文件</h3>
|
||||||
|
<p className="text-gray-600 mb-4">
|
||||||
|
支持视频、音频、图片等多种格式,系统将自动分析并处理
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={handleMaterialImport}
|
||||||
|
className="inline-flex items-center px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
|
>
|
||||||
|
<Upload className="w-5 h-5 mr-2" />
|
||||||
|
选择文件导入
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 素材列表 */}
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium text-gray-900 mb-3">项目素材</h3>
|
||||||
|
{materialsLoading ? (
|
||||||
|
<div className="flex items-center justify-center py-8">
|
||||||
|
<LoadingSpinner />
|
||||||
|
</div>
|
||||||
|
) : materials.length > 0 ? (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{materials.map((material) => (
|
||||||
|
<div key={material.id} className="border border-gray-200 rounded-lg p-4">
|
||||||
|
<h4 className="font-medium text-gray-900 truncate">{material.name}</h4>
|
||||||
|
<p className="text-sm text-gray-600 mt-1">
|
||||||
|
类型: {material.material_type}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
状态: {material.processing_status}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
大小: {(material.file_size / 1024 / 1024).toFixed(2)} MB
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center py-8 text-gray-500">
|
||||||
|
<p>暂无素材,请导入素材文件开始使用</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 项目信息侧边栏 */}
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* 项目统计 */}
|
||||||
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">项目统计</h3>
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">总素材数</span>
|
||||||
|
<span className="font-medium">{stats?.total_materials || 0}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">视频文件</span>
|
||||||
|
<span className="font-medium">{stats?.video_count || 0}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">音频文件</span>
|
||||||
|
<span className="font-medium">{stats?.audio_count || 0}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">图片文件</span>
|
||||||
|
<span className="font-medium">{stats?.image_count || 0}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span className="text-gray-600">总大小</span>
|
||||||
|
<span className="font-medium">
|
||||||
|
{stats ? (stats.total_size / 1024 / 1024 / 1024).toFixed(2) + ' GB' : '0 GB'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 最近活动 */}
|
||||||
|
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-6">
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-4">最近活动</h3>
|
||||||
|
<div className="text-center py-4 text-gray-500">
|
||||||
|
<p>暂无活动记录</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 素材导入对话框 */}
|
||||||
|
{project && (
|
||||||
|
<MaterialImportDialog
|
||||||
|
isOpen={showImportDialog}
|
||||||
|
projectId={project.id}
|
||||||
|
onClose={() => setShowImportDialog(false)}
|
||||||
|
onImportComplete={handleImportComplete}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
294
apps/desktop/src/store/materialStore.ts
Normal file
294
apps/desktop/src/store/materialStore.ts
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
import {
|
||||||
|
Material,
|
||||||
|
MaterialStats,
|
||||||
|
CreateMaterialRequest,
|
||||||
|
MaterialImportResult,
|
||||||
|
ProcessingStatus,
|
||||||
|
MaterialProcessingConfig
|
||||||
|
} from '../types/material';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 素材状态管理
|
||||||
|
* 遵循 Tauri 开发规范的状态管理模式
|
||||||
|
*/
|
||||||
|
interface MaterialState {
|
||||||
|
// 状态
|
||||||
|
materials: Material[];
|
||||||
|
currentMaterial: Material | null;
|
||||||
|
stats: MaterialStats | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
isImporting: boolean;
|
||||||
|
isProcessing: boolean;
|
||||||
|
error: string | null;
|
||||||
|
importProgress: {
|
||||||
|
current_file: string;
|
||||||
|
processed_count: number;
|
||||||
|
total_count: number;
|
||||||
|
current_status: string;
|
||||||
|
errors: string[];
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
// 操作
|
||||||
|
loadMaterials: (projectId: string) => Promise<void>;
|
||||||
|
loadMaterialStats: (projectId: string) => Promise<void>;
|
||||||
|
importMaterials: (request: CreateMaterialRequest) => Promise<MaterialImportResult>;
|
||||||
|
getMaterialById: (id: string) => Promise<Material | null>;
|
||||||
|
deleteMaterial: (id: string) => Promise<void>;
|
||||||
|
processMaterials: (materialIds: string[]) => Promise<void>;
|
||||||
|
updateMaterialStatus: (id: string, status: ProcessingStatus, errorMessage?: string) => Promise<void>;
|
||||||
|
cleanupInvalidMaterials: (projectId: string) => Promise<string>;
|
||||||
|
setCurrentMaterial: (material: Material | null) => void;
|
||||||
|
clearError: () => void;
|
||||||
|
|
||||||
|
// 工具方法
|
||||||
|
selectMaterialFiles: () => Promise<string[]>;
|
||||||
|
validateMaterialFiles: (filePaths: string[]) => Promise<string[]>;
|
||||||
|
getFileInfo: (filePath: string) => Promise<any>;
|
||||||
|
checkFFmpegAvailable: () => Promise<boolean>;
|
||||||
|
getFFmpegVersion: () => Promise<string>;
|
||||||
|
getSupportedExtensions: () => Promise<string[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMaterialStore = create<MaterialState>((set, get) => ({
|
||||||
|
// 初始状态
|
||||||
|
materials: [],
|
||||||
|
currentMaterial: null,
|
||||||
|
stats: null,
|
||||||
|
isLoading: false,
|
||||||
|
isImporting: false,
|
||||||
|
isProcessing: false,
|
||||||
|
error: null,
|
||||||
|
importProgress: null,
|
||||||
|
|
||||||
|
// 加载项目素材
|
||||||
|
loadMaterials: async (projectId: string) => {
|
||||||
|
set({ isLoading: true, error: null });
|
||||||
|
try {
|
||||||
|
const materials = await invoke<Material[]>('get_project_materials', { projectId });
|
||||||
|
set({ materials, isLoading: false });
|
||||||
|
} catch (error) {
|
||||||
|
set({
|
||||||
|
error: error as string,
|
||||||
|
isLoading: false,
|
||||||
|
materials: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 加载素材统计
|
||||||
|
loadMaterialStats: async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
const stats = await invoke<MaterialStats>('get_project_material_stats', { projectId });
|
||||||
|
set({ stats });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载素材统计失败:', error);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 导入素材
|
||||||
|
importMaterials: async (request: CreateMaterialRequest) => {
|
||||||
|
set({ isImporting: true, error: null, importProgress: {
|
||||||
|
current_file: '',
|
||||||
|
processed_count: 0,
|
||||||
|
total_count: request.file_paths.length,
|
||||||
|
current_status: '准备导入...',
|
||||||
|
errors: []
|
||||||
|
}});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await invoke<MaterialImportResult>('import_materials', { request });
|
||||||
|
|
||||||
|
// 更新素材列表
|
||||||
|
const { materials } = get();
|
||||||
|
set({
|
||||||
|
materials: [...materials, ...result.created_materials],
|
||||||
|
isImporting: false,
|
||||||
|
importProgress: null
|
||||||
|
});
|
||||||
|
|
||||||
|
// 重新加载统计信息
|
||||||
|
get().loadMaterialStats(request.project_id);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
set({
|
||||||
|
error: error as string,
|
||||||
|
isImporting: false,
|
||||||
|
importProgress: null
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取素材详情
|
||||||
|
getMaterialById: async (id: string) => {
|
||||||
|
try {
|
||||||
|
const material = await invoke<Material | null>('get_material_by_id', { id });
|
||||||
|
return material;
|
||||||
|
} catch (error) {
|
||||||
|
set({ error: error as string });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除素材
|
||||||
|
deleteMaterial: async (id: string) => {
|
||||||
|
try {
|
||||||
|
await invoke('delete_material', { id });
|
||||||
|
|
||||||
|
// 从状态中移除
|
||||||
|
const { materials } = get();
|
||||||
|
set({ materials: materials.filter(m => m.id !== id) });
|
||||||
|
|
||||||
|
// 如果删除的是当前素材,清空当前素材
|
||||||
|
const { currentMaterial } = get();
|
||||||
|
if (currentMaterial?.id === id) {
|
||||||
|
set({ currentMaterial: null });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
set({ error: error as string });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 批量处理素材
|
||||||
|
processMaterials: async (materialIds: string[]) => {
|
||||||
|
set({ isProcessing: true, error: null });
|
||||||
|
try {
|
||||||
|
const processedIds = await invoke<string[]>('batch_process_materials', { materialIds });
|
||||||
|
|
||||||
|
// 更新处理状态
|
||||||
|
const { materials } = get();
|
||||||
|
const updatedMaterials = materials.map(material => {
|
||||||
|
if (processedIds.includes(material.id)) {
|
||||||
|
return { ...material, processing_status: 'Processing' as ProcessingStatus };
|
||||||
|
}
|
||||||
|
return material;
|
||||||
|
});
|
||||||
|
|
||||||
|
set({ materials: updatedMaterials, isProcessing: false });
|
||||||
|
} catch (error) {
|
||||||
|
set({ error: error as string, isProcessing: false });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 更新素材状态
|
||||||
|
updateMaterialStatus: async (id: string, status: ProcessingStatus, errorMessage?: string) => {
|
||||||
|
try {
|
||||||
|
await invoke('update_material_status', { id, status, errorMessage });
|
||||||
|
|
||||||
|
// 更新本地状态
|
||||||
|
const { materials } = get();
|
||||||
|
const updatedMaterials = materials.map(material => {
|
||||||
|
if (material.id === id) {
|
||||||
|
return {
|
||||||
|
...material,
|
||||||
|
processing_status: status,
|
||||||
|
error_message: errorMessage,
|
||||||
|
updated_at: new Date().toISOString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return material;
|
||||||
|
});
|
||||||
|
|
||||||
|
set({ materials: updatedMaterials });
|
||||||
|
} catch (error) {
|
||||||
|
set({ error: error as string });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清理失效素材
|
||||||
|
cleanupInvalidMaterials: async (projectId: string) => {
|
||||||
|
try {
|
||||||
|
const result = await invoke<string>('cleanup_invalid_materials', { projectId });
|
||||||
|
|
||||||
|
// 重新加载素材列表
|
||||||
|
await get().loadMaterials(projectId);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
set({ error: error as string });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 设置当前素材
|
||||||
|
setCurrentMaterial: (material: Material | null) => {
|
||||||
|
set({ currentMaterial: material });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 清除错误
|
||||||
|
clearError: () => {
|
||||||
|
set({ error: null });
|
||||||
|
},
|
||||||
|
|
||||||
|
// 选择素材文件
|
||||||
|
selectMaterialFiles: async () => {
|
||||||
|
try {
|
||||||
|
const filePaths = await invoke<string[]>('select_material_files');
|
||||||
|
return filePaths;
|
||||||
|
} catch (error) {
|
||||||
|
set({ error: error as string });
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 验证素材文件
|
||||||
|
validateMaterialFiles: async (filePaths: string[]) => {
|
||||||
|
try {
|
||||||
|
const validFiles = await invoke<string[]>('validate_material_files', { filePaths });
|
||||||
|
return validFiles;
|
||||||
|
} catch (error) {
|
||||||
|
set({ error: error as string });
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取文件信息
|
||||||
|
getFileInfo: async (filePath: string) => {
|
||||||
|
try {
|
||||||
|
const fileInfo = await invoke('get_file_info', { filePath });
|
||||||
|
return fileInfo;
|
||||||
|
} catch (error) {
|
||||||
|
set({ error: error as string });
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 检查 FFmpeg 是否可用
|
||||||
|
checkFFmpegAvailable: async () => {
|
||||||
|
try {
|
||||||
|
const isAvailable = await invoke<boolean>('check_ffmpeg_available');
|
||||||
|
return isAvailable;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('检查 FFmpeg 失败:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取 FFmpeg 版本
|
||||||
|
getFFmpegVersion: async () => {
|
||||||
|
try {
|
||||||
|
const version = await invoke<string>('get_ffmpeg_version');
|
||||||
|
return version;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取 FFmpeg 版本失败:', error);
|
||||||
|
return 'Unknown';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 获取支持的扩展名
|
||||||
|
getSupportedExtensions: async () => {
|
||||||
|
try {
|
||||||
|
const extensions = await invoke<string[]>('get_supported_extensions');
|
||||||
|
return extensions;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取支持的扩展名失败:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}));
|
||||||
261
apps/desktop/src/types/material.ts
Normal file
261
apps/desktop/src/types/material.ts
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
/**
|
||||||
|
* 素材相关类型定义
|
||||||
|
* 遵循 Tauri 开发规范的类型安全设计
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type MaterialType = 'Video' | 'Audio' | 'Image' | 'Document' | 'Other';
|
||||||
|
|
||||||
|
export type ProcessingStatus = 'Pending' | 'Processing' | 'Completed' | 'Failed' | 'Skipped';
|
||||||
|
|
||||||
|
export interface VideoMetadata {
|
||||||
|
duration: number;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
fps: number;
|
||||||
|
bitrate: number;
|
||||||
|
codec: string;
|
||||||
|
format: string;
|
||||||
|
has_audio: boolean;
|
||||||
|
audio_codec?: string;
|
||||||
|
audio_bitrate?: number;
|
||||||
|
audio_sample_rate?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AudioMetadata {
|
||||||
|
duration: number;
|
||||||
|
bitrate: number;
|
||||||
|
codec: string;
|
||||||
|
sample_rate: number;
|
||||||
|
channels: number;
|
||||||
|
format: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ImageMetadata {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
format: string;
|
||||||
|
color_space?: string;
|
||||||
|
has_alpha: boolean;
|
||||||
|
dpi?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MaterialMetadata =
|
||||||
|
| { Video: VideoMetadata }
|
||||||
|
| { Audio: AudioMetadata }
|
||||||
|
| { Image: ImageMetadata }
|
||||||
|
| 'None';
|
||||||
|
|
||||||
|
export interface SceneSegment {
|
||||||
|
start_time: number;
|
||||||
|
end_time: number;
|
||||||
|
duration: number;
|
||||||
|
scene_id: number;
|
||||||
|
confidence: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SceneDetection {
|
||||||
|
scenes: SceneSegment[];
|
||||||
|
total_scenes: number;
|
||||||
|
detection_method: string;
|
||||||
|
threshold: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MaterialSegment {
|
||||||
|
id: string;
|
||||||
|
material_id: string;
|
||||||
|
segment_index: number;
|
||||||
|
start_time: number;
|
||||||
|
end_time: number;
|
||||||
|
duration: number;
|
||||||
|
file_path: string;
|
||||||
|
file_size: number;
|
||||||
|
created_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Material {
|
||||||
|
id: string;
|
||||||
|
project_id: string;
|
||||||
|
name: string;
|
||||||
|
original_path: string;
|
||||||
|
file_size: number;
|
||||||
|
md5_hash: string;
|
||||||
|
material_type: MaterialType;
|
||||||
|
processing_status: ProcessingStatus;
|
||||||
|
metadata: MaterialMetadata;
|
||||||
|
scene_detection?: SceneDetection;
|
||||||
|
segments: MaterialSegment[];
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
processed_at?: string;
|
||||||
|
error_message?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateMaterialRequest {
|
||||||
|
project_id: string;
|
||||||
|
file_paths: string[];
|
||||||
|
auto_process: boolean;
|
||||||
|
max_segment_duration?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MaterialProcessingConfig {
|
||||||
|
max_segment_duration: number;
|
||||||
|
scene_detection_threshold: number;
|
||||||
|
enable_scene_detection: boolean;
|
||||||
|
video_quality: string;
|
||||||
|
audio_quality: string;
|
||||||
|
output_format: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MaterialImportResult {
|
||||||
|
total_files: number;
|
||||||
|
processed_files: number;
|
||||||
|
skipped_files: number;
|
||||||
|
failed_files: number;
|
||||||
|
created_materials: Material[];
|
||||||
|
errors: string[];
|
||||||
|
processing_time: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MaterialStats {
|
||||||
|
total_materials: number;
|
||||||
|
video_count: number;
|
||||||
|
audio_count: number;
|
||||||
|
image_count: number;
|
||||||
|
other_count: number;
|
||||||
|
total_size: number;
|
||||||
|
total_duration: number;
|
||||||
|
processing_status_counts: Record<string, number>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FileInfo {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
size: number;
|
||||||
|
extension: string;
|
||||||
|
material_type: MaterialType;
|
||||||
|
is_supported: boolean;
|
||||||
|
modified: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 素材导入相关的 Tauri 命令类型定义
|
||||||
|
*/
|
||||||
|
export interface MaterialCommands {
|
||||||
|
// 素材管理命令
|
||||||
|
import_materials(request: CreateMaterialRequest): Promise<MaterialImportResult>;
|
||||||
|
get_project_materials(project_id: string): Promise<Material[]>;
|
||||||
|
get_material_by_id(id: string): Promise<Material | null>;
|
||||||
|
delete_material(id: string): Promise<void>;
|
||||||
|
get_project_material_stats(project_id: string): Promise<MaterialStats>;
|
||||||
|
batch_process_materials(material_ids: string[]): Promise<string[]>;
|
||||||
|
update_material_status(id: string, status: string, error_message?: string): Promise<void>;
|
||||||
|
cleanup_invalid_materials(project_id: string): Promise<string>;
|
||||||
|
|
||||||
|
// 文件操作命令
|
||||||
|
is_supported_format(file_path: string): Promise<boolean>;
|
||||||
|
get_supported_extensions(): Promise<string[]>;
|
||||||
|
select_material_files(): Promise<string[]>;
|
||||||
|
validate_material_files(file_paths: string[]): Promise<string[]>;
|
||||||
|
get_file_info(file_path: string): Promise<FileInfo>;
|
||||||
|
|
||||||
|
// FFmpeg 相关命令
|
||||||
|
check_ffmpeg_available(): Promise<boolean>;
|
||||||
|
get_ffmpeg_version(): Promise<string>;
|
||||||
|
extract_file_metadata(file_path: string): Promise<MaterialMetadata>;
|
||||||
|
detect_video_scenes(file_path: string, threshold: number): Promise<number[]>;
|
||||||
|
generate_video_thumbnail(
|
||||||
|
input_path: string,
|
||||||
|
output_path: string,
|
||||||
|
timestamp: number,
|
||||||
|
width: number,
|
||||||
|
height: number
|
||||||
|
): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 素材导入进度状态
|
||||||
|
*/
|
||||||
|
export interface ImportProgress {
|
||||||
|
current_file: string;
|
||||||
|
processed_count: number;
|
||||||
|
total_count: number;
|
||||||
|
current_status: string;
|
||||||
|
errors: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 素材卡片组件属性
|
||||||
|
*/
|
||||||
|
export interface MaterialCardProps {
|
||||||
|
material: Material;
|
||||||
|
onView: (material: Material) => void;
|
||||||
|
onEdit: (material: Material) => void;
|
||||||
|
onDelete: (id: string) => void;
|
||||||
|
onProcess: (material: Material) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 素材导入对话框属性
|
||||||
|
*/
|
||||||
|
export interface MaterialImportDialogProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
projectId: string;
|
||||||
|
onClose: () => void;
|
||||||
|
onImportComplete: (result: MaterialImportResult) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 素材列表组件属性
|
||||||
|
*/
|
||||||
|
export interface MaterialListProps {
|
||||||
|
projectId: string;
|
||||||
|
materials: Material[];
|
||||||
|
isLoading: boolean;
|
||||||
|
onRefresh: () => void;
|
||||||
|
onImport: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 素材统计组件属性
|
||||||
|
*/
|
||||||
|
export interface MaterialStatsProps {
|
||||||
|
stats: MaterialStats;
|
||||||
|
isLoading: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 视频预览组件属性
|
||||||
|
*/
|
||||||
|
export interface VideoPreviewProps {
|
||||||
|
material: Material;
|
||||||
|
onSegmentSelect?: (segment: MaterialSegment) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 场景检测结果组件属性
|
||||||
|
*/
|
||||||
|
export interface SceneDetectionProps {
|
||||||
|
sceneDetection: SceneDetection;
|
||||||
|
onSceneSelect?: (scene: SceneSegment) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 素材处理配置组件属性
|
||||||
|
*/
|
||||||
|
export interface ProcessingConfigProps {
|
||||||
|
config: MaterialProcessingConfig;
|
||||||
|
onChange: (config: MaterialProcessingConfig) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工具函数类型
|
||||||
|
*/
|
||||||
|
export interface MaterialUtils {
|
||||||
|
formatFileSize(bytes: number): string;
|
||||||
|
formatDuration(seconds: number): string;
|
||||||
|
getStatusColor(status: ProcessingStatus): string;
|
||||||
|
getTypeIcon(type: MaterialType): string;
|
||||||
|
isVideoType(type: MaterialType): boolean;
|
||||||
|
isAudioType(type: MaterialType): boolean;
|
||||||
|
isImageType(type: MaterialType): boolean;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user