This commit is contained in:
Baobhan Sith
2025-04-21 20:12:02 +08:00
parent 23bc3f4b3d
commit f49b735290
2 changed files with 291 additions and 133 deletions
+127 -133
View File
@@ -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 } // 拖拽悬停高亮
]"