update
This commit is contained in:
@@ -1,15 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onBeforeUnmount, computed, ref } from 'vue'; // 移除不再需要的导入
|
||||
import { onMounted, onBeforeUnmount, computed, ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { storeToRefs } from 'pinia'; // 导入 storeToRefs
|
||||
import { storeToRefs } from 'pinia';
|
||||
import TerminalComponent from '../components/Terminal.vue';
|
||||
import FileManagerComponent from '../components/FileManager.vue';
|
||||
import StatusMonitorComponent from '../components/StatusMonitor.vue';
|
||||
import WorkspaceConnectionListComponent from '../components/WorkspaceConnectionList.vue';
|
||||
import AddConnectionFormComponent from '../components/AddConnectionForm.vue';
|
||||
import TerminalTabBar from '../components/TerminalTabBar.vue';
|
||||
import { useSessionStore } from '../stores/session.store'; // 导入 Session Store
|
||||
import type { ConnectionInfo } from '../stores/connections.store'; // 保持 ConnectionInfo 类型导入
|
||||
import { useSessionStore, type SessionTabInfoWithStatus } from '../stores/session.store';
|
||||
import type { ConnectionInfo } from '../stores/connections.store';
|
||||
// 导入 splitpanes 组件
|
||||
import { Splitpanes, Pane } from 'splitpanes';
|
||||
// 导入管理器实例类型,用于 FileManagerComponent 的 prop 类型断言
|
||||
import type { SftpManagerInstance } from '../stores/session.store';
|
||||
|
||||
@@ -18,8 +20,7 @@ const { t } = useI18n();
|
||||
const sessionStore = useSessionStore();
|
||||
|
||||
// --- 从 Store 获取响应式状态和 Getters ---
|
||||
// 使用 storeToRefs 保持响应性,或者直接在模板中使用 sessionStore.xxx
|
||||
const { sessionTabs, activeSessionId, activeSession } = storeToRefs(sessionStore);
|
||||
const { sessionTabsWithStatus, activeSessionId, activeSession } = storeToRefs(sessionStore);
|
||||
|
||||
// --- UI 状态 (保持本地) ---
|
||||
const showAddEditForm = ref(false);
|
||||
@@ -28,25 +29,13 @@ const connectionToEdit = ref<ConnectionInfo | null>(null);
|
||||
// --- 生命周期钩子 ---
|
||||
onMounted(() => {
|
||||
console.log('[工作区视图] 组件已挂载。');
|
||||
// 可以在这里执行一些初始化操作,如果需要的话
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
console.log('[工作区视图] 组件即将卸载,清理所有会话...');
|
||||
sessionStore.cleanupAllSessions(); // 调用 store action 清理
|
||||
sessionStore.cleanupAllSessions();
|
||||
});
|
||||
|
||||
// --- 监听器 (如果需要监听 store 状态变化) ---
|
||||
// watch(activeSessionId, (newSessionId, oldSessionId) => {
|
||||
// console.log(`[工作区视图] 活动会话 ID 从 ${oldSessionId} 更改为 ${newSessionId}`);
|
||||
// if (newSessionId) {
|
||||
// nextTick(() => {
|
||||
// // TODO: 聚焦到活动会话的终端 (此逻辑可能移至 Store 或保留在此处)
|
||||
// console.log(`[工作区视图] TODO: 聚焦会话 ${newSessionId} 的终端`);
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
|
||||
// --- 本地方法 (仅处理 UI 状态) ---
|
||||
const handleRequestAddConnection = () => {
|
||||
connectionToEdit.value = null;
|
||||
@@ -72,102 +61,118 @@ onBeforeUnmount(() => {
|
||||
console.log('[工作区视图] 连接已更新');
|
||||
handleFormClose();
|
||||
};
|
||||
|
||||
// --- 移除本地会话管理函数 ---
|
||||
// findConnectionInfo, openNewSession, activateSession, closeSession,
|
||||
// handleConnectRequest, handleOpenNewSession 已移至 sessionStore
|
||||
|
||||
</script>
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="workspace-view">
|
||||
<!-- 标签栏: 绑定到 store 的状态和 actions -->
|
||||
<TerminalTabBar
|
||||
:sessions="sessionTabs"
|
||||
:sessions="sessionTabsWithStatus"
|
||||
:active-session-id="activeSessionId"
|
||||
@activate-session="sessionStore.activateSession"
|
||||
@close-session="sessionStore.closeSession"
|
||||
/>
|
||||
|
||||
<div class="status-bar">
|
||||
<!-- 状态栏显示活动会话的信息: 从 store getter 获取 -->
|
||||
{{ t('workspace.statusBar', {
|
||||
status: activeSession?.wsManager.statusMessage.value ?? t('workspace.status.disconnected'),
|
||||
id: activeSession?.connectionId ?? t('workspace.noActiveSession')
|
||||
})
|
||||
}}
|
||||
<!-- 从 activeSession getter 获取连接状态 -->
|
||||
<span :class="`status-${activeSession?.wsManager.connectionStatus.value ?? 'disconnected'}`"></span>
|
||||
</div>
|
||||
<div class="main-content-area">
|
||||
<!-- 左侧边栏: 事件绑定到 store actions -->
|
||||
<div class="left-sidebar">
|
||||
<WorkspaceConnectionListComponent
|
||||
@connect-request="sessionStore.handleConnectRequest"
|
||||
@open-new-session="sessionStore.handleOpenNewSession"
|
||||
@request-add-connection="handleRequestAddConnection"
|
||||
@request-edit-connection="handleRequestEditConnection"
|
||||
/>
|
||||
</div>
|
||||
<!-- 主工作区容器 -->
|
||||
<div class="main-workspace-container">
|
||||
<!-- 会话区域: 循环 store 中的 sessions Map -->
|
||||
<!-- 注意: v-for sessions.values() 可能不是响应式的,因为 sessions 是 shallowRef -->
|
||||
<!-- 改为 v-for session in sessionTabs,然后通过 session.sessionId 获取完整 session -->
|
||||
<div
|
||||
v-for="tabInfo in sessionTabs"
|
||||
:key="tabInfo.sessionId"
|
||||
v-show="tabInfo.sessionId === activeSessionId"
|
||||
class="main-workspace-area-session"
|
||||
>
|
||||
<!-- 获取当前循环的完整 session 对象 -->
|
||||
<template v-if="sessionStore.sessions.get(tabInfo.sessionId)">
|
||||
<div class="left-pane">
|
||||
<div class="terminal-wrapper" :data-session-id="tabInfo.sessionId">
|
||||
<!-- TerminalComponent: 事件绑定到 activeSession 的管理器方法 -->
|
||||
<TerminalComponent
|
||||
:key="tabInfo.sessionId"
|
||||
:session-id="tabInfo.sessionId"
|
||||
:is-active="tabInfo.sessionId === activeSessionId"
|
||||
@ready="sessionStore.sessions.get(tabInfo.sessionId)?.terminalManager.handleTerminalReady"
|
||||
@data="sessionStore.sessions.get(tabInfo.sessionId)?.terminalManager.handleTerminalData"
|
||||
@resize="(dims) => { console.log(`[WorkspaceView ${tabInfo.sessionId}] Received resize event:`, dims); sessionStore.sessions.get(tabInfo.sessionId)?.terminalManager.handleTerminalResize(dims); }"
|
||||
/>
|
||||
</div>
|
||||
<div class="file-manager-wrapper">
|
||||
<!-- FileManagerComponent: Props 绑定到 activeSession 的管理器 -->
|
||||
<!-- 确保传递正确的 wsDeps -->
|
||||
<FileManagerComponent
|
||||
:key="tabInfo.sessionId"
|
||||
:session-id="tabInfo.sessionId"
|
||||
:db-connection-id="sessionStore.sessions.get(tabInfo.sessionId)!.connectionId"
|
||||
:sftp-manager="sessionStore.sessions.get(tabInfo.sessionId)!.sftpManager"
|
||||
:ws-deps="{
|
||||
sendMessage: sessionStore.sessions.get(tabInfo.sessionId)!.wsManager.sendMessage,
|
||||
onMessage: sessionStore.sessions.get(tabInfo.sessionId)!.wsManager.onMessage,
|
||||
isConnected: sessionStore.sessions.get(tabInfo.sessionId)!.wsManager.isConnected,
|
||||
isSftpReady: sessionStore.sessions.get(tabInfo.sessionId)!.wsManager.isSftpReady
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-monitor-wrapper">
|
||||
<!-- StatusMonitorComponent: Props 绑定到 activeSession 的管理器状态 -->
|
||||
<StatusMonitorComponent
|
||||
:key="tabInfo.sessionId"
|
||||
:session-id="tabInfo.sessionId"
|
||||
:server-status="(sessionStore.sessions.get(tabInfo.sessionId)?.statusMonitorManager.serverStatus.value) ?? null"
|
||||
:status-error="(sessionStore.sessions.get(tabInfo.sessionId)?.statusMonitorManager.statusError.value) ?? null"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 占位符 -->
|
||||
<div v-if="!activeSessionId" class="main-workspace-area placeholder">
|
||||
<h2>{{ t('workspace.selectConnectionPrompt') }}</h2>
|
||||
<p>{{ t('workspace.selectConnectionHint') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 最外层:左右分割 (连接列表 | 中间区域 + 右侧区域) -->
|
||||
<splitpanes class="default-theme" :horizontal="false" style="height: 100%">
|
||||
|
||||
<!-- 左侧边栏 Pane -->
|
||||
<pane size="20" min-size="15" class="sidebar-pane">
|
||||
<WorkspaceConnectionListComponent
|
||||
@connect-request="sessionStore.handleConnectRequest"
|
||||
@open-new-session="sessionStore.handleOpenNewSession"
|
||||
@request-add-connection="handleRequestAddConnection"
|
||||
@request-edit-connection="handleRequestEditConnection"
|
||||
/>
|
||||
</pane>
|
||||
|
||||
<!-- 中间区域 Pane (包含终端和文件管理器) -->
|
||||
<pane size="65" min-size="30">
|
||||
<!-- 上下分割 (终端 | 文件管理器) -->
|
||||
<splitpanes :horizontal="true" style="height: 100%">
|
||||
<!-- 终端 Pane -->
|
||||
<pane size="65" min-size="20" class="terminal-pane">
|
||||
<!-- 会话终端区域: 只渲染活动会话的终端 -->
|
||||
<div
|
||||
v-for="tabInfo in sessionTabsWithStatus"
|
||||
:key="tabInfo.sessionId"
|
||||
v-show="tabInfo.sessionId === activeSessionId"
|
||||
class="terminal-session-wrapper"
|
||||
>
|
||||
<!-- 移除 v-if,依赖外层 v-show 控制显隐 -->
|
||||
<!-- :key 绑定到 tabInfo.sessionId 保证每个会话对应唯一组件实例 -->
|
||||
<!-- :is-active 动态绑定 -->
|
||||
<TerminalComponent
|
||||
:key="tabInfo.sessionId"
|
||||
:session-id="tabInfo.sessionId"
|
||||
:is-active="tabInfo.sessionId === activeSessionId"
|
||||
@ready="sessionStore.sessions.get(tabInfo.sessionId)?.terminalManager.handleTerminalReady"
|
||||
@data="sessionStore.sessions.get(tabInfo.sessionId)?.terminalManager.handleTerminalData"
|
||||
@resize="(dims) => { console.log(`[工作区视图 ${tabInfo.sessionId}] 收到 resize 事件:`, dims); sessionStore.sessions.get(tabInfo.sessionId)?.terminalManager.handleTerminalResize(dims); }"
|
||||
/>
|
||||
</div>
|
||||
<!-- 终端占位符 -->
|
||||
<div v-if="!activeSessionId" class="terminal-placeholder">
|
||||
<h2>{{ t('workspace.selectConnectionPrompt') }}</h2>
|
||||
<p>{{ t('workspace.selectConnectionHint') }}</p>
|
||||
</div>
|
||||
<!-- 终端占位符 -->
|
||||
<div v-if="!activeSessionId" class="terminal-placeholder">
|
||||
<h2>{{ t('workspace.selectConnectionPrompt') }}</h2>
|
||||
<p>{{ t('workspace.selectConnectionHint') }}</p>
|
||||
</div>
|
||||
</pane>
|
||||
<!-- 文件管理器 Pane -->
|
||||
<pane size="35" min-size="15" class="file-manager-pane">
|
||||
<!-- 为每个会话渲染文件管理器实例,用 v-show 控制 -->
|
||||
<div
|
||||
v-for="tabInfo in sessionTabsWithStatus"
|
||||
:key="tabInfo.sessionId + '-fm-wrapper'"
|
||||
v-show="tabInfo.sessionId === activeSessionId"
|
||||
class="file-manager-wrapper"
|
||||
>
|
||||
<FileManagerComponent
|
||||
v-if="sessionStore.sessions.get(tabInfo.sessionId)"
|
||||
:key="tabInfo.sessionId + '-fm'"
|
||||
:session-id="tabInfo.sessionId"
|
||||
:db-connection-id="sessionStore.sessions.get(tabInfo.sessionId)!.connectionId"
|
||||
:sftp-manager="sessionStore.sessions.get(tabInfo.sessionId)!.sftpManager"
|
||||
:ws-deps="{
|
||||
sendMessage: sessionStore.sessions.get(tabInfo.sessionId)!.wsManager.sendMessage,
|
||||
onMessage: sessionStore.sessions.get(tabInfo.sessionId)!.wsManager.onMessage,
|
||||
isConnected: sessionStore.sessions.get(tabInfo.sessionId)!.wsManager.isConnected,
|
||||
isSftpReady: sessionStore.sessions.get(tabInfo.sessionId)!.wsManager.isSftpReady
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
<!-- 文件管理器占位符 -->
|
||||
<div v-if="!activeSessionId" class="pane-placeholder">{{ t('fileManager.noActiveSession') }}</div>
|
||||
</pane>
|
||||
</splitpanes>
|
||||
</pane>
|
||||
|
||||
<!-- 右侧边栏 Pane (状态监视器) - 添加 status-monitor-pane 类 -->
|
||||
<pane size="15" min-size="10" class="sidebar-pane status-monitor-pane">
|
||||
<!-- 为每个会话渲染状态监视器实例,用 v-show 控制 -->
|
||||
<div
|
||||
v-for="tabInfo in sessionTabsWithStatus"
|
||||
:key="tabInfo.sessionId + '-sm-wrapper'"
|
||||
v-show="tabInfo.sessionId === activeSessionId"
|
||||
class="status-monitor-wrapper"
|
||||
>
|
||||
<StatusMonitorComponent
|
||||
v-if="sessionStore.sessions.get(tabInfo.sessionId)"
|
||||
:key="tabInfo.sessionId + '-sm'"
|
||||
:session-id="tabInfo.sessionId"
|
||||
:server-status="sessionStore.sessions.get(tabInfo.sessionId)!.statusMonitorManager.serverStatus.value"
|
||||
:status-error="sessionStore.sessions.get(tabInfo.sessionId)!.statusMonitorManager.statusError.value"
|
||||
/>
|
||||
</div>
|
||||
<!-- 状态监视器占位符 -->
|
||||
<div v-if="!activeSessionId" class="pane-placeholder">{{ t('statusMonitor.noActiveSession') }}</div>
|
||||
</pane>
|
||||
|
||||
</splitpanes>
|
||||
</div>
|
||||
|
||||
<!-- 添加/编辑连接表单模态框 (保持不变) -->
|
||||
@@ -185,23 +190,10 @@ onBeforeUnmount(() => {
|
||||
.workspace-view {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100vh - 60px - 30px - 60px - 2rem);
|
||||
height: calc(100vh - 60px - 30px - 2rem); /* 调整以适应您的 header/footer/padding */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: #eee;
|
||||
border-bottom: 1px solid #ccc;
|
||||
font-size: 0.9rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.status-connecting { color: orange; }
|
||||
.status-connected { color: green; }
|
||||
.status-disconnected { color: grey; }
|
||||
.status-error { color: red; }
|
||||
|
||||
.main-content-area {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
@@ -209,65 +201,82 @@ onBeforeUnmount(() => {
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.left-sidebar {
|
||||
width: 250px;
|
||||
min-width: 200px;
|
||||
height: 100%;
|
||||
border-right: 2px solid #ccc;
|
||||
overflow-y: auto;
|
||||
/* 为 Pane 添加一些基本样式 */
|
||||
.sidebar-pane, /* 用于左右侧边栏 */
|
||||
.terminal-pane,
|
||||
.file-manager-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden; /* Pane 内部内容溢出时隐藏 */
|
||||
background-color: #f8f9fa; /* 默认背景色 */
|
||||
}
|
||||
.terminal-pane {
|
||||
background-color: #1e1e1e; /* 终端背景 */
|
||||
position: relative; /* 保持相对定位用于占位符 */
|
||||
}
|
||||
.file-manager-pane {
|
||||
border-top: 1px solid #ccc; /* 终端和文件管理器之间的分隔线 */
|
||||
}
|
||||
|
||||
.main-workspace-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.main-workspace-area-session {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.left-pane {
|
||||
flex: 1;
|
||||
/* 终端会话包装器 */
|
||||
.terminal-session-wrapper {
|
||||
flex-grow: 1; /* 填充 terminal-pane */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 300px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.terminal-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #1e1e1e;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 文件管理器包装器 (内部组件应填充) */
|
||||
.file-manager-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-top: 2px solid #ccc;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.status-monitor-wrapper {
|
||||
width: 250px;
|
||||
min-width: 200px;
|
||||
border-left: 2px solid #ccc;
|
||||
overflow: hidden;
|
||||
/* 文件管理器包装器 (内部组件应填充) */
|
||||
.file-manager-wrapper {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.main-workspace-area.placeholder {
|
||||
/* 状态监视器包装器 (内部组件应填充) */
|
||||
.status-monitor-wrapper {
|
||||
flex: 1;
|
||||
display: flex; /* 使内部 StatusMonitorComponent 可以填充 */
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 新增:状态监视器 Pane 样式,使其内容居中 */
|
||||
.status-monitor-pane {
|
||||
/* 尝试使用 flex 居中,如果 StatusMonitorComponent 本身是块级元素 */
|
||||
/* display: flex; */
|
||||
/* justify-content: center; */
|
||||
/* align-items: center; */
|
||||
|
||||
/* 或者如果内容主要是文本,可以尝试 text-align */
|
||||
text-align: center; /* 尝试文本居中 */
|
||||
padding: 1rem; /* 添加一些内边距 */
|
||||
}
|
||||
.status-monitor-pane > .status-monitor-wrapper {
|
||||
/* 如果需要包装器也居中(如果它不是 flex: 1 的话) */
|
||||
/* margin: auto; */
|
||||
}
|
||||
|
||||
|
||||
/* 终端占位符 */
|
||||
.terminal-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
/* 终端占位符 */
|
||||
.terminal-placeholder {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
@@ -280,14 +289,68 @@ onBeforeUnmount(() => {
|
||||
text-align: center;
|
||||
color: #6c757d;
|
||||
padding: 2rem;
|
||||
background-color: #f8f9fa;
|
||||
background-color: #f8f9fa; /* 与 pane 背景一致 */
|
||||
}
|
||||
.main-workspace-area.placeholder h2 {
|
||||
.terminal-placeholder h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 300;
|
||||
color: #495057;
|
||||
}
|
||||
.main-workspace-area.placeholder p {
|
||||
.terminal-placeholder p {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* 面板占位符样式 */
|
||||
.pane-placeholder {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
color: #adb5bd;
|
||||
background-color: #f8f9fa;
|
||||
font-size: 0.9em;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Splitpanes 默认主题样式调整 */
|
||||
.splitpanes.default-theme .splitpanes__splitter {
|
||||
background-color: #ccc;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
border-left: 1px solid #eee; /* 可选:添加细微边框 */
|
||||
border-right: 1px solid #eee;
|
||||
}
|
||||
.splitpanes--vertical > .splitpanes__splitter {
|
||||
width: 7px; /* 垂直分割线宽度 */
|
||||
cursor: col-resize;
|
||||
}
|
||||
.splitpanes--horizontal > .splitpanes__splitter {
|
||||
height: 7px; /* 水平分割线高度 */
|
||||
cursor: row-resize;
|
||||
}
|
||||
.splitpanes.default-theme .splitpanes__splitter:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
transition: opacity 0.4s;
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
opacity: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
.splitpanes.default-theme .splitpanes__splitter:hover:before {
|
||||
opacity: 1;
|
||||
}
|
||||
.splitpanes.default-theme.splitpanes--vertical > .splitpanes__splitter:before {
|
||||
left: 2px; /* 调整指示器位置 */
|
||||
right: 2px;
|
||||
height: 100%;
|
||||
}
|
||||
.splitpanes.default-theme.splitpanes--horizontal > .splitpanes__splitter:before {
|
||||
top: 2px; /* 调整指示器位置 */
|
||||
bottom: 2px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user