import { ref, nextTick, type Ref, type ComponentPublicInstance } from 'vue'; // 导入 ComponentPublicInstance import type { FileListItem } from '../../types/sftp.types'; // 修正路径 import { type useI18n } from 'vue-i18n'; // 导入 useI18n 以获取 t 的类型 import type FileManagerContextMenu from '../../components/FileManagerContextMenu.vue'; // <-- 导入组件类型 // 定义菜单项类型 (可以根据需要扩展) export interface ContextMenuItem { label: string; action: () => void; disabled?: boolean; } // 定义剪贴板状态类型 export interface ClipboardState { hasContent: boolean; operation?: 'copy' | 'cut'; // 可以添加 sourcePaths: string[] 等更多信息,但对于禁用/启用粘贴,hasContent 就够了 } // 定义 Composable 的输入参数类型 export interface UseFileManagerContextMenuOptions { selectedItems: Ref>; lastClickedIndex: Ref; fileList: Ref>; // 使用 Readonly 避免直接修改 currentPath: Ref; isConnected: Ref; isSftpReady: Ref; clipboardState: Ref>; // +++ 新增:剪贴板状态 +++ t: ReturnType['t']; // 使用 useI18n 获取 t 的类型 // --- 回调函数 --- onRefresh: () => void; onUpload: () => void; onDownload: (items: FileListItem[]) => void; // 修改:接受 FileListItem 数组 onDelete: () => void; // 删除操作现在由外部处理 onRename: (item: FileListItem) => void; onChangePermissions: (item: FileListItem) => void; onNewFolder: () => void; onNewFile: () => void; onCopy: () => void; // +++ 新增:复制回调 +++ onCut: () => void; // +++ 新增:剪切回调 +++ onPaste: () => void; // +++ 新增:粘贴回调 +++ } export function useFileManagerContextMenu(options: UseFileManagerContextMenuOptions) { const { selectedItems, lastClickedIndex, fileList, currentPath, isConnected, isSftpReady, clipboardState, // +++ 解构剪贴板状态 +++ t, onRefresh, onUpload, onDownload, onDelete, onRename, onChangePermissions, onNewFolder, onNewFile, onCopy, // +++ 解构复制回调 +++ onCut, // +++ 解构剪切回调 +++ onPaste, // +++ 解构粘贴回调 +++ } = options; const contextMenuVisible = ref(false); const contextMenuPosition = ref({ x: 0, y: 0 }); const contextMenuItems = ref([]); const contextTargetItem = ref(null); // 修正 Ref 类型为组件实例类型 const contextMenuRef = ref | null>(null); const showContextMenu = (event: MouseEvent, item?: FileListItem) => { event.preventDefault(); const targetItem = item || null; // Adjust selection based on right-click target (逻辑保持不变) if (targetItem && !event.ctrlKey && !event.metaKey && !event.shiftKey && !selectedItems.value.has(targetItem.filename)) { selectedItems.value.clear(); selectedItems.value.add(targetItem.filename); // 使用传入的 fileList ref const index = fileList.value.findIndex((f: FileListItem) => f.filename === targetItem.filename); // 添加类型 lastClickedIndex.value = index; } else if (!targetItem) { selectedItems.value.clear(); lastClickedIndex.value = -1; } contextTargetItem.value = targetItem; let menu: ContextMenuItem[] = []; const selectionSize = selectedItems.value.size; const clickedItemIsSelected = targetItem && selectedItems.value.has(targetItem.filename); const canPerformActions = isConnected.value && isSftpReady.value; const hasClipboardContent = clipboardState.value.hasContent; // +++ 获取剪贴板状态 +++ // Build context menu items (使用传入的回调) if (selectionSize > 1 && clickedItemIsSelected) { // Multi-selection menu const selectedFileItems = Array.from(selectedItems.value) .map(filename => fileList.value.find(f => f.filename === filename)) .filter((item): item is FileListItem => !!item); // 过滤掉未找到的项并确保类型 const allFilesSelected = selectedFileItems.length === selectionSize && selectedFileItems.every(item => item.attrs.isFile); menu = [ // 调整顺序:剪切、复制优先 { label: t('fileManager.actions.cut'), action: onCut, disabled: !canPerformActions }, { label: t('fileManager.actions.copy'), action: onCopy, disabled: !canPerformActions }, ]; // --- 新增:多选下载 --- if (allFilesSelected) { menu.push({ label: t('fileManager.actions.downloadMultiple', { count: selectionSize }), action: () => onDownload(selectedFileItems), disabled: !canPerformActions }); } menu.push( // --- 分隔符 (视觉) --- { label: t('fileManager.actions.deleteMultiple', { count: selectionSize }), action: onDelete, disabled: !canPerformActions }, // --- 分隔符 (视觉) --- { label: t('fileManager.actions.refresh'), action: onRefresh, disabled: !canPerformActions } ); } else if (targetItem && targetItem.filename !== '..') { // Single item (not '..') menu // --- 修改:单选下载也调用接收数组的回调 --- menu = []; // 1. 主要操作 (下载 - 如果是文件) if (targetItem.attrs.isFile) { menu.push({ label: t('fileManager.actions.download', { name: targetItem.filename }), action: () => onDownload([targetItem]), disabled: !canPerformActions }); // 传递包含单个项的数组 } // 2. 剪切、复制、粘贴 (粘贴 - 如果是文件夹) menu.push({ label: t('fileManager.actions.cut'), action: onCut, disabled: !canPerformActions }); menu.push({ label: t('fileManager.actions.copy'), action: onCopy, disabled: !canPerformActions }); if (targetItem.attrs.isDirectory) { menu.push({ label: t('fileManager.actions.paste'), action: onPaste, disabled: !canPerformActions || !hasClipboardContent }); } // --- 分隔符 (视觉) --- // 3. 删除、重命名 menu.push({ label: t('fileManager.actions.delete'), action: onDelete, disabled: !canPerformActions }); menu.push({ label: t('fileManager.actions.rename'), action: () => onRename(targetItem), disabled: !canPerformActions }); // --- 分隔符 (视觉) --- // 4. 新建、上传 (这些更像空白处操作,但保留) menu.push({ label: t('fileManager.actions.newFolder'), action: onNewFolder, disabled: !canPerformActions }); menu.push({ label: t('fileManager.actions.newFile'), action: onNewFile, disabled: !canPerformActions }); menu.push({ label: t('fileManager.actions.upload'), action: onUpload, disabled: !canPerformActions }); // 上传放在新建之后 // --- 分隔符 (视觉) --- // 5. 权限、刷新 menu.push({ label: t('fileManager.actions.changePermissions'), action: () => onChangePermissions(targetItem), disabled: !canPerformActions }); menu.push({ label: t('fileManager.actions.refresh'), action: onRefresh, disabled: !canPerformActions }); } else if (!targetItem) { // Right-click on empty space menu menu = [ // 1. 粘贴 { label: t('fileManager.actions.paste'), action: onPaste, disabled: !canPerformActions || !hasClipboardContent }, // --- 分隔符 (视觉) --- // 2. 新建、上传 { label: t('fileManager.actions.newFolder'), action: onNewFolder, disabled: !canPerformActions }, { label: t('fileManager.actions.newFile'), action: onNewFile, disabled: !canPerformActions }, { label: t('fileManager.actions.upload'), action: onUpload, disabled: !canPerformActions }, // --- 分隔符 (视觉) --- // 3. 刷新 { label: t('fileManager.actions.refresh'), action: onRefresh, disabled: !canPerformActions }, ]; } else { // Clicked on '..' menu = [ // +++ 添加粘贴 (可以粘贴到上级目录) +++ { label: t('fileManager.actions.paste'), action: onPaste, disabled: !canPerformActions || !hasClipboardContent }, { label: t('fileManager.actions.refresh'), action: onRefresh, disabled: !canPerformActions } ]; } contextMenuItems.value = menu; // Set initial position based on click event contextMenuPosition.value = { x: event.clientX, y: event.clientY }; contextMenuVisible.value = true; // Make menu visible so we can measure it // Use nextTick to allow the DOM to update and the menu to render nextTick(() => { // Access the DOM element via $el from the component instance ref const menuElement = contextMenuRef.value?.$el as HTMLDivElement | undefined; if (menuElement && contextMenuVisible.value) { // const menuElement = contextMenuRef.value; // Old way const menuRect = menuElement.getBoundingClientRect(); // Now should work const menuWidth = menuRect.width; const menuHeight = menuRect.height; let finalX = contextMenuPosition.value.x; let finalY = contextMenuPosition.value.y; // Adjust horizontally if needed if (finalX + menuWidth > window.innerWidth) { finalX = window.innerWidth - menuWidth - 5; } // Adjust vertically if needed if (finalY + menuHeight > window.innerHeight) { finalY = window.innerHeight - menuHeight - 5; } // Ensure menu doesn't go off-screen top or left finalX = Math.max(5, finalX); finalY = Math.max(5, finalY); // Update the position state if adjustments were made if (finalX !== contextMenuPosition.value.x || finalY !== contextMenuPosition.value.y) { console.log(`[useFileManagerContextMenu] Adjusting context menu position: (${contextMenuPosition.value.x}, ${contextMenuPosition.value.y}) -> (${finalX}, ${finalY})`); contextMenuPosition.value = { x: finalX, y: finalY }; } // Add global listener to hide menu *after* positioning document.removeEventListener('click', hideContextMenu, { capture: true }); document.addEventListener('click', hideContextMenu, { capture: true, once: true }); } else { // Fallback listener if measurement fails document.removeEventListener('click', hideContextMenu, { capture: true }); document.addEventListener('click', hideContextMenu, { capture: true, once: true }); } }); }; const hideContextMenu = () => { if (!contextMenuVisible.value) return; contextMenuVisible.value = false; contextMenuItems.value = []; contextTargetItem.value = null; // 清理目标项 document.removeEventListener('click', hideContextMenu, { capture: true }); }; // 返回需要暴露的状态和方法 return { contextMenuVisible, contextMenuPosition, contextMenuItems, contextTargetItem, // 可能外部需要知道右键点击了哪个项 contextMenuRef, showContextMenu, hideContextMenu, }; }