386 lines
16 KiB
TypeScript
386 lines
16 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
||
import { useParams, useNavigate } from 'react-router-dom';
|
||
import { ArrowLeft, FolderOpen, Calendar, Settings, Upload, FileVideo, FileAudio, FileImage, HardDrive } 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';
|
||
import { FFmpegDebugPanel } from '../components/FFmpegDebugPanel';
|
||
import { MaterialCard } from '../components/MaterialCard';
|
||
import { VideoClassificationProgress } from '../components/VideoClassificationProgress';
|
||
import { AiAnalysisLogViewer } from '../components/AiAnalysisLogViewer';
|
||
import MaterialCardSkeleton from '../components/MaterialCardSkeleton';
|
||
|
||
/**
|
||
* 项目详情页面组件
|
||
* 遵循 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' | 'ai-logs'>('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 flex-col sm:flex-row sm:items-center sm:justify-between space-y-4 sm:space-y-0 mb-4">
|
||
<button
|
||
onClick={handleBack}
|
||
className="inline-flex items-center text-gray-600 hover:text-gray-900 transition-colors self-start"
|
||
>
|
||
<ArrowLeft className="w-5 h-5 mr-2" />
|
||
<span className="hidden sm:inline">返回项目列表</span>
|
||
<span className="sm:hidden">返回</span>
|
||
</button>
|
||
|
||
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
|
||
<button
|
||
onClick={handleOpenFolder}
|
||
className="inline-flex items-center px-3 sm:px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors text-sm"
|
||
>
|
||
<FolderOpen className="w-4 h-4" />
|
||
<span className="hidden sm:inline ml-2">打开文件夹</span>
|
||
</button>
|
||
|
||
<button
|
||
onClick={handleMaterialImport}
|
||
className="inline-flex items-center px-3 sm:px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors text-sm"
|
||
>
|
||
<Upload className="w-4 h-4" />
|
||
<span className="hidden sm:inline ml-2">导入素材</span>
|
||
</button>
|
||
|
||
<button className="inline-flex items-center px-3 sm:px-4 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition-colors text-sm">
|
||
<Settings className="w-4 h-4" />
|
||
<span className="hidden sm:inline ml-2">项目设置</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 项目统计概览 */}
|
||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 md:gap-6 mb-6">
|
||
{/* 总素材数 */}
|
||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 md:p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div className="min-w-0 flex-1">
|
||
<p className="text-xs md:text-sm font-medium text-gray-600 truncate">总素材数</p>
|
||
<p className="text-xl md:text-2xl font-bold text-gray-900">{stats?.total_materials || 0}</p>
|
||
</div>
|
||
<div className="w-10 h-10 md:w-12 md:h-12 bg-blue-100 rounded-lg flex items-center justify-center ml-2">
|
||
<FolderOpen className="w-5 h-5 md:w-6 md:h-6 text-blue-600" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 视频文件 */}
|
||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 md:p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div className="min-w-0 flex-1">
|
||
<p className="text-xs md:text-sm font-medium text-gray-600 truncate">视频文件</p>
|
||
<p className="text-xl md:text-2xl font-bold text-gray-900">{stats?.video_count || 0}</p>
|
||
</div>
|
||
<div className="w-10 h-10 md:w-12 md:h-12 bg-green-100 rounded-lg flex items-center justify-center ml-2">
|
||
<FileVideo className="w-5 h-5 md:w-6 md:h-6 text-green-600" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 音频文件 */}
|
||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 md:p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div className="min-w-0 flex-1">
|
||
<p className="text-xs md:text-sm font-medium text-gray-600 truncate">音频文件</p>
|
||
<p className="text-xl md:text-2xl font-bold text-gray-900">{stats?.audio_count || 0}</p>
|
||
</div>
|
||
<div className="w-10 h-10 md:w-12 md:h-12 bg-purple-100 rounded-lg flex items-center justify-center ml-2">
|
||
<FileAudio className="w-5 h-5 md:w-6 md:h-6 text-purple-600" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 图片文件 */}
|
||
<div className="bg-white rounded-lg shadow-sm border border-gray-200 p-4 md:p-6">
|
||
<div className="flex items-center justify-between">
|
||
<div className="min-w-0 flex-1">
|
||
<p className="text-xs md:text-sm font-medium text-gray-600 truncate">图片文件</p>
|
||
<p className="text-xl md:text-2xl font-bold text-gray-900">{stats?.image_count || 0}</p>
|
||
</div>
|
||
<div className="w-10 h-10 md:w-12 md:h-12 bg-orange-100 rounded-lg flex items-center justify-center ml-2">
|
||
<FileImage className="w-5 h-5 md:w-6 md:h-6 text-orange-600" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 主要内容区域 */}
|
||
<div className="space-y-6">
|
||
{/* 选项卡导航 */}
|
||
<div className="bg-white rounded-lg shadow-sm border border-gray-200">
|
||
<div className="border-b border-gray-200">
|
||
<nav className="flex space-x-4 md:space-x-8 px-4 md:px-6 overflow-x-auto" aria-label="Tabs">
|
||
<button
|
||
onClick={() => setActiveTab('materials')}
|
||
className={`py-4 px-1 border-b-2 font-medium text-sm transition-colors whitespace-nowrap ${
|
||
activeTab === 'materials'
|
||
? 'border-blue-500 text-blue-600'
|
||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||
}`}
|
||
>
|
||
<div className="flex items-center space-x-2">
|
||
<FolderOpen className="w-4 h-4" />
|
||
<span className="hidden sm:inline">素材管理</span>
|
||
<span className="sm:hidden">素材</span>
|
||
</div>
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab('debug')}
|
||
className={`py-4 px-1 border-b-2 font-medium text-sm transition-colors whitespace-nowrap ${
|
||
activeTab === 'debug'
|
||
? 'border-blue-500 text-blue-600'
|
||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||
}`}
|
||
>
|
||
<div className="flex items-center space-x-2">
|
||
<Settings className="w-4 h-4" />
|
||
<span className="hidden sm:inline">调试工具</span>
|
||
<span className="sm:hidden">调试</span>
|
||
</div>
|
||
</button>
|
||
<button
|
||
onClick={() => setActiveTab('ai-logs')}
|
||
className={`py-4 px-1 border-b-2 font-medium text-sm transition-colors whitespace-nowrap ${
|
||
activeTab === 'ai-logs'
|
||
? 'border-blue-500 text-blue-600'
|
||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||
}`}
|
||
>
|
||
<div className="flex items-center space-x-2">
|
||
<HardDrive className="w-4 h-4" />
|
||
<span className="hidden sm:inline">AI分析日志</span>
|
||
<span className="sm:hidden">AI日志</span>
|
||
</div>
|
||
</button>
|
||
</nav>
|
||
</div>
|
||
|
||
{/* 选项卡内容 */}
|
||
<div className="p-4 md:p-6">
|
||
{/* 素材管理选项卡 */}
|
||
{activeTab === 'materials' && (
|
||
<div className="space-y-6">
|
||
{/* AI视频分类进度 */}
|
||
{project && (
|
||
<VideoClassificationProgress
|
||
projectId={project.id}
|
||
autoRefresh={true}
|
||
refreshInterval={3000}
|
||
/>
|
||
)}
|
||
|
||
{/* 素材列表 */}
|
||
<div>
|
||
<div className="flex items-center justify-between mb-4">
|
||
<h3 className="text-lg font-medium text-gray-900">项目素材</h3>
|
||
<button
|
||
onClick={() => setShowImportDialog(true)}
|
||
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors"
|
||
>
|
||
<Upload className="w-4 h-4 mr-2" />
|
||
导入素材
|
||
</button>
|
||
</div>
|
||
|
||
{materialsLoading ? (
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-3 md:gap-4">
|
||
<MaterialCardSkeleton count={8} />
|
||
</div>
|
||
) : materials.length > 0 ? (
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-3 md:gap-4">
|
||
{materials.map((material) => (
|
||
<MaterialCard key={material.id} material={material} />
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="text-center py-16">
|
||
<div className="w-20 h-20 mx-auto mb-4 bg-gray-100 rounded-full flex items-center justify-center">
|
||
<FolderOpen className="w-10 h-10 text-gray-400" />
|
||
</div>
|
||
<h4 className="text-xl font-medium text-gray-900 mb-2">暂无素材</h4>
|
||
<p className="text-gray-500 mb-6 max-w-sm mx-auto">
|
||
开始导入视频、音频或图片素材,让AI帮助您进行智能分类和管理
|
||
</p>
|
||
<button
|
||
onClick={() => setShowImportDialog(true)}
|
||
className="inline-flex items-center px-6 py-3 bg-blue-600 text-white text-sm font-medium rounded-lg hover:bg-blue-700 transition-colors"
|
||
>
|
||
<Upload className="w-5 h-5 mr-2" />
|
||
导入第一个素材
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 调试工具选项卡 */}
|
||
{activeTab === 'debug' && (
|
||
<div>
|
||
<div className="mb-4">
|
||
<h3 className="text-lg font-medium text-gray-900 mb-2">调试工具</h3>
|
||
<p className="text-sm text-gray-600">
|
||
用于开发和调试的工具集合,包括FFmpeg测试和系统诊断功能。
|
||
</p>
|
||
</div>
|
||
<FFmpegDebugPanel />
|
||
</div>
|
||
)}
|
||
|
||
{/* AI分析日志选项卡 */}
|
||
{activeTab === 'ai-logs' && project && (
|
||
<div>
|
||
<div className="mb-4">
|
||
<h3 className="text-lg font-medium text-gray-900 mb-2">AI分析日志</h3>
|
||
<p className="text-sm text-gray-600">
|
||
查看项目中所有AI视频分类的详细日志,包括分类记录和任务执行情况。
|
||
</p>
|
||
</div>
|
||
<AiAnalysisLogViewer projectId={project.id} />
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 素材导入对话框 */}
|
||
{project && (
|
||
<MaterialImportDialog
|
||
isOpen={showImportDialog}
|
||
projectId={project.id}
|
||
onClose={() => setShowImportDialog(false)}
|
||
onImportComplete={handleImportComplete}
|
||
/>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|