This commit is contained in:
Baobhan Sith
2025-04-22 11:09:16 +08:00
parent 8b99adbbc4
commit ca828cefe1
2 changed files with 134 additions and 94 deletions
@@ -301,8 +301,8 @@ const handleRenameContextMenuClick = (item: FileListItem) => { // item 已有类
if (!currentSftpManager.value) return; if (!currentSftpManager.value) return;
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) {
// 修改:使用 currentSftpManager.value.renameItem // 修改:添加 ?. 访问
currentSftpManager.value.renameItem(item, newName); currentSftpManager.value?.renameItem(item, newName);
} }
}; };
@@ -314,11 +314,13 @@ const handleChangePermissionsContextMenuClick = (item: FileListItem) => { // ite
if (!/^[0-7]{3,4}$/.test(newModeStr)) { if (!/^[0-7]{3,4}$/.test(newModeStr)) {
alert(t('fileManager.errors.invalidPermissionsFormat')); alert(t('fileManager.errors.invalidPermissionsFormat'));
return; return;
// 修改:检查 currentSftpManager 是否存在
if (!currentSftpManager.value) return;
} }
const newMode = parseInt(newModeStr, 8); const newMode = parseInt(newModeStr, 8);
// 修改:使用 currentSftpManager.value.changePermissions // 修改:在调用前检查 currentSftpManager
if (!currentSftpManager.value) {
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot change permissions: SFTP manager not available.`);
return;
}
currentSftpManager.value.changePermissions(item, newMode); currentSftpManager.value.changePermissions(item, newMode);
} }
}; };
@@ -334,7 +336,11 @@ const handleNewFolderContextMenuClick = () => {
alert(t('fileManager.errors.folderExists', { name: folderName })); alert(t('fileManager.errors.folderExists', { name: folderName }));
return; return;
} }
// 修改:使用 currentSftpManager.value.createDirectory // 修改:确保在检查后调用,并检查 manager
if (!currentSftpManager.value) {
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot create directory: SFTP manager not available.`);
return;
}
currentSftpManager.value.createDirectory(folderName); currentSftpManager.value.createDirectory(folderName);
} }
}; };
@@ -350,7 +356,11 @@ const handleNewFileContextMenuClick = () => {
alert(t('fileManager.errors.fileExists', { name: fileName })); alert(t('fileManager.errors.fileExists', { name: fileName }));
return; return;
} }
// 修改:使用 currentSftpManager.value.createFile // 修改:确保在检查后调用,并检查 manager
if (!currentSftpManager.value) {
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot create file: SFTP manager not available.`);
return;
}
currentSftpManager.value.createFile(fileName); currentSftpManager.value.createFile(fileName);
} }
}; };
@@ -369,12 +379,16 @@ const triggerDownload = (item: FileListItem) => { // item 已有类型
const currentConnectionId = props.dbConnectionId; const currentConnectionId = props.dbConnectionId;
if (!currentConnectionId) { if (!currentConnectionId) {
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot download: Missing connection ID.`); console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot download: Missing connection ID.`);
// 修改:检查 currentSftpManager 是否存在
if (!currentSftpManager.value) return;
alert(t('fileManager.errors.missingConnectionId')); alert(t('fileManager.errors.missingConnectionId'));
return; return;
} }
// 修改:使用 currentSftpManager.value 的 joinPath 和 currentPath // 修改:简化检查
if (!currentSftpManager.value) {
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] Cannot download: SFTP manager is not available.`);
alert(t('fileManager.errors.sftpManagerNotFound'));
return;
}
const downloadPath = currentSftpManager.value.joinPath(currentSftpManager.value.currentPath.value, item.filename); const downloadPath = currentSftpManager.value.joinPath(currentSftpManager.value.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}-${props.instanceId}] Triggering download: ${downloadUrl}`); console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Triggering download: ${downloadUrl}`);
@@ -398,15 +412,19 @@ const {
} = useFileManagerContextMenu({ } = useFileManagerContextMenu({
selectedItems, selectedItems,
lastClickedIndex, lastClickedIndex,
// 修改:传递 manager 的 fileList 和 currentPath ref // 修改:传递 manager 的 fileList 和 currentPath ref (保持 computed)
fileList: computed(() => currentSftpManager.value?.fileList.value ?? []), fileList: computed(() => currentSftpManager.value?.fileList.value ?? []),
currentPath: computed(() => currentSftpManager.value?.currentPath.value ?? '/'), currentPath: computed(() => currentSftpManager.value?.currentPath.value ?? '/'),
isConnected: props.wsDeps.isConnected, isConnected: props.wsDeps.isConnected,
isSftpReady: props.wsDeps.isSftpReady, isSftpReady: props.wsDeps.isSftpReady,
t, t,
// --- 传递回调函数 --- // --- 传递回调函数 ---
// 修改:使用 currentSftpManager.value // 修改:确保在调用前检查 currentSftpManager.value
onRefresh: () => currentSftpManager.value?.loadDirectory(currentSftpManager.value.currentPath.value), onRefresh: () => {
if (currentSftpManager.value) {
currentSftpManager.value.loadDirectory(currentSftpManager.value.currentPath.value);
}
},
onUpload: triggerFileUpload, onUpload: triggerFileUpload,
onDownload: triggerDownload, onDownload: triggerDownload,
onDelete: handleDeleteSelectedClick, onDelete: handleDeleteSelectedClick,
@@ -436,15 +454,20 @@ const {
handleDropOnRow, handleDropOnRow,
} = useFileManagerDragAndDrop({ } = useFileManagerDragAndDrop({
isConnected: props.wsDeps.isConnected, isConnected: props.wsDeps.isConnected,
// 修改:传递 manager 的 currentPath 和 joinPath // 修改:传递 manager 的 currentPath (保持 computed)
currentPath: computed(() => currentSftpManager.value?.currentPath.value ?? '/'), currentPath: computed(() => currentSftpManager.value?.currentPath.value ?? '/'),
fileListContainerRef: fileListContainerRef, fileListContainerRef: fileListContainerRef,
joinPath: computed(() => currentSftpManager.value?.joinPath ?? ((...args: string[]) => args.join('/'))), // 提供默认 joinPath // 修改:传递一个包装函数给 joinPath
joinPath: (base: string, target: string): string => {
return currentSftpManager.value?.joinPath(base, target) ?? `${base}/${target}`.replace(/\/+/g, '/'); // 提供简单的默认实现
},
onFileUpload: startFileUpload, onFileUpload: startFileUpload,
// 修改:使用 currentSftpManager.value.renameItem // 修改:确保在调用前检查 currentSftpManager.value
onItemMove: (item, newName) => currentSftpManager.value?.renameItem(item, newName), onItemMove: (item, newName) => {
currentSftpManager.value?.renameItem(item, newName);
},
selectedItems: selectedItems, selectedItems: selectedItems,
// 修改:传递 manager 的 fileList ref // 修改:传递 manager 的 fileList ref (保持 computed)
fileList: computed(() => currentSftpManager.value?.fileList.value ?? []), fileList: computed(() => currentSftpManager.value?.fileList.value ?? []),
}); });
@@ -503,7 +526,7 @@ onMounted(() => {
watchEffect((onCleanup) => { watchEffect((onCleanup) => {
let unregisterSuccess: (() => void) | undefined; let unregisterSuccess: (() => void) | undefined;
let unregisterError: (() => void) | undefined; let unregisterError: (() => void) | undefined;
let timeoutId: NodeJS.Timeout | undefined; // 修正类型 NodeJS.Timeout let timeoutId: NodeJS.Timeout | number | undefined; // 修正类型以兼容 Node 和浏览器环境
const cleanupListeners = () => { const cleanupListeners = () => {
unregisterSuccess?.(); unregisterSuccess?.();
@@ -516,8 +539,8 @@ watchEffect((onCleanup) => {
onCleanup(cleanupListeners); onCleanup(cleanupListeners);
// 修改:检查 currentSftpManager 是否存在,并使用其 isLoading 状态 // 修改:添加 ?. 访问 isLoading
if (currentSftpManager.value && props.wsDeps.isConnected.value && props.wsDeps.isSftpReady.value && !currentSftpManager.value.isLoading.value && !initialLoadDone.value && !isFetchingInitialPath.value) { if (currentSftpManager.value && props.wsDeps.isConnected.value && props.wsDeps.isSftpReady.value && !currentSftpManager.value?.isLoading?.value && !initialLoadDone.value && !isFetchingInitialPath.value) {
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Connection ready for manager, fetching initial path.`); console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Connection ready for manager, fetching initial path.`);
isFetchingInitialPath.value = true; isFetchingInitialPath.value = true;
@@ -532,16 +555,17 @@ watchEffect((onCleanup) => {
if (!currentSftpManager.value) return; if (!currentSftpManager.value) return;
const absolutePath = payload.absolutePath; const absolutePath = payload.absolutePath;
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] 收到 '.' 的绝对路径: ${absolutePath}。开始加载目录。`); console.log(`[FileManager ${props.sessionId}-${props.instanceId}] 收到 '.' 的绝对路径: ${absolutePath}。开始加载目录。`);
// 修改:使用 currentSftpManager.value.loadDirectory // 修改:添加 ?. 访问 loadDirectory
currentSftpManager.value.loadDirectory(absolutePath); currentSftpManager.value?.loadDirectory(absolutePath);
initialLoadDone.value = true; initialLoadDone.value = true;
cleanupListeners(); cleanupListeners();
} }
}); });
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) { // 修改:使用 payload.requestedPath (如果存在) 或 message.requestId 匹配
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] 获取 '.' 的 realpath 失败:`, payload); if (message.requestId === requestId && payload?.requestedPath === requestedPath) {
console.error(`[FileManager ${props.sessionId}-${props.instanceId}] 获取 '${requestedPath}' 的 realpath 失败:`, payload);
// TODO: 可以考虑通过 manager instance 暴露错误状态 // TODO: 可以考虑通过 manager instance 暴露错误状态
// 目前仅记录日志。 // 目前仅记录日志。
cleanupListeners(); cleanupListeners();
@@ -611,21 +635,19 @@ watch(() => props.sessionId, (newSessionId, oldSessionId) => {
let unregisterFocusAction: (() => void) | null = null; // 用于存储注销函数 let unregisterFocusAction: (() => void) | null = null; // 用于存储注销函数
onMounted(() => { onMounted(() => {
// 注册一个包装函数,而不是直接注册 focusSearchInput // 注册一个 async 函数以兼容 Promise 返回类型
// 使其成为 async 函数以兼容 Promise 返回类型
const focusActionWrapper = async (): Promise<boolean | undefined> => { const focusActionWrapper = async (): Promise<boolean | undefined> => {
if (props.sessionId === sessionStore.activeSessionId) { if (props.sessionId === sessionStore.activeSessionId) {
// 如果是活动会话,调用原始聚焦函数并返回其结果 // 如果是活动会话,调用聚焦函数并返回其结果
// 由于 focusSearchInput 是同步的,我们直接返回它的 boolean 结果 console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Executing focus action for active session.`);
// async 函数会自动将其包装在 Promise 中(如果需要,但这里不需要) return focusSearchInput(); // focusSearchInput 返回 boolean
return focusSearchInput(); } else {
// 如果不是活动会话,返回 undefined 表示跳过
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Focus action skipped for inactive session.`);
return undefined;
} }
// 如果不是活动会话,返回 undefined,表示跳过
// async 函数返回 undefined 会被包装成 Promise<undefined>
console.log(`[FileManager ${props.sessionId}-${props.instanceId}] Focus action skipped (async undefined) for inactive session.`);
return undefined; // 返回 undefined 表示跳过
}; };
// 调用新的 registerFocusAction 并存储返回的注销函数 // 调用 registerFocusAction 并存储返回的注销函数
unregisterFocusAction = focusSwitcherStore.registerFocusAction('fileManagerSearch', focusActionWrapper); unregisterFocusAction = focusSwitcherStore.registerFocusAction('fileManagerSearch', focusActionWrapper);
}); });
@@ -800,8 +822,8 @@ defineExpose({ focusSearchInput });
<div class="toolbar"> <div class="toolbar">
<div class="path-bar"> <div class="path-bar">
<span v-show="!isEditingPath"> <span v-show="!isEditingPath">
<!-- 修改使用 currentSftpManager.value --> <!-- 修改简化 disabled 条件 -->
{{ t('fileManager.currentPath') }}: <strong @click="startPathEdit" :title="t('fileManager.editPathTooltip')" class="editable-path" :class="{ 'disabled': !currentSftpManager || currentSftpManager.isLoading.value || !props.wsDeps.isConnected.value }">{{ currentSftpManager?.currentPath.value ?? '/' }}</strong> {{ t('fileManager.currentPath') }}: <strong @click="startPathEdit" :title="t('fileManager.editPathTooltip')" class="editable-path" :class="{ 'disabled': !currentSftpManager || !props.wsDeps.isConnected.value }">{{ currentSftpManager?.currentPath?.value ?? '/' }}</strong>
</span> </span>
<input <input
v-show="isEditingPath" v-show="isEditingPath"
@@ -816,17 +838,17 @@ defineExpose({ focusSearchInput });
</div> </div>
<!-- 按钮移到 path-bar 外面 --> <!-- 按钮移到 path-bar 外面 -->
<div class="path-actions"> <!-- 新增包裹容器 --> <div class="path-actions"> <!-- 新增包裹容器 -->
<!-- 修改使用 currentSftpManager.value --> <!-- 修改简化 disabled 条件 -->
<button class="toolbar-button" @click.stop="currentSftpManager?.loadDirectory(currentSftpManager.currentPath.value, true)" :disabled="!currentSftpManager || currentSftpManager.isLoading.value || !props.wsDeps.isConnected.value || isEditingPath" :title="t('fileManager.actions.refresh')"><i class="fas fa-sync-alt"></i></button> <button class="toolbar-button" @click.stop="currentSftpManager?.loadDirectory(currentSftpManager?.currentPath?.value ?? '/', true)" :disabled="!currentSftpManager || !props.wsDeps.isConnected.value || isEditingPath" :title="t('fileManager.actions.refresh')"><i class="fas fa-sync-alt"></i></button>
<!-- 修改使用 currentSftpManager.value --> <!-- 修改简化 disabled 条件 -->
<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="!currentSftpManager || currentSftpManager.isLoading.value || !props.wsDeps.isConnected.value || currentSftpManager.currentPath.value === '/' || 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="!currentSftpManager || !props.wsDeps.isConnected.value || currentSftpManager?.currentPath?.value === '/' || isEditingPath" :title="t('fileManager.actions.parentDirectory')"><i class="fas fa-arrow-up"></i></button>
<!-- 修改后的搜索区域 --> <!-- 修改后的搜索区域 -->
<div class="search-container"> <div class="search-container">
<button <button
v-if="!isSearchActive" v-if="!isSearchActive"
class="toolbar-button search-activate-button" class="toolbar-button search-activate-button"
@click.stop="activateSearch" @click.stop="activateSearch"
:disabled="!currentSftpManager || currentSftpManager.isLoading.value || !props.wsDeps.isConnected.value" :disabled="!currentSftpManager || !props.wsDeps.isConnected.value"
:title="t('fileManager.searchPlaceholder')" :title="t('fileManager.searchPlaceholder')"
> >
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
@@ -853,12 +875,12 @@ 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;" />
<!-- 修改使用 currentSftpManager.value --> <!-- 修改简化 disabled 条件 -->
<button @click="triggerFileUpload" :disabled="!currentSftpManager || currentSftpManager.isLoading.value || !props.wsDeps.isConnected.value" :title="t('fileManager.actions.uploadFile')"><i class="fas fa-upload"></i> {{ t('fileManager.actions.upload') }}</button> <button @click="triggerFileUpload" :disabled="!currentSftpManager || !props.wsDeps.isConnected.value" :title="t('fileManager.actions.uploadFile')"><i class="fas fa-upload"></i> {{ t('fileManager.actions.upload') }}</button>
<!-- 修改使用 currentSftpManager.value --> <!-- 修改简化 disabled 条件 -->
<button @click="handleNewFolderContextMenuClick" :disabled="!currentSftpManager || currentSftpManager.isLoading.value || !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="!currentSftpManager || !props.wsDeps.isConnected.value" :title="t('fileManager.actions.newFolder')"><i class="fas fa-folder-plus"></i> {{ t('fileManager.actions.newFolder') }}</button>
<!-- 修改使用 currentSftpManager.value --> <!-- 修改简化 disabled 条件 -->
<button @click="handleNewFileContextMenuClick" :disabled="!currentSftpManager || currentSftpManager.isLoading.value || !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="!currentSftpManager || !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>
@@ -918,7 +940,7 @@ defineExpose({ focusSearchInput });
</thead> </thead>
<!-- Loading State --> <!-- Loading State -->
<!-- 修改使用 currentSftpManager.value --> <!-- 修改简化 v-if 条件 -->
<tbody v-if="!currentSftpManager || currentSftpManager.isLoading.value"> <tbody v-if="!currentSftpManager || currentSftpManager.isLoading.value">
<tr> <tr>
<td :colspan="5" class="loading">{{ t('fileManager.loading') }}</td> <td :colspan="5" class="loading">{{ t('fileManager.loading') }}</td>
@@ -602,56 +602,74 @@ onMounted(() => {
<div ref="leftSidebarPanelRef" :class="['sidebar-panel', 'left-sidebar-panel', { active: !!activeLeftSidebarPane }]" :style="{ width: getSidebarPaneWidth(activeLeftSidebarPane) }"> <!-- +++ Use getter for width +++ --> <div ref="leftSidebarPanelRef" :class="['sidebar-panel', 'left-sidebar-panel', { active: !!activeLeftSidebarPane }]" :style="{ width: getSidebarPaneWidth(activeLeftSidebarPane) }"> <!-- +++ Use getter for width +++ -->
<div ref="leftResizeHandleRef" class="resize-handle left-handle"></div> <!-- +++ Left Handle +++ --> <div ref="leftResizeHandleRef" class="resize-handle left-handle"></div> <!-- +++ Left Handle +++ -->
<button class="close-sidebar-btn" @click="closeSidebars" title="Close Sidebar">&times;</button> <button class="close-sidebar-btn" @click="closeSidebars" title="Close Sidebar">&times;</button>
<component <KeepAlive>
v-if="currentLeftSidebarComponent && activeLeftSidebarPane && (!['fileManager', 'statusMonitor'].includes(activeLeftSidebarPane) || activeSession)" <div :key="`left-sidebar-content-${activeLeftSidebarPane ?? 'none'}`" class="sidebar-content-wrapper">
:is="currentLeftSidebarComponent" <!-- Component rendering -->
:key="`left-panel-${activeLeftSidebarPane ?? 'null'}`" <component
v-bind="sidebarProps(activeLeftSidebarPane, 'left')"> v-if="currentLeftSidebarComponent && activeLeftSidebarPane && (!['fileManager', 'statusMonitor'].includes(activeLeftSidebarPane) || activeSession)"
</component> :is="currentLeftSidebarComponent"
<!-- Placeholder if FileManager is selected but no active session --> :key="`left-comp-${activeLeftSidebarPane}`"
<div v-else-if="activeLeftSidebarPane === 'fileManager' && !activeSession" class="sidebar-pane-content pane-placeholder empty-session"> v-bind="sidebarProps(activeLeftSidebarPane, 'left')">
<div class="empty-session-content"> </component>
<i class="fas fa-plug"></i> <!-- Placeholder for FileManager -->
<span>无活动会话</span> <div v-else-if="activeLeftSidebarPane === 'fileManager' && !activeSession" class="sidebar-pane-content pane-placeholder empty-session">
<div class="empty-session-tip">文件管理器需要活动会话</div> <div class="empty-session-content">
</div> <i class="fas fa-plug"></i>
</div> <span>无活动会话</span>
<!-- Placeholder if StatusMonitor is selected but no active session --> <div class="empty-session-tip">文件管理器需要活动会话</div>
<div v-else-if="activeLeftSidebarPane === 'statusMonitor' && !activeSession" class="sidebar-pane-content pane-placeholder empty-session"> </div>
<div class="empty-session-content"> </div>
<i class="fas fa-plug"></i> <!-- Placeholder for StatusMonitor -->
<span>无活动会话</span> <div v-else-if="activeLeftSidebarPane === 'statusMonitor' && !activeSession" class="sidebar-pane-content pane-placeholder empty-session">
<div class="empty-session-tip">状态监视器需要活动会话</div> <div class="empty-session-content">
</div> <i class="fas fa-plug"></i>
</div> <span>无活动会话</span>
<div class="empty-session-tip">状态监视器需要活动会话</div>
</div>
</div>
<!-- Placeholder for when no pane is active or other conditions fail -->
<div v-else class="sidebar-pane-content">
<!-- Optional: Add a generic placeholder message -->
</div>
</div>
</KeepAlive>
</div> </div>
<!-- Right Sidebar Panel --> <!-- Right Sidebar Panel -->
<div ref="rightSidebarPanelRef" :class="['sidebar-panel', 'right-sidebar-panel', { active: !!activeRightSidebarPane }]" :style="{ width: getSidebarPaneWidth(activeRightSidebarPane) }"> <!-- +++ Use getter for width +++ --> <div ref="rightSidebarPanelRef" :class="['sidebar-panel', 'right-sidebar-panel', { active: !!activeRightSidebarPane }]" :style="{ width: getSidebarPaneWidth(activeRightSidebarPane) }"> <!-- +++ Use getter for width +++ -->
<div ref="rightResizeHandleRef" class="resize-handle right-handle"></div> <!-- +++ Right Handle +++ --> <div ref="rightResizeHandleRef" class="resize-handle right-handle"></div> <!-- +++ Right Handle +++ -->
<button class="close-sidebar-btn" @click="closeSidebars" title="Close Sidebar">&times;</button> <button class="close-sidebar-btn" @click="closeSidebars" title="Close Sidebar">&times;</button>
<component <KeepAlive>
v-if="currentRightSidebarComponent && activeRightSidebarPane && (!['fileManager', 'statusMonitor'].includes(activeRightSidebarPane) || activeSession)" <div :key="`right-sidebar-content-${activeRightSidebarPane ?? 'none'}`" class="sidebar-content-wrapper">
:is="currentRightSidebarComponent" <!-- Component rendering -->
:key="`right-panel-${activeRightSidebarPane ?? 'null'}`" <component
v-bind="sidebarProps(activeRightSidebarPane, 'right')"> v-if="currentRightSidebarComponent && activeRightSidebarPane && (!['fileManager', 'statusMonitor'].includes(activeRightSidebarPane) || activeSession)"
</component> :is="currentRightSidebarComponent"
<!-- Placeholder if FileManager is selected but no active session --> :key="`right-comp-${activeRightSidebarPane}`"
<div v-else-if="activeRightSidebarPane === 'fileManager' && !activeSession" class="sidebar-pane-content pane-placeholder empty-session"> v-bind="sidebarProps(activeRightSidebarPane, 'right')">
<div class="empty-session-content"> </component>
<i class="fas fa-plug"></i> <!-- Placeholder for FileManager -->
<span>无活动会话</span> <div v-else-if="activeRightSidebarPane === 'fileManager' && !activeSession" class="sidebar-pane-content pane-placeholder empty-session">
<div class="empty-session-tip">文件管理器需要活动会话</div> <div class="empty-session-content">
</div> <i class="fas fa-plug"></i>
</div> <span>无活动会话</span>
<!-- Placeholder if StatusMonitor is selected but no active session --> <div class="empty-session-tip">文件管理器需要活动会话</div>
<div v-else-if="activeRightSidebarPane === 'statusMonitor' && !activeSession" class="sidebar-pane-content pane-placeholder empty-session"> </div>
<div class="empty-session-content"> </div>
<i class="fas fa-plug"></i> <!-- Placeholder for StatusMonitor -->
<span>无活动会话</span> <div v-else-if="activeRightSidebarPane === 'statusMonitor' && !activeSession" class="sidebar-pane-content pane-placeholder empty-session">
<div class="empty-session-tip">状态监视器需要活动会话</div> <div class="empty-session-content">
</div> <i class="fas fa-plug"></i>
</div> <span>无活动会话</span>
<div class="empty-session-tip">状态监视器需要活动会话</div>
</div>
</div>
<!-- Placeholder for when no pane is active or other conditions fail -->
<div v-else class="sidebar-pane-content">
<!-- Optional: Add a generic placeholder message -->
</div>
</div>
</KeepAlive>
</div> </div>
<!-- Right Sidebar Buttons (Only render if root) --> <!-- Right Sidebar Buttons (Only render if root) -->