Add comprehensive file information display to file manager

Backend improvements:
- Enhanced ls -la parsing to extract complete file metadata (size, permissions, owner, group, modified date)
- Added support for symbolic link target detection
- Changed API response format to include both files array and current path
- Improved file path construction logic

Frontend improvements:
- Updated global FileItem interface with all file metadata fields
- Removed duplicate local FileItem definitions across components
- Added formatFileSize utility with proper 0-byte handling ("0 B" instead of "-")
- Fixed 0-byte file display logic (changed from falsy check to explicit null/undefined check)
- Implemented file size display in both grid and list views
- Added smart filename collision handling with auto-incrementing suffixes
- Enhanced create-then-edit workflow to preserve items even when canceled
- Improved inline editing input styling to match shadcn design system
- Optimized input field dimensions (width constraints: 60-120px grid, max 200px list)

File creation improvements:
- Removed spaces from default names to avoid path issues (NewFile.txt, NewFolder)
- Added intelligent unique name generation (NewFile.txt → NewFile1.txt → NewFile2.txt)
- Changed cancel behavior to create items with default names instead of discarding
- Fixed SSH connection reliability with auto-reconnection for all operations

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ZacharyZcR
2025-09-16 19:12:49 +08:00
parent fb7b452a1c
commit 16de73d6ad
4 changed files with 130 additions and 65 deletions

View File

@@ -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}
</p>
)}
{file.size && file.type === 'file' && (
{file.type === 'file' && file.size !== undefined && file.size !== null && (
<p className="text-xs text-muted-foreground mt-1">
{formatFileSize(file.size)}
</p>
@@ -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({
{/* 文件大小 */}
<div className="flex-shrink-0 text-right">
{file.type === 'file' && file.size && (
{file.type === 'file' && file.size !== undefined && file.size !== null && (
<p className="text-xs text-muted-foreground">
{formatFileSize(file.size)}
</p>

View File

@@ -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;
}
// 过滤文件并添加新建的临时项目