update
This commit is contained in:
@@ -12,6 +12,7 @@ import { useSessionStore } from '../stores/session.store';
|
|||||||
import { useSettingsStore } from '../stores/settings.store';
|
import { useSettingsStore } from '../stores/settings.store';
|
||||||
import { useFocusSwitcherStore } from '../stores/focusSwitcher.store'; // +++ 导入焦点切换 Store +++
|
import { useFocusSwitcherStore } from '../stores/focusSwitcher.store'; // +++ 导入焦点切换 Store +++
|
||||||
import { useFileManagerContextMenu } from '../composables/file-manager/useFileManagerContextMenu'; // +++ 导入上下文菜单 Composable +++
|
import { useFileManagerContextMenu } from '../composables/file-manager/useFileManagerContextMenu'; // +++ 导入上下文菜单 Composable +++
|
||||||
|
import { useFileManagerSelection } from '../composables/file-manager/useFileManagerSelection'; // +++ 导入选择 Composable +++
|
||||||
// WebSocket composable 不再直接使用
|
// WebSocket composable 不再直接使用
|
||||||
import FileUploadPopup from './FileUploadPopup.vue';
|
import FileUploadPopup from './FileUploadPopup.vue';
|
||||||
import FileManagerContextMenu from './FileManagerContextMenu.vue'; // +++ 导入上下文菜单组件 +++
|
import FileManagerContextMenu from './FileManagerContextMenu.vue'; // +++ 导入上下文菜单组件 +++
|
||||||
@@ -120,8 +121,9 @@ const { shareFileEditorTabsBoolean } = storeToRefs(settingsStore); // 使用 sto
|
|||||||
|
|
||||||
// --- UI 状态 Refs (Remain mostly the same) ---
|
// --- UI 状态 Refs (Remain mostly the same) ---
|
||||||
const fileInputRef = ref<HTMLInputElement | null>(null);
|
const fileInputRef = ref<HTMLInputElement | null>(null);
|
||||||
const selectedItems = ref(new Set<string>());
|
// --- 选择状态 (移至 useFileManagerSelection) ---
|
||||||
const lastClickedIndex = ref(-1);
|
// const selectedItems = ref(new Set<string>()); // 移除旧的 ref
|
||||||
|
// const lastClickedIndex = ref(-1); // 移除旧的 ref
|
||||||
// --- 上下文菜单状态 (移至 useFileManagerContextMenu) ---
|
// --- 上下文菜单状态 (移至 useFileManagerContextMenu) ---
|
||||||
// const contextMenuVisible = ref(false);
|
// const contextMenuVisible = ref(false);
|
||||||
// const contextMenuPosition = ref({ x: 0, y: 0 });
|
// const contextMenuPosition = ref({ x: 0, y: 0 });
|
||||||
@@ -183,9 +185,107 @@ const formatMode = (mode: number): string => {
|
|||||||
return str;
|
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 使用) ---
|
// --- SFTP 操作处理函数 (定义在此处,供 Composable 使用) ---
|
||||||
const handleDeleteSelectedClick = () => {
|
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)
|
const itemsToDelete = Array.from(selectedItems.value)
|
||||||
.map(filename => fileList.value.find((f: FileListItem) => f.filename === filename)) // f 已有类型
|
.map(filename => fileList.value.find((f: FileListItem) => f.filename === filename)) // f 已有类型
|
||||||
.filter((item): item is FileListItem => item !== undefined);
|
.filter((item): item is FileListItem => item !== undefined);
|
||||||
@@ -280,7 +380,7 @@ const triggerDownload = (item: FileListItem) => { // item 已有类型
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// --- 上下文菜单逻辑 (使用 Composable) ---
|
// --- 上下文菜单逻辑 (使用 Composable, 需要 Selection 和 Action Handlers) ---
|
||||||
const {
|
const {
|
||||||
contextMenuVisible,
|
contextMenuVisible,
|
||||||
contextMenuPosition,
|
contextMenuPosition,
|
||||||
@@ -289,8 +389,8 @@ const {
|
|||||||
showContextMenu, // 使用 Composable 提供的函数
|
showContextMenu, // 使用 Composable 提供的函数
|
||||||
hideContextMenu, // <-- 获取 hideContextMenu 函数
|
hideContextMenu, // <-- 获取 hideContextMenu 函数
|
||||||
} = useFileManagerContextMenu({
|
} = useFileManagerContextMenu({
|
||||||
selectedItems,
|
selectedItems, // 传递来自 useFileManagerSelection 的 selectedItems
|
||||||
lastClickedIndex,
|
lastClickedIndex, // 传递来自 useFileManagerSelection 的 lastClickedIndex
|
||||||
fileList, // 传递 sftpManager 的 fileList
|
fileList, // 传递 sftpManager 的 fileList
|
||||||
currentPath, // 传递 sftpManager 的 currentPath
|
currentPath, // 传递 sftpManager 的 currentPath
|
||||||
isConnected: props.wsDeps.isConnected, // 传递响应式引用
|
isConnected: props.wsDeps.isConnected, // 传递响应式引用
|
||||||
@@ -310,70 +410,6 @@ const {
|
|||||||
// --- 目录加载与导航 ---
|
// --- 目录加载与导航 ---
|
||||||
// loadDirectory is provided by props.sftpManager
|
// 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) => {
|
const handleDragEnter = (event: DragEvent) => {
|
||||||
if (props.wsDeps.isConnected.value && event.dataTransfer?.types.includes('Files')) { // 恢复使用 props.wsDeps.isConnected
|
if (props.wsDeps.isConnected.value && event.dataTransfer?.types.includes('Files')) { // 恢复使用 props.wsDeps.isConnected
|
||||||
@@ -701,60 +737,6 @@ const handleFileSelected = (event: Event) => {
|
|||||||
input.value = '';
|
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 handleKeydown = (event: KeyboardEvent) => {
|
||||||
const list = filteredFileList.value;
|
const list = filteredFileList.value;
|
||||||
@@ -817,11 +799,23 @@ const scrollToSelected = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- 重置选中索引的 Watchers ---
|
// --- 重置选中索引和清空选择的 Watchers ---
|
||||||
watch(currentPath, () => { selectedIndex.value = -1; });
|
watch(currentPath, () => {
|
||||||
watch(searchQuery, () => { selectedIndex.value = -1; });
|
selectedIndex.value = -1;
|
||||||
watch(sortKey, () => { selectedIndex.value = -1; });
|
clearSelection(); // 清空选择
|
||||||
watch(sortDirection, () => { selectedIndex.value = -1; });
|
});
|
||||||
|
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
|
} else if (!props.wsDeps.isConnected.value && initialLoadDone.value) { // 恢复使用 props.wsDeps.isConnected
|
||||||
console.log(`[FileManager ${props.sessionId}] 连接丢失 (之前已加载),重置状态。`);
|
console.log(`[FileManager ${props.sessionId}] 连接丢失 (之前已加载),重置状态。`);
|
||||||
selectedItems.value.clear();
|
clearSelection(); // 清空选择
|
||||||
lastClickedIndex.value = -1;
|
|
||||||
initialLoadDone.value = false; // 重置初始加载状态
|
initialLoadDone.value = false; // 重置初始加载状态
|
||||||
|
// lastClickedIndex.value = -1; // 由 clearSelection 处理
|
||||||
isFetchingInitialPath.value = false; // 重置获取状态
|
isFetchingInitialPath.value = false; // 重置获取状态
|
||||||
cleanupListeners();
|
cleanupListeners();
|
||||||
}
|
}
|
||||||
@@ -1202,8 +1196,8 @@ const handleWheel = (event: WheelEvent) => {
|
|||||||
:class="[
|
:class="[
|
||||||
'file-row',
|
'file-row',
|
||||||
{ clickable: item.attrs.isDirectory || item.attrs.isFile },
|
{ clickable: item.attrs.isDirectory || item.attrs.isFile },
|
||||||
/* { selected: selectedItems.has(item.filename) }, */ /* 移除鼠标选择的 selected 类,统一用键盘的 */
|
{ selected: selectedItems.has(item.filename) }, /* 恢复:使用 selectedItems Set 控制选中高亮 */
|
||||||
{ selected: index + (currentPath !== '/' ? 1 : 0) === selectedIndex }, /* 键盘选中高亮 */
|
// { selected: index + (currentPath !== '/' ? 1 : 0) === selectedIndex }, /* 暂时移除键盘选中高亮 */
|
||||||
{ 'folder-row': item.attrs.isDirectory }, // 添加文件夹标识类
|
{ 'folder-row': item.attrs.isDirectory }, // 添加文件夹标识类
|
||||||
{ 'drop-target': item.attrs.isDirectory && dragOverTarget === item.filename } // 拖拽悬停高亮
|
{ '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