修复文件管理器滚动功能和软链接支持
主要改进: - 重新设计布局结构,确保状态栏始终可见 - 添加软链接图标和目标路径显示支持 - 修复滚动条功能,使用绝对定位的滚动容器 - 优化文件名编辑框的自适应宽度和居中显示 - 完善软链接点击处理逻辑 布局优化: - 外层容器:h-full flex flex-col overflow-hidden - 滚动区域:flex-1 relative overflow-hidden 包含 absolute inset-0 overflow-y-auto - 状态栏:flex-shrink-0 确保始终可见 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,8 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
MoreHorizontal,
|
MoreHorizontal,
|
||||||
RefreshCw,
|
RefreshCw,
|
||||||
ArrowUp
|
ArrowUp,
|
||||||
|
FileSymlink
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { FileItem } from "../../../types/index.js";
|
import type { FileItem } from "../../../types/index.js";
|
||||||
@@ -64,14 +65,18 @@ interface FileManagerGridProps {
|
|||||||
onCancelEdit?: () => void;
|
onCancelEdit?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getFileIcon = (fileName: string, isDirectory: boolean, viewMode: 'grid' | 'list' = 'grid') => {
|
const getFileIcon = (file: FileItem, viewMode: 'grid' | 'list' = 'grid') => {
|
||||||
const iconClass = viewMode === 'grid' ? "w-8 h-8" : "w-6 h-6";
|
const iconClass = viewMode === 'grid' ? "w-8 h-8" : "w-6 h-6";
|
||||||
|
|
||||||
if (isDirectory) {
|
if (file.type === 'directory') {
|
||||||
return <Folder className={`${iconClass} text-blue-400`} />;
|
return <Folder className={`${iconClass} text-blue-400`} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ext = fileName.split('.').pop()?.toLowerCase();
|
if (file.type === 'link') {
|
||||||
|
return <FileSymlink className={`${iconClass} text-cyan-400`} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ext = file.name.split('.').pop()?.toLowerCase();
|
||||||
|
|
||||||
switch (ext) {
|
switch (ext) {
|
||||||
case 'txt':
|
case 'txt':
|
||||||
@@ -273,6 +278,12 @@ export function FileManagerGrid({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 滚轮事件处理,确保滚动正常工作
|
||||||
|
const handleWheel = useCallback((e: React.WheelEvent) => {
|
||||||
|
// 不阻止默认滚动行为,让浏览器自己处理滚动
|
||||||
|
e.stopPropagation();
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleDrop = useCallback((e: React.DragEvent) => {
|
const handleDrop = useCallback((e: React.DragEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -394,7 +405,7 @@ export function FileManagerGrid({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col bg-dark-bg">
|
<div className="h-full flex flex-col bg-dark-bg overflow-hidden">
|
||||||
{/* 工具栏和路径导航 */}
|
{/* 工具栏和路径导航 */}
|
||||||
<div className="flex-shrink-0 border-b border-dark-border">
|
<div className="flex-shrink-0 border-b border-dark-border">
|
||||||
{/* 导航按钮 */}
|
{/* 导航按钮 */}
|
||||||
@@ -463,42 +474,44 @@ export function FileManagerGrid({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 主文件网格 */}
|
{/* 主文件网格 - 滚动区域 */}
|
||||||
<div
|
<div className="flex-1 relative overflow-hidden">
|
||||||
ref={gridRef}
|
<div
|
||||||
className={cn(
|
ref={gridRef}
|
||||||
"flex-1 p-4 overflow-auto",
|
className={cn(
|
||||||
isDragging && "bg-blue-500/10 border-2 border-dashed border-blue-500"
|
"absolute inset-0 p-4 overflow-y-auto thin-scrollbar",
|
||||||
)}
|
isDragging && "bg-blue-500/10 border-2 border-dashed border-blue-500"
|
||||||
onClick={handleGridClick}
|
)}
|
||||||
onDragEnter={handleDragEnter}
|
onClick={handleGridClick}
|
||||||
onDragLeave={handleDragLeave}
|
onDragEnter={handleDragEnter}
|
||||||
onDragOver={handleDragOver}
|
onDragLeave={handleDragLeave}
|
||||||
onDrop={handleDrop}
|
onDragOver={handleDragOver}
|
||||||
onContextMenu={(e) => onContextMenu?.(e)}
|
onDrop={handleDrop}
|
||||||
tabIndex={0}
|
onWheel={handleWheel}
|
||||||
>
|
onContextMenu={(e) => onContextMenu?.(e)}
|
||||||
{isDragging && (
|
tabIndex={0}
|
||||||
<div className="absolute inset-0 flex items-center justify-center bg-blue-500/10 backdrop-blur-sm z-10">
|
>
|
||||||
<div className="text-center">
|
{isDragging && (
|
||||||
<Download className="w-12 h-12 mx-auto mb-2 text-blue-500" />
|
<div className="absolute inset-0 flex items-center justify-center bg-blue-500/10 backdrop-blur-sm z-10 pointer-events-none">
|
||||||
<p className="text-lg font-medium text-blue-500">
|
<div className="text-center">
|
||||||
{t("fileManager.dragFilesToUpload")}
|
<Download className="w-12 h-12 mx-auto mb-2 text-blue-500" />
|
||||||
</p>
|
<p className="text-lg font-medium text-blue-500">
|
||||||
|
{t("fileManager.dragFilesToUpload")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{files.length === 0 ? (
|
{files.length === 0 ? (
|
||||||
<div className="h-full flex items-center justify-center">
|
<div className="h-full flex items-center justify-center">
|
||||||
<div className="text-center text-muted-foreground">
|
<div className="text-center text-muted-foreground">
|
||||||
<Folder className="w-16 h-16 mx-auto mb-4 opacity-50" />
|
<Folder className="w-16 h-16 mx-auto mb-4 opacity-50" />
|
||||||
<p>{t("fileManager.emptyFolder")}</p>
|
<p>{t("fileManager.emptyFolder")}</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : viewMode === 'grid' ? (
|
||||||
) : viewMode === 'grid' ? (
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4">
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-6 xl:grid-cols-8 gap-4">
|
{files.map((file) => {
|
||||||
{files.map((file) => {
|
|
||||||
const isSelected = selectedFiles.some(f => f.path === file.path);
|
const isSelected = selectedFiles.some(f => f.path === file.path);
|
||||||
|
|
||||||
// 详细调试路径比较
|
// 详细调试路径比较
|
||||||
@@ -531,11 +544,11 @@ export function FileManagerGrid({
|
|||||||
<div className="flex flex-col items-center text-center">
|
<div className="flex flex-col items-center text-center">
|
||||||
{/* 文件图标 */}
|
{/* 文件图标 */}
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
{getFileIcon(file.name, file.type === 'directory', viewMode)}
|
{getFileIcon(file, viewMode)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 文件名 */}
|
{/* 文件名 */}
|
||||||
<div className="w-full">
|
<div className="w-full flex flex-col items-center">
|
||||||
{editingFile?.path === file.path ? (
|
{editingFile?.path === file.path ? (
|
||||||
<input
|
<input
|
||||||
ref={editInputRef}
|
ref={editInputRef}
|
||||||
@@ -545,7 +558,7 @@ export function FileManagerGrid({
|
|||||||
onKeyDown={handleEditKeyDown}
|
onKeyDown={handleEditKeyDown}
|
||||||
onBlur={handleEditConfirm}
|
onBlur={handleEditConfirm}
|
||||||
className={cn(
|
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",
|
"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",
|
||||||
"text-center text-foreground placeholder:text-muted-foreground",
|
"text-center text-foreground placeholder:text-muted-foreground",
|
||||||
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[2px]"
|
"focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[2px]"
|
||||||
)}
|
)}
|
||||||
@@ -553,7 +566,7 @@ export function FileManagerGrid({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<p
|
<p
|
||||||
className="text-xs text-white truncate cursor-pointer hover:bg-white/10 px-1 py-0.5 rounded transition-colors duration-150"
|
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"
|
||||||
title={`${file.name} (点击重命名)`}
|
title={`${file.name} (点击重命名)`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
// 阻止文件选择事件
|
// 阻止文件选择事件
|
||||||
@@ -571,6 +584,11 @@ export function FileManagerGrid({
|
|||||||
{formatFileSize(file.size)}
|
{formatFileSize(file.size)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{file.type === 'link' && file.linkTarget && (
|
||||||
|
<p className="text-xs text-cyan-400 mt-1 truncate max-w-full" title={file.linkTarget}>
|
||||||
|
→ {file.linkTarget}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -600,7 +618,7 @@ export function FileManagerGrid({
|
|||||||
>
|
>
|
||||||
{/* 文件图标 */}
|
{/* 文件图标 */}
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
{getFileIcon(file.name, file.type === 'directory', viewMode)}
|
{getFileIcon(file, viewMode)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 文件信息 */}
|
{/* 文件信息 */}
|
||||||
@@ -622,7 +640,7 @@ export function FileManagerGrid({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<p
|
<p
|
||||||
className="text-sm text-white truncate cursor-pointer hover:bg-white/10 px-2 py-1 rounded transition-colors duration-150"
|
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"
|
||||||
title={`${file.name} (点击重命名)`}
|
title={`${file.name} (点击重命名)`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
// 阻止文件选择事件
|
// 阻止文件选择事件
|
||||||
@@ -635,6 +653,11 @@ export function FileManagerGrid({
|
|||||||
{file.name}
|
{file.name}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{file.type === 'link' && file.linkTarget && (
|
||||||
|
<p className="text-xs text-cyan-400 truncate" title={file.linkTarget}>
|
||||||
|
→ {file.linkTarget}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{file.modified && (
|
{file.modified && (
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{file.modified}
|
{file.modified}
|
||||||
@@ -664,6 +687,7 @@ export function FileManagerGrid({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 状态栏 */}
|
{/* 状态栏 */}
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ import {
|
|||||||
deleteSSHItem,
|
deleteSSHItem,
|
||||||
renameSSHItem,
|
renameSSHItem,
|
||||||
connectSSH,
|
connectSSH,
|
||||||
getSSHStatus
|
getSSHStatus,
|
||||||
|
identifySSHSymlink
|
||||||
} from "@/ui/main-axios.ts";
|
} from "@/ui/main-axios.ts";
|
||||||
|
|
||||||
|
|
||||||
@@ -345,9 +346,95 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
|
|||||||
setIsCreatingNewFile(true);
|
setIsCreatingNewFile(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFileOpen(file: FileItem) {
|
// Handle symlink resolution
|
||||||
|
const handleSymlinkClick = async (file: FileItem) => {
|
||||||
|
if (!currentHost || !sshSessionId) {
|
||||||
|
toast.error(t("fileManager.noSSHConnection"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 确保SSH连接有效
|
||||||
|
let currentSessionId = sshSessionId;
|
||||||
|
try {
|
||||||
|
const status = await getSSHStatus(currentSessionId);
|
||||||
|
if (!status.connected) {
|
||||||
|
const result = await connectSSH(currentSessionId, {
|
||||||
|
hostId: currentHost.id,
|
||||||
|
host: currentHost.ip,
|
||||||
|
port: currentHost.port,
|
||||||
|
username: currentHost.username,
|
||||||
|
authType: currentHost.authType,
|
||||||
|
password: currentHost.password,
|
||||||
|
key: currentHost.key,
|
||||||
|
keyPassword: currentHost.keyPassword,
|
||||||
|
credentialId: currentHost.credentialId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error(t("fileManager.failedToReconnectSSH"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (sessionErr) {
|
||||||
|
throw sessionErr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const symlinkInfo = await identifySSHSymlink(currentSessionId, file.path);
|
||||||
|
|
||||||
|
if (symlinkInfo.type === "directory") {
|
||||||
|
// 如果软链接指向目录,导航到它
|
||||||
|
setCurrentPath(symlinkInfo.target);
|
||||||
|
} else if (symlinkInfo.type === "file") {
|
||||||
|
// 如果软链接指向文件,打开文件
|
||||||
|
// 计算窗口位置(稍微错开)
|
||||||
|
const windowCount = Date.now() % 10;
|
||||||
|
const offsetX = 120 + (windowCount * 30);
|
||||||
|
const offsetY = 120 + (windowCount * 30);
|
||||||
|
|
||||||
|
// 创建目标文件对象
|
||||||
|
const targetFile: FileItem = {
|
||||||
|
...file,
|
||||||
|
path: symlinkInfo.target
|
||||||
|
};
|
||||||
|
|
||||||
|
// 创建窗口组件工厂函数
|
||||||
|
const createWindowComponent = (windowId: string) => (
|
||||||
|
<FileWindow
|
||||||
|
windowId={windowId}
|
||||||
|
file={targetFile}
|
||||||
|
sshSessionId={currentSessionId}
|
||||||
|
sshHost={currentHost}
|
||||||
|
initialX={offsetX}
|
||||||
|
initialY={offsetY}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
openWindow({
|
||||||
|
title: file.name,
|
||||||
|
x: offsetX,
|
||||||
|
y: offsetY,
|
||||||
|
width: 800,
|
||||||
|
height: 600,
|
||||||
|
isMaximized: false,
|
||||||
|
isMinimized: false,
|
||||||
|
component: createWindowComponent
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(
|
||||||
|
error?.response?.data?.error ||
|
||||||
|
error?.message ||
|
||||||
|
t("fileManager.failedToResolveSymlink")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function handleFileOpen(file: FileItem) {
|
||||||
if (file.type === 'directory') {
|
if (file.type === 'directory') {
|
||||||
setCurrentPath(file.path);
|
setCurrentPath(file.path);
|
||||||
|
} else if (file.type === 'link') {
|
||||||
|
// 处理软链接
|
||||||
|
await handleSymlinkClick(file);
|
||||||
} else {
|
} else {
|
||||||
// 在新窗口中打开文件
|
// 在新窗口中打开文件
|
||||||
if (!sshSessionId) {
|
if (!sshSessionId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user