CodeReview/src/pages/RecycleBin.tsx

333 lines
13 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 { useState, useEffect } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Input } from "@/components/ui/input";
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog";
import {
Search,
GitBranch,
Calendar,
Users,
ExternalLink,
Trash2,
RotateCcw,
AlertTriangle,
Inbox
} from "lucide-react";
import { api } from "@/shared/config/database";
import type { Project } from "@/shared/types";
import { toast } from "sonner";
import { deleteZipFile } from "@/shared/utils/zipStorage";
export default function RecycleBin() {
const [deletedProjects, setDeletedProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState("");
const [showRestoreDialog, setShowRestoreDialog] = useState(false);
const [showPermanentDeleteDialog, setShowPermanentDeleteDialog] = useState(false);
const [selectedProject, setSelectedProject] = useState<Project | null>(null);
useEffect(() => {
loadDeletedProjects();
}, []);
const loadDeletedProjects = async () => {
try {
setLoading(true);
const data = await api.getDeletedProjects();
setDeletedProjects(data);
} catch (error) {
console.error('Failed to load deleted projects:', error);
toast.error("加载已删除项目失败");
} finally {
setLoading(false);
}
};
const handleRestoreClick = (project: Project) => {
setSelectedProject(project);
setShowRestoreDialog(true);
};
const handlePermanentDeleteClick = (project: Project) => {
setSelectedProject(project);
setShowPermanentDeleteDialog(true);
};
const handleConfirmRestore = async () => {
if (!selectedProject) return;
try {
await api.restoreProject(selectedProject.id);
toast.success(`项目 "${selectedProject.name}" 已恢复`);
setShowRestoreDialog(false);
setSelectedProject(null);
loadDeletedProjects();
} catch (error) {
console.error('Failed to restore project:', error);
toast.error("恢复项目失败");
}
};
const handleConfirmPermanentDelete = async () => {
if (!selectedProject) return;
try {
// 删除项目数据
await api.permanentlyDeleteProject(selectedProject.id);
// 删除保存的ZIP文件如果有
try {
await deleteZipFile(selectedProject.id);
} catch (error) {
console.error('删除ZIP文件失败:', error);
}
toast.success(`项目 "${selectedProject.name}" 已永久删除`);
setShowPermanentDeleteDialog(false);
setSelectedProject(null);
loadDeletedProjects();
} catch (error) {
console.error('Failed to permanently delete project:', error);
toast.error("永久删除项目失败");
}
};
const filteredProjects = deletedProjects.filter(project =>
project.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
project.description?.toLowerCase().includes(searchTerm.toLowerCase())
);
const getRepositoryIcon = (type?: string) => {
switch (type) {
case 'github': return '🐙';
case 'gitlab': return '🦊';
default: return '📁';
}
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('zh-CN');
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-screen">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4"></div>
<p className="text-gray-500">...</p>
</div>
</div>
);
}
return (
<div className="space-y-6 animate-fade-in">
{/* 页面标题 */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="page-title flex items-center gap-2">
<Trash2 className="w-8 h-8 text-gray-400" />
</h1>
<p className="page-subtitle"></p>
</div>
</div>
{/* 搜索 */}
<Card>
<CardContent className="p-4">
<div className="flex items-center space-x-4">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
<Input
placeholder="搜索已删除的项目..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="pl-10"
/>
</div>
</div>
</CardContent>
</Card>
{/* 项目列表 */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredProjects.length > 0 ? (
filteredProjects.map((project) => (
<Card key={project.id} className="card-modern group opacity-75 hover:opacity-100 transition-opacity">
<CardHeader className="pb-4">
<div className="flex items-start justify-between">
<div className="flex items-center space-x-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-gray-400 to-gray-500 flex items-center justify-center text-white text-lg">
{getRepositoryIcon(project.repository_type)}
</div>
<div>
<CardTitle className="text-lg">
{project.name}
</CardTitle>
{project.description && (
<p className="text-sm text-gray-500 mt-1 line-clamp-2">
{project.description}
</p>
)}
</div>
</div>
<Badge variant="secondary" className="flex-shrink-0 bg-red-100 text-red-700">
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* 项目信息 */}
<div className="space-y-3">
{project.repository_url && (
<div className="flex items-center text-sm text-gray-500">
<GitBranch className="w-4 h-4 mr-2 flex-shrink-0" />
<a
href={project.repository_url}
target="_blank"
rel="noopener noreferrer"
className="hover:text-primary transition-colors flex items-center truncate"
>
<span className="truncate">{project.repository_url.replace('https://', '')}</span>
<ExternalLink className="w-3 h-3 ml-1 flex-shrink-0" />
</a>
</div>
)}
<div className="flex items-center justify-between text-sm text-gray-500">
<div className="flex items-center">
<Calendar className="w-4 h-4 mr-2" />
{formatDate(project.updated_at)}
</div>
<div className="flex items-center">
<Users className="w-4 h-4 mr-2" />
{project.owner?.full_name || '未知'}
</div>
</div>
</div>
{/* 编程语言 */}
{project.programming_languages && (
<div className="flex flex-wrap gap-2">
{JSON.parse(project.programming_languages).slice(0, 4).map((lang: string) => (
<Badge key={lang} variant="outline" className="text-xs">
{lang}
</Badge>
))}
{JSON.parse(project.programming_languages).length > 4 && (
<Badge variant="outline" className="text-xs">
+{JSON.parse(project.programming_languages).length - 4}
</Badge>
)}
</div>
)}
{/* 操作按钮 */}
<div className="flex gap-2 pt-2">
<Button
size="sm"
variant="outline"
className="flex-1 text-green-600 hover:text-green-700 hover:bg-green-50"
onClick={() => handleRestoreClick(project)}
>
<RotateCcw className="w-4 h-4 mr-2" />
</Button>
<Button
size="sm"
variant="outline"
className="flex-1 text-red-600 hover:text-red-700 hover:bg-red-50"
onClick={() => handlePermanentDeleteClick(project)}
>
<Trash2 className="w-4 h-4 mr-2" />
</Button>
</div>
</CardContent>
</Card>
))
) : (
<div className="col-span-full">
<Card className="card-modern">
<CardContent className="empty-state py-16">
<div className="empty-icon">
<Inbox className="w-8 h-8 text-primary" />
</div>
<h3 className="text-lg font-medium text-gray-900 mb-2">
{searchTerm ? '未找到匹配的项目' : '回收站为空'}
</h3>
<p className="text-gray-500 mb-6 max-w-md">
{searchTerm ? '尝试调整搜索条件' : '回收站中没有已删除的项目'}
</p>
</CardContent>
</Card>
</div>
)}
</div>
{/* 恢复项目确认对话框 */}
<AlertDialog open={showRestoreDialog} onOpenChange={setShowRestoreDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle></AlertDialogTitle>
<AlertDialogDescription>
<span className="font-semibold text-gray-900">"{selectedProject?.name}"</span>
<br />
<br />
使
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
onClick={handleConfirmRestore}
className="bg-green-600 hover:bg-green-700 focus:ring-green-600"
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{/* 永久删除确认对话框 */}
<AlertDialog open={showPermanentDeleteDialog} onOpenChange={setShowPermanentDeleteDialog}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2 text-red-600">
<AlertTriangle className="w-5 h-5" />
</AlertDialogTitle>
<AlertDialogDescription>
<span className="font-semibold text-red-600"></span> <span className="font-semibold text-gray-900">"{selectedProject?.name}"</span>
<br />
<br />
<div className="bg-red-50 border border-red-200 rounded-lg p-4 my-3">
<p className="text-red-800 font-semibold mb-2"> </p>
<ul className="list-disc list-inside text-red-700 space-y-1 text-sm">
<li></li>
<li></li>
<li></li>
</ul>
</div>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel></AlertDialogCancel>
<AlertDialogAction
onClick={handleConfirmPermanentDelete}
className="bg-red-600 hover:bg-red-700 focus:ring-red-600"
>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}