feat(workspace): 增强连接管理与终端状态展示

- 为连接管理页补充多级标签树、列头排序和行级更多菜单
- 支持同一 SSH 连接打开多个终端并显示终端序号
- 补充状态监控的内存与磁盘详情字段

---
 feat(workspace): enhance connection management and terminal status visibility

- add multi-level tag tree, sortable columns, and row-level more menu
- support multiple terminals per SSH connection with terminal indices
- extend status monitor with memory and disk detail fields
This commit is contained in:
yinjianm
2026-03-25 22:25:37 +08:00
parent 6553739c08
commit d730d06c5e
16 changed files with 798 additions and 72 deletions
@@ -64,6 +64,39 @@ const showConnectionListPopup = ref(false); // 连接列表弹出状态
const draggableSessions = ref<SessionTabInfoWithStatus[]>([]); // + Local state for draggable
const showTransferProgressModal = ref(false); // 控制传输进度模态框的显示状态
const activeSessionState = computed(() => {
if (!props.activeSessionId) {
return null;
}
return sessionStore.sessions.get(props.activeSessionId) ?? null;
});
const activeConnectionInfo = computed(() => {
const activeSession = activeSessionState.value;
if (!activeSession) {
return null;
}
return connectionsStore.connections.find((connection) => connection.id === Number(activeSession.connectionId)) ?? null;
});
const canAddTerminalToActiveConnection = computed(() => activeConnectionInfo.value?.type === 'SSH');
const openNewTerminalForActiveConnection = () => {
const activeConnection = activeConnectionInfo.value;
if (!activeConnection || activeConnection.type !== 'SSH') {
showConnectionListPopup.value = true;
return;
}
sessionStore.handleOpenNewSession(activeConnection.id);
};
const openConnectionPicker = () => {
showConnectionListPopup.value = true;
};
// + Watch prop changes to update local state
watch(() => props.sessions, (newSessions) => {
// Create a shallow copy to avoid modifying the prop directly
@@ -174,6 +207,22 @@ const handleContextMenuAction = (payload: { action: string; targetId: string | n
// 注意:关闭左侧通常不包括当前标签本身
emitWorkspaceEvent('session:closeToLeft', { targetSessionId: targetId });
break;
case 'new-terminal': {
const targetSessionState = sessionStore.sessions.get(targetId);
if (!targetSessionState) {
console.warn(`[TabBar] 'new-terminal' action failed: session ${targetId} not found.`);
break;
}
const targetConnectionInfo = connectionsStore.connections.find(c => c.id === Number(targetSessionState.connectionId));
if (!targetConnectionInfo || targetConnectionInfo.type !== 'SSH') {
console.warn(`[TabBar] 'new-terminal' action ignored for non-SSH connection. targetId=${targetId}`);
break;
}
sessionStore.handleOpenNewSession(targetConnectionInfo.id);
break;
}
case 'mark-for-suspend': // +++ 修改 action 名称 +++
if (typeof targetId === 'string') {
console.log(`[TabBar] Context menu action 'mark-for-suspend' requested for session ID: ${targetId}`);
@@ -213,6 +262,7 @@ const contextMenuItems = computed(() => {
// 添加标记/取消标记挂起会话菜单项(如果适用)
if (connectionInfo && connectionInfo.type === 'SSH') {
items.push({ label: 'terminalTabBar.newTerminalTooltip', action: 'new-terminal' });
const isActiveSession = targetSessionState.wsManager.isConnected.value;
if (isActiveSession) { // 只对活动的SSH会话显示相关操作
if (targetSessionState.isMarkedForSuspend) {
@@ -446,6 +496,12 @@ onBeforeUnmount(() => {
session.status === 'connecting' ? 'bg-yellow-500 animate-pulse' :
session.status === 'disconnected' ? 'bg-red-500' : 'bg-gray-400']"></span>
<span class="truncate text-sm" style="transform: translateY(-1px);">{{ session.connectionName }}</span>
<span
class="ml-2 inline-flex flex-shrink-0 items-center rounded-full border border-border px-1.5 py-0.5 text-[11px] leading-none text-text-secondary"
:title="t('terminalTabBar.terminalBadge', { index: session.terminalIndex })"
>
{{ session.terminalIndex }}
</span>
<button class="ml-2 p-0.5 rounded-full text-text-secondary hover:bg-border hover:text-foreground opacity-0 group-hover:opacity-100 transition-opacity duration-150"
:class="{'text-foreground hover:bg-header': session.sessionId === activeSessionId}"
@click="closeSession($event, session.sessionId)" :title="$t('tabs.closeTabTooltip')">
@@ -456,11 +512,18 @@ onBeforeUnmount(() => {
</li>
</template>
</draggable>
<!-- Add Tab Button -->
<button class="flex items-center justify-center px-3 h-full border-border text-text-secondary hover:bg-border hover:text-foreground transition-colors duration-150 flex-shrink-0"
@click="togglePopup" :title="$t('tabs.newTabTooltip')">
<!-- Add Terminal Button -->
<button
class="flex items-center justify-center px-3 h-full border-border text-text-secondary hover:bg-border hover:text-foreground transition-colors duration-150 flex-shrink-0"
@click="openNewTerminalForActiveConnection"
:title="canAddTerminalToActiveConnection ? t('terminalTabBar.newTerminalTooltip') : t('tabs.newTabTooltip')">
<i class="fas fa-plus text-sm"></i>
</button>
<!-- Open Connection Picker Button -->
<button class="flex items-center justify-center px-3 h-full border-border text-text-secondary hover:bg-border hover:text-foreground transition-colors duration-150 flex-shrink-0"
@click="openConnectionPicker" :title="t('terminalTabBar.openConnectionPickerTooltip')">
<i class="fas fa-server text-sm"></i>
</button>
</div>
<!-- Action Buttons -->
<div class="flex items-center ml-auto h-full flex-shrink-0">