From be845c2d9f639b067cc2ea4ca6b8805987a057a3 Mon Sep 17 00:00:00 2001 From: Baobhan Sith <80159437+Heavrnl@users.noreply.github.com> Date: Tue, 29 Apr 2025 22:50:15 +0800 Subject: [PATCH] update --- .../frontend/src/components/FileManager.vue | 25 +- .../file-manager/useFileManagerDragAndDrop.ts | 259 +++++++++--------- 2 files changed, 150 insertions(+), 134 deletions(-) diff --git a/packages/frontend/src/components/FileManager.vue b/packages/frontend/src/components/FileManager.vue index f9c99bb..0303e3b 100644 --- a/packages/frontend/src/components/FileManager.vue +++ b/packages/frontend/src/components/FileManager.vue @@ -617,14 +617,16 @@ const { // --- 拖放逻辑 (使用 Composable) --- const { - isDraggingOver, // 容器拖拽悬停状态 (外部文件) - dragOverTarget, // 行拖拽悬停目标 (内部/外部) + // isDraggingOver, // 不再直接使用容器的悬停状态 + showExternalDropOverlay, // 新增:控制蒙版显示 + dragOverTarget, // 行拖拽悬停目标 (内部) // draggedItem, // 内部状态,不需要在 FileManager 中直接使用 // --- 事件处理器 --- handleDragEnter, - handleDragOver, + handleDragOver, // 容器的 dragover (主要处理内部滚动) handleDragLeave, - handleDrop, + handleDrop, // 容器的 drop (主要用于清理) + handleOverlayDrop, // 新增:蒙版的 drop handleDragStart, handleDragEnd, handleDragOverRow, @@ -1260,9 +1262,6 @@ defineExpose({ focusSearchInput, startPathEdit });
- -
+ +
{{ t('fileManager.dropFilesHere', 'Drop files here to upload') }}
- +
diff --git a/packages/frontend/src/composables/file-manager/useFileManagerDragAndDrop.ts b/packages/frontend/src/composables/file-manager/useFileManagerDragAndDrop.ts index 1f14e81..28f01a1 100644 --- a/packages/frontend/src/composables/file-manager/useFileManagerDragAndDrop.ts +++ b/packages/frontend/src/composables/file-manager/useFileManagerDragAndDrop.ts @@ -29,9 +29,10 @@ export function useFileManagerDragAndDrop(options: UseFileManagerDragAndDropOpti } = options; // --- 拖放状态 Refs --- - const isDraggingOver = ref(false); // 是否有文件拖拽悬停在容器上 (用于外部文件) + // const isDraggingOver = ref(false); // 不再使用,由 showExternalDropOverlay 替代外部拖拽状态 + const showExternalDropOverlay = ref(false); // 新增:控制外部文件拖拽蒙版的显示 const draggedItem = ref(null); // 内部拖拽时,被拖拽的项 - const dragOverTarget = ref(null); // 内部/外部拖拽时,悬停的目标文件夹名称 (用于行高亮) + const dragOverTarget = ref(null); // 内部拖拽时,悬停的目标文件夹名称 (用于行高亮) const scrollIntervalId = ref(null); // 自动滚动计时器 ID // --- 自动滚动常量 --- @@ -48,102 +49,118 @@ export function useFileManagerDragAndDrop(options: UseFileManagerDragAndDropOpti // --- 事件处理函数 --- const handleDragEnter = (event: DragEvent) => { - if (isConnected.value && event.dataTransfer?.types.includes('Files')) { - isDraggingOver.value = true; - } + // 检查是否是外部文件拖拽 + const isExternalFileDrag = event.dataTransfer?.types.includes('Files') ?? false; + if (isConnected.value && isExternalFileDrag && !draggedItem.value) { // 确保不是内部拖拽触发 + // console.log("[DragDrop] External file drag entered container."); + showExternalDropOverlay.value = true; // 显示蒙版 + } else if (draggedItem.value) { + // console.log("[DragDrop] Internal item drag entered container area."); + // 内部拖拽进入容器但不在行上,可能需要处理效果,但不显示蒙版 + if (event.dataTransfer) event.dataTransfer.dropEffect = 'none'; // 默认在容器空白处无效 + } }; - const handleDragOver = (event: DragEvent) => { - event.preventDefault(); // 必须阻止默认行为以允许 drop + const handleDragOver = (event: DragEvent) => { + // 这个函数现在主要负责处理内部拖拽时的自动滚动 + // 外部文件拖拽的 dragover 由蒙版处理 (只阻止默认行为) const isExternalFileDrag = event.dataTransfer?.types.includes('Files') ?? false; const isInternalDrag = !!draggedItem.value; - let effect: 'copy' | 'move' | 'none' = 'none'; - let currentTargetFilename: string | null = null; - let highlightContainer = false; + // 1. 如果是外部文件拖拽,确保蒙版是显示的,并设置效果 + if (isExternalFileDrag && isConnected.value && !isInternalDrag) { // 再次确认不是内部拖拽 + showExternalDropOverlay.value = true; // 确保蒙版显示 + event.preventDefault(); // 必须阻止默认行为以允许 drop + if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy'; // 指示效果 + // 外部拖拽时,不处理容器的自动滚动,因为鼠标在蒙版上 + stopAutoScroll(); + return; // 外部拖拽不由容器 handleDragOver 处理滚动 + } - const targetElement = event.target as HTMLElement; - const targetRow = targetElement.closest('tr.file-row'); - const targetFilename = (targetRow instanceof HTMLElement) ? targetRow.dataset.filename : undefined; - const targetIsFolder = targetRow?.classList.contains('folder-row'); + // 2. 如果是内部拖拽 + if (isInternalDrag && isConnected.value) { + // 内部拖拽时,dragover 主要由行处理 (handleDragOverRow) + // 但如果鼠标在行之间或空白区域,容器的 dragover 会触发 + // 此时需要处理自动滚动,并阻止默认行为 + event.preventDefault(); // 阻止默认行为(如文本选择),允许滚动逻辑 + // 效果由 handleDragOverRow 设置,但在空白区域应为 none + const targetElement = event.target as HTMLElement; + const targetRow = targetElement.closest('tr.file-row'); + if (!targetRow && event.dataTransfer) { // 如果不在行上 + event.dataTransfer.dropEffect = 'none'; + dragOverTarget.value = null; // 清除行高亮 + } else if (event.dataTransfer) { + // 如果在行上,效果由 handleDragOverRow 控制,这里假设为 move + event.dataTransfer.dropEffect = 'move'; + } - if (isConnected.value) { - if (isExternalFileDrag) { - effect = 'copy'; - highlightContainer = true; - if (targetIsFolder && targetFilename && targetFilename !== '..') { - currentTargetFilename = targetFilename; + + // --- 处理自动滚动 --- + const container = fileListContainerRef.value; + // 只有在内部拖拽且悬停目标有效(在行上)时才滚动 + if (container && dragOverTarget.value) { // 依赖 handleDragOverRow 设置的 dragOverTarget + const rect = container.getBoundingClientRect(); + const mouseY = event.clientY - rect.top; + + if (mouseY < SCROLL_ZONE_HEIGHT) { + if (scrollIntervalId.value === null) { + scrollIntervalId.value = window.setInterval(() => { + if (container.scrollTop > 0) { + container.scrollTop -= SCROLL_SPEED; + } else { + stopAutoScroll(); + } + }, 30); + } + } else if (mouseY > container.clientHeight - SCROLL_ZONE_HEIGHT) { + if (scrollIntervalId.value === null) { + scrollIntervalId.value = window.setInterval(() => { + if (container.scrollTop < container.scrollHeight - container.clientHeight) { + container.scrollTop += SCROLL_SPEED; + } else { + stopAutoScroll(); + } + }, 30); + } } else { - currentTargetFilename = null; - } - } else if (isInternalDrag && draggedItem.value) { - highlightContainer = false; - if (targetIsFolder && targetFilename && targetFilename !== draggedItem.value.filename) { - effect = 'move'; - currentTargetFilename = targetFilename; - } else { - effect = 'none'; - currentTargetFilename = null; + stopAutoScroll(); // 在中间区域停止滚动 } } else { - effect = 'none'; - currentTargetFilename = null; - highlightContainer = false; + stopAutoScroll(); // 如果不在滚动区域或目标无效,停止滚动 } - } else { - effect = 'none'; - currentTargetFilename = null; - highlightContainer = false; + return; // 内部拖拽处理完毕 } - if (event.dataTransfer) { - event.dataTransfer.dropEffect = effect; - } - isDraggingOver.value = highlightContainer; - dragOverTarget.value = currentTargetFilename; - - // --- 处理自动滚动 --- - const container = fileListContainerRef.value; - if (container && (isExternalFileDrag || isInternalDrag) && effect !== 'none') { - const rect = container.getBoundingClientRect(); - const mouseY = event.clientY - rect.top; - - if (mouseY < SCROLL_ZONE_HEIGHT) { - if (scrollIntervalId.value === null) { - scrollIntervalId.value = window.setInterval(() => { - if (container.scrollTop > 0) { - container.scrollTop -= SCROLL_SPEED; - } else { - stopAutoScroll(); - } - }, 30); - } - } else if (mouseY > container.clientHeight - SCROLL_ZONE_HEIGHT) { - if (scrollIntervalId.value === null) { - scrollIntervalId.value = window.setInterval(() => { - if (container.scrollTop < container.scrollHeight - container.clientHeight) { - container.scrollTop += SCROLL_SPEED; - } else { - stopAutoScroll(); - } - }, 30); - } - } else { - stopAutoScroll(); - } - } else { - stopAutoScroll(); - } + // 3. 其他情况 (非文件、非内部拖拽、未连接) + if (event.dataTransfer) event.dataTransfer.dropEffect = 'none'; + stopAutoScroll(); // 停止滚动 + // 不一定需要阻止默认行为 event.preventDefault(); }; const handleDragLeave = (event: DragEvent) => { const target = event.relatedTarget as Node | null; - const container = (event.currentTarget as HTMLElement); - if (!target || !container.contains(target)) { - isDraggingOver.value = false; - dragOverTarget.value = null; - stopAutoScroll(); - } + const container = (event.currentTarget as HTMLElement); + // 检查是否真的离开了容器边界 + if (!target || !container.contains(target)) { + // console.log("[DragDrop] Drag left container boundary."); + if (showExternalDropOverlay.value) { + // console.log("[DragDrop] Hiding external drop overlay due to leaving container."); + showExternalDropOverlay.value = false; // 隐藏蒙版 + } + // isDraggingOver.value = false; // 不再使用 + dragOverTarget.value = null; // 清除行高亮 + stopAutoScroll(); // 停止滚动 + } else { + // console.log("[DragDrop] Drag left fired but still inside container."); + // 鼠标仍在容器内(可能移到了子元素上),不隐藏蒙版或清除状态 + // 但如果是内部拖拽移到了非行区域,需要清除行高亮 + if (draggedItem.value) { + const relatedRow = (target instanceof HTMLElement) ? target.closest('tr.file-row') : null; + if (!relatedRow) { + dragOverTarget.value = null; + } + } + } }; // --- 新增:递归遍历文件树的辅助函数 --- @@ -175,48 +192,46 @@ export function useFileManagerDragAndDrop(options: UseFileManagerDragAndDropOpti // --- 结束新增 --- - const handleDrop = (event: DragEvent) => { - const wasDraggingOver = isDraggingOver.value; - const currentDragTarget = dragOverTarget.value; // 拖放目标文件夹名称 - isDraggingOver.value = false; - dragOverTarget.value = null; - stopAutoScroll(); + // 新增:处理蒙版上的 Drop 事件 + const handleOverlayDrop = (event: DragEvent) => { + event.preventDefault(); // 必须阻止,以防浏览器打开文件 + // console.log("[DragDrop] Drop event on overlay."); + showExternalDropOverlay.value = false; // 隐藏蒙版 + stopAutoScroll(); // 停止滚动 - // --- 修改:使用 DataTransferItemList 和 webkitGetAsEntry 处理拖放 --- const items = event.dataTransfer?.items; if (!items || items.length === 0 || !isConnected.value) { - if (draggedItem.value) draggedItem.value = null; // 清理内部拖拽状态 + console.log("[DragDrop] Overlay drop ignored: No items or not connected."); return; } - // 检查放置目标是否有效 (由 handleDragOver 决定) - // 对于外部文件,要么容器高亮 (wasDraggingOver),要么行高亮 (currentDragTarget) - // 注意:拖放到子文件夹的功能暂时移除,所有拖放都上传到当前目录或根目录 - // 如果需要拖放到子文件夹,需要重新设计 targetFolderPath 的逻辑 - // if (!wasDraggingOver && !currentDragTarget) { - // console.log(`[DragDrop] Drop ignored: Drop target was not valid according to handleDragOver.`); - // return; - // } - - console.log(`[DragDrop] Drop event detected with ${items.length} items.`); - + console.log(`[DragDrop] Processing ${items.length} items from overlay drop.`); for (let i = 0; i < items.length; i++) { const item = items[i]; if (item.kind === 'file') { const entry = item.webkitGetAsEntry(); if (entry) { - console.log(`[DragDrop] Processing entry: ${entry.name}, isFile: ${entry.isFile}, isDirectory: ${entry.isDirectory}`); - traverseFileTree(entry); // 开始遍历文件树,初始相对路径为空 + // console.log(`[DragDrop] Processing entry from overlay: ${entry.name}`); + traverseFileTree(entry); // 处理文件/文件夹 } else { - console.warn(`[DragDrop] Could not get entry for item ${i}`); + console.warn(`[DragDrop] Could not get entry for item ${i} from overlay.`); } - } else { - console.log(`[DragDrop] Skipping non-file item kind: ${item.kind}`); } } - // --- 结束修改 --- + }; - draggedItem.value = null; // 确保清理内部拖拽状态 + // 原有的 handleDrop (容器的 drop) 现在基本不需要了, + // 因为外部 drop 由蒙版处理,内部 drop 由行处理并阻止冒泡。 + // 保留一个空的或只做清理的函数以防万一。 + const handleDrop = (event: DragEvent) => { + // console.log("[DragDrop] Container drop event triggered (should be rare)."); + // 清理所有状态以防异常情况 + showExternalDropOverlay.value = false; + draggedItem.value = null; // 清理内部拖拽状态 + dragOverTarget.value = null; // 清理行高亮 + stopAutoScroll(); // 停止滚动 + // 阻止默认行为以防万一 + event.preventDefault(); }; const handleDragStart = (item: FileListItem) => { @@ -235,7 +250,9 @@ export function useFileManagerDragAndDrop(options: UseFileManagerDragAndDropOpti }; const handleDragOverRow = (targetItem: FileListItem, event: DragEvent) => { - event.preventDefault(); // 允许 drop + event.preventDefault(); // 允许 drop 在行上 + event.stopPropagation(); // 阻止事件冒泡到容器的 handleDragOver + // 内部拖拽逻辑: 只能拖拽非 '..' 项,目标必须是文件夹或 '..',且不能是自身 if (!draggedItem.value || draggedItem.value.filename === '..' || (targetItem.filename !== '..' && (!targetItem.attrs.isDirectory || draggedItem.value.filename === targetItem.filename))) { if (event.dataTransfer) event.dataTransfer.dropEffect = 'none'; @@ -244,7 +261,7 @@ export function useFileManagerDragAndDrop(options: UseFileManagerDragAndDropOpti } // 设置放置效果为 'move' 并记录目标 if (event.dataTransfer) event.dataTransfer.dropEffect = 'move'; - dragOverTarget.value = targetItem.filename; + dragOverTarget.value = targetItem.filename; // 更新悬停目标 }; const handleDragLeaveRow = (targetItem: FileListItem) => { @@ -255,26 +272,18 @@ export function useFileManagerDragAndDrop(options: UseFileManagerDragAndDropOpti }; const handleDropOnRow = (targetItem: FileListItem, event: DragEvent) => { - event.preventDefault(); - // 检查是否是外部文件拖拽 (dataTransfer.files 存在) - const files = event.dataTransfer?.files; - if (files && files.length > 0) { - // 如果是外部文件拖拽,不阻止冒泡,让父容器的 handleDrop 处理上传 - // console.log(`[DragDrop] External file drop detected on row, letting parent handle.`); - // 不需要清除 draggedItem.value,因为外部拖拽时它应该为 null - // dragOverTarget.value = null; // 清除悬停状态 (父容器 handleDrop 会处理) - return; // 让事件冒泡到父 div 的 handleDrop - } + event.preventDefault(); // 确保阻止默认行为(例如导航) + event.stopPropagation(); // 阻止事件冒泡到容器的 handleDrop - // --- 以下是处理内部文件移动的逻辑 --- - event.stopPropagation(); // 仅在处理内部移动时阻止冒泡 + // --- 处理内部文件移动的逻辑 --- const sourceItem = draggedItem.value; const currentDragOverTarget = dragOverTarget.value; // 保存当前目标,然后清除 dragOverTarget.value = null; // 清除悬停状态 // 验证内部拖放操作的有效性 + // 检查: 是否有拖拽项? 拖拽项不是 '..'? 目标项是文件夹或 '..'? 拖拽项不是目标项自身? 放置的目标确实是悬停的目标? if (!sourceItem || sourceItem.filename === '..' || (targetItem.filename !== '..' && !targetItem.attrs.isDirectory) || sourceItem.filename === targetItem.filename || targetItem.filename !== currentDragOverTarget) { - // console.log(`[DragDrop] Internal drop on row ignored: Invalid target, source, or drop occurred outside the intended target row. Source: ${sourceItem?.filename}, Target: ${targetItem.filename}, Drop Target: ${currentDragOverTarget}`); + console.log(`[DragDrop] Internal drop on row ignored: Invalid conditions. Source: ${sourceItem?.filename}, Target: ${targetItem.filename}, Drop Target: ${currentDragOverTarget}`); if (sourceItem) draggedItem.value = null; // 清理拖拽状态 return; } @@ -359,14 +368,16 @@ export function useFileManagerDragAndDrop(options: UseFileManagerDragAndDropOpti // --- 返回状态和处理函数 --- return { - isDraggingOver, + // isDraggingOver, // 不再导出 + showExternalDropOverlay, // 新增导出 dragOverTarget, draggedItem, // 需要暴露以供 handleDragOverRow 等函数内部判断 // --- 事件处理器 --- handleDragEnter, handleDragOver, handleDragLeave, - handleDrop, + handleDrop, // 容器的 drop (主要用于清理) + handleOverlayDrop, // 新增导出:蒙版的 drop handleDragStart, handleDragEnd, handleDragOverRow,