This commit is contained in:
Baobhan Sith
2025-04-29 22:50:15 +08:00
parent 4c7d7fc302
commit be845c2d9f
2 changed files with 150 additions and 134 deletions
@@ -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 });
<div
ref="fileListContainerRef"
class="flex-grow overflow-y-auto relative outline-none"
:class="{
'outline-dashed outline-2 outline-offset-[-2px] outline-primary bg-primary/5': isDraggingOver
}"
@dragenter.prevent="handleDragEnter"
@dragover.prevent="handleDragOver"
@dragleave.prevent="handleDragLeave"
@@ -1274,13 +1273,19 @@ defineExpose({ focusSearchInput, startPathEdit });
tabindex="0"
:style="{ '--row-size-multiplier': rowSizeMultiplier }"
>
<!-- Drag over overlay text (optional) -->
<div v-if="isDraggingOver" class="absolute inset-0 flex items-center justify-center bg-black/60 text-white text-lg font-medium rounded pointer-events-none z-10">
<!-- 新增外部文件拖拽蒙版 -->
<div
v-if="showExternalDropOverlay"
class="absolute inset-0 flex items-center justify-center bg-black/70 text-white text-xl font-semibold rounded z-50 pointer-events-auto"
@dragover.prevent
@dragleave.prevent="handleDragLeave"
@drop.prevent="handleOverlayDrop"
>
{{ t('fileManager.dropFilesHere', 'Drop files here to upload') }}
</div>
<!-- File Table -->
<table ref="tableRef" class="w-full border-collapse table-fixed border-border rounded" @contextmenu.prevent>
<table ref="tableRef" class="w-full border-collapse table-fixed border-border rounded" :class="{'pointer-events-none': showExternalDropOverlay}" @contextmenu.prevent>
<colgroup>
<col :style="{ width: `${colWidths.type}px` }">
<col :style="{ width: `${colWidths.name}px` }">
@@ -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<FileListItem | null>(null); // 内部拖拽时,被拖拽的项
const dragOverTarget = ref<string | null>(null); // 内部/外部拖拽时,悬停的目标文件夹名称 (用于行高亮)
const dragOverTarget = ref<string | null>(null); // 内部拖拽时,悬停的目标文件夹名称 (用于行高亮)
const scrollIntervalId = ref<number | null>(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,