hldy_vue/src/components/terminal/Terminal.vue

345 lines
9.9 KiB
Vue
Raw Normal View History

2025-06-09 14:46:36 +08:00
<template>
2025-06-12 14:15:00 +08:00
<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>
2025-06-09 14:46:36 +08:00
</div>
</template>
<script lang="ts">
import { defineComponent, ref, nextTick, onMounted, onBeforeUnmount } from 'vue'
import { message } from 'ant-design-vue'
interface TerminalLine {
2025-06-12 14:15:00 +08:00
type: 'input' | 'output' | 'error' | 'system'
text: string
2025-06-09 14:46:36 +08:00
}
interface ConnectionForm {
2025-06-12 14:15:00 +08:00
host: string
port: number
username: string
password: string
2025-06-09 14:46:36 +08:00
}
export default defineComponent({
2025-06-12 14:15:00 +08:00
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: '121.36.88.64',
port: 22,
username: '',
password: ''
})
const formRules = {
host: [{ required: true, message: '请输入主机地址' }],
port: [{ required: true, message: '请输入端口' }],
username: [{ required: true, message: '请输入用户名' }],
password: [{ required: true, message: '请输入密码' }]
}
2025-06-09 14:46:36 +08:00
2025-06-12 14:15:00 +08:00
const clientId = Math.random().toString(36).substring(2, 10)
2025-06-09 14:46:36 +08:00
2025-06-12 14:15:00 +08:00
const addOutput = (type: TerminalLine['type'], text: string) => {
outputLines.value.push({ type, text })
scrollToBottom()
}
2025-06-09 14:46:36 +08:00
2025-06-12 14:15:00 +08:00
const scrollToBottom = () => {
nextTick(() => {
if (terminalBody.value) {
terminalBody.value.scrollTop = terminalBody.value.scrollHeight
}
})
2025-06-09 14:46:36 +08:00
}
2025-06-12 14:15:00 +08:00
const connect = () => {
connectionModalVisible.value = true
}
2025-06-09 14:46:36 +08:00
2025-06-12 14:15:00 +08:00
const handleConnect = () => {
connecting.value = true
initWebSocket()
}
2025-06-09 14:46:36 +08:00
2025-06-12 14:15:00 +08:00
const initWebSocket = () => {
const wsUrl = `ws://localhost:8080/nursing-unit/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
}
2025-06-09 14:46:36 +08:00
}
2025-06-12 14:15:00 +08:00
const disconnect = () => {
if (connected.value && socket.value) {
socket.value.send('disconnect')
socket.value.close()
}
}
2025-06-09 14:46:36 +08:00
2025-06-12 14:15:00 +08:00
const executeCommand = () => {
if (!currentCommand.value.trim()) return
2025-06-09 14:46:36 +08:00
2025-06-12 14:15:00 +08:00
if (!connected.value) {
addOutput('system', '请先建立SSH连接')
return
}
2025-06-09 14:46:36 +08:00
2025-06-12 14:15:00 +08:00
// 添加到历史记录
commandHistory.value.push(currentCommand.value)
historyIndex.value = commandHistory.value.length
2025-06-09 14:46:36 +08:00
2025-06-12 14:15:00 +08:00
// 显示输入的命令
addOutput('input', currentCommand.value)
2025-06-09 14:46:36 +08:00
2025-06-12 14:15:00 +08:00
// 发送命令
socket.value?.send(currentCommand.value)
2025-06-09 14:46:36 +08:00
2025-06-12 14:15:00 +08:00
currentCommand.value = ''
scrollToBottom()
}
2025-06-09 14:46:36 +08:00
2025-06-12 14:15:00 +08:00
const historyUp = () => {
if (commandHistory.value.length === 0) return
2025-06-09 14:46:36 +08:00
2025-06-12 14:15:00 +08:00
if (historyIndex.value > 0) {
historyIndex.value--
currentCommand.value = commandHistory.value[historyIndex.value]
}
}
2025-06-09 14:46:36 +08:00
2025-06-12 14:15:00 +08:00
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 = ''
}
}
2025-06-09 14:46:36 +08:00
2025-06-12 14:15:00 +08:00
const focusInput = () => {
nextTick(() => {
commandInput.value?.focus()
})
}
2025-06-09 14:46:36 +08:00
2025-06-12 14:15:00 +08:00
const handleKeyDown = (event: KeyboardEvent) => {
// 检测 Ctrl+C 组合键
if (event.ctrlKey && event.key === 'c') {
event.preventDefault()
if (connected.value && socket.value) {
// 发送中断信号
socket.value.send('\x03') // Ctrl+C 的 ASCII 码
addOutput('system', '^C') // 显示中断符号
}
}
}
onMounted(() => {
focusInput()
// 添加键盘事件监听
window.addEventListener('keydown', handleKeyDown)
})
onBeforeUnmount(() => {
if (socket.value) {
socket.value.close()
}
// 移除键盘事件监听
window.removeEventListener('keydown', handleKeyDown)
})
return {
outputLines,
currentCommand,
connectionModalVisible,
connecting,
connectionForm,
formRules,
terminalBody,
commandInput,
connect,
handleConnect,
disconnect,
executeCommand,
historyUp,
historyDown
}
2025-06-09 14:46:36 +08:00
}
})
</script>
<style scoped>
.terminal-container {
2025-06-12 14:15:00 +08:00
height: 500px;
display: flex;
flex-direction: column;
background-color: #121212;
color: #f0f0f0;
border-radius: 4px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
2025-06-09 14:46:36 +08:00
}
.terminal-header {
2025-06-12 14:15:00 +08:00
padding: 8px 12px;
background-color: #1a1a1a;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #2a2a2a;
font-family: 'Lucida Console', 'Consolas', monospace;
2025-06-09 14:46:36 +08:00
}
.terminal-body {
2025-06-12 14:15:00 +08:00
flex: 1;
padding: 10px;
overflow-y: auto;
font-family: 'Lucida Console', 'Consolas', monospace;
font-size: 14px;
line-height: 1.5;
background-color: #121212;
2025-06-09 14:46:36 +08:00
}
.terminal-line {
2025-06-12 14:15:00 +08:00
margin-bottom: 4px;
word-break: break-all;
white-space: pre-wrap;
2025-06-09 14:46:36 +08:00
}
.terminal-input-line {
2025-06-12 14:15:00 +08:00
display: flex;
align-items: center;
background-color: rgba(255, 255, 255, 0.05);
padding: 8px;
border-radius: 2px;
2025-06-09 14:46:36 +08:00
}
.input-prompt {
2025-06-12 14:15:00 +08:00
color: #00ff00;
margin-right: 8px;
font-weight: bold;
2025-06-09 14:46:36 +08:00
}
.terminal-input {
2025-06-12 14:15:00 +08:00
flex: 1;
background: transparent;
border: none;
color: #ffffff;
font-family: 'Lucida Console', 'Consolas', monospace;
font-size: 14px;
outline: none;
padding: 4px 8px;
border-radius: 2px;
background-color: rgba(255, 255, 255, 0.08);
}
.terminal-input:focus {
background-color: rgba(255, 255, 255, 0.12);
2025-06-09 14:46:36 +08:00
}
.output {
2025-06-12 14:15:00 +08:00
color: #f0f0f0;
2025-06-09 14:46:36 +08:00
}
.error {
2025-06-12 14:15:00 +08:00
color: #ff5555;
2025-06-09 14:46:36 +08:00
}
.input {
2025-06-12 14:15:00 +08:00
color: #00ffff;
2025-06-09 14:46:36 +08:00
}
.system {
2025-06-12 14:15:00 +08:00
color: #ffff55;
}
.terminal-body::-webkit-scrollbar {
width: 6px;
}
.terminal-body::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}
.terminal-body::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
border-radius: 3px;
}
.terminal-body::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
2025-06-09 14:46:36 +08:00
}
</style>