mixvideo-v2/apps/desktop/src/components/ProjectList.tsx

281 lines
9.3 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 { useNavigate } from 'react-router-dom';
import { invoke } from '@tauri-apps/api/core';
import { useProjectStore } from '../store/projectStore';
import { useUIStore } from '../store/uiStore';
import { ProjectCard } from './ProjectCard';
import { EmptyState } from './EmptyState';
import { LoadingSpinner } from './LoadingSpinner';
import { ErrorMessage } from './ErrorMessage';
import { DeleteConfirmDialog } from './DeleteConfirmDialog';
import { PageLoadingSkeleton } from './SkeletonLoader';
import { InteractiveButton } from './InteractiveButton';
import { Plus, Trash2 } from 'lucide-react';
/**
* 项目列表组件
* 遵循 Tauri 开发规范的组件设计模式
*/
export const ProjectList: React.FC = () => {
const navigate = useNavigate();
const {
projects,
isLoading,
error,
loadProjects,
deleteProject,
setCurrentProject,
clearError
} = useProjectStore();
const {
openCreateProjectModal,
openEditProjectModal
} = useUIStore();
const [isCleaningUp, setIsCleaningUp] = useState(false);
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
const [projectToDelete, setProjectToDelete] = useState<string | null>(null);
const [deleting, setDeleting] = useState(false);
// 清理无效项目记录
const handleCleanupInvalidProjects = async () => {
try {
setIsCleaningUp(true);
const result = await invoke<string>('cleanup_invalid_projects');
alert(result); // 显示清理结果
// 重新加载项目列表
await loadProjects();
} catch (error) {
console.error('清理失败:', error);
alert('清理失败: ' + error);
} finally {
setIsCleaningUp(false);
}
};
// 组件挂载时加载项目
useEffect(() => {
loadProjects();
}, [loadProjects]);
// 处理项目打开
const handleProjectOpen = (project: any) => {
setCurrentProject(project);
// 导航到项目详情页面
navigate(`/project/${project.id}`);
};
// 处理项目编辑
const handleProjectEdit = (project: any) => {
openEditProjectModal(project);
};
// 处理项目删除
const handleProjectDelete = (id: string) => {
setProjectToDelete(id);
setShowDeleteConfirm(true);
};
// 确认删除项目
const confirmDeleteProject = async () => {
if (!projectToDelete) return;
try {
setDeleting(true);
await deleteProject(projectToDelete);
setShowDeleteConfirm(false);
setProjectToDelete(null);
} catch (error) {
console.error('删除项目失败:', error);
} finally {
setDeleting(false);
}
};
// 取消删除
const cancelDeleteProject = () => {
setShowDeleteConfirm(false);
setProjectToDelete(null);
};
// 错误状态
if (error) {
return (
<div className="w-full animate-fade-in">
{/* 页面头部 - 错误状态 */}
<div className="relative mb-8">
<div className="absolute inset-0 bg-gradient-to-r from-red-50 to-orange-50 rounded-3xl opacity-50"></div>
<div className="relative flex flex-col sm:flex-row sm:items-center justify-between p-6 rounded-3xl border border-red-100 bg-white/80 backdrop-blur-sm">
<div className="mb-4 sm:mb-0">
<h1 className="text-3xl font-bold text-gray-900 mb-2"></h1>
<p className="text-red-600 text-sm">
</p>
</div>
<div className="flex items-center gap-3">
<button
className="btn btn-secondary btn-sm"
onClick={handleCleanupInvalidProjects}
disabled={isCleaningUp}
title="清理无效的项目记录"
>
<Trash2 size={16} />
{isCleaningUp ? '清理中...' : '清理'}
</button>
<button
className="flex items-center gap-1.5 px-4 py-2 bg-gradient-to-r from-primary-500 to-primary-600 text-white rounded-lg hover:from-primary-600 hover:to-primary-700 transition-all duration-200 shadow-sm hover:shadow-md text-sm font-medium"
onClick={openCreateProjectModal}
>
<Plus size={20} />
</button>
</div>
</div>
</div>
<div className="animate-fade-in-up">
<ErrorMessage
message={error}
onRetry={loadProjects}
onDismiss={clearError}
/>
</div>
</div>
);
}
// 加载状态 - 使用美观的骨架屏
if (isLoading && projects.length === 0) {
return (
<div className="w-full">
<PageLoadingSkeleton
showHeader={true}
showStats={false}
cardCount={6}
/>
</div>
);
}
return (
<div className="w-full animate-fade-in">
{/* 页面头部 - 增强设计 */}
<div className="relative mb-8">
<div className="absolute inset-0 bg-gradient-to-r from-primary-50 to-blue-50 rounded-3xl opacity-50"></div>
<div className="relative flex flex-col sm:flex-row sm:items-center justify-between p-6 rounded-3xl border border-gray-100 bg-white/80 backdrop-blur-sm">
<div className="mb-4 sm:mb-0">
<h1 className="text-3xl font-bold text-gradient-primary mb-2"></h1>
<p className="text-gray-600 text-sm">
</p>
</div>
<div className="flex items-center gap-3">
<InteractiveButton
variant="secondary"
size="sm"
onClick={handleCleanupInvalidProjects}
disabled={isLoading || isCleaningUp}
loading={isCleaningUp}
icon={<Trash2 size={16} />}
ripple={true}
>
{isCleaningUp ? '清理中...' : '清理'}
</InteractiveButton>
<InteractiveButton
variant="primary"
size="md"
onClick={openCreateProjectModal}
disabled={isLoading}
icon={<Plus size={20} />}
ripple={true}
>
</InteractiveButton>
</div>
</div>
</div>
{/* 项目统计信息 */}
{projects.length > 0 && (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mb-8">
<div className="card card-body text-center">
<div className="text-2xl font-bold text-primary-600 mb-1">
{projects.length}
</div>
<div className="text-sm text-gray-600"></div>
</div>
<div className="card card-body text-center">
<div className="text-2xl font-bold text-green-600 mb-1">
{projects.filter(p => new Date(p.updated_at) > new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)).length}
</div>
<div className="text-sm text-gray-600"></div>
</div>
<div className="card card-body text-center">
<div className="text-2xl font-bold text-blue-600 mb-1">
{projects.filter(p => new Date(p.created_at) > new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)).length}
</div>
<div className="text-sm text-gray-600"></div>
</div>
</div>
)}
{/* 项目内容区域 */}
{projects.length === 0 ? (
<div className="animate-fade-in-up">
<EmptyState
illustration="folder"
title="还没有项目"
description="创建您的第一个项目开始使用 MixVideo"
actionText="新建项目"
onAction={openCreateProjectModal}
showTips
tips={[
"提示:您可以通过拖拽文件夹到此处快速创建项目",
"支持导入现有的视频项目文件夹"
]}
/>
</div>
) : (
<div className="space-y-6">
{/* 优化的项目网格 - 更好的响应式布局 */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-5">
{projects.map((project, index) => (
<div
key={project.id}
className="animate-fade-in-up"
style={{ animationDelay: `${index * 0.08}s` }}
>
<ProjectCard
project={project}
onOpen={handleProjectOpen}
onEdit={handleProjectEdit}
onDelete={handleProjectDelete}
/>
</div>
))}
</div>
</div>
)}
{/* 加载遮罩 - 改进设计 */}
{isLoading && projects.length > 0 && (
<div className="fixed inset-0 bg-black/30 backdrop-blur-sm flex items-center justify-center z-40 animate-fade-in">
<div className="bg-white rounded-2xl p-6 shadow-2xl">
<LoadingSpinner size="medium" text="更新中..." />
</div>
</div>
)}
<DeleteConfirmDialog
isOpen={showDeleteConfirm}
title="删除项目"
message="确定要删除这个项目吗?此操作不可撤销。"
deleting={deleting}
onConfirm={confirmDeleteProject}
onCancel={cancelDeleteProject}
/>
</div>
);
};