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}`) +}