hldy_vue/src/components/terminal/Terminal.vue

309 lines
7.4 KiB
Vue
Raw Normal View History

2025-06-09 14:46:36 +08:00
<template>
<div class="terminal-container">
<div class="terminal-header">
<span>SSH终端</span>
<div>
<a-button @click="connect" type="primary" size="small">连接</a-button>
<a-button @click="disconnect" size="small" style="margin-left: 8px">断开</a-button>
</div>
</div>
<div class="terminal-body" ref="terminalBody">
<div v-for="(line, index) in outputLines" :key="index" class="terminal-line">
<span v-if="line.type === 'input'" class="input-prompt">$ </span>
<span :class="line.type">{{ line.text }}</span>
</div>
<div class="terminal-input-line">
<span class="input-prompt">$ </span>
<input
v-model="currentCommand"
@keyup.enter="executeCommand"
@keyup.up="historyUp"
@keyup.down="historyDown"
ref="commandInput"
class="terminal-input"
/>
</div>
</div>
<a-modal
v-model:visible="connectionModalVisible"
title="SSH连接配置"
@ok="handleConnect"
:ok-button-props="{ disabled: connecting }"
:cancel-button-props="{ disabled: connecting }"
>
<a-form :model="connectionForm" :rules="formRules" layout="vertical">
<a-form-item label="主机地址" name="host">
<a-input v-model:value="connectionForm.host" />
</a-form-item>
<a-form-item label="端口" name="port">
<a-input-number v-model:value="connectionForm.port" :min="1" :max="65535" />
</a-form-item>
<a-form-item label="用户名" name="username">
<a-input v-model:value="connectionForm.username" />
</a-form-item>
<a-form-item label="密码" name="password">
<a-input-password v-model:value="connectionForm.password" />
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, nextTick, onMounted, onBeforeUnmount } from 'vue'
import { message } from 'ant-design-vue'
interface TerminalLine {
type: 'input' | 'output' | 'error' | 'system'
text: string
}
interface ConnectionForm {
host: string
port: number
username: string
password: string
}
export default defineComponent({
name: 'SshTerminal',
setup() {
const outputLines = ref<TerminalLine[]>([])
const currentCommand = ref('')
const commandHistory = ref<string[]>([])
const historyIndex = ref(-1)
const connectionModalVisible = ref(false)
const connecting = ref(false)
const connected = ref(false)
const socket = ref<WebSocket | null>(null)
const terminalBody = ref<HTMLElement | null>(null)
const commandInput = ref<HTMLInputElement | null>(null)
const connectionForm = ref<ConnectionForm>({
host: '',
port: 22,
username: '',
password: ''
})
const formRules = {
host: [{ required: true, message: '请输入主机地址' }],
port: [{ required: true, message: '请输入端口' }],
username: [{ required: true, message: '请输入用户名' }],
password: [{ required: true, message: '请输入密码' }]
}
const clientId = Math.random().toString(36).substring(2, 10)
const addOutput = (type: TerminalLine['type'], text: string) => {
outputLines.value.push({ type, text })
scrollToBottom()
}
const scrollToBottom = () => {
nextTick(() => {
if (terminalBody.value) {
terminalBody.value.scrollTop = terminalBody.value.scrollHeight
}
})
}
const connect = () => {
connectionModalVisible.value = true
}
const handleConnect = () => {
connecting.value = true
initWebSocket()
}
const initWebSocket = () => {
const wsUrl = `ws://${window.location.host}/jeecg-boot/ws/ssh/${clientId}`
socket.value = new WebSocket(wsUrl)
socket.value.onopen = () => {
const { host, port, username, password } = connectionForm.value
const connectMsg = `connect:${host}:${port}:${username}:${password}`
socket.value?.send(connectMsg)
connected.value = true
connecting.value = false
connectionModalVisible.value = false
addOutput('system', 'SSH连接已建立')
}
socket.value.onmessage = (event) => {
addOutput('output', event.data)
}
socket.value.onerror = (error) => {
addOutput('error', '连接错误: ' + error)
connecting.value = false
connected.value = false
}
socket.value.onclose = () => {
if (connected.value) {
addOutput('system', 'SSH连接已断开')
}
connected.value = false
connecting.value = false
}
}
const disconnect = () => {
if (connected.value && socket.value) {
socket.value.send('disconnect')
socket.value.close()
}
}
const executeCommand = () => {
if (!currentCommand.value.trim()) return
if (!connected.value) {
addOutput('system', '请先建立SSH连接')
return
}
// 添加到历史记录
commandHistory.value.push(currentCommand.value)
historyIndex.value = commandHistory.value.length
// 显示输入的命令
addOutput('input', currentCommand.value)
// 发送命令
socket.value?.send(currentCommand.value)
currentCommand.value = ''
scrollToBottom()
}
const historyUp = () => {
if (commandHistory.value.length === 0) return
if (historyIndex.value > 0) {
historyIndex.value--
currentCommand.value = commandHistory.value[historyIndex.value]
}
}
const historyDown = () => {
if (historyIndex.value < commandHistory.value.length - 1) {
historyIndex.value++
currentCommand.value = commandHistory.value[historyIndex.value]
} else if (historyIndex.value === commandHistory.value.length - 1) {
historyIndex.value++
currentCommand.value = ''
}
}
const focusInput = () => {
nextTick(() => {
commandInput.value?.focus()
})
}
onMounted(() => {
focusInput()
})
onBeforeUnmount(() => {
if (socket.value) {
socket.value.close()
}
})
return {
outputLines,
currentCommand,
connectionModalVisible,
connecting,
connectionForm,
formRules,
terminalBody,
commandInput,
connect,
handleConnect,
disconnect,
executeCommand,
historyUp,
historyDown
}
}
})
</script>
<style scoped>
.terminal-container {
height: 100%;
display: flex;
flex-direction: column;
background-color: #1e1e1e;
color: #f0f0f0;
border-radius: 4px;
overflow: hidden;
}
.terminal-header {
padding: 8px 16px;
background-color: #333;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #444;
}
.terminal-body {
flex: 1;
padding: 8px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 14px;
line-height: 1.5;
}
.terminal-line {
margin-bottom: 4px;
word-break: break-all;
white-space: pre-wrap;
}
.terminal-input-line {
display: flex;
align-items: center;
}
.input-prompt {
color: #4caf50;
margin-right: 8px;
}
.terminal-input {
flex: 1;
background: transparent;
border: none;
color: #fff;
font-family: 'Courier New', monospace;
font-size: 14px;
outline: none;
}
.output {
color: #f0f0f0;
}
.error {
color: #ff5252;
}
.input {
color: #bbdefb;
}
.system {
color: #ff9800;
}
</style>