339 lines
13 KiB
TypeScript
339 lines
13 KiB
TypeScript
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>
|
||
);
|
||
};
|