309 lines
7.4 KiB
Vue
309 lines
7.4 KiB
Vue
|
<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>
|