update
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch, watchEffect, type PropType, readonly, defineExpose } from 'vue'; // 恢复导入, 添加 watch, defineExpose
|
import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch, watchEffect, type PropType, readonly, defineExpose, shallowRef } from 'vue'; // 添加 shallowRef
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
import { useRoute } from 'vue-router'; // 保留用于生成下载 URL (如果下载逻辑移动则可移除)
|
import { useRoute } from 'vue-router'; // 保留用于生成下载 URL (如果下载逻辑移动则可移除)
|
||||||
import { storeToRefs } from 'pinia'; // 导入 storeToRefs
|
import { storeToRefs } from 'pinia'; // 导入 storeToRefs
|
||||||
@@ -34,11 +34,16 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
// 注入此会话特定的 SFTP 管理器实例
|
// 新增:文件管理器实例 ID
|
||||||
sftpManager: {
|
instanceId: {
|
||||||
type: Object as PropType<SftpManagerInstance>,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
// // 注入此会话特定的 SFTP 管理器实例 (移除)
|
||||||
|
// sftpManager: {
|
||||||
|
// type: Object as PropType<SftpManagerInstance>,
|
||||||
|
// required: true,
|
||||||
|
// },
|
||||||
// 注入数据库连接 ID
|
// 注入数据库连接 ID
|
||||||
dbConnectionId: {
|
dbConnectionId: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -54,10 +59,23 @@ const props = defineProps({
|
|||||||
// --- 核心 Composables ---
|
// --- 核心 Composables ---
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const route = useRoute(); // Keep for download URL generation for now
|
const route = useRoute(); // Keep for download URL generation for now
|
||||||
// 移除本地 currentPath ref
|
const sessionStore = useSessionStore(); // 实例化 Session Store
|
||||||
// const currentPath = ref<string>('.');
|
|
||||||
|
|
||||||
|
// --- 获取此实例的 SFTP 管理器 ---
|
||||||
|
const sftpManagerInstance = sessionStore.getOrCreateSftpManager(props.sessionId, props.instanceId);
|
||||||
|
|
||||||
|
// --- 错误处理:如果无法获取管理器 ---
|
||||||
|
if (!sftpManagerInstance) {
|
||||||
|
// 抛出错误或显示错误消息,阻止组件进一步渲染
|
||||||
|
// 这里我们简单地抛出一个错误
|
||||||
|
throw new Error(`[FileManager ${props.sessionId}-${props.instanceId}] Failed to get or create SFTP manager instance.`);
|
||||||
|
// 或者可以设置一个错误状态 ref,并在模板中显示错误信息
|
||||||
|
// const managerError = ref(`Failed to get SFTP manager for instance ${props.instanceId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 从获取到的管理器实例中解构状态和方法 ---
|
||||||
|
// 使用 shallowRef 包装 manager 实例本身可能不是最佳选择,因为 manager 不是响应式对象
|
||||||
|
// 直接解构其内部的 ref 和函数
|
||||||
const {
|
const {
|
||||||
fileList,
|
fileList,
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -68,12 +86,12 @@ const {
|
|||||||
deleteItems,
|
deleteItems,
|
||||||
renameItem,
|
renameItem,
|
||||||
changePermissions,
|
changePermissions,
|
||||||
readFile, // Provided by the manager
|
readFile, // Provided by the manager instance
|
||||||
writeFile, // Provided by the manager
|
writeFile, // Provided by the manager instance
|
||||||
joinPath,
|
joinPath,
|
||||||
currentPath, // 从 sftpManager 获取 currentPath
|
currentPath, // 从 manager instance 获取 currentPath
|
||||||
cleanup: cleanupSftpHandlers, // Get the cleanup function from the manager
|
// cleanup: cleanupSftpHandlers, // cleanup 由 store 在 onBeforeUnmount 中处理
|
||||||
} = props.sftpManager; // 直接从 props 获取
|
} = sftpManagerInstance; // 从获取的实例解构
|
||||||
|
|
||||||
// 文件上传模块 - Needs WebSocket dependencies and session context
|
// 文件上传模块 - Needs WebSocket dependencies and session context
|
||||||
const {
|
const {
|
||||||
@@ -82,14 +100,14 @@ const {
|
|||||||
cancelUpload,
|
cancelUpload,
|
||||||
// cleanup: cleanupUploader, // 假设 uploader 也提供 cleanup
|
// cleanup: cleanupUploader, // 假设 uploader 也提供 cleanup
|
||||||
} = useFileUploader(
|
} = useFileUploader(
|
||||||
currentPath, // 使用从 sftpManager 获取的 currentPath
|
currentPath, // 使用从 manager instance 解构的 currentPath
|
||||||
fileList, // 传递来自 sftpManager 的 fileList ref
|
fileList, // 使用从 manager instance 解构的 fileList
|
||||||
props.wsDeps // 传递注入的 WebSocket 依赖项
|
props.wsDeps // 仍然传递注入的 WebSocket 依赖项
|
||||||
);
|
);
|
||||||
|
|
||||||
// 实例化 Stores
|
// 实例化其他 Stores
|
||||||
const fileEditorStore = useFileEditorStore(); // 用于共享模式
|
const fileEditorStore = useFileEditorStore(); // 用于共享模式
|
||||||
const sessionStore = useSessionStore();
|
// const sessionStore = useSessionStore(); // 已在上面实例化
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const focusSwitcherStore = useFocusSwitcherStore(); // +++ 实例化焦点切换 Store +++
|
const focusSwitcherStore = useFocusSwitcherStore(); // +++ 实例化焦点切换 Store +++
|
||||||
|
|
||||||
@@ -210,29 +228,33 @@ const handleSort = (key: keyof FileListItem | 'type' | 'size' | 'mtime') => {
|
|||||||
// 定义单击时的动作回调 (移到 Selection 实例化之前)
|
// 定义单击时的动作回调 (移到 Selection 实例化之前)
|
||||||
const handleItemAction = (item: FileListItem) => {
|
const handleItemAction = (item: FileListItem) => {
|
||||||
if (item.attrs.isDirectory) {
|
if (item.attrs.isDirectory) {
|
||||||
|
// 使用从 manager instance 解构的 isLoading
|
||||||
if (isLoading.value) {
|
if (isLoading.value) {
|
||||||
console.log(`[FileManager ${props.sessionId}] Ignoring directory click, already loading...`);
|
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Ignoring directory click, already loading...`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newPath = item.filename === '..'
|
const newPath = item.filename === '..'
|
||||||
|
// 使用从 manager instance 解构的 currentPath 和 joinPath
|
||||||
? currentPath.value.substring(0, currentPath.value.lastIndexOf('/')) || '/'
|
? currentPath.value.substring(0, currentPath.value.lastIndexOf('/')) || '/'
|
||||||
: joinPath(currentPath.value, item.filename);
|
: joinPath(currentPath.value, item.filename);
|
||||||
|
// 使用从 manager instance 解构的 loadDirectory
|
||||||
loadDirectory(newPath);
|
loadDirectory(newPath);
|
||||||
} else if (item.attrs.isFile) {
|
} else if (item.attrs.isFile) {
|
||||||
|
// 使用从 manager instance 解构的 currentPath 和 joinPath
|
||||||
const filePath = joinPath(currentPath.value, item.filename);
|
const filePath = joinPath(currentPath.value, item.filename);
|
||||||
const fileInfo: FileInfo = { name: item.filename, fullPath: filePath };
|
const fileInfo: FileInfo = { name: item.filename, fullPath: filePath };
|
||||||
|
|
||||||
if (settingsStore.showPopupFileEditorBoolean) {
|
if (settingsStore.showPopupFileEditorBoolean) {
|
||||||
console.log(`[FileManager ${props.sessionId}] Triggering popup for: ${filePath}`);
|
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Triggering popup for: ${filePath}`);
|
||||||
fileEditorStore.triggerPopup(filePath, props.sessionId);
|
fileEditorStore.triggerPopup(filePath, props.sessionId); // Popup 仍然关联 sessionId
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shareFileEditorTabsBoolean.value) {
|
if (shareFileEditorTabsBoolean.value) {
|
||||||
console.log(`[FileManager ${props.sessionId}] Opening file in shared mode (store handles loading): ${filePath}`);
|
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Opening file in shared mode (store handles loading): ${filePath}`);
|
||||||
fileEditorStore.openFile(filePath, props.sessionId);
|
fileEditorStore.openFile(filePath, props.sessionId); // Shared mode 关联 sessionId
|
||||||
} else {
|
} else {
|
||||||
console.log(`[FileManager ${props.sessionId}] Opening file in independent mode (store handles loading): ${filePath}`);
|
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Opening file in independent mode (store handles loading): ${filePath}`);
|
||||||
sessionStore.openFileInSession(props.sessionId, fileInfo);
|
sessionStore.openFileInSession(props.sessionId, fileInfo); // Independent mode 关联 sessionId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -253,6 +275,7 @@ const {
|
|||||||
// --- SFTP 操作处理函数 (定义在此处,供 Composable 使用) ---
|
// --- SFTP 操作处理函数 (定义在此处,供 Composable 使用) ---
|
||||||
const handleDeleteSelectedClick = () => {
|
const handleDeleteSelectedClick = () => {
|
||||||
// 现在 selectedItems 来自 useFileManagerSelection
|
// 现在 selectedItems 来自 useFileManagerSelection
|
||||||
|
// 使用 props.wsDeps 和从 manager instance 解构的 fileList
|
||||||
if (!props.wsDeps.isConnected.value || selectedItems.value.size === 0) return;
|
if (!props.wsDeps.isConnected.value || selectedItems.value.size === 0) return;
|
||||||
const itemsToDelete = Array.from(selectedItems.value)
|
const itemsToDelete = Array.from(selectedItems.value)
|
||||||
.map(filename => fileList.value.find((f: FileListItem) => f.filename === filename)) // f 已有类型
|
.map(filename => fileList.value.find((f: FileListItem) => f.filename === filename)) // f 已有类型
|
||||||
@@ -267,7 +290,8 @@ const handleDeleteSelectedClick = () => {
|
|||||||
: t('fileManager.prompts.confirmDeleteFile', { name: itemsToDelete[0].filename });
|
: t('fileManager.prompts.confirmDeleteFile', { name: itemsToDelete[0].filename });
|
||||||
|
|
||||||
if (confirm(confirmMsg)) {
|
if (confirm(confirmMsg)) {
|
||||||
deleteItems(itemsToDelete); // Use deleteItems from props
|
// 使用从 manager instance 解构的 deleteItems
|
||||||
|
deleteItems(itemsToDelete);
|
||||||
selectedItems.value.clear();
|
selectedItems.value.clear();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -276,7 +300,8 @@ const handleRenameContextMenuClick = (item: FileListItem) => { // item 已有类
|
|||||||
if (!props.wsDeps.isConnected.value || !item) return; // 恢复使用 props.wsDeps
|
if (!props.wsDeps.isConnected.value || !item) return; // 恢复使用 props.wsDeps
|
||||||
const newName = prompt(t('fileManager.prompts.enterNewName', { oldName: item.filename }), item.filename);
|
const newName = prompt(t('fileManager.prompts.enterNewName', { oldName: item.filename }), item.filename);
|
||||||
if (newName && newName !== item.filename) {
|
if (newName && newName !== item.filename) {
|
||||||
renameItem(item, newName); // Use renameItem from props.sftpManager
|
// 使用从 manager instance 解构的 renameItem
|
||||||
|
renameItem(item, newName);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -290,7 +315,8 @@ const handleChangePermissionsContextMenuClick = (item: FileListItem) => { // ite
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newMode = parseInt(newModeStr, 8);
|
const newMode = parseInt(newModeStr, 8);
|
||||||
changePermissions(item, newMode); // Use changePermissions from props.sftpManager
|
// 使用从 manager instance 解构的 changePermissions
|
||||||
|
changePermissions(item, newMode);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -298,11 +324,13 @@ const handleNewFolderContextMenuClick = () => {
|
|||||||
if (!props.wsDeps.isConnected.value) return; // 恢复使用 props.wsDeps
|
if (!props.wsDeps.isConnected.value) return; // 恢复使用 props.wsDeps
|
||||||
const folderName = prompt(t('fileManager.prompts.enterFolderName'));
|
const folderName = prompt(t('fileManager.prompts.enterFolderName'));
|
||||||
if (folderName) {
|
if (folderName) {
|
||||||
|
// 使用从 manager instance 解构的 fileList
|
||||||
if (fileList.value.some((item: FileListItem) => item.filename === folderName)) { // item 已有类型
|
if (fileList.value.some((item: FileListItem) => item.filename === folderName)) { // item 已有类型
|
||||||
alert(t('fileManager.errors.folderExists', { name: folderName }));
|
alert(t('fileManager.errors.folderExists', { name: folderName }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
createDirectory(folderName); // Use createDirectory from props.sftpManager
|
// 使用从 manager instance 解构的 createDirectory
|
||||||
|
createDirectory(folderName);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -310,11 +338,13 @@ const handleNewFileContextMenuClick = () => {
|
|||||||
if (!props.wsDeps.isConnected.value) return; // 恢复使用 props.wsDeps
|
if (!props.wsDeps.isConnected.value) return; // 恢复使用 props.wsDeps
|
||||||
const fileName = prompt(t('fileManager.prompts.enterFileName'));
|
const fileName = prompt(t('fileManager.prompts.enterFileName'));
|
||||||
if (fileName) {
|
if (fileName) {
|
||||||
|
// 使用从 manager instance 解构的 fileList
|
||||||
if (fileList.value.some((item: FileListItem) => item.filename === fileName)) { // item 已有类型
|
if (fileList.value.some((item: FileListItem) => item.filename === fileName)) { // item 已有类型
|
||||||
alert(t('fileManager.errors.fileExists', { name: fileName }));
|
alert(t('fileManager.errors.fileExists', { name: fileName }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
createFile(fileName); // Use createFile from props.sftpManager
|
// 使用从 manager instance 解构的 createFile
|
||||||
|
createFile(fileName);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -328,17 +358,17 @@ const triggerDownload = (item: FileListItem) => { // item 已有类型
|
|||||||
alert(t('fileManager.errors.notConnected'));
|
alert(t('fileManager.errors.notConnected'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// connectionId might need to be passed differently, maybe via sftpManager or wsDeps
|
// connectionId 仍然从 props 获取
|
||||||
// 使用 props 传入的 dbConnectionId
|
const currentConnectionId = props.dbConnectionId;
|
||||||
const currentConnectionId = props.dbConnectionId; // <-- 使用 Prop
|
|
||||||
if (!currentConnectionId) {
|
if (!currentConnectionId) {
|
||||||
console.error(`[FileManager ${props.sessionId}] Cannot download: Missing connection ID.`);
|
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot download: Missing connection ID.`);
|
||||||
alert(t('fileManager.errors.missingConnectionId'));
|
alert(t('fileManager.errors.missingConnectionId'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const downloadPath = joinPath(currentPath.value, item.filename); // Use joinPath from props
|
// 使用从 manager instance 解构的 joinPath 和 currentPath
|
||||||
|
const downloadPath = joinPath(currentPath.value, item.filename);
|
||||||
const downloadUrl = `/api/v1/sftp/download?connectionId=${currentConnectionId}&remotePath=${encodeURIComponent(downloadPath)}`;
|
const downloadUrl = `/api/v1/sftp/download?connectionId=${currentConnectionId}&remotePath=${encodeURIComponent(downloadPath)}`;
|
||||||
console.log(`[FileManager ${props.sessionId}] Triggering download: ${downloadUrl}`);
|
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Triggering download: ${downloadUrl}`);
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.href = downloadUrl;
|
link.href = downloadUrl;
|
||||||
link.setAttribute('download', item.filename);
|
link.setAttribute('download', item.filename);
|
||||||
@@ -359,13 +389,13 @@ const {
|
|||||||
} = useFileManagerContextMenu({
|
} = useFileManagerContextMenu({
|
||||||
selectedItems, // 传递来自 useFileManagerSelection 的 selectedItems
|
selectedItems, // 传递来自 useFileManagerSelection 的 selectedItems
|
||||||
lastClickedIndex, // 传递来自 useFileManagerSelection 的 lastClickedIndex
|
lastClickedIndex, // 传递来自 useFileManagerSelection 的 lastClickedIndex
|
||||||
fileList, // 传递 sftpManager 的 fileList
|
fileList, // 传递从 manager instance 解构的 fileList
|
||||||
currentPath, // 传递 sftpManager 的 currentPath
|
currentPath, // 传递从 manager instance 解构的 currentPath
|
||||||
isConnected: props.wsDeps.isConnected, // 传递响应式引用
|
isConnected: props.wsDeps.isConnected, // 仍然传递 props.wsDeps
|
||||||
isSftpReady: props.wsDeps.isSftpReady, // 传递响应式引用
|
isSftpReady: props.wsDeps.isSftpReady, // 仍然传递 props.wsDeps
|
||||||
t, // 传递 i18n 的 t 函数
|
t, // 传递 i18n 的 t 函数
|
||||||
// --- 传递回调函数 ---
|
// --- 传递回调函数 ---
|
||||||
onRefresh: () => loadDirectory(currentPath.value),
|
onRefresh: () => loadDirectory(currentPath.value), // 使用解构的 loadDirectory 和 currentPath
|
||||||
onUpload: triggerFileUpload,
|
onUpload: triggerFileUpload,
|
||||||
onDownload: triggerDownload,
|
onDownload: triggerDownload,
|
||||||
onDelete: handleDeleteSelectedClick,
|
onDelete: handleDeleteSelectedClick,
|
||||||
@@ -394,14 +424,14 @@ const {
|
|||||||
handleDragLeaveRow,
|
handleDragLeaveRow,
|
||||||
handleDropOnRow,
|
handleDropOnRow,
|
||||||
} = useFileManagerDragAndDrop({
|
} = useFileManagerDragAndDrop({
|
||||||
isConnected: props.wsDeps.isConnected,
|
isConnected: props.wsDeps.isConnected, // 仍然传递 props.wsDeps
|
||||||
currentPath: currentPath, // 从 sftpManager 获取
|
currentPath: currentPath, // 使用从 manager instance 解构的 currentPath
|
||||||
fileListContainerRef: fileListContainerRef, // 传递容器 ref
|
fileListContainerRef: fileListContainerRef, // 传递容器 ref
|
||||||
joinPath: joinPath, // 从 sftpManager 获取
|
joinPath: joinPath, // 使用从 manager instance 解构的 joinPath
|
||||||
onFileUpload: startFileUpload, // 从 useFileUploader 获取
|
onFileUpload: startFileUpload, // 从 useFileUploader 获取
|
||||||
onItemMove: renameItem, // 从 sftpManager 获取
|
onItemMove: renameItem, // 使用从 manager instance 解构的 renameItem
|
||||||
selectedItems: selectedItems, // 从 useFileManagerSelection 获取
|
selectedItems: selectedItems, // 从 useFileManagerSelection 获取
|
||||||
fileList: fileList, // 从 sftpManager 获取
|
fileList: fileList, // 使用从 manager instance 解构的 fileList
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@@ -420,7 +450,7 @@ const {
|
|||||||
handleKeydown, // 使用 Composable 返回的 handleKeydown
|
handleKeydown, // 使用 Composable 返回的 handleKeydown
|
||||||
} = useFileManagerKeyboardNavigation({
|
} = useFileManagerKeyboardNavigation({
|
||||||
filteredFileList: filteredFileList, // 传递过滤后的列表
|
filteredFileList: filteredFileList, // 传递过滤后的列表
|
||||||
currentPath: currentPath, // 传递当前路径
|
currentPath: currentPath, // 使用从 manager instance 解构的 currentPath
|
||||||
fileListContainerRef: fileListContainerRef, // 传递容器引用
|
fileListContainerRef: fileListContainerRef, // 传递容器引用
|
||||||
// 当 Enter 键按下时,模拟鼠标单击
|
// 当 Enter 键按下时,模拟鼠标单击
|
||||||
onEnterPress: (item) => handleItemClick(new MouseEvent('click'), item),
|
onEnterPress: (item) => handleItemClick(new MouseEvent('click'), item),
|
||||||
@@ -449,7 +479,7 @@ watch(sortDirection, () => {
|
|||||||
|
|
||||||
// --- 生命周期钩子 ---
|
// --- 生命周期钩子 ---
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
console.log(`[FileManager ${props.sessionId}] Component mounted.`);
|
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Component mounted.`);
|
||||||
// Initial load logic is handled by watchEffect
|
// Initial load logic is handled by watchEffect
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -471,13 +501,12 @@ watchEffect((onCleanup) => {
|
|||||||
|
|
||||||
onCleanup(cleanupListeners);
|
onCleanup(cleanupListeners);
|
||||||
|
|
||||||
// 恢复使用 props.wsDeps.isConnected 和 props.wsDeps.isSftpReady
|
// 使用 props.wsDeps 和从 manager instance 解构的 isLoading
|
||||||
// 恢复使用 props.sftpManager.isLoading
|
|
||||||
if (props.wsDeps.isConnected.value && props.wsDeps.isSftpReady.value && !isLoading.value && !initialLoadDone.value && !isFetchingInitialPath.value) {
|
if (props.wsDeps.isConnected.value && props.wsDeps.isSftpReady.value && !isLoading.value && !initialLoadDone.value && !isFetchingInitialPath.value) {
|
||||||
console.log(`[FileManager ${props.sessionId}] Connection ready, fetching initial path.`);
|
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Connection ready, fetching initial path.`);
|
||||||
isFetchingInitialPath.value = true;
|
isFetchingInitialPath.value = true;
|
||||||
|
|
||||||
// 恢复使用 props.wsDeps 中的 sendMessage 和 onMessage
|
// 仍然使用 props.wsDeps 中的 sendMessage 和 onMessage
|
||||||
const { sendMessage: wsSend, onMessage: wsOnMessage } = props.wsDeps;
|
const { sendMessage: wsSend, onMessage: wsOnMessage } = props.wsDeps;
|
||||||
const requestId = generateRequestId(); // 使用本地辅助函数
|
const requestId = generateRequestId(); // 使用本地辅助函数
|
||||||
const requestedPath = '.';
|
const requestedPath = '.';
|
||||||
@@ -485,9 +514,9 @@ watchEffect((onCleanup) => {
|
|||||||
unregisterSuccess = wsOnMessage('sftp:realpath:success', (payload: any, message: WebSocketMessage) => { // message 已有类型
|
unregisterSuccess = wsOnMessage('sftp:realpath:success', (payload: any, message: WebSocketMessage) => { // message 已有类型
|
||||||
if (message.requestId === requestId && payload.requestedPath === requestedPath) {
|
if (message.requestId === requestId && payload.requestedPath === requestedPath) {
|
||||||
const absolutePath = payload.absolutePath;
|
const absolutePath = payload.absolutePath;
|
||||||
console.log(`[FileManager ${props.sessionId}] 收到 '.' 的绝对路径: ${absolutePath}。开始加载目录。`);
|
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] 收到 '.' 的绝对路径: ${absolutePath}。开始加载目录。`);
|
||||||
// 不再直接修改 currentPath.value,而是调用 loadDirectory,它内部会更新路径
|
// 使用从 manager instance 解构的 loadDirectory
|
||||||
loadDirectory(absolutePath); // 使用 props 中的 loadDirectory
|
loadDirectory(absolutePath);
|
||||||
initialLoadDone.value = true;
|
initialLoadDone.value = true;
|
||||||
cleanupListeners();
|
cleanupListeners();
|
||||||
}
|
}
|
||||||
@@ -495,23 +524,23 @@ watchEffect((onCleanup) => {
|
|||||||
|
|
||||||
unregisterError = wsOnMessage('sftp:realpath:error', (payload: any, message: WebSocketMessage) => { // message 已有类型
|
unregisterError = wsOnMessage('sftp:realpath:error', (payload: any, message: WebSocketMessage) => { // message 已有类型
|
||||||
if (message.requestId === requestId && message.path === requestedPath) {
|
if (message.requestId === requestId && message.path === requestedPath) {
|
||||||
console.error(`[FileManager ${props.sessionId}] 获取 '.' 的 realpath 失败:`, payload);
|
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] 获取 '.' 的 realpath 失败:`, payload);
|
||||||
// 适当地显示错误,也许设置 props.sftpManager.error?
|
// TODO: 可以考虑通过 manager instance 暴露错误状态
|
||||||
// 目前仅记录日志。
|
// 目前仅记录日志。
|
||||||
cleanupListeners();
|
cleanupListeners();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`[FileManager ${props.sessionId}] 发送 sftp:realpath 请求 (ID: ${requestId}) for path: ${requestedPath}`);
|
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] 发送 sftp:realpath 请求 (ID: ${requestId}) for path: ${requestedPath}`);
|
||||||
wsSend({ type: 'sftp:realpath', requestId: requestId, payload: { path: requestedPath } });
|
wsSend({ type: 'sftp:realpath', requestId: requestId, payload: { path: requestedPath } });
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
console.error(`[FileManager ${props.sessionId}] 获取 '.' 的 realpath 超时 (ID: ${requestId})。`);
|
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] 获取 '.' 的 realpath 超时 (ID: ${requestId})。`);
|
||||||
cleanupListeners();
|
cleanupListeners();
|
||||||
}, 10000); // 10 秒超时
|
}, 10000); // 10 秒超时
|
||||||
|
|
||||||
} else if (!props.wsDeps.isConnected.value && initialLoadDone.value) { // 恢复使用 props.wsDeps.isConnected
|
} else if (!props.wsDeps.isConnected.value && initialLoadDone.value) { // 仍然使用 props.wsDeps.isConnected
|
||||||
console.log(`[FileManager ${props.sessionId}] 连接丢失 (之前已加载),重置状态。`);
|
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] 连接丢失 (之前已加载),重置状态。`);
|
||||||
clearSelection(); // 清空选择
|
clearSelection(); // 清空选择
|
||||||
initialLoadDone.value = false; // 重置初始加载状态
|
initialLoadDone.value = false; // 重置初始加载状态
|
||||||
// lastClickedIndex.value = -1; // 由 clearSelection 处理
|
// lastClickedIndex.value = -1; // 由 clearSelection 处理
|
||||||
@@ -525,8 +554,10 @@ watch(() => focusSwitcherStore.activateFileManagerSearchTrigger, (newValue, oldV
|
|||||||
// 确保只在触发器值增加时执行(避免初始加载或重置时触发)
|
// 确保只在触发器值增加时执行(避免初始加载或重置时触发)
|
||||||
// 并且当前组件的 sessionId 与活动 sessionId 匹配
|
// 并且当前组件的 sessionId 与活动 sessionId 匹配
|
||||||
// 检查 newValue > oldValue 确保是递增触发,避免重复执行
|
// 检查 newValue > oldValue 确保是递增触发,避免重复执行
|
||||||
|
// 检查是否是当前活动会话的此实例(如果需要区分实例)
|
||||||
|
// 目前假设搜索触发器对会话内的所有 FileManager 生效
|
||||||
if (newValue > (oldValue ?? 0) && props.sessionId === sessionStore.activeSessionId) {
|
if (newValue > (oldValue ?? 0) && props.sessionId === sessionStore.activeSessionId) {
|
||||||
console.log(`[FileManager ${props.sessionId}] Received search activation trigger for active session.`);
|
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Received search activation trigger for active session.`);
|
||||||
activateSearch(); // 调用组件内部的激活搜索方法
|
activateSearch(); // 调用组件内部的激活搜索方法
|
||||||
}
|
}
|
||||||
}, { immediate: false }); // 添加 immediate: false 避免初始值为 0 时触发
|
}, { immediate: false }); // 添加 immediate: false 避免初始值为 0 时触发
|
||||||
@@ -549,7 +580,7 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
// 如果不是活动会话,返回 undefined,表示跳过
|
// 如果不是活动会话,返回 undefined,表示跳过
|
||||||
// async 函数返回 undefined 会被包装成 Promise<undefined>
|
// async 函数返回 undefined 会被包装成 Promise<undefined>
|
||||||
console.log(`[FileManager ${props.sessionId}] Focus action skipped (async undefined) for inactive session.`);
|
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Focus action skipped (async undefined) for inactive session.`);
|
||||||
return undefined; // 返回 undefined 表示跳过
|
return undefined; // 返回 undefined 表示跳过
|
||||||
};
|
};
|
||||||
// 调用新的 registerFocusAction 并存储返回的注销函数
|
// 调用新的 registerFocusAction 并存储返回的注销函数
|
||||||
@@ -560,12 +591,14 @@ onBeforeUnmount(() => {
|
|||||||
// 调用存储的注销函数
|
// 调用存储的注销函数
|
||||||
if (unregisterFocusAction) {
|
if (unregisterFocusAction) {
|
||||||
unregisterFocusAction();
|
unregisterFocusAction();
|
||||||
console.log(`[FileManager ${props.sessionId}] Unregistered focus action on unmount.`);
|
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Unregistered focus action on unmount.`);
|
||||||
}
|
}
|
||||||
// 清理对函数的引用
|
// 清理对函数的引用
|
||||||
unregisterFocusAction = null;
|
unregisterFocusAction = null;
|
||||||
// 调用注入的 SFTP 管理器提供的清理函数 (移到这里确保注销后清理)
|
// // 调用注入的 SFTP 管理器提供的清理函数 (移除,由 store 处理)
|
||||||
cleanupSftpHandlers();
|
// cleanupSftpHandlers();
|
||||||
|
// 调用 store 的清理方法
|
||||||
|
sessionStore.removeSftpManager(props.sessionId, props.instanceId);
|
||||||
});
|
});
|
||||||
|
|
||||||
// --- 列宽调整逻辑 (保持不变) ---
|
// --- 列宽调整逻辑 (保持不变) ---
|
||||||
@@ -617,10 +650,10 @@ const stopResize = () => {
|
|||||||
|
|
||||||
// --- 路径编辑逻辑 ---
|
// --- 路径编辑逻辑 ---
|
||||||
const startPathEdit = () => {
|
const startPathEdit = () => {
|
||||||
// 恢复使用 props.sftpManager.isLoading 和 props.wsDeps.isConnected
|
// 使用从 manager instance 解构的 isLoading 和 props.wsDeps.isConnected
|
||||||
// 注意:这里仍然使用从 sftpManager 解构的 isLoading
|
|
||||||
if (isLoading.value || !props.wsDeps.isConnected.value) return;
|
if (isLoading.value || !props.wsDeps.isConnected.value) return;
|
||||||
editablePath.value = currentPath.value; // 使用 sftpManager 的 currentPath 初始化编辑框
|
// 使用从 manager instance 解构的 currentPath 初始化编辑框
|
||||||
|
editablePath.value = currentPath.value;
|
||||||
isEditingPath.value = true;
|
isEditingPath.value = true;
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
pathInputRef.value?.focus();
|
pathInputRef.value?.focus();
|
||||||
@@ -634,11 +667,12 @@ const handlePathInput = async (event?: Event) => {
|
|||||||
}
|
}
|
||||||
const newPath = editablePath.value.trim();
|
const newPath = editablePath.value.trim();
|
||||||
isEditingPath.value = false;
|
isEditingPath.value = false;
|
||||||
if (newPath === currentPath.value || !newPath) { // 与 sftpManager 的 currentPath 比较
|
// 使用从 manager instance 解构的 currentPath 比较
|
||||||
|
if (newPath === currentPath.value || !newPath) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(`[FileManager ${props.sessionId}] 尝试导航到新路径: ${newPath}`);
|
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] 尝试导航到新路径: ${newPath}`);
|
||||||
// 调用 props 中的 loadDirectory
|
// 使用从 manager instance 解构的 loadDirectory
|
||||||
await loadDirectory(newPath);
|
await loadDirectory(newPath);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -689,7 +723,7 @@ const handleWheel = (event: WheelEvent) => {
|
|||||||
const focusSearchInput = (): boolean => {
|
const focusSearchInput = (): boolean => {
|
||||||
// 检查当前会话是否激活,防止后台实例响应
|
// 检查当前会话是否激活,防止后台实例响应
|
||||||
if (props.sessionId !== sessionStore.activeSessionId) {
|
if (props.sessionId !== sessionStore.activeSessionId) {
|
||||||
console.log(`[FileManager ${props.sessionId}] Ignoring focus request for inactive session.`);
|
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Ignoring focus request for inactive session.`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -699,18 +733,18 @@ const focusSearchInput = (): boolean => {
|
|||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (searchInputRef.value) {
|
if (searchInputRef.value) {
|
||||||
searchInputRef.value.focus();
|
searchInputRef.value.focus();
|
||||||
console.log(`[FileManager ${props.sessionId}] Search activated and input focused.`);
|
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Search activated and input focused.`);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`[FileManager ${props.sessionId}] Search activated but input ref not found after nextTick.`);
|
console.warn(`[FileManager ${props.sessionId}-${props.instanceId}] Search activated but input ref not found after nextTick.`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return true; // 假设会成功
|
return true; // 假设会成功
|
||||||
} else if (searchInputRef.value) {
|
} else if (searchInputRef.value) {
|
||||||
searchInputRef.value.focus();
|
searchInputRef.value.focus();
|
||||||
console.log(`[FileManager ${props.sessionId}] Search already active, input focused.`);
|
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Search already active, input focused.`);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
console.warn(`[FileManager ${props.sessionId}] Could not focus search input.`);
|
console.warn(`[FileManager ${props.sessionId}-${props.instanceId}] Could not focus search input.`);
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
defineExpose({ focusSearchInput });
|
defineExpose({ focusSearchInput });
|
||||||
@@ -722,7 +756,7 @@ defineExpose({ focusSearchInput });
|
|||||||
<div class="toolbar">
|
<div class="toolbar">
|
||||||
<div class="path-bar">
|
<div class="path-bar">
|
||||||
<span v-show="!isEditingPath">
|
<span v-show="!isEditingPath">
|
||||||
<!-- 恢复使用 props.sftpManager.isLoading 和 props.wsDeps.isConnected -->
|
<!-- 使用从 manager instance 解构的 isLoading 和 currentPath, 以及 props.wsDeps -->
|
||||||
{{ t('fileManager.currentPath') }}: <strong @click="startPathEdit" :title="t('fileManager.editPathTooltip')" class="editable-path" :class="{ 'disabled': isLoading || !props.wsDeps.isConnected.value }">{{ currentPath }}</strong>
|
{{ t('fileManager.currentPath') }}: <strong @click="startPathEdit" :title="t('fileManager.editPathTooltip')" class="editable-path" :class="{ 'disabled': isLoading || !props.wsDeps.isConnected.value }">{{ currentPath }}</strong>
|
||||||
</span>
|
</span>
|
||||||
<input
|
<input
|
||||||
@@ -738,9 +772,9 @@ defineExpose({ focusSearchInput });
|
|||||||
</div>
|
</div>
|
||||||
<!-- 按钮移到 path-bar 外面 -->
|
<!-- 按钮移到 path-bar 外面 -->
|
||||||
<div class="path-actions"> <!-- 新增包裹容器 -->
|
<div class="path-actions"> <!-- 新增包裹容器 -->
|
||||||
<!-- 恢复使用 props.sftpManager.isLoading 和 props.wsDeps.isConnected.value -->
|
<!-- 使用从 manager instance 解构的 isLoading, loadDirectory, currentPath 和 props.wsDeps -->
|
||||||
<button class="toolbar-button" @click.stop="loadDirectory(currentPath, true)" :disabled="isLoading || !props.wsDeps.isConnected.value || isEditingPath" :title="t('fileManager.actions.refresh')"><i class="fas fa-sync-alt"></i></button> <!-- 添加 true 参数以强制刷新 -->
|
<button class="toolbar-button" @click.stop="loadDirectory(currentPath, true)" :disabled="isLoading || !props.wsDeps.isConnected.value || isEditingPath" :title="t('fileManager.actions.refresh')"><i class="fas fa-sync-alt"></i></button> <!-- 添加 true 参数以强制刷新 -->
|
||||||
<!-- 恢复使用 props.sftpManager.isLoading 和 props.wsDeps.isConnected.value -->
|
<!-- 使用从 manager instance 解构的 isLoading, currentPath 和 props.wsDeps -->
|
||||||
<button class="toolbar-button" @click.stop="handleItemClick($event, { filename: '..', longname: '..', attrs: { isDirectory: true, isFile: false, isSymbolicLink: false, size: 0, uid: 0, gid: 0, mode: 0, atime: 0, mtime: 0 } })" :disabled="isLoading || !props.wsDeps.isConnected.value || currentPath === '/' || isEditingPath" :title="t('fileManager.actions.parentDirectory')"><i class="fas fa-arrow-up"></i></button>
|
<button class="toolbar-button" @click.stop="handleItemClick($event, { filename: '..', longname: '..', attrs: { isDirectory: true, isFile: false, isSymbolicLink: false, size: 0, uid: 0, gid: 0, mode: 0, atime: 0, mtime: 0 } })" :disabled="isLoading || !props.wsDeps.isConnected.value || currentPath === '/' || isEditingPath" :title="t('fileManager.actions.parentDirectory')"><i class="fas fa-arrow-up"></i></button>
|
||||||
<!-- 修改后的搜索区域 -->
|
<!-- 修改后的搜索区域 -->
|
||||||
<div class="search-container">
|
<div class="search-container">
|
||||||
@@ -775,11 +809,11 @@ defineExpose({ focusSearchInput });
|
|||||||
</div> <!-- 结束包裹容器 -->
|
</div> <!-- 结束包裹容器 -->
|
||||||
<div class="actions-bar">
|
<div class="actions-bar">
|
||||||
<input type="file" ref="fileInputRef" @change="handleFileSelected" multiple style="display: none;" />
|
<input type="file" ref="fileInputRef" @change="handleFileSelected" multiple style="display: none;" />
|
||||||
<!-- 恢复使用 props.sftpManager.isLoading 和 props.wsDeps.isConnected.value -->
|
<!-- 使用从 manager instance 解构的 isLoading 和 props.wsDeps -->
|
||||||
<button @click="triggerFileUpload" :disabled="isLoading || !props.wsDeps.isConnected.value" :title="t('fileManager.actions.uploadFile')"><i class="fas fa-upload"></i> {{ t('fileManager.actions.upload') }}</button>
|
<button @click="triggerFileUpload" :disabled="isLoading || !props.wsDeps.isConnected.value" :title="t('fileManager.actions.uploadFile')"><i class="fas fa-upload"></i> {{ t('fileManager.actions.upload') }}</button>
|
||||||
<!-- 恢复使用 props.sftpManager.isLoading 和 props.wsDeps.isConnected.value -->
|
<!-- 使用从 manager instance 解构的 isLoading 和 props.wsDeps -->
|
||||||
<button @click="handleNewFolderContextMenuClick" :disabled="isLoading || !props.wsDeps.isConnected.value" :title="t('fileManager.actions.newFolder')"><i class="fas fa-folder-plus"></i> {{ t('fileManager.actions.newFolder') }}</button>
|
<button @click="handleNewFolderContextMenuClick" :disabled="isLoading || !props.wsDeps.isConnected.value" :title="t('fileManager.actions.newFolder')"><i class="fas fa-folder-plus"></i> {{ t('fileManager.actions.newFolder') }}</button>
|
||||||
<!-- 恢复使用 props.sftpManager.isLoading 和 props.wsDeps.isConnected.value -->
|
<!-- 使用从 manager instance 解构的 isLoading 和 props.wsDeps -->
|
||||||
<button @click="handleNewFileContextMenuClick" :disabled="isLoading || !props.wsDeps.isConnected.value" :title="t('fileManager.actions.newFile')"><i class="far fa-file-alt"></i> {{ t('fileManager.actions.newFile') }}</button>
|
<button @click="handleNewFileContextMenuClick" :disabled="isLoading || !props.wsDeps.isConnected.value" :title="t('fileManager.actions.newFile')"><i class="far fa-file-alt"></i> {{ t('fileManager.actions.newFile') }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -840,6 +874,7 @@ defineExpose({ focusSearchInput });
|
|||||||
</thead>
|
</thead>
|
||||||
|
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
|
<!-- 使用从 manager instance 解构的 isLoading -->
|
||||||
<tbody v-if="isLoading">
|
<tbody v-if="isLoading">
|
||||||
<tr>
|
<tr>
|
||||||
<td :colspan="5" class="loading">{{ t('fileManager.loading') }}</td> <!-- Span across all columns -->
|
<td :colspan="5" class="loading">{{ t('fileManager.loading') }}</td> <!-- Span across all columns -->
|
||||||
@@ -847,6 +882,7 @@ defineExpose({ focusSearchInput });
|
|||||||
</tbody>
|
</tbody>
|
||||||
|
|
||||||
<!-- Empty Directory State (Root Only) -->
|
<!-- Empty Directory State (Root Only) -->
|
||||||
|
<!-- 使用从 manager instance 解构的 currentPath -->
|
||||||
<tbody v-else-if="sortedFileList.length === 0 && currentPath === '/'">
|
<tbody v-else-if="sortedFileList.length === 0 && currentPath === '/'">
|
||||||
<tr>
|
<tr>
|
||||||
<td :colspan="5" class="no-files">{{ t('fileManager.emptyDirectory') }}</td> <!-- Span across all columns -->
|
<td :colspan="5" class="no-files">{{ t('fileManager.emptyDirectory') }}</td> <!-- Span across all columns -->
|
||||||
@@ -856,6 +892,7 @@ defineExpose({ focusSearchInput });
|
|||||||
<!-- File List State -->
|
<!-- File List State -->
|
||||||
<tbody v-else @contextmenu.prevent="showContextMenu($event)"> <!-- 使用 Composable 的 showContextMenu -->
|
<tbody v-else @contextmenu.prevent="showContextMenu($event)"> <!-- 使用 Composable 的 showContextMenu -->
|
||||||
<!-- '..' 条目 -->
|
<!-- '..' 条目 -->
|
||||||
|
<!-- 使用从 manager instance 解构的 currentPath -->
|
||||||
<tr v-if="currentPath !== '/'"
|
<tr v-if="currentPath !== '/'"
|
||||||
class="clickable file-row folder-row"
|
class="clickable file-row folder-row"
|
||||||
:class="{
|
:class="{
|
||||||
@@ -882,7 +919,8 @@ defineExpose({ focusSearchInput });
|
|||||||
:class="[
|
:class="[
|
||||||
'file-row',
|
'file-row',
|
||||||
{ clickable: item.attrs.isDirectory || item.attrs.isFile },
|
{ clickable: item.attrs.isDirectory || item.attrs.isFile },
|
||||||
{ selected: selectedItems.has(item.filename) || (index + (currentPath !== '/' ? 1 : 0) === selectedIndex) }, /* 使用 Composable 的 selectedIndex */
|
/* 使用 Composable 的 selectedIndex 和从 manager instance 解构的 currentPath */
|
||||||
|
{ selected: selectedItems.has(item.filename) || (index + (currentPath !== '/' ? 1 : 0) === selectedIndex) },
|
||||||
// { selected: index + (currentPath !== '/' ? 1 : 0) === selectedIndex }, /* 保持注释 */
|
// { selected: index + (currentPath !== '/' ? 1 : 0) === selectedIndex }, /* 保持注释 */
|
||||||
{ 'folder-row': item.attrs.isDirectory }, // 添加文件夹标识类
|
{ 'folder-row': item.attrs.isDirectory }, // 添加文件夹标识类
|
||||||
{ 'drop-target': item.attrs.isDirectory && dragOverTarget === item.filename } // 使用 Composable 的 dragOverTarget
|
{ 'drop-target': item.attrs.isDirectory && dragOverTarget === item.filename } // 使用 Composable 的 dragOverTarget
|
||||||
|
|||||||
@@ -161,15 +161,18 @@ const componentProps = computed(() => {
|
|||||||
case 'fileManager':
|
case 'fileManager':
|
||||||
// 仅当有活动会话时才返回实际 props,否则返回空对象
|
// 仅当有活动会话时才返回实际 props,否则返回空对象
|
||||||
if (!currentActiveSession) return {};
|
if (!currentActiveSession) return {};
|
||||||
|
// 传递 instanceId (使用布局节点的 ID), sessionId, dbConnectionId
|
||||||
|
// 移除 sftpManager 和 wsDeps
|
||||||
return {
|
return {
|
||||||
sessionId: props.activeSessionId ?? '', // 确保 sessionId 不为 null
|
sessionId: props.activeSessionId ?? '', // 确保 sessionId 不为 null
|
||||||
|
instanceId: props.layoutNode.id, // 使用布局节点 ID 作为实例 ID
|
||||||
dbConnectionId: currentActiveSession.connectionId,
|
dbConnectionId: currentActiveSession.connectionId,
|
||||||
sftpManager: currentActiveSession.sftpManager, // 此时 currentActiveSession 必不为 null
|
// sftpManager: currentActiveSession.sftpManager, // 移除 sftpManager,因为它现在由 FileManager 内部管理
|
||||||
wsDeps: { // 确保传递 wsDeps
|
wsDeps: { // 恢复 wsDeps
|
||||||
sendMessage: currentActiveSession.wsManager.sendMessage,
|
sendMessage: currentActiveSession.wsManager.sendMessage,
|
||||||
onMessage: currentActiveSession.wsManager.onMessage,
|
onMessage: currentActiveSession.wsManager.onMessage,
|
||||||
isConnected: currentActiveSession.wsManager.isConnected,
|
isConnected: currentActiveSession.wsManager.isConnected, // 恢复 isConnected
|
||||||
isSftpReady: currentActiveSession.wsManager.isSftpReady
|
isSftpReady: currentActiveSession.wsManager.isSftpReady // 恢复 isSftpReady
|
||||||
},
|
},
|
||||||
class: 'pane-content', // class 可以保留,或者在模板中处理
|
class: 'pane-content', // class 可以保留,或者在模板中处理
|
||||||
// FileManager 可能也需要转发事件,例如文件操作相关的,暂时省略
|
// FileManager 可能也需要转发事件,例如文件操作相关的,暂时省略
|
||||||
@@ -239,7 +242,8 @@ const componentProps = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// --- New computed property for sidebar component props and events ---
|
// --- New computed property for sidebar component props and events ---
|
||||||
const sidebarProps = computed(() => (paneName: PaneName | null) => {
|
// 修改以接收 side 参数,用于确定 instanceId
|
||||||
|
const sidebarProps = computed(() => (paneName: PaneName | null, side: 'left' | 'right') => {
|
||||||
if (!paneName) return {};
|
if (!paneName) return {};
|
||||||
|
|
||||||
const baseProps = { class: 'sidebar-pane-content' }; // Base props for all sidebar components
|
const baseProps = { class: 'sidebar-pane-content' }; // Base props for all sidebar components
|
||||||
@@ -279,16 +283,20 @@ const sidebarProps = computed(() => (paneName: PaneName | null) => {
|
|||||||
case 'fileManager':
|
case 'fileManager':
|
||||||
// Only provide props if there's an active session
|
// Only provide props if there's an active session
|
||||||
if (activeSession.value) {
|
if (activeSession.value) {
|
||||||
|
// 传递 instanceId (根据 side), sessionId, dbConnectionId
|
||||||
|
// 移除 sftpManager 和 wsDeps
|
||||||
|
const instanceId = side === 'left' ? 'sidebar-left' : 'sidebar-right';
|
||||||
return {
|
return {
|
||||||
...baseProps,
|
...baseProps,
|
||||||
sessionId: activeSession.value.sessionId, // Corrected: Use sessionId
|
sessionId: activeSession.value.sessionId,
|
||||||
|
instanceId: instanceId, // 使用 'sidebar-left' 或 'sidebar-right'
|
||||||
dbConnectionId: activeSession.value.connectionId,
|
dbConnectionId: activeSession.value.connectionId,
|
||||||
sftpManager: activeSession.value.sftpManager,
|
// sftpManager: activeSession.value.sftpManager, // 移除 sftpManager
|
||||||
wsDeps: {
|
wsDeps: { // 恢复 wsDeps
|
||||||
sendMessage: activeSession.value.wsManager.sendMessage,
|
sendMessage: activeSession.value.wsManager.sendMessage,
|
||||||
onMessage: activeSession.value.wsManager.onMessage,
|
onMessage: activeSession.value.wsManager.onMessage,
|
||||||
isConnected: activeSession.value.wsManager.isConnected,
|
isConnected: activeSession.value.wsManager.isConnected, // 直接传递 ref
|
||||||
isSftpReady: activeSession.value.wsManager.isSftpReady
|
isSftpReady: activeSession.value.wsManager.isSftpReady // 直接传递 ref
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@@ -511,14 +519,15 @@ onMounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
<!-- FileManager 需要 keep-alive 处理 -->
|
<!-- FileManager 需要 keep-alive 处理 -->
|
||||||
<template v-else-if="layoutNode.component === 'fileManager'">
|
<template v-else-if="layoutNode.component === 'fileManager'">
|
||||||
<keep-alive>
|
<!-- <keep-alive> Temporarily removed for debugging InvalidCharacterError -->
|
||||||
|
<template v-if="activeSession">
|
||||||
<component
|
<component
|
||||||
v-if="activeSession"
|
|
||||||
:is="currentMainComponent"
|
:is="currentMainComponent"
|
||||||
:key="activeSessionId"
|
:key="`${activeSessionId}-${layoutNode.id}`"
|
||||||
v-bind="componentProps"
|
v-bind="componentProps">
|
||||||
/>
|
</component>
|
||||||
</keep-alive>
|
</template>
|
||||||
|
<!-- </keep-alive> -->
|
||||||
<div v-if="!activeSession" class="pane-placeholder empty-session">
|
<div v-if="!activeSession" class="pane-placeholder empty-session">
|
||||||
<div class="empty-session-content">
|
<div class="empty-session-content">
|
||||||
<i class="fas fa-plug"></i>
|
<i class="fas fa-plug"></i>
|
||||||
@@ -597,8 +606,8 @@ onMounted(() => {
|
|||||||
v-if="currentLeftSidebarComponent && activeLeftSidebarPane && (!['fileManager', 'statusMonitor'].includes(activeLeftSidebarPane) || activeSession)"
|
v-if="currentLeftSidebarComponent && activeLeftSidebarPane && (!['fileManager', 'statusMonitor'].includes(activeLeftSidebarPane) || activeSession)"
|
||||||
:is="currentLeftSidebarComponent"
|
:is="currentLeftSidebarComponent"
|
||||||
:key="`left-panel-${activeLeftSidebarPane ?? 'null'}`"
|
:key="`left-panel-${activeLeftSidebarPane ?? 'null'}`"
|
||||||
v-bind="sidebarProps(activeLeftSidebarPane)"
|
v-bind="sidebarProps(activeLeftSidebarPane, 'left')">
|
||||||
/>
|
</component>
|
||||||
<!-- Placeholder if FileManager is selected but no active session -->
|
<!-- Placeholder if FileManager is selected but no active session -->
|
||||||
<div v-else-if="activeLeftSidebarPane === 'fileManager' && !activeSession" class="sidebar-pane-content pane-placeholder empty-session">
|
<div v-else-if="activeLeftSidebarPane === 'fileManager' && !activeSession" class="sidebar-pane-content pane-placeholder empty-session">
|
||||||
<div class="empty-session-content">
|
<div class="empty-session-content">
|
||||||
@@ -625,8 +634,8 @@ onMounted(() => {
|
|||||||
v-if="currentRightSidebarComponent && activeRightSidebarPane && (!['fileManager', 'statusMonitor'].includes(activeRightSidebarPane) || activeSession)"
|
v-if="currentRightSidebarComponent && activeRightSidebarPane && (!['fileManager', 'statusMonitor'].includes(activeRightSidebarPane) || activeSession)"
|
||||||
:is="currentRightSidebarComponent"
|
:is="currentRightSidebarComponent"
|
||||||
:key="`right-panel-${activeRightSidebarPane ?? 'null'}`"
|
:key="`right-panel-${activeRightSidebarPane ?? 'null'}`"
|
||||||
v-bind="sidebarProps(activeRightSidebarPane)"
|
v-bind="sidebarProps(activeRightSidebarPane, 'right')">
|
||||||
/>
|
</component>
|
||||||
<!-- Placeholder if FileManager is selected but no active session -->
|
<!-- Placeholder if FileManager is selected but no active session -->
|
||||||
<div v-else-if="activeRightSidebarPane === 'fileManager' && !activeSession" class="sidebar-pane-content pane-placeholder empty-session">
|
<div v-else-if="activeRightSidebarPane === 'fileManager' && !activeSession" class="sidebar-pane-content pane-placeholder empty-session">
|
||||||
<div class="empty-session-content">
|
<div class="empty-session-content">
|
||||||
@@ -943,3 +952,4 @@ onMounted(() => {
|
|||||||
border-bottom: 1px solid var(--border-color-lighter, #f1f3f5);
|
border-bottom: 1px solid var(--border-color-lighter, #f1f3f5);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -61,10 +61,11 @@ export interface SessionState {
|
|||||||
connectionId: string; // 数据库中的连接 ID
|
connectionId: string; // 数据库中的连接 ID
|
||||||
connectionName: string; // 用于显示
|
connectionName: string; // 用于显示
|
||||||
wsManager: WsManagerInstance;
|
wsManager: WsManagerInstance;
|
||||||
sftpManager: SftpManagerInstance;
|
// sftpManager: SftpManagerInstance; // 移除单个实例
|
||||||
|
sftpManagers: Map<string, SftpManagerInstance>; // 使用 Map 管理多个实例
|
||||||
terminalManager: SshTerminalInstance;
|
terminalManager: SshTerminalInstance;
|
||||||
statusMonitorManager: StatusMonitorInstance;
|
statusMonitorManager: StatusMonitorInstance;
|
||||||
currentSftpPath: Ref<string>; // SFTP 当前路径
|
// currentSftpPath: Ref<string>; // 移除,由每个 sftpManager 内部管理
|
||||||
// --- 新增:独立编辑器状态 ---
|
// --- 新增:独立编辑器状态 ---
|
||||||
editorTabs: Ref<FileTab[]>; // 编辑器标签页列表
|
editorTabs: Ref<FileTab[]>; // 编辑器标签页列表
|
||||||
activeEditorTabId: Ref<string | null>; // 当前活动的编辑器标签页 ID
|
activeEditorTabId: Ref<string | null>; // 当前活动的编辑器标签页 ID
|
||||||
@@ -137,14 +138,9 @@ export const useSessionStore = defineStore('session', () => {
|
|||||||
|
|
||||||
// 1. 创建管理器实例 (从 WorkspaceView 迁移)
|
// 1. 创建管理器实例 (从 WorkspaceView 迁移)
|
||||||
const wsManager = createWebSocketConnectionManager(newSessionId, dbConnId, t);
|
const wsManager = createWebSocketConnectionManager(newSessionId, dbConnId, t);
|
||||||
const currentSftpPath = ref<string>('.'); // SFTP 路径状态
|
// const currentSftpPath = ref<string>('.'); // 移除单个 SFTP 路径状态
|
||||||
const wsDeps: WebSocketDependencies = {
|
// const wsDeps: WebSocketDependencies = { ... }; // wsDeps 将在 getOrCreateSftpManager 中创建
|
||||||
sendMessage: wsManager.sendMessage,
|
// const sftpManager = createSftpActionsManager(newSessionId, currentSftpPath, wsDeps, t); // 移除单个 sftpManager 创建
|
||||||
onMessage: wsManager.onMessage,
|
|
||||||
isConnected: wsManager.isConnected,
|
|
||||||
isSftpReady: wsManager.isSftpReady,
|
|
||||||
};
|
|
||||||
const sftpManager = createSftpActionsManager(newSessionId, currentSftpPath, wsDeps, t);
|
|
||||||
const sshTerminalDeps: SshTerminalDependencies = {
|
const sshTerminalDeps: SshTerminalDependencies = {
|
||||||
sendMessage: wsManager.sendMessage,
|
sendMessage: wsManager.sendMessage,
|
||||||
onMessage: wsManager.onMessage,
|
onMessage: wsManager.onMessage,
|
||||||
@@ -163,10 +159,11 @@ export const useSessionStore = defineStore('session', () => {
|
|||||||
connectionId: dbConnId,
|
connectionId: dbConnId,
|
||||||
connectionName: connInfo.name || connInfo.host,
|
connectionName: connInfo.name || connInfo.host,
|
||||||
wsManager: wsManager,
|
wsManager: wsManager,
|
||||||
sftpManager: sftpManager,
|
// sftpManager: sftpManager, // 移除
|
||||||
|
sftpManagers: new Map<string, SftpManagerInstance>(), // 初始化 Map
|
||||||
terminalManager: terminalManager,
|
terminalManager: terminalManager,
|
||||||
statusMonitorManager: statusMonitorManager,
|
statusMonitorManager: statusMonitorManager,
|
||||||
currentSftpPath: currentSftpPath,
|
// currentSftpPath: currentSftpPath, // 移除
|
||||||
// --- 初始化编辑器状态 ---
|
// --- 初始化编辑器状态 ---
|
||||||
editorTabs: ref([]), // 初始化为空数组
|
editorTabs: ref([]), // 初始化为空数组
|
||||||
activeEditorTabId: ref(null), // 初始化为 null
|
activeEditorTabId: ref(null), // 初始化为 null
|
||||||
@@ -257,8 +254,17 @@ export const useSessionStore = defineStore('session', () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const sftpManager = session.sftpManager;
|
// 获取默认的 sftpManager 实例来执行保存操作
|
||||||
console.log(`[SessionStore] 开始保存文件: ${tab.filePath} (会话 ${sessionId}, Tab ID: ${tab.id})`);
|
const sftpManager = getOrCreateSftpManager(sessionId, 'primary');
|
||||||
|
if (!sftpManager) {
|
||||||
|
console.error(`[SessionStore] 保存失败:无法获取会话 ${sessionId} 的 primary sftpManager。`);
|
||||||
|
tab.saveStatus = 'error';
|
||||||
|
tab.saveError = t('fileManager.errors.sftpManagerNotFound');
|
||||||
|
setTimeout(() => { if (tab.saveStatus === 'error') { tab.saveStatus = 'idle'; tab.saveError = null; } }, 5000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[SessionStore] 开始保存文件: ${tab.filePath} (会话 ${sessionId}, Tab ID: ${tab.id}) using primary sftpManager`);
|
||||||
tab.isSaving = true;
|
tab.isSaving = true;
|
||||||
tab.saveStatus = 'saving';
|
tab.saveStatus = 'saving';
|
||||||
tab.saveError = null;
|
tab.saveError = null;
|
||||||
@@ -298,8 +304,14 @@ export const useSessionStore = defineStore('session', () => {
|
|||||||
// 1. 调用实例上的清理和断开方法 (从 WorkspaceView 迁移)
|
// 1. 调用实例上的清理和断开方法 (从 WorkspaceView 迁移)
|
||||||
sessionToClose.wsManager.disconnect();
|
sessionToClose.wsManager.disconnect();
|
||||||
console.log(`[SessionStore] 已为会话 ${sessionId} 调用 wsManager.disconnect()`);
|
console.log(`[SessionStore] 已为会话 ${sessionId} 调用 wsManager.disconnect()`);
|
||||||
sessionToClose.sftpManager.cleanup();
|
// 清理该会话下的所有 sftpManager 实例
|
||||||
console.log(`[SessionStore] 已为会话 ${sessionId} 调用 sftpManager.cleanup()`);
|
sessionToClose.sftpManagers.forEach((manager, instanceId) => {
|
||||||
|
manager.cleanup();
|
||||||
|
console.log(`[SessionStore] 已为会话 ${sessionId} 的 sftpManager (实例 ${instanceId}) 调用 cleanup()`);
|
||||||
|
});
|
||||||
|
sessionToClose.sftpManagers.clear();
|
||||||
|
// sessionToClose.sftpManager.cleanup(); // 移除旧的调用
|
||||||
|
// console.log(`[SessionStore] 已为会话 ${sessionId} 调用 sftpManager.cleanup()`); // 移除旧的日志
|
||||||
sessionToClose.terminalManager.cleanup();
|
sessionToClose.terminalManager.cleanup();
|
||||||
console.log(`[SessionStore] 已为会话 ${sessionId} 调用 terminalManager.cleanup()`);
|
console.log(`[SessionStore] 已为会话 ${sessionId} 调用 terminalManager.cleanup()`);
|
||||||
sessionToClose.statusMonitorManager.cleanup();
|
sessionToClose.statusMonitorManager.cleanup();
|
||||||
@@ -428,7 +440,12 @@ export const useSessionStore = defineStore('session', () => {
|
|||||||
tabToLoad.loadingError = null;
|
tabToLoad.loadingError = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sftpManager = session.sftpManager; // 获取当前会话的 sftpManager
|
// 获取默认的 sftpManager 实例来执行读取操作
|
||||||
|
const sftpManager = getOrCreateSftpManager(sessionId, 'primary');
|
||||||
|
if (!sftpManager) {
|
||||||
|
throw new Error(t('fileManager.errors.sftpManagerNotFound'));
|
||||||
|
}
|
||||||
|
console.log(`[SessionStore ${sessionId}] 使用 primary sftpManager 读取文件 ${fileInfo.fullPath}`);
|
||||||
const fileData = await sftpManager.readFile(fileInfo.fullPath);
|
const fileData = await sftpManager.readFile(fileInfo.fullPath);
|
||||||
console.log(`[SessionStore ${sessionId}] 文件 ${fileInfo.fullPath} 读取成功。编码: ${fileData.encoding}`);
|
console.log(`[SessionStore ${sessionId}] 文件 ${fileInfo.fullPath} 读取成功。编码: ${fileData.encoding}`);
|
||||||
|
|
||||||
@@ -531,6 +548,52 @@ export const useSessionStore = defineStore('session', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取或创建指定会话和实例 ID 的 SFTP 管理器。
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @param instanceId 文件管理器实例 ID (e.g., 'sidebar', 'panel-xyz')
|
||||||
|
* @returns SftpManagerInstance 或 null (如果会话不存在)
|
||||||
|
*/
|
||||||
|
const getOrCreateSftpManager = (sessionId: string, instanceId: string): SftpManagerInstance | null => {
|
||||||
|
const session = sessions.value.get(sessionId);
|
||||||
|
if (!session) {
|
||||||
|
console.error(`[SessionStore] 尝试为不存在的会话 ${sessionId} 获取 SFTP 管理器`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let manager = session.sftpManagers.get(instanceId);
|
||||||
|
if (!manager) {
|
||||||
|
console.log(`[SessionStore] 为会话 ${sessionId} 创建新的 SFTP 管理器实例: ${instanceId}`);
|
||||||
|
const currentSftpPath = ref<string>('.'); // 每个实例有自己的路径
|
||||||
|
const wsDeps: WebSocketDependencies = {
|
||||||
|
sendMessage: session.wsManager.sendMessage,
|
||||||
|
onMessage: session.wsManager.onMessage,
|
||||||
|
isConnected: session.wsManager.isConnected,
|
||||||
|
isSftpReady: session.wsManager.isSftpReady,
|
||||||
|
};
|
||||||
|
manager = createSftpActionsManager(sessionId, currentSftpPath, wsDeps, t);
|
||||||
|
session.sftpManagers.set(instanceId, manager);
|
||||||
|
}
|
||||||
|
return manager;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 移除并清理指定会话和实例 ID 的 SFTP 管理器。
|
||||||
|
* @param sessionId 会话 ID
|
||||||
|
* @param instanceId 文件管理器实例 ID
|
||||||
|
*/
|
||||||
|
const removeSftpManager = (sessionId: string, instanceId: string) => {
|
||||||
|
const session = sessions.value.get(sessionId);
|
||||||
|
if (session) {
|
||||||
|
const manager = session.sftpManagers.get(instanceId);
|
||||||
|
if (manager) {
|
||||||
|
manager.cleanup();
|
||||||
|
session.sftpManagers.delete(instanceId);
|
||||||
|
console.log(`[SessionStore] 已移除并清理会话 ${sessionId} 的 SFTP 管理器实例: ${instanceId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// State
|
// State
|
||||||
sessions,
|
sessions,
|
||||||
@@ -546,6 +609,8 @@ export const useSessionStore = defineStore('session', () => {
|
|||||||
handleConnectRequest,
|
handleConnectRequest,
|
||||||
handleOpenNewSession,
|
handleOpenNewSession,
|
||||||
cleanupAllSessions,
|
cleanupAllSessions,
|
||||||
|
getOrCreateSftpManager, // 导出新的 Action
|
||||||
|
removeSftpManager, // 导出新的 Action
|
||||||
// --- 新增:导出编辑器相关 Actions ---
|
// --- 新增:导出编辑器相关 Actions ---
|
||||||
openFileInSession,
|
openFileInSession,
|
||||||
closeEditorTabInSession,
|
closeEditorTabInSession,
|
||||||
|
|||||||
Reference in New Issue
Block a user