Fix: 修复终端标签切换时重连问题
将终端工作区提升为主布局常驻层,离开终端路由时只隐藏不卸载组件。 新增活动终端显隐状态跟踪,页面恢复时自动重新适配尺寸和聚焦。 改动范围: - frontend/src/layouts/MainLayout.vue - frontend/src/views/TerminalWorkspaceView.vue - frontend/src/components/TerminalWidget.vue
This commit is contained in:
@@ -1,14 +1,15 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { nextTick, ref, onMounted, onUnmounted, watch } from 'vue'
|
||||||
import { Terminal } from 'xterm'
|
import { Terminal } from 'xterm'
|
||||||
import { AttachAddon } from '@xterm/addon-attach'
|
import { AttachAddon } from '@xterm/addon-attach'
|
||||||
import { FitAddon } from '@xterm/addon-fit'
|
import { FitAddon } from '@xterm/addon-fit'
|
||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from '../stores/auth'
|
||||||
import 'xterm/css/xterm.css'
|
import 'xterm/css/xterm.css'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
connectionId: number
|
connectionId: number
|
||||||
}>()
|
active?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
const containerRef = ref<HTMLElement | null>(null)
|
const containerRef = ref<HTMLElement | null>(null)
|
||||||
const status = ref<'connecting' | 'connected' | 'error'>('connecting')
|
const status = ref<'connecting' | 'connected' | 'error'>('connecting')
|
||||||
@@ -16,8 +17,23 @@ const errorMessage = ref('')
|
|||||||
|
|
||||||
let term: Terminal | null = null
|
let term: Terminal | null = null
|
||||||
let fitAddon: FitAddon | null = null
|
let fitAddon: FitAddon | null = null
|
||||||
let ws: WebSocket | null = null
|
let ws: WebSocket | null = null
|
||||||
let resizeObserver: ResizeObserver | null = null
|
let resizeObserver: ResizeObserver | null = null
|
||||||
|
|
||||||
|
function fitTerminal() {
|
||||||
|
fitAddon?.fit()
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusTerminal() {
|
||||||
|
term?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshTerminalLayout() {
|
||||||
|
nextTick(() => {
|
||||||
|
fitTerminal()
|
||||||
|
focusTerminal()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
function getWsUrl(): string {
|
function getWsUrl(): string {
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
@@ -71,8 +87,8 @@ onMounted(() => {
|
|||||||
|
|
||||||
fitAddon = new FitAddon()
|
fitAddon = new FitAddon()
|
||||||
term.loadAddon(fitAddon)
|
term.loadAddon(fitAddon)
|
||||||
term.open(containerRef.value)
|
term.open(containerRef.value)
|
||||||
fitAddon.fit()
|
fitTerminal()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ws = new WebSocket(getWsUrl())
|
ws = new WebSocket(getWsUrl())
|
||||||
@@ -83,11 +99,11 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ws.onopen = () => {
|
ws.onopen = () => {
|
||||||
status.value = 'connected'
|
status.value = 'connected'
|
||||||
const attachAddon = new AttachAddon(ws!)
|
const attachAddon = new AttachAddon(ws!)
|
||||||
term!.loadAddon(attachAddon)
|
term!.loadAddon(attachAddon)
|
||||||
fitAddon?.fit()
|
refreshTerminalLayout()
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.onerror = () => {
|
ws.onerror = () => {
|
||||||
if (status.value === 'connecting') {
|
if (status.value === 'connecting') {
|
||||||
@@ -102,11 +118,21 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resizeObserver = new ResizeObserver(() => {
|
resizeObserver = new ResizeObserver(() => {
|
||||||
fitAddon?.fit()
|
fitTerminal()
|
||||||
})
|
})
|
||||||
resizeObserver.observe(containerRef.value)
|
resizeObserver.observe(containerRef.value)
|
||||||
})
|
|
||||||
|
if (props.active !== false) {
|
||||||
|
refreshTerminalLayout()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => props.active, (active) => {
|
||||||
|
if (active) {
|
||||||
|
refreshTerminalLayout()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
cleanup()
|
cleanup()
|
||||||
|
|||||||
@@ -1,19 +1,22 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import { RouterLink, useRoute, useRouter } from 'vue-router'
|
import { RouterLink, useRoute, useRouter } from 'vue-router'
|
||||||
import { useAuthStore } from '../stores/auth'
|
import { useAuthStore } from '../stores/auth'
|
||||||
import { useConnectionsStore } from '../stores/connections'
|
import { useConnectionsStore } from '../stores/connections'
|
||||||
import { useTerminalTabsStore } from '../stores/terminalTabs'
|
import { useTerminalTabsStore } from '../stores/terminalTabs'
|
||||||
import { ArrowLeftRight, Server, LogOut, Menu, X, Terminal } from 'lucide-vue-next'
|
import TerminalWorkspaceView from '../views/TerminalWorkspaceView.vue'
|
||||||
|
import { ArrowLeftRight, Server, LogOut, Menu, X, Terminal } from 'lucide-vue-next'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const connectionsStore = useConnectionsStore()
|
const connectionsStore = useConnectionsStore()
|
||||||
const tabsStore = useTerminalTabsStore()
|
const tabsStore = useTerminalTabsStore()
|
||||||
const sidebarOpen = ref(false)
|
const sidebarOpen = ref(false)
|
||||||
|
|
||||||
const terminalTabs = computed(() => tabsStore.tabs)
|
const terminalTabs = computed(() => tabsStore.tabs)
|
||||||
|
const showTerminalWorkspace = computed(() => route.path === '/terminal')
|
||||||
|
const keepTerminalWorkspaceMounted = computed(() => showTerminalWorkspace.value || terminalTabs.value.length > 0)
|
||||||
|
|
||||||
connectionsStore.fetchConnections().catch(() => {})
|
connectionsStore.fetchConnections().catch(() => {})
|
||||||
|
|
||||||
@@ -119,8 +122,13 @@ function handleTabClose(tabId: string, event: Event) {
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
@click="sidebarOpen = false"
|
@click="sidebarOpen = false"
|
||||||
/>
|
/>
|
||||||
<main class="flex-1 overflow-auto min-w-0">
|
<main class="flex-1 overflow-auto min-w-0">
|
||||||
<RouterView />
|
<div v-if="keepTerminalWorkspaceMounted" v-show="showTerminalWorkspace" class="h-full">
|
||||||
</main>
|
<TerminalWorkspaceView :visible="showTerminalWorkspace" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
<RouterView v-slot="{ Component }">
|
||||||
|
<component :is="Component" v-if="!showTerminalWorkspace" />
|
||||||
|
</RouterView>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { useTerminalTabsStore } from '../stores/terminalTabs'
|
|||||||
import { useConnectionsStore } from '../stores/connections'
|
import { useConnectionsStore } from '../stores/connections'
|
||||||
import TerminalWidget from '../components/TerminalWidget.vue'
|
import TerminalWidget from '../components/TerminalWidget.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
visible?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
const tabsStore = useTerminalTabsStore()
|
const tabsStore = useTerminalTabsStore()
|
||||||
const connectionsStore = useConnectionsStore()
|
const connectionsStore = useConnectionsStore()
|
||||||
|
|
||||||
@@ -34,7 +38,10 @@ onMounted(() => {
|
|||||||
v-show="tab.active"
|
v-show="tab.active"
|
||||||
class="h-full"
|
class="h-full"
|
||||||
>
|
>
|
||||||
<TerminalWidget :connection-id="tab.connectionId" />
|
<TerminalWidget
|
||||||
|
:connection-id="tab.connectionId"
|
||||||
|
:active="tab.active && props.visible !== false"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user