diff --git a/src/backend/ssh/file-manager.ts b/src/backend/ssh/file-manager.ts index 9614a93d..bc1aebe2 100644 --- a/src/backend/ssh/file-manager.ts +++ b/src/backend/ssh/file-manager.ts @@ -311,20 +311,50 @@ app.get("/ssh/file_manager/ssh/listFiles", (req, res) => { const parts = line.split(/\s+/); if (parts.length >= 9) { const permissions = parts[0]; - const name = parts.slice(8).join(" "); + const linkCount = parts[1]; + const owner = parts[2]; + const group = parts[3]; + const size = parseInt(parts[4], 10); + + // 日期可能占夨3个部分(月 日 时间)或者是(月 日 年) + let dateStr = ""; + let nameStartIndex = 8; + + if (parts[5] && parts[6] && parts[7]) { + // 常规格式: 月 日 时间/年 + dateStr = `${parts[5]} ${parts[6]} ${parts[7]}`; + } + + const name = parts.slice(nameStartIndex).join(" "); const isDirectory = permissions.startsWith("d"); const isLink = permissions.startsWith("l"); if (name === "." || name === "..") continue; + // 解析符号链接目标 + let actualName = name; + let linkTarget = undefined; + if (isLink && name.includes(" -> ")) { + const linkParts = name.split(" -> "); + actualName = linkParts[0]; + linkTarget = linkParts[1]; + } + files.push({ - name, + name: actualName, type: isDirectory ? "directory" : isLink ? "link" : "file", + size: isDirectory ? undefined : size, // 目录不显示大小 + modified: dateStr, + permissions, + owner, + group, + linkTarget, // 符号链接的目标 + path: `${sshPath.endsWith('/') ? sshPath : sshPath + '/'}${actualName}` // 添加完整路径 }); } } - res.json(files); + res.json({ files, path: sshPath }); }); }); }); diff --git a/src/types/index.ts b/src/types/index.ts index dbc1d987..fa9c5e1f 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -182,8 +182,14 @@ export interface FileItem { name: string; path: string; isPinned?: boolean; - type: "file" | "directory"; + type: "file" | "directory" | "link"; sshSessionId?: string; + size?: number; + modified?: string; + permissions?: string; + owner?: string; + group?: string; + linkTarget?: string; } export interface ShortcutItem { diff --git a/src/ui/Desktop/Apps/File Manager/FileManagerGrid.tsx b/src/ui/Desktop/Apps/File Manager/FileManagerGrid.tsx index 964631a5..9e72d17e 100644 --- a/src/ui/Desktop/Apps/File Manager/FileManagerGrid.tsx +++ b/src/ui/Desktop/Apps/File Manager/FileManagerGrid.tsx @@ -18,16 +18,31 @@ import { ArrowUp } from "lucide-react"; import { useTranslation } from "react-i18next"; +import type { FileItem } from "../../../types/index.js"; -interface FileItem { - name: string; - type: "file" | "directory" | "link"; - path: string; - size?: number; - modified?: string; - permissions?: string; - owner?: string; - group?: string; +// 格式化文件大小 +function formatFileSize(bytes?: number): string { + // 处理未定义或null的情况 + if (bytes === undefined || bytes === null) return '-'; + + // 0字节的文件显示为 "0 B" + if (bytes === 0) return '0 B'; + + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + let size = bytes; + let unitIndex = 0; + + while (size >= 1024 && unitIndex < units.length - 1) { + size /= 1024; + unitIndex++; + } + + // 对于小于10的数值显示一位小数,大于10的显示整数 + const formattedSize = size < 10 && unitIndex > 0 + ? size.toFixed(1) + : Math.round(size).toString(); + + return `${formattedSize} ${units[unitIndex]}`; } interface FileManagerGridProps { @@ -114,12 +129,6 @@ const getFileIcon = (fileName: string, isDirectory: boolean, viewMode: 'grid' | } }; -const formatFileSize = (bytes?: number): string => { - if (!bytes) return ''; - const sizes = ['B', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(1024)); - return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`; -}; export function FileManagerGrid({ files, @@ -535,7 +544,11 @@ export function FileManagerGrid({ onChange={(e) => setEditingName(e.target.value)} onKeyDown={handleEditKeyDown} onBlur={handleEditConfirm} - className="w-full text-xs bg-blue-50 text-gray-900 px-2 py-1 rounded border border-blue-300 text-center shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + className={cn( + "max-w-[120px] min-w-[60px] w-full rounded-md border border-input bg-background px-2 py-1 text-xs shadow-xs transition-[color,box-shadow] outline-none", + "text-center text-foreground placeholder:text-muted-foreground", + "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[2px]" + )} onClick={(e) => e.stopPropagation()} /> ) : ( @@ -553,7 +566,7 @@ export function FileManagerGrid({ {file.name}
)} - {file.size && file.type === 'file' && ( + {file.type === 'file' && file.size !== undefined && file.size !== null && ({formatFileSize(file.size)}
@@ -600,7 +613,11 @@ export function FileManagerGrid({ onChange={(e) => setEditingName(e.target.value)} onKeyDown={handleEditKeyDown} onBlur={handleEditConfirm} - className="w-full text-sm bg-blue-50 text-gray-900 px-3 py-1.5 rounded-md border border-blue-300 shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all duration-200" + className={cn( + "flex-1 min-w-0 max-w-[200px] rounded-md border border-input bg-background px-2 py-1 text-sm shadow-xs transition-[color,box-shadow] outline-none", + "text-foreground placeholder:text-muted-foreground", + "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[2px]" + )} onClick={(e) => e.stopPropagation()} /> ) : ( @@ -627,7 +644,7 @@ export function FileManagerGrid({ {/* 文件大小 */}{formatFileSize(file.size)}
diff --git a/src/ui/Desktop/Apps/File Manager/FileManagerModern.tsx b/src/ui/Desktop/Apps/File Manager/FileManagerModern.tsx index 0e225a2f..8169aad0 100644 --- a/src/ui/Desktop/Apps/File Manager/FileManagerModern.tsx +++ b/src/ui/Desktop/Apps/File Manager/FileManagerModern.tsx @@ -20,7 +20,7 @@ import { Eye, Settings } from "lucide-react"; -import type { SSHHost } from "../../../types/index.js"; +import type { SSHHost, FileItem } from "../../../types/index.js"; import { listSSHFiles, uploadSSHFile, @@ -33,16 +33,6 @@ import { getSSHStatus } from "@/ui/main-axios.ts"; -interface FileItem { - name: string; - type: "file" | "directory" | "link"; - path: string; - size?: number; - modified?: string; - permissions?: string; - owner?: string; - group?: string; -} interface FileManagerModernProps { initialHost?: SSHHost | null; @@ -187,19 +177,15 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) { return; } - const contents = await listSSHFiles(sshSessionId, path); - console.log("Directory contents loaded:", contents?.length || 0, "items"); - console.log("Raw file data from backend:", contents); + const response = await listSSHFiles(sshSessionId, path); + console.log("Directory response from backend:", response); - // 为文件添加完整路径 - const filesWithPath = (contents || []).map(file => ({ - ...file, - path: path + (path.endsWith("/") ? "" : "/") + file.name - })); + // 处理新的返回格式 { files: FileItem[], path: string } + const files = Array.isArray(response) ? response : response?.files || []; + console.log("Directory contents loaded:", files.length, "items"); + console.log("Files with sizes:", files.map(f => ({ name: f.name, size: f.size, type: f.type }))); - console.log("Files with constructed paths:", filesWithPath.map(f => ({ name: f.name, path: f.path }))); - - setFiles(filesWithPath); + setFiles(files); clearSelection(); } catch (error: any) { console.error("Failed to load directory:", error); @@ -321,42 +307,42 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) { } function handleCreateNewFolder() { - const defaultName = "NewFolder"; // 移除空格避免路径问题 + const baseName = "NewFolder"; + const uniqueName = generateUniqueName(baseName, 'directory'); const folderPath = currentPath.endsWith('/') - ? `${currentPath}${defaultName}` - : `${currentPath}/${defaultName}`; + ? `${currentPath}${uniqueName}` + : `${currentPath}/${uniqueName}`; - // 直接进入编辑模式,不在服务器创建文件夹 + // 直接进入编辑模式,使用唯一名字 const newFolder: FileItem = { - name: defaultName, + name: uniqueName, type: 'directory', path: folderPath }; - console.log('Starting edit for new folder (not created yet):', newFolder); + console.log('Starting edit for new folder with unique name:', newFolder); setEditingFile(newFolder); setIsCreatingNewFile(true); - console.log('Edit state set:', { editingFile: newFolder, isCreatingNewFile: true }); } function handleCreateNewFile() { - const defaultName = "NewFile.txt"; // 移除空格避免路径问题 + const baseName = "NewFile.txt"; + const uniqueName = generateUniqueName(baseName, 'file'); const filePath = currentPath.endsWith('/') - ? `${currentPath}${defaultName}` - : `${currentPath}/${defaultName}`; + ? `${currentPath}${uniqueName}` + : `${currentPath}/${uniqueName}`; - // 直接进入编辑模式,不在服务器创建文件 + // 直接进入编辑模式,使用唯一名字 const newFile: FileItem = { - name: defaultName, + name: uniqueName, type: 'file', path: filePath, size: 0 }; - console.log('Starting edit for new file (not created yet):', newFile); + console.log('Starting edit for new file with unique name:', newFile); setEditingFile(newFile); setIsCreatingNewFile(true); - console.log('Edit state set:', { editingFile: newFile, isCreatingNewFile: true }); } function handleFileOpen(file: FileItem) { @@ -549,14 +535,40 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) { setEditingFile(file); } - // 取消编辑 - function handleCancelEdit() { + // 取消编辑(现在也会保留项目) + async function handleCancelEdit() { if (isCreatingNewFile && editingFile) { - // 如果是新建文件/文件夹且取消了编辑,删除刚创建的项目 - handleDeleteFiles([editingFile]); - setIsCreatingNewFile(false); + // 取消时也使用默认名字创建项目 + console.log('Creating item with default name on cancel:', editingFile.name); + await handleRenameConfirm(editingFile, editingFile.name); + } else { + setEditingFile(null); } - setEditingFile(null); + } + + // 生成唯一名字(处理重名冲突) + function generateUniqueName(baseName: string, type: 'file' | 'directory'): string { + const existingNames = files.map(f => f.name.toLowerCase()); + let candidateName = baseName; + let counter = 1; + + // 如果名字已存在,尝试添加数字后缀 + while (existingNames.includes(candidateName.toLowerCase())) { + if (type === 'file' && baseName.includes('.')) { + // 对于文件,在文件名和扩展名之间添加数字 + const lastDotIndex = baseName.lastIndexOf('.'); + const nameWithoutExt = baseName.substring(0, lastDotIndex); + const extension = baseName.substring(lastDotIndex); + candidateName = `${nameWithoutExt}${counter}${extension}`; + } else { + // 对于文件夹或没有扩展名的文件,直接添加数字 + candidateName = `${baseName}${counter}`; + } + counter++; + } + + console.log(`Generated unique name: ${baseName} -> ${candidateName}`); + return candidateName; } // 过滤文件并添加新建的临时项目