From 51b479a8f9d8eb7927834c7b636d8d1185b2bd13 Mon Sep 17 00:00:00 2001 From: liumangmang Date: Wed, 18 Mar 2026 23:05:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E5=86=85=E5=A4=9A=20SSH=20=E7=BB=88=E7=AB=AF=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 terminalTabs store 管理标签页状态 - 新增 TerminalWorkspaceView 终端工作区视图 - 修改 ConnectionsView 终端按钮改用标签页模式 - 修改 TerminalView 作为兼容入口自动跳转到工作区 - 同一连接默认只保留一个标签页 - 切换标签时保持各自 SSH 会话不断开 --- frontend/src/router/index.ts | 17 ++-- frontend/src/stores/terminalTabs.ts | 74 +++++++++++++++ frontend/src/views/ConnectionsView.vue | 35 +++---- frontend/src/views/TerminalView.vue | 51 +++++----- frontend/src/views/TerminalWorkspaceView.vue | 98 ++++++++++++++++++++ package-lock.json | 6 ++ 6 files changed, 229 insertions(+), 52 deletions(-) create mode 100644 frontend/src/stores/terminalTabs.ts create mode 100644 frontend/src/views/TerminalWorkspaceView.vue create mode 100644 package-lock.json diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index e95ab11..5bc102e 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -24,16 +24,21 @@ const routes: RouteRecordRaw[] = [ name: 'Transfers', component: () => import('../views/TransfersView.vue'), }, - { - path: 'connections', - name: 'Connections', - component: () => import('../views/ConnectionsView.vue'), - }, + { + path: 'connections', + name: 'Connections', + component: () => import('../views/ConnectionsView.vue'), + }, + { + path: 'terminal', + name: 'TerminalWorkspace', + component: () => import('../views/TerminalWorkspaceView.vue'), + }, { path: 'terminal/:id', name: 'Terminal', component: () => import('../views/TerminalView.vue'), - }, + }, { path: 'sftp/:id', name: 'Sftp', diff --git a/frontend/src/stores/terminalTabs.ts b/frontend/src/stores/terminalTabs.ts new file mode 100644 index 0000000..bca5153 --- /dev/null +++ b/frontend/src/stores/terminalTabs.ts @@ -0,0 +1,74 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import type { Connection } from '../api/connections' + +export interface TerminalTab { + id: string + connectionId: number + title: string + active: boolean +} + +export const useTerminalTabsStore = defineStore('terminalTabs', () => { + const tabs = ref([]) + const activeTabId = ref(null) + + const activeTab = computed(() => tabs.value.find(t => t.id === activeTabId.value) || null) + + function generateTabId() { + return `tab-${Date.now()}-${Math.random().toString(16).slice(2)}` + } + + function openOrFocus(connection: Connection) { + // 检查是否已存在该连接的标签页 + const existing = tabs.value.find(t => t.connectionId === connection.id) + if (existing) { + activate(existing.id) + return existing.id + } + + // 创建新标签页 + const newTab: TerminalTab = { + id: generateTabId(), + connectionId: connection.id, + title: connection.name, + active: true, + } + + tabs.value.push(newTab) + activate(newTab.id) + return newTab.id + } + + function activate(tabId: string) { + tabs.value.forEach(t => { + t.active = t.id === tabId + }) + activeTabId.value = tabId + } + + function close(tabId: string) { + const index = tabs.value.findIndex(t => t.id === tabId) + if (index === -1) return + + const wasActive = tabs.value[index]!.active + tabs.value.splice(index, 1) + + // 如果关闭的是活动标签,激活相邻标签 + if (wasActive && tabs.value.length > 0) { + const newIndex = Math.min(index, tabs.value.length - 1) + activate(tabs.value[newIndex]!.id) + } else if (tabs.value.length === 0) { + activeTabId.value = null + } + } + + return { + tabs, + activeTabId, + activeTab, + openOrFocus, + activate, + close, + } +}) diff --git a/frontend/src/views/ConnectionsView.vue b/frontend/src/views/ConnectionsView.vue index 2915166..b272727 100644 --- a/frontend/src/views/ConnectionsView.vue +++ b/frontend/src/views/ConnectionsView.vue @@ -2,21 +2,23 @@ import { ref, onMounted } from 'vue' import { useRouter } from 'vue-router' import { useConnectionsStore } from '../stores/connections' -import type { Connection, ConnectionCreateRequest } from '../api/connections' +import { useTerminalTabsStore } from '../stores/terminalTabs' +import type { Connection, ConnectionCreateRequest } from '../api/connections' import ConnectionForm from '../components/ConnectionForm.vue' -import { - Server, - Plus, - Terminal, - FolderOpen, - Pencil, - Trash2, - Key, - Lock, -} from 'lucide-vue-next' +import { + Server, + Plus, + Terminal, + FolderOpen, + Pencil, + Trash2, + Key, + Lock, +} from 'lucide-vue-next' const router = useRouter() const store = useConnectionsStore() +const tabsStore = useTerminalTabsStore() const showForm = ref(false) const editingConn = ref(null) @@ -53,12 +55,13 @@ async function handleDelete(conn: Connection) { } function openTerminal(conn: Connection) { - router.push(`/terminal/${conn.id}`) -} - -function openSftp(conn: Connection) { - router.push(`/sftp/${conn.id}`) + tabsStore.openOrFocus(conn) + router.push('/terminal') } + +function openSftp(conn: Connection) { + router.push(`/sftp/${conn.id}`) +}