Fix: 修复终端标签切换时重连问题

将终端工作区提升为主布局常驻层,离开终端路由时只隐藏不卸载组件。
新增活动终端显隐状态跟踪,页面恢复时自动重新适配尺寸和聚焦。

改动范围:
- frontend/src/layouts/MainLayout.vue
- frontend/src/views/TerminalWorkspaceView.vue
- frontend/src/components/TerminalWidget.vue
This commit is contained in:
liumangmang
2026-03-20 15:36:47 +08:00
parent c1efd72d6d
commit 7b7399912b
3 changed files with 82 additions and 41 deletions

View File

@@ -1,14 +1,15 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { Terminal } from 'xterm'
import { AttachAddon } from '@xterm/addon-attach'
import { FitAddon } from '@xterm/addon-fit'
import { useAuthStore } from '../stores/auth'
import 'xterm/css/xterm.css'
const props = defineProps<{
connectionId: number
}>()
<script setup lang="ts">
import { nextTick, ref, onMounted, onUnmounted, watch } from 'vue'
import { Terminal } from 'xterm'
import { AttachAddon } from '@xterm/addon-attach'
import { FitAddon } from '@xterm/addon-fit'
import { useAuthStore } from '../stores/auth'
import 'xterm/css/xterm.css'
const props = defineProps<{
connectionId: number
active?: boolean
}>()
const containerRef = ref<HTMLElement | null>(null)
const status = ref<'connecting' | 'connected' | 'error'>('connecting')
@@ -16,8 +17,23 @@ const errorMessage = ref('')
let term: Terminal | null = null
let fitAddon: FitAddon | null = null
let ws: WebSocket | null = null
let resizeObserver: ResizeObserver | null = null
let ws: WebSocket | null = null
let resizeObserver: ResizeObserver | null = null
function fitTerminal() {
fitAddon?.fit()
}
function focusTerminal() {
term?.focus()
}
function refreshTerminalLayout() {
nextTick(() => {
fitTerminal()
focusTerminal()
})
}
function getWsUrl(): string {
const authStore = useAuthStore()
@@ -71,8 +87,8 @@ onMounted(() => {
fitAddon = new FitAddon()
term.loadAddon(fitAddon)
term.open(containerRef.value)
fitAddon.fit()
term.open(containerRef.value)
fitTerminal()
try {
ws = new WebSocket(getWsUrl())
@@ -83,11 +99,11 @@ onMounted(() => {
}
ws.onopen = () => {
status.value = 'connected'
const attachAddon = new AttachAddon(ws!)
term!.loadAddon(attachAddon)
fitAddon?.fit()
}
status.value = 'connected'
const attachAddon = new AttachAddon(ws!)
term!.loadAddon(attachAddon)
refreshTerminalLayout()
}
ws.onerror = () => {
if (status.value === 'connecting') {
@@ -102,11 +118,21 @@ onMounted(() => {
}
}
resizeObserver = new ResizeObserver(() => {
fitAddon?.fit()
})
resizeObserver.observe(containerRef.value)
})
resizeObserver = new ResizeObserver(() => {
fitTerminal()
})
resizeObserver.observe(containerRef.value)
if (props.active !== false) {
refreshTerminalLayout()
}
})
watch(() => props.active, (active) => {
if (active) {
refreshTerminalLayout()
}
})
onUnmounted(() => {
cleanup()

View File

@@ -1,19 +1,22 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { RouterLink, useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '../stores/auth'
import { useConnectionsStore } from '../stores/connections'
import { useTerminalTabsStore } from '../stores/terminalTabs'
import { ArrowLeftRight, Server, LogOut, Menu, X, Terminal } from 'lucide-vue-next'
<script setup lang="ts">
import { ref, computed } from 'vue'
import { RouterLink, useRoute, useRouter } from 'vue-router'
import { useAuthStore } from '../stores/auth'
import { useConnectionsStore } from '../stores/connections'
import { useTerminalTabsStore } from '../stores/terminalTabs'
import TerminalWorkspaceView from '../views/TerminalWorkspaceView.vue'
import { ArrowLeftRight, Server, LogOut, Menu, X, Terminal } from 'lucide-vue-next'
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
const connectionsStore = useConnectionsStore()
const tabsStore = useTerminalTabsStore()
const sidebarOpen = ref(false)
const terminalTabs = computed(() => tabsStore.tabs)
const sidebarOpen = ref(false)
const terminalTabs = computed(() => tabsStore.tabs)
const showTerminalWorkspace = computed(() => route.path === '/terminal')
const keepTerminalWorkspaceMounted = computed(() => showTerminalWorkspace.value || terminalTabs.value.length > 0)
connectionsStore.fetchConnections().catch(() => {})
@@ -119,8 +122,13 @@ function handleTabClose(tabId: string, event: Event) {
aria-hidden="true"
@click="sidebarOpen = false"
/>
<main class="flex-1 overflow-auto min-w-0">
<RouterView />
</main>
</div>
</template>
<main class="flex-1 overflow-auto min-w-0">
<div v-if="keepTerminalWorkspaceMounted" v-show="showTerminalWorkspace" class="h-full">
<TerminalWorkspaceView :visible="showTerminalWorkspace" />
</div>
<RouterView v-slot="{ Component }">
<component :is="Component" v-if="!showTerminalWorkspace" />
</RouterView>
</main>
</div>
</template>

View File

@@ -4,6 +4,10 @@ import { useTerminalTabsStore } from '../stores/terminalTabs'
import { useConnectionsStore } from '../stores/connections'
import TerminalWidget from '../components/TerminalWidget.vue'
const props = defineProps<{
visible?: boolean
}>()
const tabsStore = useTerminalTabsStore()
const connectionsStore = useConnectionsStore()
@@ -34,7 +38,10 @@ onMounted(() => {
v-show="tab.active"
class="h-full"
>
<TerminalWidget :connection-id="tab.connectionId" />
<TerminalWidget
:connection-id="tab.connectionId"
:active="tab.active && props.visible !== false"
/>
</div>
</div>
</div>