diff --git a/packages/frontend/src/components/FileManager.vue b/packages/frontend/src/components/FileManager.vue index af98c07..461852f 100644 --- a/packages/frontend/src/components/FileManager.vue +++ b/packages/frontend/src/components/FileManager.vue @@ -14,9 +14,10 @@ import { useFileManagerSelection } from '../composables/file-manager/useFileMana import { useFileManagerDragAndDrop } from '../composables/file-manager/useFileManagerDragAndDrop'; import { useFileManagerKeyboardNavigation } from '../composables/file-manager/useFileManagerKeyboardNavigation'; import FileUploadPopup from './FileUploadPopup.vue'; -import FileManagerContextMenu from './FileManagerContextMenu.vue'; +import FileManagerContextMenu from './FileManagerContextMenu.vue'; +import FileManagerActionModal from './FileManagerActionModal.vue'; // +++ 新增导入 +++ import type { FileListItem } from '../types/sftp.types'; -import type { WebSocketMessage } from '../types/websocket.types'; +import type { WebSocketMessage } from '../types/websocket.types'; type SftpManagerInstance = ReturnType; @@ -124,6 +125,13 @@ const fileListContainerRef = ref(null); // 文件列表 const dropOverlayRef = ref(null); // +++ 新增:拖拽蒙版引用 +++ // const scrollIntervalId = ref(null); // 已移至 useFileManagerDragAndDrop +// +++ 新增:操作模态框状态 +++ +const isActionModalVisible = ref(false); +const currentActionType = ref<'delete' | 'rename' | 'chmod' | 'newFile' | 'newFolder' | null>(null); +const actionItem = ref(null); // For single item operations +const actionItems = ref([]); // For multi-item operations (e.g., delete) +const actionInitialValue = ref(''); // For pre-filling input in modal + // +++ 新增:剪贴板状态 +++ const clipboardState = ref({ hasContent: false }); const clipboardSourcePaths = ref([]); // 存储源完整路径 @@ -275,6 +283,90 @@ const { }); +// --- 操作模态框辅助函数 --- +const openActionModal = ( + type: 'delete' | 'rename' | 'chmod' | 'newFile' | 'newFolder', + item?: FileListItem | null, // For single item operations like rename, chmod + items?: FileListItem[], // For multi-item operations like delete + initialValue?: string // For pre-filling input, e.g., old name for rename +) => { + currentActionType.value = type; + actionItem.value = item || null; + actionItems.value = items || (item ? [item] : []); // Ensure actionItems has the item(s) + actionInitialValue.value = initialValue || ''; + isActionModalVisible.value = true; +}; + +const handleModalClose = () => { + isActionModalVisible.value = false; + // Reset states if needed, though they'll be overwritten on next open + currentActionType.value = null; + actionItem.value = null; + actionItems.value = []; + actionInitialValue.value = ''; +}; + +const handleModalConfirm = (value?: string) => { + if (!currentSftpManager.value || !currentActionType.value) { + handleModalClose(); + return; + } + const manager = currentSftpManager.value; + + switch (currentActionType.value) { + case 'delete': + if (actionItems.value.length > 0) { + manager.deleteItems(actionItems.value); + selectedItems.value.clear(); // Clear selection after delete + } + break; + case 'rename': + if (actionItem.value && value && value !== actionItem.value.filename) { + manager.renameItem(actionItem.value, value); + } + break; + case 'chmod': + if (actionItem.value && value && /^[0-7]{3,4}$/.test(value)) { + const newMode = parseInt(value, 8); + manager.changePermissions(actionItem.value, newMode); + } else if (value) { // value exists but is invalid + // Optionally, re-open modal with error or use a notification + // For now, just log and close + console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Invalid chmod value from modal: ${value}`); + // It might be better to show an error in the modal itself and not close it. + // The modal currently has its own validation, so this path might not be hit often. + } + break; + case 'newFile': + if (value) { + if (manager.fileList.value.some((item: FileListItem) => item.filename === value)) { + // alert(t('fileManager.errors.fileExists', { name: value })); // Consider using modal for this error too + console.warn(`[FileManager ${props.sessionId}-${props.instanceId}] File ${value} already exists. Modal should prevent this.`); + // Re-open modal or show error in modal + // For now, we rely on modal's internal logic or a notification system + // To prevent closing, we can avoid calling handleModalClose here if an error occurs. + // However, the current modal design closes on confirm. + // A more robust solution would be for the modal to emit 'error' or handle validation internally. + return; // Prevent closing if error + } + manager.createFile(value); + } + break; + case 'newFolder': + if (value) { + if (manager.fileList.value.some((item: FileListItem) => item.filename === value)) { + // alert(t('fileManager.errors.folderExists', { name: value })); + console.warn(`[FileManager ${props.sessionId}-${props.instanceId}] Folder ${value} already exists. Modal should prevent this.`); + return; // Prevent closing if error + } + manager.createDirectory(value); + } + break; + } + handleModalClose(); // Close modal after action +}; + + // --- SFTP 操作处理函数 (定义在此处,供 Composable 使用) --- const handleDeleteSelectedClick = () => { // 修改:检查 currentSftpManager 是否存在 @@ -282,92 +374,36 @@ const handleDeleteSelectedClick = () => { // 使用 props.wsDeps 和 currentSftpManager.value.fileList if (!props.wsDeps.isConnected.value || selectedItems.value.size === 0) return; const itemsToDelete = Array.from(selectedItems.value) - .map(filename => currentSftpManager.value?.fileList.value.find((f: FileListItem) => f.filename === filename)) // 从 manager 获取列表 + .map(filename => currentSftpManager.value?.fileList.value.find((f: FileListItem) => f.filename === filename)) .filter((item): item is FileListItem => item !== undefined); - if (itemsToDelete.length === 0) return; + if (itemsToDelete.length === 0) return; - const names = itemsToDelete.map(i => i.filename).join(', '); - const confirmMsg = itemsToDelete.length > 1 - ? t('fileManager.prompts.confirmDeleteMultiple', { count: itemsToDelete.length, names: names }) - : itemsToDelete[0].attrs.isDirectory - ? t('fileManager.prompts.confirmDeleteFolder', { name: itemsToDelete[0].filename }) - : t('fileManager.prompts.confirmDeleteFile', { name: itemsToDelete[0].filename }); - - if (confirm(confirmMsg)) { - // 修改:使用 currentSftpManager.value.deleteItems - currentSftpManager.value?.deleteItems(itemsToDelete); - selectedItems.value.clear(); - } + openActionModal('delete', null, itemsToDelete); }; const handleRenameContextMenuClick = (item: FileListItem) => { // item 已有类型 if (!props.wsDeps.isConnected.value || !item) return; // 恢复使用 props.wsDeps - // 修改:检查 currentSftpManager 是否存在 if (!currentSftpManager.value) return; - const newName = prompt(t('fileManager.prompts.enterNewName', { oldName: item.filename }), item.filename); - if (newName && newName !== item.filename) { - // 修改:添加 ?. 访问 - currentSftpManager.value?.renameItem(item, newName); - } + openActionModal('rename', item, undefined, item.filename); }; const handleChangePermissionsContextMenuClick = (item: FileListItem) => { // item 已有类型 if (!props.wsDeps.isConnected.value || !item) return; // 恢复使用 props.wsDeps + if (!currentSftpManager.value) return; const currentModeOctal = (item.attrs.mode & 0o777).toString(8).padStart(3, '0'); - const newModeStr = prompt(t('fileManager.prompts.enterNewPermissions', { name: item.filename, currentMode: currentModeOctal }), currentModeOctal); - if (newModeStr) { - if (!/^[0-7]{3,4}$/.test(newModeStr)) { - alert(t('fileManager.errors.invalidPermissionsFormat')); - return; - } - const newMode = parseInt(newModeStr, 8); - // 修改:在调用前检查 currentSftpManager - if (!currentSftpManager.value) { - console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot change permissions: SFTP manager not available.`); - return; - } - currentSftpManager.value.changePermissions(item, newMode); - } + openActionModal('chmod', item, undefined, currentModeOctal); }; const handleNewFolderContextMenuClick = () => { if (!props.wsDeps.isConnected.value) return; // 恢复使用 props.wsDeps - // 修改:检查 currentSftpManager 是否存在 if (!currentSftpManager.value) return; - const folderName = prompt(t('fileManager.prompts.enterFolderName')); - if (folderName) { - // 修改:使用 currentSftpManager.value.fileList - if (currentSftpManager.value.fileList.value.some((item: FileListItem) => item.filename === folderName)) { - alert(t('fileManager.errors.folderExists', { name: folderName })); - return; - } - // 修改:确保在检查后调用,并检查 manager - if (!currentSftpManager.value) { - console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot create directory: SFTP manager not available.`); - return; - } - currentSftpManager.value.createDirectory(folderName); - } + openActionModal('newFolder'); }; const handleNewFileContextMenuClick = () => { if (!props.wsDeps.isConnected.value) return; // 恢复使用 props.wsDeps - // 修改:检查 currentSftpManager 是否存在 if (!currentSftpManager.value) return; - const fileName = prompt(t('fileManager.prompts.enterFileName')); - if (fileName) { - // 修改:使用 currentSftpManager.value.fileList - if (currentSftpManager.value.fileList.value.some((item: FileListItem) => item.filename === fileName)) { - alert(t('fileManager.errors.fileExists', { name: fileName })); - return; - } - // 修改:确保在检查后调用,并检查 manager - if (!currentSftpManager.value) { - console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot create file: SFTP manager not available.`); - return; - } - currentSftpManager.value.createFile(fileName); - } + openActionModal('newFile'); }; // +++ 新增:复制、剪切、粘贴处理函数 +++ @@ -1497,11 +1533,22 @@ const handleOpenEditorClick = () => { :is-visible="contextMenuVisible" :position="contextMenuPosition" :items="contextMenuItems" - @close-request="hideContextMenu" - /> + @close-request="hideContextMenu" + /> + + + - + \ No newline at end of file diff --git a/packages/frontend/src/locales/en-US.json b/packages/frontend/src/locales/en-US.json index 4bcd0ea..1600bd2 100644 --- a/packages/frontend/src/locales/en-US.json +++ b/packages/frontend/src/locales/en-US.json @@ -338,7 +338,45 @@ "dropFilesHere": "Drop files here to upload", "changeEncodingTooltip": "Change file encoding", "loadingEncoding": "Loading...", - "noSearchResults": "No search results found" + "noSearchResults": "No search results found", + "modals": { + "titles": { + "delete": "Delete \"{name}\"", + "deleteMultiple": "Delete {count} Items", + "rename": "Rename \"{name}\"", + "chmod": "Change Permissions for \"{name}\"", + "newFile": "Create New File", + "newFolder": "Create New Folder" + }, + "buttons": { + "delete": "Delete", + "rename": "Rename", + "changePermissions": "Set Permissions", + "create": "Create", + "confirm": "Confirm", + "cancel": "Cancel", + "close": "Close" + }, + "messages": { + "confirmDelete": "Are you sure you want to delete the {type} \"{name}\"? This action cannot be undone.", + "confirmDeleteMultiple": "Are you sure you want to delete these {count} items? This action cannot be undone.\nItems: {names}" + }, + "labels": { + "newName": "New name:", + "newPermissions": "New permissions (octal):", + "fileName": "File name:", + "folderName": "Folder name:", + "folder": "folder", + "file": "file" + }, + "placeholders": { + "newName": "Enter new name", + "newPermissions": "e.g., 755 or 0755", + "newFile": "Enter file name", + "newFolder": "Enter folder name" + }, + "chmodHelp": "Enter permissions in octal format (e.g., 755 or 0755)." + } }, "statusMonitor": { "title": "Server Status", diff --git a/packages/frontend/src/locales/ja-JP.json b/packages/frontend/src/locales/ja-JP.json index 563ecb8..b3098f7 100644 --- a/packages/frontend/src/locales/ja-JP.json +++ b/packages/frontend/src/locales/ja-JP.json @@ -336,9 +336,47 @@ }, "changeEncodingTooltip": "ファイルエンコーディングを変更", "loadingEncoding": "読み込み中...", - "noSearchResults": "検索結果が見つかりませんでした" - }, - "focusSwitcher": { + "noSearchResults": "検索結果が見つかりませんでした", + "modals": { + "titles": { + "delete": "\"{name}\" を削除", + "deleteMultiple": "{count} 個のアイテムを削除", + "rename": "\"{name}\" の名前を変更", + "chmod": "\"{name}\" の権限を変更", + "newFile": "新しいファイルを作成", + "newFolder": "新しいフォルダーを作成" + }, + "buttons": { + "delete": "削除", + "rename": "名前を変更", + "changePermissions": "権限を設定", + "create": "作成", + "confirm": "確認", + "cancel": "キャンセル", + "close": "閉じる" + }, + "messages": { + "confirmDelete": "{type} \"{name}\" を削除してもよろしいですか?この操作は元に戻せません。", + "confirmDeleteMultiple": "これらの {count} 個のアイテムを削除してもよろしいですか?この操作は元に戻せません。\nアイテム: {names}" + }, + "labels": { + "newName": "新しい名前:", + "newPermissions": "新しい権限 (8進数):", + "fileName": "ファイル名:", + "folderName": "フォルダー名:", + "folder": "フォルダー", + "file": "ファイル" + }, + "placeholders": { + "newName": "新しい名前を入力", + "newPermissions": "例: 755 または 0755", + "newFile": "ファイル名を入力", + "newFolder": "フォルダー名を入力" + }, + "chmodHelp": "8進数形式で権限を入力してください (例: 755 または 0755)。" + } + }, + "focusSwitcher": { "allInputsConfigured": "すべての利用可能な入力ソースが設定されました", "altSwitchHint": "ヒント:Alt キーを押すと、設定された入力ソース間でフォーカスを素早く切り替えることができます。", "availableInputs": "利用可能な入力ソース", diff --git a/packages/frontend/src/locales/zh-CN.json b/packages/frontend/src/locales/zh-CN.json index 0616a08..d5e7a61 100644 --- a/packages/frontend/src/locales/zh-CN.json +++ b/packages/frontend/src/locales/zh-CN.json @@ -338,7 +338,45 @@ "dropFilesHere": "将文件拖拽到此处上传", "changeEncodingTooltip": "更改文件编码", "loadingEncoding": "加载中...", -"noSearchResults": "未找到匹配的搜索结果" + "noSearchResults": "未找到匹配的搜索结果", + "modals": { + "titles": { + "delete": "删除 \"{name}\"", + "deleteMultiple": "删除 {count} 个项目", + "rename": "重命名 \"{name}\"", + "chmod": "修改 \"{name}\" 的权限", + "newFile": "创建新文件", + "newFolder": "创建新文件夹" + }, + "buttons": { + "delete": "删除", + "rename": "重命名", + "changePermissions": "设置权限", + "create": "创建", + "confirm": "确认", + "cancel": "取消", + "close": "关闭" + }, + "messages": { + "confirmDelete": "您确定要删除{type} \"{name}\" 吗?此操作无法撤销。", + "confirmDeleteMultiple": "您确定要删除这 {count} 个项目吗?此操作无法撤销。\n项目: {names}" + }, + "labels": { + "newName": "新名称:", + "newPermissions": "新权限 (八进制):", + "fileName": "文件名:", + "folderName": "文件夹名称:", + "folder": "文件夹", + "file": "文件" + }, + "placeholders": { + "newName": "输入新名称", + "newPermissions": "例如 755 或 0755", + "newFile": "输入文件名", + "newFolder": "输入文件夹名称" + }, + "chmodHelp": "请输入八进制格式的权限 (例如 755 或 0755)。" + } }, "statusMonitor": { "title": "服务器状态",