feat: 为文件管理器添加右键“解/压缩”功能

Refs #28
This commit is contained in:
Baobhan Sith
2025-05-13 00:26:55 +08:00
parent 688b9df707
commit 52b797837e
18 changed files with 1126 additions and 82 deletions
@@ -5,13 +5,13 @@ import { useRoute } from 'vue-router';
import { storeToRefs } from 'pinia';
import { createSftpActionsManager, type WebSocketDependencies } from '../composables/useSftpActions';
import { useFileUploader } from '../composables/useFileUploader';
import { useFileEditorStore, type FileInfo } from '../stores/fileEditor.store'; // 确保已导入
import { useFileEditorStore, type FileInfo } from '../stores/fileEditor.store';
import { useSessionStore } from '../stores/session.store';
import { useSettingsStore } from '../stores/settings.store';
import { useFocusSwitcherStore } from '../stores/focusSwitcher.store';
import { useFileManagerContextMenu, type ClipboardState } from '../composables/file-manager/useFileManagerContextMenu';
import { useFileManagerSelection } from '../composables/file-manager/useFileManagerSelection';
import { useFileManagerDragAndDrop } from '../composables/file-manager/useFileManagerDragAndDrop';
import { useFileManagerContextMenu, type ClipboardState, type CompressFormat } from '../composables/file-manager/useFileManagerContextMenu';
import { useFileManagerSelection } from '../composables/file-manager/useFileManagerSelection';
import { useFileManagerDragAndDrop } from '../composables/file-manager/useFileManagerDragAndDrop';
import { useFileManagerKeyboardNavigation } from '../composables/file-manager/useFileManagerKeyboardNavigation';
import FileUploadPopup from './FileUploadPopup.vue';
import FileManagerContextMenu from './FileManagerContextMenu.vue';
@@ -34,11 +34,6 @@ const props = defineProps({
type: String,
required: true,
},
// // 注入此会话特定的 SFTP 管理器实例 (移除)
// sftpManager: {
// type: Object as PropType<SftpManagerInstance>,
// required: true,
// },
// 注入数据库连接 ID
dbConnectionId: {
type: String,
@@ -610,9 +605,33 @@ const triggerDownloadDirectory = (item: FileListItem) => {
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Network error during directory download:`, error);
alert(`${t('fileManager.errors.downloadDirectoryFailed', 'Failed to download directory')}: Network error.`);
});
// --- 结束修改 ---
};
// +++ 添加压缩/解压处理函数 +++
const handleCompress = (items: FileListItem[], format: CompressFormat) => {
if (!currentSftpManager.value) {
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot compress: SFTP manager not available.`);
// TODO: Show error notification
return;
}
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Requesting compression for ${items.length} items, format: ${format}`);
// 调用 SFTP 管理器上的新方法 (将在 useSftpActions.ts 中实现)
currentSftpManager.value.compressItems(items, format);
};
const handleDecompress = (item: FileListItem) => {
if (!currentSftpManager.value) {
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot decompress: SFTP manager not available.`);
// TODO: Show error notification
return;
}
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Requesting decompression for item: ${item.filename}`);
// 调用 SFTP 管理器上的新方法 (将在 useSftpActions.ts 中实现)
currentSftpManager.value.decompressItem(item);
};
// --- 结束新增 ---
// --- 上下文菜单逻辑 (使用 Composable, 需要 Selection 和 Action Handlers) ---
@@ -651,6 +670,9 @@ const {
onCut: handleCut, // +++ 传递剪切回调 +++
onPaste: handlePaste, // +++ 传递粘贴回调 +++
onDownloadDirectory: triggerDownloadDirectory, // +++ 传递文件夹下载回调 +++
// +++ 传递压缩/解压回调 +++
onCompressRequest: handleCompress,
onDecompressRequest: handleDecompress,
});
// --- 目录加载与导航 ---
@@ -38,19 +38,21 @@ const handleItemClick = (item: ContextMenuItem) => {
@click.stop
>
<ul class="list-none p-1 m-0">
<li
v-for="(menuItem, index) in items"
:key="index"
@click.stop="handleItemClick(menuItem)"
:class="[
'px-4 py-1.5 cursor-pointer text-foreground text-sm flex items-center transition-colors duration-150 rounded',
menuItem.disabled
? 'text-text-secondary cursor-not-allowed opacity-60 bg-background'
: 'hover:bg-border' // Changed hover background for better visibility
]"
>
{{ menuItem.label }}
</li>
<template v-for="(menuItem, index) in items" :key="index">
<li v-if="menuItem.separator" class="border-t border-border/50 my-1 mx-1"></li>
<li
v-else
@click.stop="handleItemClick(menuItem)"
:class="[
'px-4 py-1.5 cursor-pointer text-foreground text-sm flex items-center transition-colors duration-150 rounded mx-1', // Added mx-1 for consistency
menuItem.disabled
? 'text-text-secondary cursor-not-allowed opacity-60' // Removed bg-background for disabled
: 'hover:bg-primary/10 hover:text-primary' // Use primary hover like TabBarContextMenu
]"
>
{{ menuItem.label }}
</li>
</template>
</ul>
</div>
</template>
@@ -33,7 +33,6 @@ const props = defineProps({
required: false,
default: null,
},
// +++ 添加 isMobile prop +++
isMobile: {
type: Boolean,
default: false,
@@ -8,8 +8,12 @@ export interface ContextMenuItem {
label: string;
action: () => void;
disabled?: boolean;
separator?: boolean; // 添加分隔符类型
}
// 支持的压缩格式
export type CompressFormat = 'zip' | 'targz' | 'tarbz2';
// 定义剪贴板状态类型
export interface ClipboardState {
hasContent: boolean;
@@ -40,8 +44,19 @@ export interface UseFileManagerContextMenuOptions {
onCopy: () => void; // +++ 复制回调 +++
onCut: () => void; // +++ 剪切回调 +++
onPaste: () => void; // +++ 粘贴回调 +++
// --- 压缩/解压回调 ---
onCompressRequest: (items: FileListItem[], format: CompressFormat) => void; // +++ 压缩回调 +++
onDecompressRequest: (item: FileListItem) => void; // +++ 解压回调 +++
}
// 辅助函数:检查文件是否为支持的压缩格式
const SUPPORTED_ARCHIVE_EXTENSIONS = ['.zip', '.tar.gz', '.tgz', '.tar.bz2', '.tbz2'];
function isSupportedArchive(filename: string): boolean {
const lowerCaseFilename = filename.toLowerCase();
return SUPPORTED_ARCHIVE_EXTENSIONS.some(ext => lowerCaseFilename.endsWith(ext));
}
export function useFileManagerContextMenu(options: UseFileManagerContextMenuOptions) {
const {
selectedItems,
@@ -64,6 +79,8 @@ export function useFileManagerContextMenu(options: UseFileManagerContextMenuOpti
onCut, // +++ 解构剪切回调 +++
onPaste, // +++ 解构粘贴回调 +++
onDownloadDirectory, // +++ 解构文件夹下载回调 +++
onCompressRequest, // +++ 解构压缩回调 +++
onDecompressRequest, // +++ 解构解压回调 +++
} = options;
const contextMenuVisible = ref(false);
@@ -119,6 +136,16 @@ export function useFileManagerContextMenu(options: UseFileManagerContextMenuOpti
}
// --- 多选压缩 ---
menu.push(
{ label: t('fileManager.contextMenu.compressZip'), action: () => onCompressRequest(selectedFileItems, 'zip'), disabled: !(isConnected.value && isSftpReady.value) },
{ label: t('fileManager.contextMenu.compressTarGz'), action: () => onCompressRequest(selectedFileItems, 'targz'), disabled: !(isConnected.value && isSftpReady.value) },
{ label: t('fileManager.contextMenu.compressTarBz2'), action: () => onCompressRequest(selectedFileItems, 'tarbz2'), disabled: !(isConnected.value && isSftpReady.value) },
);
menu.push({ label: '', action: () => {}, disabled: true, separator: true }); // Separator
menu.push(
// --- 分隔符 (视觉) ---
{ label: t('fileManager.actions.deleteMultiple', { count: selectionSize }), action: onDelete, disabled: !(isConnected.value && isSftpReady.value) },
@@ -135,7 +162,7 @@ export function useFileManagerContextMenu(options: UseFileManagerContextMenuOpti
} else if (targetItem.attrs.isDirectory) {
menu.push({ label: t('fileManager.actions.downloadFolder', { name: targetItem.filename }), action: () => onDownloadDirectory(targetItem), disabled: !(isConnected.value && isSftpReady.value) }); // 文件夹下载
}
// --- 结束修改 ---
// 2. 剪切、复制、粘贴 (粘贴 - 如果是文件夹)
@@ -146,11 +173,35 @@ export function useFileManagerContextMenu(options: UseFileManagerContextMenuOpti
}
// --- 分隔符 (视觉) ---
// The invalid object literal was here and is now removed.
// The separator below handles the division correctly.
// Ensure separator is pushed separately and correctly
menu.push({ label: '', action: () => {}, disabled: true, separator: true }); // Separator
// 3. 删除、重命名
menu.push({ label: t('fileManager.actions.delete'), action: onDelete, disabled: !(isConnected.value && isSftpReady.value) });
menu.push({ label: t('fileManager.actions.rename'), action: () => onRename(targetItem), disabled: !(isConnected.value && isSftpReady.value) });
// --- 分隔符 (视觉) ---
// Ensure separator is pushed separately and correctly
menu.push({ label: '', action: () => {}, disabled: true, separator: true }); // Separator
// --- 压缩 & 解压 ---
const canCompress = isConnected.value && isSftpReady.value;
const canDecompress = isConnected.value && isSftpReady.value && targetItem.attrs.isFile && isSupportedArchive(targetItem.filename);
// menu.push({ label: t('fileManager.contextMenu.compress'), action: () => {}, disabled: true }); // Removed isSubmenuHeader
menu.push({ label: t('fileManager.contextMenu.compressZip'), action: () => onCompressRequest([targetItem], 'zip'), disabled: !canCompress });
menu.push({ label: t('fileManager.contextMenu.compressTarGz'), action: () => onCompressRequest([targetItem], 'targz'), disabled: !canCompress });
menu.push({ label: t('fileManager.contextMenu.compressTarBz2'), action: () => onCompressRequest([targetItem], 'tarbz2'), disabled: !canCompress });
menu.push({ label: t('fileManager.contextMenu.decompress'), action: () => onDecompressRequest(targetItem), disabled: !canDecompress });
// --- 分隔符 (视觉) ---
menu.push({ label: '', action: () => {}, disabled: true, separator: true }); // Separator
// --- 分隔符 (视觉) ---
// 4. 新建、上传 (这些更像空白处操作,但保留)
@@ -189,8 +189,7 @@ export function useFileManagerDragAndDrop(options: UseFileManagerDragAndDropOpti
});
}
};
// --- 结束新增 ---
// 处理蒙版上的 Drop 事件
const handleOverlayDrop = (event: DragEvent) => {
@@ -1,5 +1,5 @@
import { ref, readonly, reactive, computed, type Ref, type ComputedRef } from 'vue'; // 引入 reactive 和 computed
import type { FileListItem, FileAttributes, EditorFileContent, SftpReadFileSuccessPayload, SftpReadFileRequestPayload } from '../types/sftp.types'; // +++ 添加 SftpReadFileRequestPayload 导入 +++
import type { FileListItem, FileAttributes, EditorFileContent, SftpReadFileSuccessPayload, SftpReadFileRequestPayload } from '../types/sftp.types';
import type { WebSocketMessage, MessagePayload, MessageHandler } from '../types/websocket.types';
// 导入 UI 通知 store
import { useUiNotificationsStore } from '../stores/uiNotifications.store'; // 更正导入
@@ -15,6 +15,38 @@ export interface WebSocketDependencies {
isSftpReady: Readonly<Ref<boolean>>;
}
/**
* @interface SftpManagerInstance
* @description Defines the shape of the object returned by createSftpActionsManager.
*/
export interface SftpManagerInstance {
// State
fileList: Readonly<ComputedRef<FileListItem[]>>;
isLoading: Readonly<Ref<boolean>>;
fileTree: Readonly<FileTreeNode>;
initialLoadDone: Readonly<Ref<boolean>>;
currentPath: Readonly<Ref<string>>;
// Methods
loadDirectory: (path: string, forceRefresh?: boolean) => void;
createDirectory: (newDirName: string) => void;
createFile: (newFileName: string) => void;
deleteItems: (items: FileListItem[]) => void;
renameItem: (item: FileListItem, newName: string) => void;
changePermissions: (item: FileListItem, mode: number) => void;
readFile: (path: string, encoding?: string) => Promise<SftpReadFileSuccessPayload>;
writeFile: (path: string, content: string, encoding?: string) => Promise<void>;
copyItems: (sourcePaths: string[], destinationDir: string) => void;
moveItems: (sourcePaths: string[], destinationDir: string) => void;
compressItems: (items: FileListItem[], format: 'zip' | 'targz' | 'tarbz2') => Promise<void>; // Assume async
decompressItem: (item: FileListItem) => Promise<void>; // Assume async
joinPath: (base: string, name: string) => string;
setInitialLoadDone: (value: boolean) => void;
// Cleanup function
cleanup: () => void;
}
// Helper function
const generateRequestId = (): string => `req-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
@@ -56,7 +88,7 @@ export function createSftpActionsManager(
currentPathRef: Ref<string>,
wsDeps: WebSocketDependencies,
t: Function
) {
): SftpManagerInstance { // Add explicit return type
const { sendMessage, onMessage, isConnected, isSftpReady } = wsDeps; // 使用注入的依赖
// const fileList = ref<FileListItem[]>([]); // 不再直接使用 fileList ref
@@ -455,10 +487,134 @@ export function createSftpActionsManager(
});
console.log(`[SFTP ${instanceSessionId}] 发送 sftp:move 请求 (ID: ${requestId}) Sources: ${sourcePaths.join(', ')}, Dest: ${destinationDir}`);
// 可选:显示一个“正在移动...”的通知
};
};
const compressItems = (items: FileListItem[], format: 'zip' | 'targz' | 'tarbz2'): Promise<void> => {
return new Promise((resolve, reject) => {
if (!isSftpReady.value) {
const errMsg = t('fileManager.errors.sftpNotReady');
uiNotificationsStore.showError(errMsg);
console.warn(`[SFTP ${instanceSessionId}] 尝试压缩项目但 SFTP 未就绪。`);
return reject(new Error(errMsg));
}
const sourcePaths = items.map(item => joinPath(currentPathRef.value, item.filename));
const requestId = generateRequestId();
const parentDir = currentPathRef.value;
// --- 修改:使用更智能的压缩包命名 ---
let archiveBaseName = 'archive';
if (items.length === 1) {
archiveBaseName = items[0].filename.split('.')[0]; // 使用第一个项目的文件名(不含扩展名)
} else if (items.length > 1) {
// 如果有多个项目,尝试使用共同的父目录名,或者保持 'archive'
const parentFolderName = parentDir.split('/').pop();
if (parentFolderName && parentFolderName !== 'root' && parentFolderName !== '') {
archiveBaseName = parentFolderName;
}
}
const archiveName = `${archiveBaseName}.${format === 'targz' ? 'tar.gz' : (format === 'tarbz2' ? 'tar.bz2' : format)}`;
const destinationPath = joinPath(parentDir, archiveName);
let unregisterSuccess: (() => void) | null = null;
let unregisterError: (() => void) | null = null;
const timeoutId = setTimeout(() => {
unregisterSuccess?.();
unregisterError?.();
const errMsg = t('fileManager.errors.compressTimeout'); // 使用 i18n
uiNotificationsStore.showError(errMsg);
reject(new Error(errMsg));
}, 60000); // 60 秒超时
unregisterSuccess = onMessage('sftp:compress:success', (payload: MessagePayload, message: WebSocketMessage) => {
if (message.requestId === requestId) {
clearTimeout(timeoutId);
unregisterSuccess?.();
unregisterError?.();
uiNotificationsStore.showSuccess(t('fileManager.notifications.compressSuccess', { name: archiveName })); // 使用 i18n
loadDirectory(currentPathRef.value, true); // 强制刷新当前目录
resolve();
}
});
unregisterError = onMessage('sftp:compress:error', (payload: MessagePayload, message: WebSocketMessage) => {
const errorPayload = payload as { error: string, details?: string };
if (message.requestId === requestId) {
clearTimeout(timeoutId);
unregisterSuccess?.();
unregisterError?.();
const errorMsg = errorPayload.details || errorPayload.error || t('fileManager.errors.compressFailed'); // 基础错误信息
uiNotificationsStore.showError(t('fileManager.errors.compressErrorDetailed', { error: errorMsg })); // 使用 i18n 包装详细错误
reject(new Error(errorMsg));
}
});
console.log(`[SFTP ${instanceSessionId}] 发送 sftp:compress 请求 (ID: ${requestId}) Sources: ${sourcePaths.join(', ')}, Dest: ${destinationPath}, Format: ${format}`);
sendMessage({
type: 'sftp:compress',
requestId: requestId,
payload: { sources: sourcePaths, destination: destinationPath, format: format }
});
});
};
const decompressItem = (item: FileListItem): Promise<void> => {
return new Promise((resolve, reject) => {
if (!isSftpReady.value) {
const errMsg = t('fileManager.errors.sftpNotReady');
uiNotificationsStore.showError(errMsg);
console.warn(`[SFTP ${instanceSessionId}] 尝试解压项目 ${item.filename} 但 SFTP 未就绪。`);
return reject(new Error(errMsg));
}
const sourcePath = joinPath(currentPathRef.value, item.filename);
const destinationDir = currentPathRef.value; // 默认解压到当前目录
const requestId = generateRequestId();
let unregisterSuccess: (() => void) | null = null;
let unregisterError: (() => void) | null = null;
const timeoutId = setTimeout(() => {
unregisterSuccess?.();
unregisterError?.();
const errMsg = t('fileManager.errors.decompressTimeout'); // 使用 i18n
uiNotificationsStore.showError(errMsg);
reject(new Error(errMsg));
}, 60000); // 60 秒超时
unregisterSuccess = onMessage('sftp:decompress:success', (payload: MessagePayload, message: WebSocketMessage) => {
if (message.requestId === requestId) {
clearTimeout(timeoutId);
unregisterSuccess?.();
unregisterError?.();
uiNotificationsStore.showSuccess(t('fileManager.notifications.decompressSuccess', { name: item.filename })); // 使用 i18n
loadDirectory(currentPathRef.value, true); // 强制刷新当前目录
resolve();
}
});
unregisterError = onMessage('sftp:decompress:error', (payload: MessagePayload, message: WebSocketMessage) => {
const errorPayload = payload as { error: string, details?: string };
if (message.requestId === requestId) {
clearTimeout(timeoutId);
unregisterSuccess?.();
unregisterError?.();
const errorMsg = errorPayload.details || errorPayload.error || t('fileManager.errors.decompressFailed'); // 基础错误信息
uiNotificationsStore.showError(t('fileManager.errors.decompressErrorDetailed', { error: errorMsg })); // 使用 i18n 包装详细错误
reject(new Error(errorMsg));
}
});
console.log(`[SFTP ${instanceSessionId}] 发送 sftp:decompress 请求 (ID: ${requestId}) Source: ${sourcePath}, Dest: ${destinationDir}`);
sendMessage({
type: 'sftp:decompress',
requestId: requestId,
payload: { source: sourcePath, destination: destinationDir }
});
});
};
// --- Message Handlers ---
// --- Message Handlers ---
const onSftpReaddirSuccess = (payload: MessagePayload, message: WebSocketMessage) => {
const fileListPayload = payload as FileListItem[];
@@ -603,7 +759,7 @@ export function createSftpActionsManager(
const addOrUpdateNodeInTree = (parentPath: string, item: FileListItem): boolean => {
// --- 修改:调用 findNodeByPath 时允许创建缺失的父节点 ---
const parentNode = findNodeByPath(fileTree, parentPath, true);
// --- 结束修改 ---
// 如果父节点被成功找到或创建
if (parentNode) {
@@ -957,6 +1113,29 @@ export function createSftpActionsManager(
unregisterCallbacks.push(onMessage('sftp:move:success', onMoveSuccess));
unregisterCallbacks.push(onMessage('sftp:move:error', onActionError));
// +++ 处理命令未找到错误 +++
const onCommandNotFound = (payload: MessagePayload, message: WebSocketMessage) => {
const { operation, command, message: details } = payload as { operation: 'compress' | 'decompress', command: string, message?: string };
console.error(`[SFTP ${instanceSessionId}] Command '${command}' not found on server for ${operation}. Details: ${details}`);
let errorMsgKey = '';
if (operation === 'compress') {
errorMsgKey = 'fileManager.errors.commandNotFoundCompress';
} else if (operation === 'decompress') {
errorMsgKey = 'fileManager.errors.commandNotFoundDecompress';
}
if (errorMsgKey) {
uiNotificationsStore.showError(t(errorMsgKey, { command }));
} else {
uiNotificationsStore.showError(t('fileManager.errors.genericCommandNotFound', { command, operation }));
}
};
unregisterCallbacks.push(onMessage('sftp:command_not_found', onCommandNotFound));
// --- 结束处理 ---
// 注意:sftp:compress:success, sftp:compress:error, sftp:decompress:success, sftp:decompress:error
// 的消息处理器直接在 compressItems 和 decompressItem 方法内部通过 onMessage 临时注册和注销,
// 因为它们与特定的 Promise 相关联。
// 移除 onUnmounted 块
// *** 计算属性 fileList ***
@@ -976,11 +1155,11 @@ export function createSftpActionsManager(
return {
// State
fileList: readonly(fileList), // 暴露计算属性
isLoading: readonly(isLoading),
// error: readonly(error), // 移除 error
fileTree: readonly(fileTree), // 可以选择性地暴露只读的文件树
initialLoadDone: readonly(initialLoadDone), // +++ 暴露只读的初始加载状态 +++
fileList: fileList, // 暴露计算属性 (类型已在接口中定义为 Readonly<ComputedRef>)
isLoading: isLoading, // (类型已在接口中定义为 Readonly<Ref>)
// error: readonly(error), // 移除 error
fileTree: fileTree, // (类型已在接口中定义为 Readonly<FileTreeNode>)
initialLoadDone: initialLoadDone, // (类型已在接口中定义为 Readonly<Ref>)
// Methods
loadDirectory,
@@ -992,13 +1171,15 @@ export function createSftpActionsManager(
readFile,
writeFile,
copyItems, // +++ 暴露 copyItems +++
moveItems, // +++ 暴露 moveItems +++
joinPath, // 暴露辅助函数
// clearSftpError, // 移除 clearSftpError
moveItems, // +++ 暴露 moveItems +++
compressItems, // +++ 暴露 compressItems +++
decompressItem, // +++ 暴露 decompressItem +++
joinPath, // 暴露辅助函数
// clearSftpError, // 移除 clearSftpError
// Cleanup function
currentPath: readonly(currentPathRef), // 暴露只读的当前路径 ref
setInitialLoadDone: (value: boolean) => { initialLoadDone.value = value; }, // +++ 暴露设置初始加载状态的方法 +++
currentPath: currentPathRef, // (类型已在接口中定义为 Readonly<Ref>)
setInitialLoadDone: (value: boolean) => { initialLoadDone.value = value; }, // +++ 暴露设置初始加载状态的方法 +++
// Cleanup function
// Cleanup function
@@ -21,7 +21,7 @@ export function createWebSocketConnectionManager(
sessionId: string,
dbConnectionId: string,
t: ReturnType<typeof useI18n>['t'],
options?: { isResumeFlow?: boolean; getIsMarkedForSuspend?: () => boolean } // +++ 添加 getIsMarkedForSuspend 回调 +++
options?: { isResumeFlow?: boolean; getIsMarkedForSuspend?: () => boolean }
) {
// --- Instance State ---
// 每个实例拥有独立的 WebSocket 对象、状态和消息处理器
+26 -8
View File
@@ -330,6 +330,13 @@
"paste": "Paste",
"openEditor": "Open Editor"
},
"contextMenu": {
"compress": "Compress",
"compressZip": "Compress to zip",
"compressTarGz": "Compress to tar.gz",
"compressTarBz2": "Compress to tar.bz2",
"decompress": "Decompress"
},
"headers": {
"type": "Type",
"name": "Name",
@@ -365,14 +372,25 @@
"terminalManagerNotFound": "Terminal manager not found",
"sendCommandFailed": "Failed to send command",
"downloadDirectoryFailed": "Failed to download directory",
"downloadDirectoryNotImplemented": "Directory download feature is not yet implemented on the server."
},
"notifications": {
"copySuccess": "Copy successful",
"moveSuccess": "Move successful",
"cdCommandSent": "CD command sent to terminal"
},
"warnings": {
"downloadDirectoryNotImplemented": "Directory download feature is not yet implemented on the server.",
"compressFailed": "Compression failed",
"compressTimeout": "Compression timed out",
"compressErrorDetailed": "Compression failed: {error}",
"decompressFailed": "Decompression failed",
"decompressTimeout": "Decompression timed out",
"decompressErrorDetailed": "Decompression failed: {error}",
"commandNotFoundCompress": "Command '{command}' not found on server, cannot complete compression.",
"commandNotFoundDecompress": "Command '{command}' not found on server, cannot complete decompression.",
"genericCommandNotFound": "Command '{command}' not found on server, cannot complete '{operation}' operation."
},
"notifications": {
"copySuccess": "Copy successful",
"moveSuccess": "Move successful",
"cdCommandSent": "CD command sent to terminal",
"compressSuccess": "Compressed {name} successfully",
"decompressSuccess": "Decompressed {name} successfully"
},
"warnings": {
"moveSameDirectory": "Cannot cut and paste in the same directory."
},
"prompts": {
+20 -2
View File
@@ -336,6 +336,13 @@
"upload": "アップロード",
"uploadFile": "ファイルをアップロード"
},
"contextMenu": {
"compress": "圧縮",
"compressZip": "zip に圧縮",
"compressTarGz": "tar.gz に圧縮",
"compressTarBz2": "tar.bz2 に圧縮",
"decompress": "解凍"
},
"currentPath": "現在のパス",
"dropFilesHere": "ファイルをここにドラッグ&ドロップしてアップロード",
"editPathTooltip": "パスをクリックして編集",
@@ -364,7 +371,16 @@
"sendCommandFailed": "コマンドの送信に失敗しました",
"sftpManagerNotFound": "SFTP マネージャーが見つかりません",
"sftpNotReady": "SFTP セッションの準備ができていません",
"terminalManagerNotFound": "ターミナルマネージャーが見つかりません"
"terminalManagerNotFound": "ターミナルマネージャーが見つかりません",
"compressFailed": "圧縮に失敗しました",
"compressTimeout": "圧縮がタイムアウトしました",
"compressErrorDetailed": "圧縮に失敗しました: {error}",
"decompressFailed": "解凍に失敗しました",
"decompressTimeout": "解凍がタイムアウトしました",
"decompressErrorDetailed": "解凍に失敗しました: {error}",
"commandNotFoundCompress": "サーバーにコマンド '{command}' が見つからないため、圧縮操作を完了できません。",
"commandNotFoundDecompress": "サーバーにコマンド '{command}' が見つからないため、解凍操作を完了できません。",
"genericCommandNotFound": "サーバーにコマンド '{command}' が見つからないため、'{operation}' 操作を完了できません。"
},
"headers": {
"modified": "変更日",
@@ -379,7 +395,9 @@
"notifications": {
"cdCommandSent": "CD コマンドがターミナルに送信されました",
"copySuccess": "コピーに成功しました",
"moveSuccess": "移動に成功しました"
"moveSuccess": "移動に成功しました",
"compressSuccess": "{name} を正常に圧縮しました",
"decompressSuccess": "{name} を正常に解凍しました"
},
"prompts": {
"confirmDeleteFile": "ファイル \"{name}\" を削除しますか?この操作は元に戻せません。",
+26 -8
View File
@@ -329,6 +329,13 @@
"paste": "粘贴",
"openEditor": "打开编辑器"
},
"contextMenu": {
"compress": "压缩",
"compressZip": "压缩为 zip",
"compressTarGz": "压缩为 tar.gz",
"compressTarBz2": "压缩为 ttar.bz2",
"decompress": "解压"
},
"headers": {
"type": "类型",
"name": "名称",
@@ -364,14 +371,25 @@
"terminalManagerNotFound": "未找到终端管理器",
"sendCommandFailed": "发送命令失败",
"downloadDirectoryFailed": "下载文件夹失败",
"downloadDirectoryNotImplemented": "服务器尚未实现文件夹下载功能。"
},
"notifications": {
"copySuccess": "复制成功",
"moveSuccess": "移动成功",
"cdCommandSent": "CD 命令已发送到终端"
},
"warnings": {
"downloadDirectoryNotImplemented": "服务器尚未实现文件夹下载功能。",
"compressFailed": "压缩失败",
"compressTimeout": "压缩超时",
"compressErrorDetailed": "压缩失败: {error}",
"decompressFailed": "解压失败",
"decompressTimeout": "解压超时",
"decompressErrorDetailed": "解压失败: {error}",
"commandNotFoundCompress": "服务器上缺少 '{command}' 命令,无法完成压缩操作。",
"commandNotFoundDecompress": "服务器上缺少 '{command}' 命令,无法完成解压操作。",
"genericCommandNotFound": "服务器上缺少 '{command}' 命令,无法完成 '{operation}' 操作。"
},
"notifications": {
"copySuccess": "复制成功",
"moveSuccess": "移动成功",
"cdCommandSent": "CD 命令已发送到终端",
"compressSuccess": "压缩 {name} 成功",
"decompressSuccess": "解压 {name} 成功"
},
"warnings": {
"moveSameDirectory": "不能在同一目录下剪切和粘贴。"
},
"prompts": {
@@ -17,7 +17,7 @@ export const sessionTabsWithStatus = computed((): SessionTabInfoWithStatus[] =>
sessionId: session.sessionId,
connectionName: session.connectionName,
status: session.wsManager.connectionStatus.value, // 从 wsManager 获取状态
isMarkedForSuspend: session.isMarkedForSuspend, // +++ 添加 isMarkedForSuspend 状态 +++
isMarkedForSuspend: session.isMarkedForSuspend,
}));
});
@@ -72,7 +72,7 @@ const showLayoutConfigurator = ref(false); // 控制布局配置器可见性
// --- ---
const currentSearchTerm = ref(''); //
const mobileTerminalRef = ref<InstanceType<typeof Terminal> | null>(null); // +++ mobileTerminalRef +++
const mobileTerminalRef = ref<InstanceType<typeof Terminal> | null>(null);
const isVirtualKeyboardVisible = ref(true); // +++ State for virtual keyboard visibility +++
// --- ---