/** * Projects Page * Cyberpunk Terminal Aesthetic */ import { useState, useEffect, useRef } from "react"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Progress } from "@/components/ui/progress"; import { Plus, Search, GitBranch, Calendar, Users, Code, Shield, Activity, Upload, FileText, AlertCircle, Trash2, Edit, CheckCircle, Terminal, Github, Folder, ArrowUpRight, Key } from "lucide-react"; import { api } from "@/shared/config/database"; import { validateZipFile } from "@/features/projects/services"; import type { Project, CreateProjectForm } from "@/shared/types"; import { uploadZipFile, getZipFileInfo, type ZipFileMeta } from "@/shared/utils/zipStorage"; import { isRepositoryProject, isZipProject, getSourceTypeBadge } from "@/shared/utils/projectUtils"; import { Link } from "react-router-dom"; import { toast } from "sonner"; import CreateTaskDialog from "@/components/audit/CreateTaskDialog"; import TerminalProgressDialog from "@/components/audit/TerminalProgressDialog"; import { SUPPORTED_LANGUAGES, REPOSITORY_PLATFORMS } from "@/shared/constants"; export default function Projects() { const [projects, setProjects] = useState([]); const [loading, setLoading] = useState(true); const [searchTerm, setSearchTerm] = useState(""); const [showCreateDialog, setShowCreateDialog] = useState(false); const [showCreateTaskDialog, setShowCreateTaskDialog] = useState(false); const [selectedProjectForTask, setSelectedProjectForTask] = useState(""); const [showTerminal, setShowTerminal] = useState(false); const [currentTaskId, setCurrentTaskId] = useState(null); const [uploadProgress, setUploadProgress] = useState(0); const [uploading, setUploading] = useState(false); const fileInputRef = useRef(null); const [showDeleteDialog, setShowDeleteDialog] = useState(false); const [projectToDelete, setProjectToDelete] = useState(null); const [showEditDialog, setShowEditDialog] = useState(false); const [projectToEdit, setProjectToEdit] = useState(null); const [editForm, setEditForm] = useState({ name: "", description: "", source_type: "repository", repository_url: "", repository_type: "gitea", default_branch: "main", programming_languages: [] }); const [createForm, setCreateForm] = useState({ name: "", description: "", source_type: "repository", repository_url: "", repository_type: "gitea", default_branch: "main", programming_languages: [] }); const [selectedFile, setSelectedFile] = useState(null); // 编辑对话框中的ZIP文件状态 const [editZipInfo, setEditZipInfo] = useState(null); const [editZipFile, setEditZipFile] = useState(null); const [loadingEditZipInfo, setLoadingEditZipInfo] = useState(false); const editZipInputRef = useRef(null); // 将小写语言名转换为显示格式 const formatLanguageName = (lang: string): string => { const nameMap: Record = { 'javascript': 'JavaScript', 'typescript': 'TypeScript', 'python': 'Python', 'java': 'Java', 'go': 'Go', 'rust': 'Rust', 'cpp': 'C++', 'csharp': 'C#', 'php': 'PHP', 'ruby': 'Ruby', 'swift': 'Swift', 'kotlin': 'Kotlin' }; return nameMap[lang] || lang.charAt(0).toUpperCase() + lang.slice(1); }; const supportedLanguages = SUPPORTED_LANGUAGES.map(formatLanguageName); useEffect(() => { loadProjects(); }, []); const loadProjects = async () => { try { setLoading(true); const data = await api.getProjects(); setProjects(data); } catch (error) { console.error('Failed to load projects:', error); toast.error("加载项目失败"); } finally { setLoading(false); } }; const handleFastScanStarted = (taskId: string) => { setCurrentTaskId(taskId); setShowTerminal(true); }; const handleCreateProject = async () => { if (!createForm.name.trim()) { toast.error("请输入项目名称"); return; } try { await api.createProject({ ...createForm, } as any); import('@/shared/utils/logger').then(({ logger }) => { logger.logUserAction('创建项目', { projectName: createForm.name, repositoryType: createForm.repository_type, languages: createForm.programming_languages, }); }); toast.success("项目创建成功"); setShowCreateDialog(false); resetCreateForm(); loadProjects(); } catch (error) { console.error('Failed to create project:', error); import('@/shared/utils/errorHandler').then(({ handleError }) => { handleError(error, '创建项目失败'); }); const errorMessage = error instanceof Error ? error.message : '未知错误'; toast.error(`创建项目失败: ${errorMessage}`); } }; const resetCreateForm = () => { setCreateForm({ name: "", description: "", source_type: "repository", repository_url: "", repository_type: "gitea", default_branch: "main", programming_languages: [] }); setSelectedFile(null); if (fileInputRef.current) { fileInputRef.current.value = ''; } }; const handleFileSelect = (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (!file) return; const validation = validateZipFile(file); if (!validation.valid) { toast.error(validation.error); return; } setSelectedFile(file); event.target.value = ''; }; const handleUploadAndCreate = async () => { if (!selectedFile) { toast.error("请先选择ZIP文件"); return; } if (!createForm.name.trim()) { toast.error("请先输入项目名称"); return; } try { setUploading(true); setUploadProgress(0); const progressInterval = setInterval(() => { setUploadProgress(prev => { if (prev >= 100) { clearInterval(progressInterval); return 100; } return prev + 20; }); }, 100); const project = await api.createProject({ ...createForm, source_type: "zip", repository_type: "other", repository_url: undefined } as any); try { await uploadZipFile(project.id, selectedFile); } catch (error) { console.error('保存ZIP文件失败:', error); } clearInterval(progressInterval); setUploadProgress(100); import('@/shared/utils/logger').then(({ logger }) => { logger.logUserAction('上传ZIP文件创建项目', { projectName: project.name, fileName: selectedFile.name, fileSize: selectedFile.size, }); }); setShowCreateDialog(false); resetCreateForm(); loadProjects(); toast.success(`项目 "${project.name}" 已创建`, { description: 'ZIP文件已保存,您可以启动代码审计', duration: 4000 }); } catch (error: any) { console.error('Upload failed:', error); import('@/shared/utils/errorHandler').then(({ handleError }) => { handleError(error, '上传ZIP文件失败'); }); const errorMessage = error?.message || '未知错误'; toast.error(`上传失败: ${errorMessage}`); } finally { setUploading(false); setUploadProgress(0); } }; const filteredProjects = projects.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 ; case 'gitea': return ; case 'other': return ; default: return ; } }; const formatDate = (dateString: string) => { return new Date(dateString).toLocaleDateString('zh-CN'); }; const handleCreateTask = (projectId: string) => { setSelectedProjectForTask(projectId); setShowCreateTaskDialog(true); }; const handleEditClick = async (project: Project) => { setProjectToEdit(project); setEditForm({ name: project.name, description: project.description || "", source_type: project.source_type || "repository", repository_url: project.repository_url || "", repository_type: project.repository_type || "github", default_branch: project.default_branch || "main", programming_languages: project.programming_languages ? JSON.parse(project.programming_languages) : [] }); setEditZipFile(null); setEditZipInfo(null); setShowEditDialog(true); if (project.source_type === 'zip') { setLoadingEditZipInfo(true); try { const zipInfo = await getZipFileInfo(project.id); setEditZipInfo(zipInfo); } catch (error) { console.error('加载ZIP文件信息失败:', error); } finally { setLoadingEditZipInfo(false); } } }; const handleSaveEdit = async () => { if (!projectToEdit) return; if (!editForm.name.trim()) { toast.error("项目名称不能为空"); return; } try { await api.updateProject(projectToEdit.id, editForm); if (editZipFile && editForm.source_type === 'zip') { const result = await uploadZipFile(projectToEdit.id, editZipFile); if (result.success) { toast.success(`ZIP文件已更新: ${result.original_filename}`); } else { toast.error(`ZIP文件上传失败: ${result.message}`); } } toast.success(`项目 "${editForm.name}" 已更新`); setShowEditDialog(false); setProjectToEdit(null); setEditZipFile(null); setEditZipInfo(null); loadProjects(); } catch (error) { console.error('Failed to update project:', error); toast.error("更新项目失败"); } }; const handleToggleLanguage = (lang: string) => { const currentLanguages = editForm.programming_languages || []; const newLanguages = currentLanguages.includes(lang) ? currentLanguages.filter(l => l !== lang) : [...currentLanguages, lang]; setEditForm({ ...editForm, programming_languages: newLanguages }); }; const handleDeleteClick = (project: Project) => { setProjectToDelete(project); setShowDeleteDialog(true); }; const handleConfirmDelete = async () => { if (!projectToDelete) return; try { await api.deleteProject(projectToDelete.id); import('@/shared/utils/logger').then(({ logger }) => { logger.logUserAction('删除项目', { projectId: projectToDelete.id, projectName: projectToDelete.name, }); }); toast.success(`项目 "${projectToDelete.name}" 已移到回收站`, { description: '您可以在回收站中恢复此项目', duration: 4000 }); setShowDeleteDialog(false); setProjectToDelete(null); loadProjects(); } catch (error) { console.error('Failed to delete project:', error); import('@/shared/utils/errorHandler').then(({ handleError }) => { handleError(error, '删除项目失败'); }); const errorMessage = error instanceof Error ? error.message : '未知错误'; toast.error(`删除项目失败: ${errorMessage}`); } }; const handleTaskCreated = () => { toast.success("审计任务已创建", { description: '因为网络和代码文件大小等因素,审计时长通常至少需要1分钟,请耐心等待...', duration: 5000 }); }; if (loading) { return (

加载项目数据...

); } return (
{/* Grid background */}
{/* 创建项目对话框 */} {/* Terminal Header */}
new_project@deepaudit
初始化新项目
Git 仓库 上传源码
setCreateForm({ ...createForm, name: e.target.value })} placeholder="输入项目名称" className="cyber-input" />