From f9601530858571ece236a138d925bb7810f101d1 Mon Sep 17 00:00:00 2001 From: Baobhan Sith <80159437+Heavrnl@users.noreply.github.com> Date: Mon, 21 Apr 2025 20:36:14 +0800 Subject: [PATCH] update --- .../frontend/src/components/FileManager.vue | 84 ++++---------- .../useFileManagerKeyboardNavigation.ts | 104 ++++++++++++++++++ 2 files changed, 123 insertions(+), 65 deletions(-) create mode 100644 packages/frontend/src/composables/file-manager/useFileManagerKeyboardNavigation.ts diff --git a/packages/frontend/src/components/FileManager.vue b/packages/frontend/src/components/FileManager.vue index 3f714fa..0fa39e5 100644 --- a/packages/frontend/src/components/FileManager.vue +++ b/packages/frontend/src/components/FileManager.vue @@ -14,6 +14,7 @@ import { useFocusSwitcherStore } from '../stores/focusSwitcher.store'; // +++ import { useFileManagerContextMenu } from '../composables/file-manager/useFileManagerContextMenu'; // +++ 导入上下文菜单 Composable +++ import { useFileManagerSelection } from '../composables/file-manager/useFileManagerSelection'; // +++ 导入选择 Composable +++ import { useFileManagerDragAndDrop } from '../composables/file-manager/useFileManagerDragAndDrop'; // +++ 导入拖放 Composable +++ +import { useFileManagerKeyboardNavigation } from '../composables/file-manager/useFileManagerKeyboardNavigation'; // +++ 导入键盘导航 Composable +++ // WebSocket composable 不再直接使用 import FileUploadPopup from './FileUploadPopup.vue'; import FileManagerContextMenu from './FileManagerContextMenu.vue'; // +++ 导入上下文菜单组件 +++ @@ -152,7 +153,8 @@ const fileListContainerRef = ref(null); // 文件列表 // const scrollIntervalId = ref(null); // 已移至 useFileManagerDragAndDrop const rowSizeMultiplier = ref(1); // 新增:行大小(字体)乘数 -const selectedIndex = ref(-1); // 新增:键盘选中索引 +// --- 键盘导航状态 (移至 useFileManagerKeyboardNavigation) --- +// const selectedIndex = ref(-1); // --- Column Resizing State (Remains the same) --- const tableRef = ref(null); @@ -451,69 +453,21 @@ const handleFileSelected = (event: Event) => { input.value = ''; }; -// --- 键盘导航和执行 --- -const handleKeydown = (event: KeyboardEvent) => { - const list = filteredFileList.value; - const hasParentLink = currentPath.value !== '/'; - const totalItems = list.length + (hasParentLink ? 1 : 0); // 包含 '..' 的总项目数 +// --- 键盘导航逻辑 (使用 Composable) --- +const { + selectedIndex, // 使用 Composable 返回的 selectedIndex + handleKeydown, // 使用 Composable 返回的 handleKeydown +} = useFileManagerKeyboardNavigation({ + filteredFileList: filteredFileList, // 传递过滤后的列表 + currentPath: currentPath, // 传递当前路径 + fileListContainerRef: fileListContainerRef, // 传递容器引用 + // 当 Enter 键按下时,模拟鼠标单击 + onEnterPress: (item) => handleItemClick(new MouseEvent('click'), item), +}); - if (totalItems === 0) return; - - let currentEffectiveIndex = selectedIndex.value; // 0 代表 '..', 1+ 代表 filteredList 的 index + 1 - - switch (event.key) { - case 'ArrowDown': - event.preventDefault(); - currentEffectiveIndex = (currentEffectiveIndex + 1) % totalItems; - selectedIndex.value = currentEffectiveIndex; - scrollToSelected(); - break; - case 'ArrowUp': - event.preventDefault(); - currentEffectiveIndex = (currentEffectiveIndex - 1 + totalItems) % totalItems; - selectedIndex.value = currentEffectiveIndex; - scrollToSelected(); - break; - case 'Enter': - event.preventDefault(); - if (selectedIndex.value === 0 && hasParentLink) { - // 选中 '..' - handleItemClick(new MouseEvent('click'), { filename: '..', longname: '..', attrs: { isDirectory: true, isFile: false, isSymbolicLink: false, size: 0, uid: 0, gid: 0, mode: 0, atime: 0, mtime: 0 } }); - } else if (selectedIndex.value > 0) { - // 选中列表中的项 - const itemIndexInFilteredList = selectedIndex.value - (hasParentLink ? 1 : 0); - if (itemIndexInFilteredList >= 0 && itemIndexInFilteredList < list.length) { - handleItemClick(new MouseEvent('click'), list[itemIndexInFilteredList]); - } - } - break; - } -}; - -const scrollToSelected = async () => { - await nextTick(); - if (selectedIndex.value < 0 || !fileListContainerRef.value) return; - - const container = fileListContainerRef.value; - // 使用 querySelectorAll 获取所有行,包括 '..' - const rows = container.querySelectorAll('tr.file-row'); - if (selectedIndex.value >= rows.length) return; // 索引超出范围 - - const selectedRow = rows[selectedIndex.value] as HTMLElement; - - if (selectedRow) { - const containerRect = container.getBoundingClientRect(); - const rowRect = selectedRow.getBoundingClientRect(); - - if (rowRect.top < containerRect.top) { - container.scrollTop -= containerRect.top - rowRect.top; - } else if (rowRect.bottom > containerRect.bottom) { - container.scrollTop += rowRect.bottom - containerRect.bottom; - } - } -}; // --- 重置选中索引和清空选择的 Watchers --- +// 注意:这里的 Watchers 现在需要修改 Composable 返回的 selectedIndex.value watch(currentPath, () => { selectedIndex.value = -1; clearSelection(); // 清空选择 @@ -821,7 +775,7 @@ const handleWheel = (event: WheelEvent) => { @dragover.prevent="handleDragOver" @dragleave.prevent="handleDragLeave" @drop.prevent="handleDrop" - @click="fileListContainerRef?.focus()" + @click="fileListContainerRef?.focus()" @keydown="handleKeydown" tabindex="0" :style="{ '--row-size-multiplier': rowSizeMultiplier }" @@ -886,7 +840,7 @@ const handleWheel = (event: WheelEvent) => { { :class="[ 'file-row', { clickable: item.attrs.isDirectory || item.attrs.isFile }, - { selected: selectedItems.has(item.filename) || (index + (currentPath !== '/' ? 1 : 0) === selectedIndex) }, /* 结合鼠标和键盘选中高亮 */ - // { selected: index + (currentPath !== '/' ? 1 : 0) === selectedIndex }, /* 移除单独的键盘高亮 */ + { selected: selectedItems.has(item.filename) || (index + (currentPath !== '/' ? 1 : 0) === selectedIndex) }, /* 使用 Composable 的 selectedIndex */ + // { selected: index + (currentPath !== '/' ? 1 : 0) === selectedIndex }, /* 保持注释 */ { 'folder-row': item.attrs.isDirectory }, // 添加文件夹标识类 { 'drop-target': item.attrs.isDirectory && dragOverTarget === item.filename } // 使用 Composable 的 dragOverTarget ]" diff --git a/packages/frontend/src/composables/file-manager/useFileManagerKeyboardNavigation.ts b/packages/frontend/src/composables/file-manager/useFileManagerKeyboardNavigation.ts new file mode 100644 index 0000000..a2a6dbc --- /dev/null +++ b/packages/frontend/src/composables/file-manager/useFileManagerKeyboardNavigation.ts @@ -0,0 +1,104 @@ +import { ref, computed, nextTick, type Ref, type ComputedRef } from 'vue'; +import type { FileListItem } from '../../types/sftp.types'; + +// 定义 Composable 的输入参数类型 +export interface UseFileManagerKeyboardNavigationOptions { + // 响应式引用 + filteredFileList: ComputedRef>; // 当前显示的(已过滤/排序)文件列表 + currentPath: Ref; // 当前路径 (用于判断是否显示 '..') + fileListContainerRef: Ref; // 文件列表容器的引用 (用于滚动) + + // 回调函数 + onEnterPress: (item: FileListItem) => void; // 按下 Enter 键时触发的回调 +} + +export function useFileManagerKeyboardNavigation(options: UseFileManagerKeyboardNavigationOptions) { + const { + filteredFileList, + currentPath, + fileListContainerRef, + onEnterPress, + } = options; + + // --- 状态 Refs --- + const selectedIndex = ref(-1); // 键盘选中索引 (-1 表示未选中, 0 代表 '..', 1+ 代表 filteredList 的 index + 1) + + // --- 计算属性 --- + // 是否显示 '..' 行 + const hasParentLink = computed(() => currentPath.value !== '/'); + // 列表总项目数 (包括 '..') + const totalItems = computed(() => filteredFileList.value.length + (hasParentLink.value ? 1 : 0)); + + // --- 滚动到选中项 --- + const scrollToSelected = async () => { + await nextTick(); + if (selectedIndex.value < 0 || !fileListContainerRef.value) return; + + const container = fileListContainerRef.value; + // 使用 querySelectorAll 获取所有行,包括 '..' + const rows = container.querySelectorAll('tr.file-row'); + if (selectedIndex.value >= rows.length) return; // 索引超出范围 + + const selectedRow = rows[selectedIndex.value] as HTMLElement; + + if (selectedRow) { + const containerRect = container.getBoundingClientRect(); + const rowRect = selectedRow.getBoundingClientRect(); + + if (rowRect.top < containerRect.top) { + container.scrollTop -= containerRect.top - rowRect.top; + } else if (rowRect.bottom > containerRect.bottom) { + container.scrollTop += rowRect.bottom - containerRect.bottom; + } + } + }; + + // --- 键盘事件处理 --- + const handleKeydown = (event: KeyboardEvent) => { + if (totalItems.value === 0) return; + + let currentEffectiveIndex = selectedIndex.value; + + switch (event.key) { + case 'ArrowDown': + event.preventDefault(); + currentEffectiveIndex = (currentEffectiveIndex + 1) % totalItems.value; + selectedIndex.value = currentEffectiveIndex; + scrollToSelected(); + break; + case 'ArrowUp': + event.preventDefault(); + currentEffectiveIndex = (currentEffectiveIndex - 1 + totalItems.value) % totalItems.value; + selectedIndex.value = currentEffectiveIndex; + scrollToSelected(); + break; + case 'Enter': + event.preventDefault(); + if (selectedIndex.value === 0 && hasParentLink.value) { + // 选中 '..' + // 创建一个临时的 '..' FileListItem 对象传递给回调 + onEnterPress({ filename: '..', longname: '..', attrs: { isDirectory: true, isFile: false, isSymbolicLink: false, size: 0, uid: 0, gid: 0, mode: 0, atime: 0, mtime: 0 } }); + } else if (selectedIndex.value > 0) { + // 选中列表中的项 + const itemIndexInFilteredList = selectedIndex.value - (hasParentLink.value ? 1 : 0); + if (itemIndexInFilteredList >= 0 && itemIndexInFilteredList < filteredFileList.value.length) { + onEnterPress(filteredFileList.value[itemIndexInFilteredList]); + } + } + break; + } + }; + + // --- 重置索引的 Watchers (移至 FileManager.vue 中,因为它需要监听更多状态) --- + // watch(currentPath, () => { selectedIndex.value = -1; }); + // watch(searchQuery, () => { selectedIndex.value = -1; }); + // watch(sortKey, () => { selectedIndex.value = -1; }); + // watch(sortDirection, () => { selectedIndex.value = -1; }); + + // --- 返回状态和处理函数 --- + return { + selectedIndex, // 暴露键盘选中索引 + handleKeydown, // 暴露键盘事件处理器 + // scrollToSelected, // 内部使用,通常不需要暴露 + }; +} \ No newline at end of file