优化文件管理器视觉设计和安全性

核心改进:
- 统一图标色调:所有文件类型图标改为黑白配色,消除彩色差异
- 恢复原始主题:修复shadcn样式导致的过暗背景问题
- 增强大文件安全:后端10MB文件大小限制,防止内存溢出
- 优化警告样式:Large File Warning使用shadcn设计规范

技术细节:
- getFileIcon()全部使用text-muted-foreground统一色调
- 恢复bg-dark-bg/border-dark-border原始主题色彩
- readFile API增加stat文件大小检查和错误处理
- FileViewer警告组件使用destructive色彩体系

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ZacharyZcR
2025-09-16 20:15:42 +08:00
parent 12733685d7
commit bf166d602f
4 changed files with 109 additions and 49 deletions

View File

@@ -69,11 +69,11 @@ const getFileIcon = (file: FileItem, viewMode: 'grid' | 'list' = 'grid') => {
const iconClass = viewMode === 'grid' ? "w-8 h-8" : "w-6 h-6";
if (file.type === 'directory') {
return <Folder className={`${iconClass} text-blue-400`} />;
return <Folder className={`${iconClass} text-muted-foreground`} />;
}
if (file.type === 'link') {
return <FileSymlink className={`${iconClass} text-cyan-400`} />;
return <FileSymlink className={`${iconClass} text-muted-foreground`} />;
}
const ext = file.name.split('.').pop()?.toLowerCase();
@@ -82,30 +82,30 @@ const getFileIcon = (file: FileItem, viewMode: 'grid' | 'list' = 'grid') => {
case 'txt':
case 'md':
case 'readme':
return <FileText className={`${iconClass} text-gray-400`} />;
return <FileText className={`${iconClass} text-muted-foreground`} />;
case 'png':
case 'jpg':
case 'jpeg':
case 'gif':
case 'bmp':
case 'svg':
return <FileImage className={`${iconClass} text-green-400`} />;
return <FileImage className={`${iconClass} text-muted-foreground`} />;
case 'mp4':
case 'avi':
case 'mkv':
case 'mov':
return <FileVideo className={`${iconClass} text-purple-400`} />;
return <FileVideo className={`${iconClass} text-muted-foreground`} />;
case 'mp3':
case 'wav':
case 'flac':
case 'ogg':
return <FileAudio className={`${iconClass} text-pink-400`} />;
return <FileAudio className={`${iconClass} text-muted-foreground`} />;
case 'zip':
case 'tar':
case 'gz':
case 'rar':
case '7z':
return <Archive className={`${iconClass} text-orange-400`} />;
return <Archive className={`${iconClass} text-muted-foreground`} />;
case 'js':
case 'ts':
case 'jsx':
@@ -119,7 +119,7 @@ const getFileIcon = (file: FileItem, viewMode: 'grid' | 'list' = 'grid') => {
case 'rb':
case 'go':
case 'rs':
return <Code className={`${iconClass} text-yellow-400`} />;
return <Code className={`${iconClass} text-muted-foreground`} />;
case 'json':
case 'xml':
case 'yaml':
@@ -128,9 +128,9 @@ const getFileIcon = (file: FileItem, viewMode: 'grid' | 'list' = 'grid') => {
case 'ini':
case 'conf':
case 'config':
return <Settings className={`${iconClass} text-cyan-400`} />;
return <Settings className={`${iconClass} text-muted-foreground`} />;
default:
return <File className={`${iconClass} text-gray-400`} />;
return <File className={`${iconClass} text-muted-foreground`} />;
}
};
@@ -613,7 +613,7 @@ export function FileManagerGrid({
<div className="flex items-center px-3 py-2 text-sm">
<button
onClick={() => navigateToPath(-1)}
className="hover:text-blue-400 hover:underline mr-1"
className="hover:text-primary hover:underline mr-1"
>
/
</button>
@@ -621,7 +621,7 @@ export function FileManagerGrid({
<React.Fragment key={index}>
<button
onClick={() => navigateToPath(index)}
className="hover:text-blue-400 hover:underline"
className="hover:text-primary hover:underline"
>
{part}
</button>
@@ -656,8 +656,8 @@ export function FileManagerGrid({
{isDragging && (
<div className="absolute inset-0 flex items-center justify-center bg-blue-500/10 backdrop-blur-sm z-10 pointer-events-none">
<div className="text-center">
<Download className="w-12 h-12 mx-auto mb-2 text-blue-500" />
<p className="text-lg font-medium text-blue-500">
<Download className="w-12 h-12 mx-auto mb-2 text-primary" />
<p className="text-lg font-medium text-primary">
{t("fileManager.dragFilesToUpload")}
</p>
</div>
@@ -693,8 +693,8 @@ export function FileManagerGrid({
data-file-path={file.path}
className={cn(
"group p-3 rounded-lg cursor-pointer transition-all",
"hover:bg-dark-hover border-2 border-transparent",
isSelected && "bg-blue-500/20 border-blue-500"
"hover:bg-accent hover:text-accent-foreground border-2 border-transparent",
isSelected && "bg-primary/20 border-primary"
)}
title={`${file.name} - Selected: ${isSelected} - SelectedCount: ${selectedFiles.length}`}
onClick={(e) => handleFileClick(file, e)}
@@ -721,7 +721,7 @@ export function FileManagerGrid({
onKeyDown={handleEditKeyDown}
onBlur={handleEditConfirm}
className={cn(
"max-w-[120px] min-w-[60px] w-fit rounded-md border border-input bg-background px-2 py-1 text-xs shadow-xs transition-[color,box-shadow] outline-none",
"max-w-[120px] min-w-[60px] w-fit rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 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]"
)}
@@ -729,7 +729,7 @@ export function FileManagerGrid({
/>
) : (
<p
className="text-xs text-white truncate cursor-pointer hover:bg-white/10 px-1 py-0.5 rounded transition-colors duration-150 w-fit max-w-full text-center"
className="text-xs text-foreground truncate cursor-pointer hover:bg-accent px-1 py-0.5 rounded transition-colors duration-150 w-fit max-w-full text-center"
title={`${file.name} (点击重命名)`}
onClick={(e) => {
// 阻止文件选择事件
@@ -748,7 +748,7 @@ export function FileManagerGrid({
</p>
)}
{file.type === 'link' && file.linkTarget && (
<p className="text-xs text-cyan-400 mt-1 truncate max-w-full" title={file.linkTarget}>
<p className="text-xs text-primary mt-1 truncate max-w-full" title={file.linkTarget}>
{file.linkTarget}
</p>
)}
@@ -770,8 +770,8 @@ export function FileManagerGrid({
data-file-path={file.path}
className={cn(
"flex items-center gap-3 p-2 rounded cursor-pointer transition-all",
"hover:bg-dark-hover",
isSelected && "bg-blue-500/20"
"hover:bg-accent hover:text-accent-foreground",
isSelected && "bg-primary/20"
)}
onClick={(e) => handleFileClick(file, e)}
onContextMenu={(e) => {
@@ -796,7 +796,7 @@ export function FileManagerGrid({
onKeyDown={handleEditKeyDown}
onBlur={handleEditConfirm}
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",
"flex-1 min-w-0 max-w-[200px] rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800 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]"
)}
@@ -804,7 +804,7 @@ export function FileManagerGrid({
/>
) : (
<p
className="text-sm text-white truncate cursor-pointer hover:bg-white/10 px-1 py-0.5 rounded transition-colors duration-150 w-fit max-w-full"
className="text-sm text-foreground truncate cursor-pointer hover:bg-accent px-1 py-0.5 rounded transition-colors duration-150 w-fit max-w-full"
title={`${file.name} (点击重命名)`}
onClick={(e) => {
// 阻止文件选择事件
@@ -818,7 +818,7 @@ export function FileManagerGrid({
</p>
)}
{file.type === 'link' && file.linkTarget && (
<p className="text-xs text-cyan-400 truncate" title={file.linkTarget}>
<p className="text-xs text-primary truncate" title={file.linkTarget}>
{file.linkTarget}
</p>
)}
@@ -855,7 +855,7 @@ export function FileManagerGrid({
{/* 框选矩形 */}
{isSelecting && selectionRect && (
<div
className="absolute pointer-events-none border-2 border-blue-500 bg-blue-500/10 z-50"
className="absolute pointer-events-none border-2 border-primary bg-primary/10 z-50"
style={{
left: selectionRect.x,
top: selectionRect.y,

View File

@@ -579,17 +579,17 @@ export function FileViewer({
{/* 大文件警告对话框 */}
{showLargeFileWarning && (
<div className="h-full flex items-center justify-center bg-background">
<div className="bg-card border border-orange-200 rounded-lg p-6 max-w-md mx-4 shadow-lg">
<div className="bg-card border border-destructive/30 rounded-lg p-6 max-w-md mx-4 shadow-lg">
<div className="flex items-start gap-3 mb-4">
<AlertCircle className="w-6 h-6 text-orange-500 flex-shrink-0 mt-0.5" />
<AlertCircle className="w-6 h-6 text-destructive flex-shrink-0 mt-0.5" />
<div>
<h3 className="font-medium text-foreground mb-2">Large File Warning</h3>
<p className="text-sm text-muted-foreground mb-3">
This file is {formatFileSize(file.size)} in size, which may cause performance issues when opened as text.
</p>
{isTooLarge ? (
<div className="bg-red-50 border border-red-200 rounded p-3 mb-4">
<p className="text-sm text-red-700 font-medium">
<div className="bg-destructive/10 border border-destructive/30 rounded p-3 mb-4">
<p className="text-sm text-destructive font-medium">
File is too large (&gt; 10MB) and cannot be opened as text for security reasons.
</p>
</div>

View File

@@ -126,11 +126,17 @@ export function FileWindow({
} catch (error: any) {
console.error('Failed to load file:', error);
// 如果是连接错误,提供更明确的错误信息
if (error.message?.includes('connection') || error.message?.includes('established')) {
// 检查是否是大文件错误
const errorData = error?.response?.data;
if (errorData?.tooLarge) {
toast.error(`File too large: ${errorData.error}`, {
duration: 10000, // 10 seconds for important message
});
} else if (error.message?.includes('connection') || error.message?.includes('established')) {
// 如果是连接错误,提供更明确的错误信息
toast.error(`SSH connection failed. Please check your connection to ${sshHost.name} (${sshHost.ip}:${sshHost.port})`);
} else {
toast.error(`Failed to load file: ${error.message || 'Unknown error'}`);
toast.error(`Failed to load file: ${error.message || errorData?.error || 'Unknown error'}`);
}
} finally {
setIsLoading(false);