mixvideo-v2/apps/desktop/src/pages/ProjectDetails.tsx

339 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useEffect, useState } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { ArrowLeft, FolderOpen, Calendar, Settings, Upload, FileVideo, Clock, ExternalLink } from 'lucide-react';
import { useProjectStore } from '../store/projectStore';
import { useMaterialStore } from '../store/materialStore';
import { Project } from '../types/project';
import { MaterialImportResult, Material, MaterialSegment } from '../types/material';
import { LoadingSpinner } from '../components/LoadingSpinner';
import { ErrorMessage } from '../components/ErrorMessage';
import { MaterialImportDialog } from '../components/MaterialImportDialog';
import { FFmpegDebugPanel } from '../components/FFmpegDebugPanel';
import { MaterialCard } from '../components/MaterialCard';
/**
* 项目详情页面组件
* 遵循 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);
const [activeTab, setActiveTab] = useState<'materials' | 'debug'>('materials');
// 加载项目详情
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');
// 处理 Windows 路径格式,移除 \\?\ 前缀
let normalizedPath = project.path;
if (normalizedPath.startsWith('\\\\?\\')) {
normalizedPath = normalizedPath.substring(4);
}
console.log('尝试打开路径:', normalizedPath);
await openPath(normalizedPath);
} catch (error) {
console.error('打开文件夹失败:', error);
// 如果 openPath 失败,尝试使用 revealItemInDir
try {
const { revealItemInDir } = await import('@tauri-apps/plugin-opener');
let normalizedPath = project.path;
if (normalizedPath.startsWith('\\\\?\\')) {
normalizedPath = normalizedPath.substring(4);
}
console.log('尝试使用 revealItemInDir 打开:', normalizedPath);
await revealItemInDir(normalizedPath);
} catch (fallbackError) {
console.error('备用方法也失败:', fallbackError);
// 可以在这里显示用户友好的错误提示
alert('无法打开文件夹,请检查路径是否存在');
}
}
}
};
// 素材导入处理
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">
{/* 选项卡导航 */}
<div className="flex items-center justify-between mb-6">
<div className="flex space-x-1">
<button
onClick={() => setActiveTab('materials')}
className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors ${
activeTab === 'materials'
? 'bg-blue-100 text-blue-700'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
}`}
>
</button>
<button
onClick={() => setActiveTab('debug')}
className={`px-4 py-2 text-sm font-medium rounded-lg transition-colors ${
activeTab === 'debug'
? 'bg-blue-100 text-blue-700'
: 'text-gray-600 hover:text-gray-900 hover:bg-gray-100'
}`}
>
</button>
</div>
</div>
{/* 选项卡内容 */}
{activeTab === 'materials' && (
<div>
{/* 素材导入区域 */}
<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) => (
<MaterialCard key={material.id} material={material} />
))}
</div>
) : (
<div className="text-center py-8 text-gray-500">
<p>使</p>
</div>
)}
</div>
</div>
)}
{/* 调试工具选项卡 */}
{activeTab === 'debug' && (
<FFmpegDebugPanel />
)}
</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>
);
};