Initial commit: SSH Manager (backend + frontend)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
141
frontend/src/components/TerminalWidget.vue
Normal file
141
frontend/src/components/TerminalWidget.vue
Normal file
@@ -0,0 +1,141 @@
|
||||
<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
|
||||
}>()
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const status = ref<'connecting' | 'connected' | 'error'>('connecting')
|
||||
const errorMessage = ref('')
|
||||
|
||||
let term: Terminal | null = null
|
||||
let fitAddon: FitAddon | null = null
|
||||
let ws: WebSocket | null = null
|
||||
let resizeObserver: ResizeObserver | null = null
|
||||
|
||||
function getWsUrl(): string {
|
||||
const authStore = useAuthStore()
|
||||
const token = authStore.token
|
||||
if (!token) {
|
||||
throw new Error('Not authenticated')
|
||||
}
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const host = window.location.host
|
||||
return `${protocol}//${host}/ws/terminal?connectionId=${props.connectionId}&token=${encodeURIComponent(token)}`
|
||||
}
|
||||
|
||||
function cleanup() {
|
||||
if (resizeObserver && containerRef.value) {
|
||||
resizeObserver.unobserve(containerRef.value)
|
||||
}
|
||||
resizeObserver = null
|
||||
if (ws) {
|
||||
ws.close()
|
||||
ws = null
|
||||
}
|
||||
if (term) {
|
||||
term.dispose()
|
||||
term = null
|
||||
}
|
||||
fitAddon = null
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const authStore = useAuthStore()
|
||||
if (!authStore.token) {
|
||||
status.value = 'error'
|
||||
errorMessage.value = '未登录'
|
||||
return
|
||||
}
|
||||
|
||||
if (!containerRef.value) return
|
||||
|
||||
term = new Terminal({
|
||||
cursorBlink: true,
|
||||
theme: {
|
||||
background: '#0f172a',
|
||||
foreground: '#f1f5f9',
|
||||
cursor: '#22d3ee',
|
||||
cursorAccent: '#0f172a',
|
||||
selectionBackground: 'rgba(34, 211, 238, 0.3)',
|
||||
},
|
||||
fontSize: 14,
|
||||
fontFamily: 'Menlo, Monaco, "Courier New", monospace',
|
||||
})
|
||||
|
||||
fitAddon = new FitAddon()
|
||||
term.loadAddon(fitAddon)
|
||||
term.open(containerRef.value)
|
||||
fitAddon.fit()
|
||||
|
||||
try {
|
||||
ws = new WebSocket(getWsUrl())
|
||||
} catch (e) {
|
||||
status.value = 'error'
|
||||
errorMessage.value = e instanceof Error ? e.message : '连接失败'
|
||||
return
|
||||
}
|
||||
|
||||
ws.onopen = () => {
|
||||
status.value = 'connected'
|
||||
const attachAddon = new AttachAddon(ws!)
|
||||
term!.loadAddon(attachAddon)
|
||||
fitAddon?.fit()
|
||||
}
|
||||
|
||||
ws.onerror = () => {
|
||||
if (status.value === 'connecting') {
|
||||
status.value = 'error'
|
||||
errorMessage.value = 'WebSocket 连接失败'
|
||||
}
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
if (status.value === 'connected') {
|
||||
term?.writeln('\r\n[连接已关闭]')
|
||||
}
|
||||
}
|
||||
|
||||
resizeObserver = new ResizeObserver(() => {
|
||||
fitAddon?.fit()
|
||||
})
|
||||
resizeObserver.observe(containerRef.value)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
cleanup()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-full flex flex-col bg-slate-950 rounded-lg border border-slate-700 overflow-hidden">
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="flex-1 min-h-0 p-4 xterm-container"
|
||||
:class="{ 'flex items-center justify-center': status !== 'connected' }"
|
||||
style="min-height: 300px"
|
||||
>
|
||||
<div v-if="status === 'connecting'" class="text-slate-400">
|
||||
正在连接 SSH...
|
||||
</div>
|
||||
<div v-else-if="status === 'error'" class="text-red-400">
|
||||
{{ errorMessage }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
.xterm-container .xterm {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
.xterm-container .xterm-viewport {
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user