/** * File Selection Dialog * Cyberpunk Terminal Aesthetic */ import React, { useState, useEffect, useMemo, useCallback } from "react"; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Checkbox } from "@/components/ui/checkbox"; import { Badge } from "@/components/ui/badge"; import { Search, FileText, CheckSquare, Square, FolderOpen, Folder, ChevronRight, ChevronDown, FileCode, FileJson, File, Filter, RotateCcw, RefreshCw, Terminal, } from "lucide-react"; import { api } from "@/shared/config/database"; import { toast } from "sonner"; interface FileSelectionDialogProps { open: boolean; onOpenChange: (open: boolean) => void; projectId: string; branch?: string; excludePatterns?: string[]; onConfirm: (selectedFiles: string[]) => void; } interface FileNode { path: string; size: number; } interface FolderNode { name: string; path: string; files: FileNode[]; subfolders: Map; expanded: boolean; } // 文件类型图标映射 const getFileIcon = (path: string) => { const ext = path.split(".").pop()?.toLowerCase() || ""; const codeExts = [ "js", "ts", "tsx", "jsx", "py", "java", "go", "rs", "cpp", "c", "h", "cs", "php", "rb", "swift", "kt", "sh", ]; const configExts = ["json", "yml", "yaml", "toml", "xml", "ini"]; if (codeExts.includes(ext)) { return ; } if (configExts.includes(ext)) { return ; } return ; }; // 获取文件扩展名 const getExtension = (path: string): string => { const ext = path.split(".").pop()?.toLowerCase() || ""; return ext; }; // 构建文件夹树结构 const buildFolderTree = (files: FileNode[]): FolderNode => { const root: FolderNode = { name: "", path: "", files: [], subfolders: new Map(), expanded: true, }; files.forEach((file) => { const parts = file.path.split("/"); let current = root; // 遍历路径的每个部分(除了文件名) for (let i = 0; i < parts.length - 1; i++) { const folderName = parts[i]; const folderPath = parts.slice(0, i + 1).join("/"); if (!current.subfolders.has(folderName)) { current.subfolders.set(folderName, { name: folderName, path: folderPath, files: [], subfolders: new Map(), expanded: true, }); } current = current.subfolders.get(folderName)!; } // 添加文件到当前文件夹 current.files.push(file); }); return root; }; export default function FileSelectionDialog({ open, onOpenChange, projectId, branch, excludePatterns, onConfirm, }: FileSelectionDialogProps) { const [files, setFiles] = useState([]); const [loading, setLoading] = useState(false); const [selectedFiles, setSelectedFiles] = useState>(new Set()); const [searchTerm, setSearchTerm] = useState(""); const [expandedFolders, setExpandedFolders] = useState>(new Set()); const [viewMode, setViewMode] = useState<"tree" | "flat">("tree"); const [filterType, setFilterType] = useState(""); useEffect(() => { if (open && projectId) { loadFiles(); } else { setFiles([]); setSelectedFiles(new Set()); setSearchTerm(""); setExpandedFolders(new Set()); setFilterType(""); } }, [open, projectId, branch, excludePatterns]); const loadFiles = async () => { try { setLoading(true); const data = await api.getProjectFiles(projectId, branch, excludePatterns); setFiles(data); setSelectedFiles(new Set(data.map((f) => f.path))); // 默认展开所有文件夹 const folders = new Set(); data.forEach((f) => { const parts = f.path.split("/"); for (let i = 1; i < parts.length; i++) { folders.add(parts.slice(0, i).join("/")); } }); setExpandedFolders(folders); } catch (error) { console.error("Failed to load files:", error); toast.error("加载文件列表失败"); } finally { setLoading(false); } }; // 获取所有文件类型 const fileTypes = useMemo(() => { const types = new Map(); files.forEach((f) => { const ext = getExtension(f.path); if (ext) { types.set(ext, (types.get(ext) || 0) + 1); } }); return Array.from(types.entries()).sort((a, b) => b[1] - a[1]); }, [files]); // 过滤后的文件 const filteredFiles = useMemo(() => { let result = files; // 按搜索词过滤 if (searchTerm) { const term = searchTerm.toLowerCase(); result = result.filter((f) => f.path.toLowerCase().includes(term)); } // 按文件类型过滤 if (filterType) { result = result.filter((f) => getExtension(f.path) === filterType); } return result; }, [files, searchTerm, filterType]); // 构建文件夹树 const folderTree = useMemo(() => buildFolderTree(filteredFiles), [filteredFiles]); const handleToggleFile = useCallback((path: string) => { setSelectedFiles((prev) => { const newSelected = new Set(prev); if (newSelected.has(path)) { newSelected.delete(path); } else { newSelected.add(path); } return newSelected; }); }, []); const handleToggleFolder = useCallback( (folderPath: string) => { // 获取该文件夹下的所有文件 const folderFiles = filteredFiles.filter( (f) => f.path.startsWith(folderPath + "/") || f.path === folderPath ); setSelectedFiles((prev) => { const newSelected = new Set(prev); const allSelected = folderFiles.every((f) => newSelected.has(f.path)); if (allSelected) { // 取消选择该文件夹下的所有文件 folderFiles.forEach((f) => newSelected.delete(f.path)); } else { // 选择该文件夹下的所有文件 folderFiles.forEach((f) => newSelected.add(f.path)); } return newSelected; }); }, [filteredFiles] ); const handleExpandFolder = useCallback((folderPath: string) => { setExpandedFolders((prev) => { const newExpanded = new Set(prev); if (newExpanded.has(folderPath)) { newExpanded.delete(folderPath); } else { newExpanded.add(folderPath); } return newExpanded; }); }, []); const handleSelectAll = () => { setSelectedFiles(new Set(filteredFiles.map((f) => f.path))); }; const handleDeselectAll = () => { const filteredPaths = new Set(filteredFiles.map((f) => f.path)); setSelectedFiles((prev) => { const newSelected = new Set(prev); filteredPaths.forEach((p) => newSelected.delete(p)); return newSelected; }); }; const handleInvertSelection = () => { const filteredPaths = new Set(filteredFiles.map((f) => f.path)); setSelectedFiles((prev) => { const newSelected = new Set(prev); filteredPaths.forEach((p) => { if (newSelected.has(p)) { newSelected.delete(p); } else { newSelected.add(p); } }); return newSelected; }); }; const handleConfirm = () => { if (selectedFiles.size === 0) { toast.error("请至少选择一个文件"); return; } onConfirm(Array.from(selectedFiles)); onOpenChange(false); }; const formatSize = (bytes: number) => { if (bytes === 0) return ""; if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / 1024 / 1024).toFixed(1)} MB`; }; // 检查文件夹的选中状态 const getFolderSelectionState = ( folderPath: string ): "all" | "some" | "none" => { const folderFiles = filteredFiles.filter((f) => f.path.startsWith(folderPath + "/") ); if (folderFiles.length === 0) return "none"; const selectedCount = folderFiles.filter((f) => selectedFiles.has(f.path) ).length; if (selectedCount === 0) return "none"; if (selectedCount === folderFiles.length) return "all"; return "some"; }; // 渲染文件夹树 const renderFolderTree = (node: FolderNode, depth: number = 0) => { const items: React.ReactNode[] = []; // 渲染子文件夹 Array.from(node.subfolders.values()) .sort((a, b) => a.name.localeCompare(b.name)) .forEach((folder) => { const isExpanded = expandedFolders.has(folder.path); const selectionState = getFolderSelectionState(folder.path); items.push(
e.stopPropagation()}> { if (el) { (el as HTMLButtonElement).dataset.state = selectionState === "some" ? "indeterminate" : selectionState === "all" ? "checked" : "unchecked"; } }} onCheckedChange={() => handleToggleFolder(folder.path)} className="border-gray-600 data-[state=checked]:bg-primary data-[state=checked]:border-primary data-[state=indeterminate]:bg-gray-500" />
{isExpanded ? ( ) : ( )} handleExpandFolder(folder.path)} > {folder.name} { filteredFiles.filter((f) => f.path.startsWith(folder.path + "/") ).length }
{isExpanded && renderFolderTree(folder, depth + 1)}
); }); // 渲染文件 node.files .sort((a, b) => a.path.localeCompare(b.path)) .forEach((file) => { const fileName = file.path.split("/").pop() || file.path; items.push(
handleToggleFile(file.path)} >
e.stopPropagation()}> handleToggleFile(file.path)} className="border-gray-600 data-[state=checked]:bg-primary data-[state=checked]:border-primary" />
{getFileIcon(file.path)} {fileName} {file.size > 0 && ( {formatSize(file.size)} )}
); }); return items; }; // 渲染扁平列表 const renderFlatList = () => { return filteredFiles.map((file) => (
handleToggleFile(file.path)} >
e.stopPropagation()}> handleToggleFile(file.path)} className="border-gray-600 data-[state=checked]:bg-primary data-[state=checked]:border-primary" />
{getFileIcon(file.path)}

{file.path}

{file.size > 0 && ( {formatSize(file.size)} )}
)); }; return (
选择要审计的文件
{excludePatterns && excludePatterns.length > 0 && ( 已排除 {excludePatterns.length} 种模式 )}
{/* 工具栏 */}
{/* 搜索框 */}
setSearchTerm(e.target.value)} className="pl-10 h-9 cyber-input" />
{/* 文件类型筛选 */} {fileTypes.length > 0 && (
)} {/* 视图切换 */}
{/* 操作按钮 */}
{(searchTerm || filterType) && ( )}
{searchTerm || filterType ? ( 筛选: {filteredFiles.length}/{files.length} 个文件, 已选 {selectedFiles.size} ) : ( 共 {files.length} 个文件,已选 {selectedFiles.size} )}
{/* 文件列表 */}
{loading ? (
) : filteredFiles.length > 0 ? (
{viewMode === "tree" ? renderFolderTree(folderTree) : renderFlatList()}
) : (

{searchTerm || filterType ? "没有匹配的文件" : "没有找到文件"}

)}
提示:点击文件夹可展开/折叠,点击文件夹复选框可批量选择
); }