281 lines
9.3 KiB
TypeScript
281 lines
9.3 KiB
TypeScript
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>
|
||
);
|
||
};
|