update
This commit is contained in:
@@ -12,6 +12,7 @@ import { useSessionStore } from '../stores/session.store';
|
||||
import { useSettingsStore } from '../stores/settings.store';
|
||||
import { useFocusSwitcherStore } from '../stores/focusSwitcher.store'; // +++ 导入焦点切换 Store +++
|
||||
import { useFileManagerContextMenu } from '../composables/file-manager/useFileManagerContextMenu'; // +++ 导入上下文菜单 Composable +++
|
||||
import { useFileManagerSelection } from '../composables/file-manager/useFileManagerSelection'; // +++ 导入选择 Composable +++
|
||||
// WebSocket composable 不再直接使用
|
||||
import FileUploadPopup from './FileUploadPopup.vue';
|
||||
import FileManagerContextMenu from './FileManagerContextMenu.vue'; // +++ 导入上下文菜单组件 +++
|
||||
@@ -120,8 +121,9 @@ const { shareFileEditorTabsBoolean } = storeToRefs(settingsStore); // 使用 sto
|
||||
|
||||
// --- UI 状态 Refs (Remain mostly the same) ---
|
||||
const fileInputRef = ref<HTMLInputElement | null>(null);
|
||||
const selectedItems = ref(new Set<string>());
|
||||
const lastClickedIndex = ref(-1);
|
||||
// --- 选择状态 (移至 useFileManagerSelection) ---
|
||||
// const selectedItems = ref(new Set<string>()); // 移除旧的 ref
|
||||
// const lastClickedIndex = ref(-1); // 移除旧的 ref
|
||||
// --- 上下文菜单状态 (移至 useFileManagerContextMenu) ---
|
||||
// const contextMenuVisible = ref(false);
|
||||
// const contextMenuPosition = ref({ x: 0, y: 0 });
|
||||
@@ -183,9 +185,107 @@ const formatMode = (mode: number): string => {
|
||||
return str;
|
||||
};
|
||||
|
||||
// --- 排序与过滤逻辑 (保持在此处,Selection 和 ContextMenu 依赖它) ---
|
||||
const sortedFileList = computed(() => {
|
||||
// Ensure fileList.value is used (it's reactive from the manager)
|
||||
if (!fileList.value) return [];
|
||||
const list = [...fileList.value];
|
||||
const key = sortKey.value;
|
||||
const direction = sortDirection.value === 'asc' ? 1 : -1;
|
||||
|
||||
list.sort((a, b) => {
|
||||
if (key !== 'type') {
|
||||
if (a.attrs.isDirectory && !b.attrs.isDirectory) return -1;
|
||||
if (!a.attrs.isDirectory && b.attrs.isDirectory) return 1;
|
||||
}
|
||||
let valA: string | number | boolean;
|
||||
let valB: string | number | boolean;
|
||||
switch (key) {
|
||||
case 'type':
|
||||
valA = a.attrs.isDirectory ? 0 : (a.attrs.isSymbolicLink ? 1 : 2);
|
||||
valB = b.attrs.isDirectory ? 0 : (b.attrs.isSymbolicLink ? 1 : 2);
|
||||
break;
|
||||
case 'filename': valA = a.filename.toLowerCase(); valB = b.filename.toLowerCase(); break;
|
||||
case 'size': valA = a.attrs.isFile ? a.attrs.size : -1; valB = b.attrs.isFile ? b.attrs.size : -1; break;
|
||||
case 'mtime': valA = a.attrs.mtime; valB = b.attrs.mtime; break;
|
||||
default: valA = a.filename.toLowerCase(); valB = b.filename.toLowerCase();
|
||||
}
|
||||
if (valA < valB) return -1 * direction;
|
||||
if (valA > valB) return 1 * direction;
|
||||
if (key !== 'filename') return a.filename.localeCompare(b.filename);
|
||||
return 0;
|
||||
});
|
||||
return list;
|
||||
});
|
||||
|
||||
const filteredFileList = computed(() => {
|
||||
if (!searchQuery.value) {
|
||||
return sortedFileList.value; // 如果没有搜索查询,返回原始排序列表
|
||||
}
|
||||
const lowerCaseQuery = searchQuery.value.toLowerCase();
|
||||
return sortedFileList.value.filter(item =>
|
||||
item.filename.toLowerCase().includes(lowerCaseQuery)
|
||||
);
|
||||
});
|
||||
|
||||
const handleSort = (key: keyof FileListItem | 'type' | 'size' | 'mtime') => {
|
||||
if (sortKey.value === key) {
|
||||
sortDirection.value = sortDirection.value === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
sortKey.value = key;
|
||||
sortDirection.value = 'asc';
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// --- 列表项点击与选择逻辑 (使用 Composable) ---
|
||||
// 定义单击时的动作回调 (移到 Selection 实例化之前)
|
||||
const handleItemAction = (item: FileListItem) => {
|
||||
if (item.attrs.isDirectory) {
|
||||
if (isLoading.value) {
|
||||
console.log(`[FileManager ${props.sessionId}] Ignoring directory click, already loading...`);
|
||||
return;
|
||||
}
|
||||
const newPath = item.filename === '..'
|
||||
? currentPath.value.substring(0, currentPath.value.lastIndexOf('/')) || '/'
|
||||
: joinPath(currentPath.value, item.filename);
|
||||
loadDirectory(newPath);
|
||||
} else if (item.attrs.isFile) {
|
||||
const filePath = joinPath(currentPath.value, item.filename);
|
||||
const fileInfo: FileInfo = { name: item.filename, fullPath: filePath };
|
||||
|
||||
if (settingsStore.showPopupFileEditorBoolean) {
|
||||
console.log(`[FileManager ${props.sessionId}] Triggering popup for: ${filePath}`);
|
||||
fileEditorStore.triggerPopup(filePath, props.sessionId);
|
||||
}
|
||||
|
||||
if (shareFileEditorTabsBoolean.value) {
|
||||
console.log(`[FileManager ${props.sessionId}] Opening file in shared mode (store handles loading): ${filePath}`);
|
||||
fileEditorStore.openFile(filePath, props.sessionId);
|
||||
} else {
|
||||
console.log(`[FileManager ${props.sessionId}] Opening file in independent mode (store handles loading): ${filePath}`);
|
||||
sessionStore.openFileInSession(props.sessionId, fileInfo);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 实例化选择 Composable (需要 filteredFileList 和 handleItemAction)
|
||||
const {
|
||||
selectedItems, // 使用 Composable 返回的 selectedItems
|
||||
lastClickedIndex, // 获取 lastClickedIndex 以传递给 ContextMenu
|
||||
handleItemClick, // 使用 Composable 返回的 handleItemClick
|
||||
clearSelection, // 获取清空选择的方法
|
||||
} = useFileManagerSelection({
|
||||
// 传递当前显示的列表 (已排序和过滤)
|
||||
displayedFileList: filteredFileList, // 现在 filteredFileList 已定义
|
||||
onItemAction: handleItemAction, // 传递动作回调
|
||||
});
|
||||
|
||||
|
||||
// --- SFTP 操作处理函数 (定义在此处,供 Composable 使用) ---
|
||||
const handleDeleteSelectedClick = () => {
|
||||
if (!props.wsDeps.isConnected.value || selectedItems.value.size === 0) return; // 恢复使用 props.wsDeps
|
||||
// 现在 selectedItems 来自 useFileManagerSelection
|
||||
if (!props.wsDeps.isConnected.value || selectedItems.value.size === 0) return;
|
||||
const itemsToDelete = Array.from(selectedItems.value)
|
||||
.map(filename => fileList.value.find((f: FileListItem) => f.filename === filename)) // f 已有类型
|
||||
.filter((item): item is FileListItem => item !== undefined);
|
||||
@@ -280,7 +380,7 @@ const triggerDownload = (item: FileListItem) => { // item 已有类型
|
||||
};
|
||||
|
||||
|
||||
// --- 上下文菜单逻辑 (使用 Composable) ---
|
||||
// --- 上下文菜单逻辑 (使用 Composable, 需要 Selection 和 Action Handlers) ---
|
||||
const {
|
||||
contextMenuVisible,
|
||||
contextMenuPosition,
|
||||
@@ -289,8 +389,8 @@ const {
|
||||
showContextMenu, // 使用 Composable 提供的函数
|
||||
hideContextMenu, // <-- 获取 hideContextMenu 函数
|
||||
} = useFileManagerContextMenu({
|
||||
selectedItems,
|
||||
lastClickedIndex,
|
||||
selectedItems, // 传递来自 useFileManagerSelection 的 selectedItems
|
||||
lastClickedIndex, // 传递来自 useFileManagerSelection 的 lastClickedIndex
|
||||
fileList, // 传递 sftpManager 的 fileList
|
||||
currentPath, // 传递 sftpManager 的 currentPath
|
||||
isConnected: props.wsDeps.isConnected, // 传递响应式引用
|
||||
@@ -310,70 +410,6 @@ const {
|
||||
// --- 目录加载与导航 ---
|
||||
// loadDirectory is provided by props.sftpManager
|
||||
|
||||
// --- 列表项点击与选择逻辑 ---
|
||||
// handleItemClick 中的 item 参数已有类型
|
||||
|
||||
// --- 列表项点击与选择逻辑 ---
|
||||
const handleItemClick = (event: MouseEvent, item: FileListItem) => { // item 已有类型
|
||||
const itemIndex = fileList.value.findIndex((f: FileListItem) => f.filename === item.filename); // f 已有类型
|
||||
if (itemIndex === -1 && item.filename !== '..') return;
|
||||
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
if (item.filename === '..') return;
|
||||
if (selectedItems.value.has(item.filename)) selectedItems.value.delete(item.filename);
|
||||
else selectedItems.value.add(item.filename);
|
||||
lastClickedIndex.value = itemIndex;
|
||||
} else if (event.shiftKey && lastClickedIndex.value !== -1) {
|
||||
if (item.filename === '..') return;
|
||||
selectedItems.value.clear();
|
||||
const start = Math.min(lastClickedIndex.value, itemIndex);
|
||||
const end = Math.max(lastClickedIndex.value, itemIndex);
|
||||
for (let i = start; i <= end; i++) {
|
||||
// Use fileList from props
|
||||
if (fileList.value[i]) selectedItems.value.add(fileList.value[i].filename);
|
||||
}
|
||||
} else {
|
||||
selectedItems.value.clear();
|
||||
if (item.filename !== '..') {
|
||||
selectedItems.value.add(item.filename);
|
||||
lastClickedIndex.value = itemIndex;
|
||||
} else {
|
||||
lastClickedIndex.value = -1;
|
||||
}
|
||||
|
||||
if (item.attrs.isDirectory) {
|
||||
if (isLoading.value) { // Use isLoading from props
|
||||
console.log(`[FileManager ${props.sessionId}] Ignoring directory click, already loading...`);
|
||||
return;
|
||||
}
|
||||
const newPath = item.filename === '..'
|
||||
? currentPath.value.substring(0, currentPath.value.lastIndexOf('/')) || '/' // 使用 sftpManager 的 currentPath
|
||||
: joinPath(currentPath.value, item.filename); // Use joinPath from props
|
||||
loadDirectory(newPath); // Use loadDirectory from props
|
||||
} else if (item.attrs.isFile) {
|
||||
const filePath = joinPath(currentPath.value, item.filename); // Use joinPath from props
|
||||
const fileInfo: FileInfo = { name: item.filename, fullPath: filePath };
|
||||
|
||||
// 检查是否需要触发弹窗 (无论共享模式如何)
|
||||
if (settingsStore.showPopupFileEditorBoolean) {
|
||||
console.log(`[FileManager ${props.sessionId}] Triggering popup for: ${filePath}`);
|
||||
fileEditorStore.triggerPopup(filePath, props.sessionId); // <-- 传递参数
|
||||
}
|
||||
|
||||
// 根据共享模式决定如何打开/加载文件
|
||||
if (shareFileEditorTabsBoolean.value) {
|
||||
// 共享模式:调用全局 fileEditorStore (它会处理标签页和加载)
|
||||
console.log(`[FileManager ${props.sessionId}] Opening file in shared mode (store handles loading): ${filePath}`);
|
||||
fileEditorStore.openFile(filePath, props.sessionId);
|
||||
} else {
|
||||
// 独立模式:调用 sessionStore (它会处理标签页和加载)
|
||||
console.log(`[FileManager ${props.sessionId}] Opening file in independent mode (store handles loading): ${filePath}`);
|
||||
sessionStore.openFileInSession(props.sessionId, fileInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// --- 拖放上传逻辑 ---
|
||||
const handleDragEnter = (event: DragEvent) => {
|
||||
if (props.wsDeps.isConnected.value && event.dataTransfer?.types.includes('Files')) { // 恢复使用 props.wsDeps.isConnected
|
||||
@@ -701,60 +737,6 @@ const handleFileSelected = (event: Event) => {
|
||||
input.value = '';
|
||||
};
|
||||
|
||||
// --- 排序逻辑 ---
|
||||
// Uses fileList from props.sftpManager
|
||||
const sortedFileList = computed(() => {
|
||||
// Ensure fileList.value is used (it's reactive from the manager)
|
||||
if (!fileList.value) return [];
|
||||
const list = [...fileList.value];
|
||||
const key = sortKey.value;
|
||||
const direction = sortDirection.value === 'asc' ? 1 : -1;
|
||||
|
||||
list.sort((a, b) => {
|
||||
if (key !== 'type') {
|
||||
if (a.attrs.isDirectory && !b.attrs.isDirectory) return -1;
|
||||
if (!a.attrs.isDirectory && b.attrs.isDirectory) return 1;
|
||||
}
|
||||
let valA: string | number | boolean;
|
||||
let valB: string | number | boolean;
|
||||
switch (key) {
|
||||
case 'type':
|
||||
valA = a.attrs.isDirectory ? 0 : (a.attrs.isSymbolicLink ? 1 : 2);
|
||||
valB = b.attrs.isDirectory ? 0 : (b.attrs.isSymbolicLink ? 1 : 2);
|
||||
break;
|
||||
case 'filename': valA = a.filename.toLowerCase(); valB = b.filename.toLowerCase(); break;
|
||||
case 'size': valA = a.attrs.isFile ? a.attrs.size : -1; valB = b.attrs.isFile ? b.attrs.size : -1; break;
|
||||
case 'mtime': valA = a.attrs.mtime; valB = b.attrs.mtime; break;
|
||||
default: valA = a.filename.toLowerCase(); valB = b.filename.toLowerCase();
|
||||
}
|
||||
if (valA < valB) return -1 * direction;
|
||||
if (valA > valB) return 1 * direction;
|
||||
if (key !== 'filename') return a.filename.localeCompare(b.filename);
|
||||
return 0;
|
||||
});
|
||||
return list;
|
||||
});
|
||||
|
||||
// 新增:过滤后的文件列表计算属性
|
||||
const filteredFileList = computed(() => {
|
||||
if (!searchQuery.value) {
|
||||
return sortedFileList.value; // 如果没有搜索查询,返回原始排序列表
|
||||
}
|
||||
const lowerCaseQuery = searchQuery.value.toLowerCase();
|
||||
return sortedFileList.value.filter(item =>
|
||||
item.filename.toLowerCase().includes(lowerCaseQuery)
|
||||
);
|
||||
});
|
||||
|
||||
const handleSort = (key: keyof FileListItem | 'type' | 'size' | 'mtime') => {
|
||||
if (sortKey.value === key) {
|
||||
sortDirection.value = sortDirection.value === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
sortKey.value = key;
|
||||
sortDirection.value = 'asc';
|
||||
}
|
||||
};
|
||||
|
||||
// --- 键盘导航和执行 ---
|
||||
const handleKeydown = (event: KeyboardEvent) => {
|
||||
const list = filteredFileList.value;
|
||||
@@ -817,11 +799,23 @@ const scrollToSelected = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// --- 重置选中索引的 Watchers ---
|
||||
watch(currentPath, () => { selectedIndex.value = -1; });
|
||||
watch(searchQuery, () => { selectedIndex.value = -1; });
|
||||
watch(sortKey, () => { selectedIndex.value = -1; });
|
||||
watch(sortDirection, () => { selectedIndex.value = -1; });
|
||||
// --- 重置选中索引和清空选择的 Watchers ---
|
||||
watch(currentPath, () => {
|
||||
selectedIndex.value = -1;
|
||||
clearSelection(); // 清空选择
|
||||
});
|
||||
watch(searchQuery, () => {
|
||||
selectedIndex.value = -1;
|
||||
clearSelection(); // 清空选择
|
||||
});
|
||||
watch(sortKey, () => {
|
||||
selectedIndex.value = -1;
|
||||
clearSelection(); // 清空选择
|
||||
});
|
||||
watch(sortDirection, () => {
|
||||
selectedIndex.value = -1;
|
||||
clearSelection(); // 清空选择
|
||||
});
|
||||
|
||||
|
||||
// --- 生命周期钩子 ---
|
||||
@@ -889,9 +883,9 @@ watchEffect((onCleanup) => {
|
||||
|
||||
} else if (!props.wsDeps.isConnected.value && initialLoadDone.value) { // 恢复使用 props.wsDeps.isConnected
|
||||
console.log(`[FileManager ${props.sessionId}] 连接丢失 (之前已加载),重置状态。`);
|
||||
selectedItems.value.clear();
|
||||
lastClickedIndex.value = -1;
|
||||
clearSelection(); // 清空选择
|
||||
initialLoadDone.value = false; // 重置初始加载状态
|
||||
// lastClickedIndex.value = -1; // 由 clearSelection 处理
|
||||
isFetchingInitialPath.value = false; // 重置获取状态
|
||||
cleanupListeners();
|
||||
}
|
||||
@@ -1202,8 +1196,8 @@ const handleWheel = (event: WheelEvent) => {
|
||||
:class="[
|
||||
'file-row',
|
||||
{ clickable: item.attrs.isDirectory || item.attrs.isFile },
|
||||
/* { selected: selectedItems.has(item.filename) }, */ /* 移除鼠标选择的 selected 类,统一用键盘的 */
|
||||
{ selected: index + (currentPath !== '/' ? 1 : 0) === selectedIndex }, /* 键盘选中高亮 */
|
||||
{ selected: selectedItems.has(item.filename) }, /* 恢复:使用 selectedItems Set 控制选中高亮 */
|
||||
// { selected: index + (currentPath !== '/' ? 1 : 0) === selectedIndex }, /* 暂时移除键盘选中高亮 */
|
||||
{ 'folder-row': item.attrs.isDirectory }, // 添加文件夹标识类
|
||||
{ 'drop-target': item.attrs.isDirectory && dragOverTarget === item.filename } // 拖拽悬停高亮
|
||||
]"
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import { ref, type Ref } from 'vue';
|
||||
import type { FileListItem } from '../../types/sftp.types'; // 确保路径正确
|
||||
|
||||
// 定义 Composable 的输入参数类型
|
||||
export interface UseFileManagerSelectionOptions {
|
||||
// 注意:这里传入的应该是当前渲染在表格中的列表 (可能已排序/过滤)
|
||||
// 在 FileManager.vue 中,这通常是 filteredFileList 或 sortedFileList
|
||||
displayedFileList: Ref<Readonly<FileListItem[]>>;
|
||||
// 回调函数,当需要执行导航或打开文件时调用
|
||||
onItemAction: (item: FileListItem) => void;
|
||||
}
|
||||
|
||||
export function useFileManagerSelection(options: UseFileManagerSelectionOptions) {
|
||||
const { displayedFileList, onItemAction } = options;
|
||||
|
||||
const selectedItems = ref(new Set<string>());
|
||||
const lastClickedIndex = ref(-1); // 索引相对于 displayedFileList
|
||||
|
||||
const handleItemClick = (event: MouseEvent, item: FileListItem) => {
|
||||
console.log(`[Selection] handleItemClick called for item: ${item.filename}`, { event, item });
|
||||
let shouldPerformAction = false; // 初始化标志
|
||||
const ctrlOrMeta = event.ctrlKey || event.metaKey;
|
||||
const shift = event.shiftKey;
|
||||
console.log(`[Selection] Modifiers: Ctrl/Meta=${ctrlOrMeta}, Shift=${shift}`);
|
||||
|
||||
// 查找点击项在当前显示列表中的索引
|
||||
const itemIndex = displayedFileList.value.findIndex((f) => f.filename === item.filename);
|
||||
console.log(`[Selection] Item index in displayed list: ${itemIndex}`);
|
||||
|
||||
// 如果找不到项(理论上不应发生),或者点击的是 '..'
|
||||
// (注意: '..' 通常是单独处理或在列表开头,这里假设它不在 displayedFileList 中,或者其点击事件由外部单独处理)
|
||||
// 我们主要处理 displayedFileList 中的项的选择逻辑
|
||||
if (itemIndex === -1) {
|
||||
// 如果点击的是 '..'
|
||||
// 如果点击的是 '..'
|
||||
if (item.filename === '..') {
|
||||
console.log("[Selection] Clicked on '..'");
|
||||
// 只有在没有修饰键时才执行 '..' 的动作
|
||||
if (!ctrlOrMeta && !shift) {
|
||||
console.log("[Selection] '..' clicked without modifiers. Clearing selection and marking for action.");
|
||||
console.log('[Selection] Before clear:', new Set(selectedItems.value), 'Last index:', lastClickedIndex.value);
|
||||
selectedItems.value.clear();
|
||||
lastClickedIndex.value = -1;
|
||||
shouldPerformAction = true; // 标记执行动作
|
||||
console.log('[Selection] After clear:', new Set(selectedItems.value), 'Last index:', lastClickedIndex.value);
|
||||
} else {
|
||||
console.log("[Selection] '..' clicked with modifiers. Ignoring action.");
|
||||
}
|
||||
// 如果有修饰键,则不执行动作,直接返回 (修改:移到下面统一处理)
|
||||
// (不需要修改 selectedItems 或 lastClickedIndex)
|
||||
// 注意:这里没有 else,因为如果 shouldPerformAction 仍为 false,后面的 if 会阻止调用 onItemAction
|
||||
} else {
|
||||
// 如果不是 '..' 且找不到索引,则忽略无效点击
|
||||
return;
|
||||
}
|
||||
// 如果是 '..' 且没有修饰键,会继续到函数末尾的 action 调用检查
|
||||
// 如果是 '..' 且有修饰键,则在此处返回 (因为上面没有 return) -> 不对,应该在上面 if block 里 return
|
||||
// 统一处理 '..' 的返回逻辑:如果有修饰键,则不继续执行后续的选择/动作逻辑
|
||||
if (item.filename === '..' && (ctrlOrMeta || shift)) {
|
||||
console.log("[Selection] Returning early for '..' click with modifiers.");
|
||||
return;
|
||||
}
|
||||
// 如果不是 '..' 且找不到索引,也返回
|
||||
if (item.filename !== '..' && itemIndex === -1) {
|
||||
console.log("[Selection] Item not found in displayed list (and not '..'). Returning.");
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// --- 主要选择逻辑 ---
|
||||
console.log('[Selection] Before selection logic:', new Set(selectedItems.value), 'Last index:', lastClickedIndex.value);
|
||||
|
||||
// --- 调整后的主要选择逻辑 ---
|
||||
if (ctrlOrMeta) { // 1. 检查 Ctrl/Meta
|
||||
console.log('[Selection] Branch: Ctrl/Meta Click');
|
||||
event.preventDefault();
|
||||
event.stopPropagation(); // <-- 阻止冒泡
|
||||
// Ctrl/Cmd + Click: Toggle selection
|
||||
// '..' 不应参与多选 (已在前面处理)
|
||||
// if (item.filename === '..') return; // '..' 已在前面处理
|
||||
if (selectedItems.value.has(item.filename)) {
|
||||
console.log(`[Selection] Ctrl/Meta: Removing ${item.filename}`);
|
||||
selectedItems.value.delete(item.filename);
|
||||
} else {
|
||||
console.log(`[Selection] Ctrl/Meta: Adding ${item.filename}`);
|
||||
selectedItems.value.add(item.filename); // Keep the add operation
|
||||
}
|
||||
// Removed the extra else block here
|
||||
lastClickedIndex.value = itemIndex; // 更新最后点击的索引
|
||||
console.log('[Selection] After Ctrl/Meta:', new Set(selectedItems.value), 'Last index:', lastClickedIndex.value);
|
||||
console.log('[Selection] After Ctrl/Meta:', new Set(selectedItems.value), 'Last index:', lastClickedIndex.value);
|
||||
} else if (shift) { // 2. 检查 Shift (移除 lastClickedIndex !== -1 条件)
|
||||
console.log('[Selection] Branch: Shift Click');
|
||||
event.preventDefault();
|
||||
event.stopPropagation(); // <-- 阻止冒泡
|
||||
// Shift + Click: Range selection
|
||||
// '..' 不应参与范围选择 (已在前面处理)
|
||||
// if (item.filename === '..') return; // '..' 已在前面处理
|
||||
console.log('[Selection] Shift: Clearing previous selection.');
|
||||
selectedItems.value.clear();
|
||||
// 如果 lastClickedIndex 是 -1 (例如第一次 Shift 点击),则只选中当前项
|
||||
const start = lastClickedIndex.value === -1 ? itemIndex : Math.min(lastClickedIndex.value, itemIndex);
|
||||
const end = lastClickedIndex.value === -1 ? itemIndex : Math.max(lastClickedIndex.value, itemIndex);
|
||||
console.log(`[Selection] Shift: Range from ${start} to ${end}`);
|
||||
for (let i = start; i <= end; i++) {
|
||||
// 确保索引有效且不是 '..'
|
||||
const fileToAdd = displayedFileList.value[i];
|
||||
if (fileToAdd && fileToAdd.filename !== '..') {
|
||||
console.log(`[Selection] Shift: Adding ${fileToAdd.filename}`);
|
||||
selectedItems.value.add(fileToAdd.filename);
|
||||
}
|
||||
}
|
||||
// Shift-click 也更新 lastClickedIndex 为当前点击项
|
||||
lastClickedIndex.value = itemIndex;
|
||||
console.log('[Selection] After Shift:', new Set(selectedItems.value), 'Last index:', lastClickedIndex.value);
|
||||
console.log('[Selection] After Shift:', new Set(selectedItems.value), 'Last index:', lastClickedIndex.value);
|
||||
} else { // 3. 处理普通单击 (没有修饰键)
|
||||
console.log('[Selection] Branch: Single Click');
|
||||
// Single Click: Select only the clicked item and perform action
|
||||
console.log('[Selection] Single Click: Clearing previous selection.');
|
||||
selectedItems.value.clear();
|
||||
// '..' 不应被加入 selectedItems (已在前面处理 shouldPerformAction)
|
||||
if (item.filename !== '..') {
|
||||
console.log(`[Selection] Single Click: Adding ${item.filename}`);
|
||||
selectedItems.value.add(item.filename);
|
||||
lastClickedIndex.value = itemIndex; // 更新最后点击的索引
|
||||
} else {
|
||||
// 点击 '..' 的 lastClickedIndex 已在前面处理
|
||||
// lastClickedIndex.value = -1;
|
||||
}
|
||||
console.log('[Selection] After Single Click selection update:', new Set(selectedItems.value), 'Last index:', lastClickedIndex.value);
|
||||
|
||||
|
||||
// --- 调用外部传入的动作回调 ---
|
||||
// 只有单击时才执行导航或打开文件
|
||||
// 标记执行动作 (只在普通单击时)
|
||||
shouldPerformAction = true;
|
||||
}
|
||||
|
||||
// 在函数末尾根据标志决定是否执行动作
|
||||
console.log(`[Selection] Final check: shouldPerformAction = ${shouldPerformAction}`);
|
||||
if (shouldPerformAction) {
|
||||
console.log(`[Selection] Calling onItemAction for ${item.filename}`);
|
||||
onItemAction(item);
|
||||
} else {
|
||||
console.log(`[Selection] Skipping onItemAction for ${item.filename}`);
|
||||
}
|
||||
};
|
||||
|
||||
// 清空选择的辅助函数,可能在其他地方(如路径改变时)需要
|
||||
const clearSelection = () => {
|
||||
selectedItems.value.clear();
|
||||
lastClickedIndex.value = -1;
|
||||
};
|
||||
|
||||
return {
|
||||
selectedItems,
|
||||
lastClickedIndex, // 只读暴露,主要由内部管理
|
||||
handleItemClick,
|
||||
clearSelection, // 暴露清空选择的方法
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user