This commit is contained in:
Baobhan Sith
2025-04-29 21:28:00 +08:00
parent 4ce5d90512
commit e0a234210d
8 changed files with 1075 additions and 19 deletions
@@ -470,7 +470,10 @@ const triggerDownload = (items: FileListItem[]) => { // 修改:接受 FileList
// 为每个文件创建一个链接并点击
const link = document.createElement('a');
link.href = downloadUrl;
link.setAttribute('download', item.filename); // 使用原始文件名
// --- 修正:移除文件名中的双引号以兼容 Chrome ---
const safeFilename = item.filename.replace(/"/g, ''); // 移除所有双引号
link.setAttribute('download', safeFilename);
// --- 结束修正 ---
document.body.appendChild(link);
link.click();
@@ -482,6 +485,95 @@ const triggerDownload = (items: FileListItem[]) => { // 修改:接受 FileList
};
// +++ 新增:文件夹下载触发器 +++
const triggerDownloadDirectory = (item: FileListItem) => {
if (!props.wsDeps.isConnected.value) {
alert(t('fileManager.errors.notConnected'));
return;
}
const currentConnectionId = props.dbConnectionId;
if (!currentConnectionId) {
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot download directory: Missing connection ID.`);
alert(t('fileManager.errors.missingConnectionId'));
return;
}
if (!currentSftpManager.value) {
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot download directory: SFTP manager is not available.`);
alert(t('fileManager.errors.sftpManagerNotFound'));
return;
}
// 确保是目录
if (!item.attrs.isDirectory) {
console.warn(`[FileManager ${props.sessionId}-${props.instanceId}] Skipping directory download for non-directory item: ${item.filename}`);
return;
}
const directoryPath = currentSftpManager.value.joinPath(currentSftpManager.value.currentPath.value, item.filename);
// 定义新的后端 API 端点 URL (稍后实现)
const downloadUrl = `/api/v1/sftp/download-directory?connectionId=${currentConnectionId}&remotePath=${encodeURIComponent(directoryPath)}`;
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Attempting directory download for ${item.filename}: ${downloadUrl}`);
// --- 修改:使用 fetch 尝试下载,并处理后端未实现的情况 ---
fetch(downloadUrl)
.then(async response => {
if (response.ok) {
// 后端实现成功,尝试触发下载
const blob = await response.blob();
// 从 Content-Disposition 头获取文件名 (需要后端设置)
const contentDisposition = response.headers.get('content-disposition');
let filename = `${item.filename}.zip`; // 默认文件名
if (contentDisposition) {
const filenameMatch = contentDisposition.match(/filename="?(.+)"?/i);
if (filenameMatch && filenameMatch.length > 1) {
filename = filenameMatch[1];
}
}
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
// --- 修正:移除 ZIP 文件名中的双引号以兼容 Chrome ---
const safeZipFilename = filename.replace(/"/g, '');
link.setAttribute('download', safeZipFilename);
// --- 结束修正 ---
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href); // 释放对象 URL
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Directory download triggered for: ${filename}`);
} else {
// 处理错误,例如 404 Not Found
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Directory download failed: ${response.status} ${response.statusText}`);
// 尝试读取错误信息体
let errorMsg = `Server responded with status ${response.status}`;
try {
const errorData = await response.json(); // 假设后端返回 JSON 错误
errorMsg = errorData.message || errorMsg;
} catch (e) {
// 如果响应体不是 JSON 或读取失败
try {
const textError = await response.text();
if (textError) errorMsg = textError;
} catch (e2) { /* ignore */}
}
if (response.status === 404) {
alert(t('fileManager.errors.downloadDirectoryNotImplemented', 'Directory download feature is not yet implemented on the server.'));
} else {
alert(`${t('fileManager.errors.downloadDirectoryFailed', 'Failed to download directory')}: ${errorMsg}`);
}
}
})
.catch(error => {
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Network error during directory download:`, error);
alert(`${t('fileManager.errors.downloadDirectoryFailed', 'Failed to download directory')}: Network error.`);
});
// --- 结束修改 ---
};
// --- 结束新增 ---
// --- 上下文菜单逻辑 (使用 Composable, 需要 Selection 和 Action Handlers) ---
const {
contextMenuVisible,
@@ -517,6 +609,7 @@ const {
onCopy: handleCopy, // +++ 传递复制回调 +++
onCut: handleCut, // +++ 传递剪切回调 +++
onPaste: handlePaste, // +++ 传递粘贴回调 +++
onDownloadDirectory: triggerDownloadDirectory, // +++ 传递文件夹下载回调 +++
});
// --- 目录加载与导航 ---
@@ -30,7 +30,8 @@ export interface UseFileManagerContextMenuOptions {
// --- 回调函数 ---
onRefresh: () => void;
onUpload: () => void;
onDownload: (items: FileListItem[]) => void; // 修改:接受 FileListItem 数组
onDownload: (items: FileListItem[]) => void; // 文件下载回调
onDownloadDirectory: (item: FileListItem) => void; // +++ 新增:文件夹下载回调 +++
onDelete: () => void; // 删除操作现在由外部处理
onRename: (item: FileListItem) => void;
onChangePermissions: (item: FileListItem) => void;
@@ -62,6 +63,7 @@ export function useFileManagerContextMenu(options: UseFileManagerContextMenuOpti
onCopy, // +++ 解构复制回调 +++
onCut, // +++ 解构剪切回调 +++
onPaste, // +++ 解构粘贴回调 +++
onDownloadDirectory, // +++ 解构文件夹下载回调 +++
} = options;
const contextMenuVisible = ref(false);
@@ -110,9 +112,13 @@ export function useFileManagerContextMenu(options: UseFileManagerContextMenuOpti
];
// --- 新增:多选下载 ---
if (allFilesSelected) {
menu.push({ label: t('fileManager.actions.downloadMultiple', { count: selectionSize }), action: () => onDownload(selectedFileItems), disabled: !canPerformActions });
}
// 多选时暂时禁用文件夹下载,只允许下载文件
// 如果需要支持多选文件夹下载或混合下载,需要更复杂的逻辑和后端支持(例如打包成 zip)
// 目前仅在 allFilesSelected 为 true 时启用多文件下载
if (allFilesSelected) {
menu.push({ label: t('fileManager.actions.downloadMultiple', { count: selectionSize }), action: () => onDownload(selectedFileItems), disabled: !canPerformActions });
}
menu.push(
// --- 分隔符 (视觉) ---
@@ -122,13 +128,16 @@ export function useFileManagerContextMenu(options: UseFileManagerContextMenuOpti
);
} 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 }); // 传递包含单个项的数组
menu.push({ label: t('fileManager.actions.download', { name: targetItem.filename }), action: () => onDownload([targetItem]), disabled: !canPerformActions }); // 文件下载
} else if (targetItem.attrs.isDirectory) {
menu.push({ label: t('fileManager.actions.downloadFolder', { name: targetItem.filename }), action: () => onDownloadDirectory(targetItem), disabled: !canPerformActions }); // 文件夹下载
}
// --- 结束修改 ---
// 2. 剪切、复制、粘贴 (粘贴 - 如果是文件夹)
menu.push({ label: t('fileManager.actions.cut'), action: onCut, disabled: !canPerformActions });
+4 -1
View File
@@ -251,6 +251,7 @@
"deleteMultiple": "Delete {count} items",
"download": "Download",
"downloadMultiple": "Download {count} items",
"downloadFolder": "Download Folder",
"cancel": "Cancel",
"save": "Save",
"closeTab": "Close Tab",
@@ -291,7 +292,9 @@
"sftpManagerNotFound": "SFTP manager not found",
"noActiveSession": "No active session found",
"terminalManagerNotFound": "Terminal manager not found",
"sendCommandFailed": "Failed to send command"
"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",
+4 -1
View File
@@ -251,6 +251,7 @@
"deleteMultiple": "删除 {count} 个项目",
"download": "下载",
"downloadMultiple": "下载 {count} 个项目",
"downloadFolder": "下载文件夹",
"cancel": "取消",
"save": "保存",
"closeTab": "关闭标签页",
@@ -291,7 +292,9 @@
"sftpManagerNotFound": "SFTP 管理器未找到",
"noActiveSession": "未找到活动会话",
"terminalManagerNotFound": "未找到终端管理器",
"sendCommandFailed": "发送命令失败"
"sendCommandFailed": "发送命令失败",
"downloadDirectoryFailed": "下载文件夹失败",
"downloadDirectoryNotImplemented": "服务器尚未实现文件夹下载功能。"
},
"notifications": {
"copySuccess": "复制成功",