Implement file manager sidebar context menu functionality
- Add right-click menu for Recent items: remove single item or clear all - Add right-click menu for Pinned items: unpin functionality - Add right-click menu for Shortcut items: remove shortcut functionality - Implement menu close on outside click and ESC key - Optimize data refresh mechanism: auto-reload sidebar data after operations - Add success/failure toast notifications for user feedback
This commit is contained in:
@@ -16,8 +16,12 @@ import {
|
|||||||
getRecentFiles,
|
getRecentFiles,
|
||||||
getPinnedFiles,
|
getPinnedFiles,
|
||||||
getFolderShortcuts,
|
getFolderShortcuts,
|
||||||
listSSHFiles
|
listSSHFiles,
|
||||||
|
removeRecentFile,
|
||||||
|
removePinnedFile,
|
||||||
|
removeFolderShortcut
|
||||||
} from "@/ui/main-axios.ts";
|
} from "@/ui/main-axios.ts";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export interface SidebarItem {
|
export interface SidebarItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -55,6 +59,19 @@ export function FileManagerSidebar({
|
|||||||
const [directoryTree, setDirectoryTree] = useState<SidebarItem[]>([]);
|
const [directoryTree, setDirectoryTree] = useState<SidebarItem[]>([]);
|
||||||
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set(['root']));
|
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set(['root']));
|
||||||
|
|
||||||
|
// 右键菜单状态
|
||||||
|
const [contextMenu, setContextMenu] = useState<{
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
isVisible: boolean;
|
||||||
|
item: SidebarItem | null;
|
||||||
|
}>({
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
isVisible: false,
|
||||||
|
item: null
|
||||||
|
});
|
||||||
|
|
||||||
// 加载快捷功能数据
|
// 加载快捷功能数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadQuickAccessData();
|
loadQuickAccessData();
|
||||||
@@ -110,6 +127,111 @@ export function FileManagerSidebar({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 删除功能实现
|
||||||
|
const handleRemoveRecentFile = async (item: SidebarItem) => {
|
||||||
|
if (!currentHost?.id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await removeRecentFile(currentHost.id, item.path);
|
||||||
|
loadQuickAccessData(); // 重新加载数据
|
||||||
|
toast.success(`已从最近访问中移除"${item.name}"`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to remove recent file:', error);
|
||||||
|
toast.error('移除失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUnpinFile = async (item: SidebarItem) => {
|
||||||
|
if (!currentHost?.id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await removePinnedFile(currentHost.id, item.path);
|
||||||
|
loadQuickAccessData(); // 重新加载数据
|
||||||
|
toast.success(`已取消固定"${item.name}"`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to unpin file:', error);
|
||||||
|
toast.error('取消固定失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveShortcut = async (item: SidebarItem) => {
|
||||||
|
if (!currentHost?.id) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await removeFolderShortcut(currentHost.id, item.path);
|
||||||
|
loadQuickAccessData(); // 重新加载数据
|
||||||
|
toast.success(`已移除快捷方式"${item.name}"`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to remove shortcut:', error);
|
||||||
|
toast.error('移除快捷方式失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClearAllRecent = async () => {
|
||||||
|
if (!currentHost?.id || recentItems.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 批量删除所有recent文件
|
||||||
|
await Promise.all(
|
||||||
|
recentItems.map(item => removeRecentFile(currentHost.id, item.path))
|
||||||
|
);
|
||||||
|
loadQuickAccessData(); // 重新加载数据
|
||||||
|
toast.success(`已清除所有最近访问记录`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to clear recent files:', error);
|
||||||
|
toast.error('清除失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 右键菜单处理
|
||||||
|
const handleContextMenu = (e: React.MouseEvent, item: SidebarItem) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
setContextMenu({
|
||||||
|
x: e.clientX,
|
||||||
|
y: e.clientY,
|
||||||
|
isVisible: true,
|
||||||
|
item
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeContextMenu = () => {
|
||||||
|
setContextMenu(prev => ({ ...prev, isVisible: false, item: null }));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 点击外部关闭菜单
|
||||||
|
useEffect(() => {
|
||||||
|
if (!contextMenu.isVisible) return;
|
||||||
|
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
const target = event.target as Element;
|
||||||
|
const menuElement = document.querySelector('[data-sidebar-context-menu]');
|
||||||
|
|
||||||
|
if (!menuElement?.contains(target)) {
|
||||||
|
closeContextMenu();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
closeContextMenu();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 延迟添加监听器,避免立即触发
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
}, 50);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
document.removeEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, [contextMenu.isVisible]);
|
||||||
|
|
||||||
const loadDirectoryTree = async () => {
|
const loadDirectoryTree = async () => {
|
||||||
if (!sshSessionId) return;
|
if (!sshSessionId) return;
|
||||||
|
|
||||||
@@ -238,6 +360,12 @@ export function FileManagerSidebar({
|
|||||||
)}
|
)}
|
||||||
style={{ paddingLeft: `${8 + level * 16}px` }}
|
style={{ paddingLeft: `${8 + level * 16}px` }}
|
||||||
onClick={() => handleItemClick(item)}
|
onClick={() => handleItemClick(item)}
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
// 只有快捷功能项才需要右键菜单
|
||||||
|
if (item.type === 'recent' || item.type === 'pinned' || item.type === 'shortcut') {
|
||||||
|
handleContextMenu(e, item);
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{item.type === 'folder' && (
|
{item.type === 'folder' && (
|
||||||
<button
|
<button
|
||||||
@@ -290,6 +418,7 @@ export function FileManagerSidebar({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
<div className="h-full flex flex-col bg-dark-bg border-r border-dark-border">
|
<div className="h-full flex flex-col bg-dark-bg border-r border-dark-border">
|
||||||
<div className="flex-1 relative overflow-hidden">
|
<div className="flex-1 relative overflow-hidden">
|
||||||
<div className="absolute inset-0 overflow-y-auto thin-scrollbar p-2 space-y-4">
|
<div className="absolute inset-0 overflow-y-auto thin-scrollbar p-2 space-y-4">
|
||||||
@@ -311,5 +440,77 @@ export function FileManagerSidebar({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 右键菜单 */}
|
||||||
|
{contextMenu.isVisible && contextMenu.item && (
|
||||||
|
<>
|
||||||
|
<div className="fixed inset-0 z-40" />
|
||||||
|
<div
|
||||||
|
data-sidebar-context-menu
|
||||||
|
className="fixed bg-dark-bg border border-dark-border rounded-lg shadow-xl py-1 min-w-[160px] z-50"
|
||||||
|
style={{
|
||||||
|
left: contextMenu.x,
|
||||||
|
top: contextMenu.y
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{contextMenu.item.type === 'recent' && (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
className="w-full px-3 py-2 text-left text-sm flex items-center gap-3 hover:bg-dark-hover text-white"
|
||||||
|
onClick={() => {
|
||||||
|
handleRemoveRecentFile(contextMenu.item!);
|
||||||
|
closeContextMenu();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Clock className="w-4 h-4" />
|
||||||
|
<span>从最近访问中移除</span>
|
||||||
|
</button>
|
||||||
|
{recentItems.length > 1 && (
|
||||||
|
<>
|
||||||
|
<div className="border-t border-dark-border my-1" />
|
||||||
|
<button
|
||||||
|
className="w-full px-3 py-2 text-left text-sm flex items-center gap-3 hover:bg-dark-hover text-red-400 hover:bg-red-500/10"
|
||||||
|
onClick={() => {
|
||||||
|
handleClearAllRecent();
|
||||||
|
closeContextMenu();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Clock className="w-4 h-4" />
|
||||||
|
<span>清除所有最近访问</span>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{contextMenu.item.type === 'pinned' && (
|
||||||
|
<button
|
||||||
|
className="w-full px-3 py-2 text-left text-sm flex items-center gap-3 hover:bg-dark-hover text-white"
|
||||||
|
onClick={() => {
|
||||||
|
handleUnpinFile(contextMenu.item!);
|
||||||
|
closeContextMenu();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Star className="w-4 h-4" />
|
||||||
|
<span>取消固定</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{contextMenu.item.type === 'shortcut' && (
|
||||||
|
<button
|
||||||
|
className="w-full px-3 py-2 text-left text-sm flex items-center gap-3 hover:bg-dark-hover text-white"
|
||||||
|
onClick={() => {
|
||||||
|
handleRemoveShortcut(contextMenu.item!);
|
||||||
|
closeContextMenu();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Bookmark className="w-4 h-4" />
|
||||||
|
<span>移除快捷方式</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user