feat: 文件管理器操作替换为自定义模态框

This commit is contained in:
Baobhan Sith
2025-05-07 09:41:12 +08:00
parent 78ee62e947
commit 2a0cf41dc7
5 changed files with 493 additions and 74 deletions
+116 -69
View File
@@ -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<typeof createSftpActionsManager>;
@@ -124,6 +125,13 @@ const fileListContainerRef = ref<HTMLDivElement | null>(null); // 文件列表
const dropOverlayRef = ref<HTMLDivElement | null>(null); // +++ 新增:拖拽蒙版引用 +++
// const scrollIntervalId = ref<number | null>(null); // 已移至 useFileManagerDragAndDrop
// +++ 新增:操作模态框状态 +++
const isActionModalVisible = ref(false);
const currentActionType = ref<'delete' | 'rename' | 'chmod' | 'newFile' | 'newFolder' | null>(null);
const actionItem = ref<FileListItem | null>(null); // For single item operations
const actionItems = ref<FileListItem[]>([]); // For multi-item operations (e.g., delete)
const actionInitialValue = ref(''); // For pre-filling input in modal
// +++ 新增:剪贴板状态 +++
const clipboardState = ref<ClipboardState>({ hasContent: false });
const clipboardSourcePaths = ref<string[]>([]); // 存储源完整路径
@@ -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"
/>
<!-- Action Modal -->
<FileManagerActionModal
:is-visible="isActionModalVisible"
:action-type="currentActionType"
:item="actionItem"
:items="actionItems"
:initial-value="actionInitialValue"
@close="handleModalClose"
@confirm="handleModalConfirm"
/>
</div>
</div>
</template>
<style scoped>