feat: opt page style
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { Terminal } from '@xterm/xterm'
|
||||
import { FitAddon } from '@xterm/addon-fit'
|
||||
import '@xterm/xterm/css/xterm.css'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
fontSize?: number
|
||||
autoConnect?: boolean
|
||||
initialCommand?: string
|
||||
}>(),
|
||||
{
|
||||
fontSize: 13,
|
||||
autoConnect: true,
|
||||
initialCommand: ''
|
||||
}
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
connected: []
|
||||
disconnected: []
|
||||
}>()
|
||||
|
||||
const terminalRef = ref<HTMLDivElement | null>(null)
|
||||
let terminal: Terminal | null = null
|
||||
let fitAddon: FitAddon | null = null
|
||||
let ws: WebSocket | null = null
|
||||
let isPtyMode = false
|
||||
let inputBuffer = ''
|
||||
let commandHistory: string[] = []
|
||||
let historyIndex = -1
|
||||
|
||||
function initTerminal(forceConnect = false) {
|
||||
if (!terminalRef.value) return
|
||||
|
||||
// 清理旧终端
|
||||
if (terminal) {
|
||||
terminal.dispose()
|
||||
terminal = null
|
||||
}
|
||||
if (ws) {
|
||||
ws.close()
|
||||
ws = null
|
||||
}
|
||||
inputBuffer = ''
|
||||
isPtyMode = false
|
||||
|
||||
terminal = new Terminal({
|
||||
cursorBlink: true,
|
||||
fontSize: props.fontSize,
|
||||
fontFamily: 'Consolas, Monaco, monospace',
|
||||
theme: {
|
||||
background: '#1e1e1e',
|
||||
foreground: '#d4d4d4',
|
||||
cursor: '#d4d4d4',
|
||||
}
|
||||
})
|
||||
|
||||
fitAddon = new FitAddon()
|
||||
terminal.loadAddon(fitAddon)
|
||||
terminal.open(terminalRef.value)
|
||||
fitAddon.fit()
|
||||
terminal.focus()
|
||||
|
||||
// autoConnect 或者强制连接时才连接
|
||||
if (props.autoConnect || forceConnect) {
|
||||
connectWebSocket()
|
||||
}
|
||||
|
||||
// 清除当前输入行(Windows 模式用)
|
||||
function clearLine() {
|
||||
for (let i = 0; i < inputBuffer.length; i++) {
|
||||
terminal?.write('\b \b')
|
||||
}
|
||||
}
|
||||
|
||||
// 处理用户输入
|
||||
terminal.onData((data) => {
|
||||
if (!ws || ws.readyState !== WebSocket.OPEN) return
|
||||
|
||||
if (isPtyMode) {
|
||||
ws.send(data)
|
||||
return
|
||||
}
|
||||
|
||||
if (data === '\r') {
|
||||
terminal?.write('\r\n')
|
||||
if (inputBuffer.trim()) {
|
||||
commandHistory.push(inputBuffer)
|
||||
historyIndex = commandHistory.length
|
||||
ws.send(inputBuffer + '\r\n')
|
||||
}
|
||||
inputBuffer = ''
|
||||
} else if (data === '\x1b[A') {
|
||||
if (commandHistory.length > 0 && historyIndex > 0) {
|
||||
clearLine()
|
||||
historyIndex--
|
||||
inputBuffer = commandHistory[historyIndex] ?? ''
|
||||
terminal?.write(inputBuffer)
|
||||
}
|
||||
} else if (data === '\x1b[B') {
|
||||
clearLine()
|
||||
if (historyIndex < commandHistory.length - 1) {
|
||||
historyIndex++
|
||||
inputBuffer = commandHistory[historyIndex] ?? ''
|
||||
terminal?.write(inputBuffer)
|
||||
} else {
|
||||
historyIndex = commandHistory.length
|
||||
inputBuffer = ''
|
||||
}
|
||||
} else if (data === '\x7f' || data === '\b') {
|
||||
if (inputBuffer.length > 0) {
|
||||
inputBuffer = inputBuffer.slice(0, -1)
|
||||
terminal?.write('\b \b')
|
||||
}
|
||||
} else if (data === '\x03') {
|
||||
ws.send('\x03')
|
||||
inputBuffer = ''
|
||||
historyIndex = commandHistory.length
|
||||
terminal?.write('^C\r\n')
|
||||
} else if (data >= ' ' || data === '\t') {
|
||||
inputBuffer += data
|
||||
terminal?.write(data)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function connectWebSocket() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const wsUrl = `${protocol}//${window.location.host}/api/terminal/ws`
|
||||
|
||||
ws = new WebSocket(wsUrl)
|
||||
|
||||
ws.onopen = () => {
|
||||
terminal?.writeln('\x1b[32m已连接到终端\x1b[0m')
|
||||
terminal?.writeln('')
|
||||
terminal?.focus()
|
||||
emit('connected')
|
||||
|
||||
// 如果有初始命令,延迟发送
|
||||
if (props.initialCommand) {
|
||||
setTimeout(() => {
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(props.initialCommand + '\r\n')
|
||||
}
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
if (event.data === '__PTY_MODE__') {
|
||||
isPtyMode = true
|
||||
return
|
||||
}
|
||||
if (event.data === '__PIPE_MODE__') {
|
||||
isPtyMode = false
|
||||
return
|
||||
}
|
||||
terminal?.write(event.data)
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
terminal?.writeln('')
|
||||
terminal?.writeln('\x1b[31m连接已断开\x1b[0m')
|
||||
emit('disconnected')
|
||||
}
|
||||
|
||||
ws.onerror = () => {
|
||||
terminal?.writeln('\x1b[31m连接错误\x1b[0m')
|
||||
}
|
||||
}
|
||||
|
||||
function reconnect() {
|
||||
if (ws) {
|
||||
ws.close()
|
||||
}
|
||||
inputBuffer = ''
|
||||
isPtyMode = false
|
||||
terminal?.clear()
|
||||
connectWebSocket()
|
||||
}
|
||||
|
||||
function dispose() {
|
||||
if (ws) {
|
||||
ws.close()
|
||||
ws = null
|
||||
}
|
||||
if (terminal) {
|
||||
terminal.dispose()
|
||||
terminal = null
|
||||
}
|
||||
}
|
||||
|
||||
function handleResize() {
|
||||
fitAddon?.fit()
|
||||
}
|
||||
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
reconnect,
|
||||
dispose,
|
||||
initTerminal
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', handleResize)
|
||||
setTimeout(initTerminal, 100)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
dispose()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="terminalRef" class="terminal-container w-full h-full bg-[#1e1e1e]" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.terminal-container :deep(.xterm-viewport) {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #4a4a4a #1e1e1e;
|
||||
}
|
||||
|
||||
.terminal-container :deep(.xterm-viewport::-webkit-scrollbar) {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.terminal-container :deep(.xterm-viewport::-webkit-scrollbar-track) {
|
||||
background: #1e1e1e;
|
||||
}
|
||||
|
||||
.terminal-container :deep(.xterm-viewport::-webkit-scrollbar-thumb) {
|
||||
background: #4a4a4a;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.terminal-container :deep(.xterm-viewport::-webkit-scrollbar-thumb:hover) {
|
||||
background: #5a5a5a;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user