实现跨边界拖拽功能:支持从浏览器拖拽文件到系统

主要改进:
- 使用 File System Access API 实现真正的跨应用边界文件传输
- 支持拖拽到窗口外自动触发系统保存对话框
- 智能路径记忆功能,记住用户上次选择的保存位置
- 多文件自动打包为 ZIP 格式
- 现代浏览器优先使用新 API,旧浏览器降级到传统下载
- 完整的视觉反馈和进度显示

技术实现:
- 新增 useDragToSystemDesktop hook 处理系统级拖拽
- 扩展 Electron 主进程支持拖拽临时文件管理
- 集成 JSZip 库支持多文件打包
- 使用 IndexedDB 存储用户偏好的保存路径
- 优化文件管理器拖拽事件处理链

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ZacharyZcR
2025-09-17 00:08:15 +08:00
parent 3f90faf1d0
commit d79d435594
11 changed files with 1206 additions and 10 deletions

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from "react";
import React, { useState, useEffect, useRef, useCallback } from "react";
import { FileManagerGrid } from "./FileManagerGrid";
import { FileManagerContextMenu } from "./FileManagerContextMenu";
import { useFileSelection } from "./hooks/useFileSelection";
@@ -6,6 +6,9 @@ import { useDragAndDrop } from "./hooks/useDragAndDrop";
import { WindowManager, useWindowManager } from "./components/WindowManager";
import { FileWindow } from "./components/FileWindow";
import { DiffWindow } from "./components/DiffWindow";
import { useDragToDesktop } from "../../../hooks/useDragToDesktop";
import { useDragToSystemDesktop } from "../../../hooks/useDragToSystemDesktop";
import { DragIndicator } from "../../../components/DragIndicator";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { toast } from "sonner";
@@ -110,6 +113,18 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
maxFileSize: 100 // 100MB
});
// 拖拽到桌面功能
const dragToDesktop = useDragToDesktop({
sshSessionId: sshSessionId || '',
sshHost: currentHost!
});
// 系统级拖拽到桌面功能(新方案)
const systemDrag = useDragToSystemDesktop({
sshSessionId: sshSessionId || '',
sshHost: currentHost!
});
// 初始化SSH连接
useEffect(() => {
if (currentHost) {
@@ -124,6 +139,41 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
}
}, [sshSessionId, currentPath]);
// 文件拖拽到外部处理
const handleFileDragStart = useCallback((files: FileItem[]) => {
// 记录当前拖拽的文件
systemDrag.startDragToSystem(files, {
enableToast: true,
onSuccess: () => {
clearSelection();
},
onError: (error) => {
console.error('拖拽失败:', error);
}
});
}, [systemDrag, clearSelection]);
const handleFileDragEnd = useCallback((e: DragEvent) => {
// 检查是否拖拽到窗口外
const margin = 50;
const isOutside = (
e.clientX < margin ||
e.clientX > window.innerWidth - margin ||
e.clientY < margin ||
e.clientY > window.innerHeight - margin
);
if (isOutside) {
// 延迟执行,避免与其他事件冲突
setTimeout(() => {
systemDrag.handleDragEnd(e);
}, 100);
} else {
// 取消拖拽
systemDrag.cancelDragToSystem();
}
}, [systemDrag]);
async function initializeSSHConnection() {
if (!currentHost) return;
@@ -1055,6 +1105,39 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
toast.success(`正在对比文件: ${file1.name}${file2.name}`);
}
// 拖拽到桌面处理函数
async function handleDragToDesktop(files: FileItem[]) {
if (!currentHost || !sshSessionId) {
toast.error(t("fileManager.noSSHConnection"));
return;
}
try {
// 优先使用新的系统级拖拽方案
if (systemDrag.isFileSystemAPISupported) {
await systemDrag.handleDragToSystem(files, {
enableToast: true,
onSuccess: () => {
console.log('系统级拖拽成功');
},
onError: (error) => {
console.error('系统级拖拽失败:', error);
}
});
} else {
// 降级到Electron方案
if (files.length === 1) {
await dragToDesktop.dragFileToDesktop(files[0]);
} else if (files.length > 1) {
await dragToDesktop.dragFilesToDesktop(files);
}
}
} catch (error: any) {
console.error('拖拽到桌面失败:', error);
toast.error(`拖拽失败: ${error.message || '未知错误'}`);
}
}
// 过滤文件并添加新建的临时项目
let filteredFiles = files.filter(file =>
file.name.toLowerCase().includes(searchQuery.toLowerCase())
@@ -1205,6 +1288,8 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
onUndo={handleUndo}
onFileDrop={handleFileDrop}
onFileDiff={handleFileDiff}
onSystemDragStart={handleFileDragStart}
onSystemDragEnd={handleFileDragEnd}
/>
{/* 右键菜单 */}
@@ -1234,6 +1319,31 @@ function FileManagerContent({ initialHost, onClose }: FileManagerModernProps) {
onNewFile={handleCreateNewFile}
onRefresh={() => loadDirectory(currentPath)}
hasClipboard={!!clipboard}
onDragToDesktop={() => handleDragToDesktop(contextMenu.files)}
/>
{/* 拖拽到桌面指示器 */}
<DragIndicator
isVisible={
dragToDesktop.isDownloading ||
dragToDesktop.isDragging ||
systemDrag.isDownloading ||
systemDrag.isDragging
}
isDragging={
systemDrag.isDragging || dragToDesktop.isDragging
}
isDownloading={
systemDrag.isDownloading || dragToDesktop.isDownloading
}
progress={
systemDrag.isDownloading || systemDrag.isDragging
? systemDrag.progress
: dragToDesktop.progress
}
fileName={selectedFiles.length === 1 ? selectedFiles[0]?.name : undefined}
fileCount={selectedFiles.length}
error={systemDrag.error || dragToDesktop.error}
/>
</div>
</div>