feat: 新增终端主机页面

This commit is contained in:
TsMask
2024-02-28 17:35:44 +08:00
parent 87b2fd965c
commit e86ae12801
5 changed files with 920 additions and 2 deletions

View File

@@ -0,0 +1,225 @@
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { FitAddon } from 'xterm-addon-fit';
import { Terminal } from 'xterm';
import 'xterm/css/xterm.css';
import { RESULT_CODE_ERROR } from '@/constants/result-constants';
import { OptionsType, WS } from '@/plugins/ws-websocket';
const ws = new WS();
const emit = defineEmits(['connect', 'close']);
const props = defineProps({
/**终端ID必传 */
id: {
type: String,
required: true,
},
/**连接主机ID必传 */
hostId: {
type: String,
required: true,
},
/**窗口单行字符数 */
cols: {
type: Number,
default: 80,
},
/**窗口行数 */
rows: {
type: Number,
default: 40,
},
});
/**终端输入DOM节点实例对象 */
const terminalDom = ref<HTMLElement | undefined>(undefined);
/**终端输入实例对象 */
const terminal = ref<any>(null);
/**终端输入渲染 */
function handleRanderXterm(container: HTMLElement | undefined) {
if (!container) return;
const xterm = new Terminal({
cols: props.cols,
rows: props.rows,
lineHeight: 1.2,
fontSize: 12,
fontFamily: "Monaco, Menlo, Consolas, 'Courier New', monospace",
theme: {
background: '#000000',
},
cursorBlink: true, // 光标闪烁
cursorStyle: 'block',
scrollback: 1000,
scrollSensitivity: 15,
tabStopWidth: 4,
});
// 挂载
xterm.open(container);
// 自适应尺寸
const fitAddon = new FitAddon();
xterm.loadAddon(fitAddon);
// 终端输入字符按键监听
xterm.onData(char => {
ws.send({
requestId: `ssh_${props.hostId}`,
type: 'ssh',
data: char,
});
// const printable = char.match(/[\x20-\x7E]/); // 匹配可打印字符的正则表达式
// if (char === '\r' || char === '\x0D') {
// // 处理回车键,添加换行
// xterm.writeln('');
// } else if (char === '\x08' || char === '\x7F') {
// // 处理退格键,删除最后一个字符
// xterm.write('\b \b');
// } else if (printable) {
// // 处理可打印字符
// xterm.write(char);
// }
});
// 终端输入按键监听
// xterm.onKey(({ key, domEvent }) => {
// // console.log(key, domEvent);
// // 单键输入
// // switch (domEvent.key) {
// // case 'ArrowUp':
// // // 按“↑”方向键时要做的事。
// // break;
// // case 'ArrowDown':
// // // 按“↓”方向键时要做的事。
// // break;
// // case 'ArrowLeft':
// // // 按“←”方向键时要做的事。
// // break;
// // case 'ArrowRight':
// // // 按“→”方向键时要做的事。
// // break;
// // case 'Enter':
// // // 处理回车键,添加换行
// // term.writeln('');
// // break;
// // case 'Backspace':
// // // 处理退格键,删除最后一个字符
// // term.write('\b \b');
// // break;
// // case 'Escape':
// // // 按“ESC”键时要做的事。
// // break;
// // default:
// // return; // 什么都没按就退出吧。
// // }
// });
// 终端尺寸变化触发
xterm.onResize(({ cols, rows }) => {
// console.log('尺寸', cols, rows);
ws.send({
requestId: `ssh_resize_${props.hostId}`,
type: 'ssh_resize',
data: { cols, rows },
});
});
// 创建 ResizeObserver 实例
var observer = new ResizeObserver(entries => {
fitAddon.fit();
});
// 监听元素大小变化
observer.observe(container);
terminal.value = xterm;
}
/**连接打开后回调 */
function wsOpen(ev: any) {
// console.info('wsOpen', ev);
nextTick(() => {
handleRanderXterm(terminalDom.value);
// 连接事件
emit('connect', {
timeStamp: ev.timeStamp,
cols: terminal.value.cols,
rows: terminal.value.rows,
hostId: props.hostId,
id: props.id,
});
});
}
/**连接错误后回调 */
function wsError(ev: any) {
// console.error('wsError', ev);
if (terminal.value != null) {
let message = 'disconnected';
terminal.value.write(`\x1b[31m${message}\x1b[m\r\n`);
} else if (terminalDom.value) {
terminalDom.value.style.background = '#000';
terminalDom.value.style.color = '#ff4d4f';
terminalDom.value.style.height = '60%';
terminalDom.value.innerText = 'disconnected';
}
}
/**连接关闭后回调 */
function wsClose(code: number) {
// console.warn('wsClose', code);
if (terminal.value != null) {
let message = 'disconnected';
terminal.value.write(`\x1b[31m${message}\x1b[m\r\n`);
}
// 关闭事件
emit('close', {
code: code,
hostId: props.hostId,
id: props.id,
});
}
/**接收消息后回调 */
function wsMessage(res: Record<string, any>) {
// console.log('wsMessage', res);
const { code, requestId, data } = res;
if (code === RESULT_CODE_ERROR) {
console.warn(res.msg);
return;
}
if (!requestId) return;
if (terminal.value != null) {
terminal.value.write(data);
}
}
onMounted(() => {
if (props.hostId) {
// 建立链接
const options: OptionsType = {
url: '/ws/ssh',
params: {
hostId: props.hostId,
cols: props.cols,
rows: props.rows,
},
onmessage: wsMessage,
onerror: wsError,
onopen: wsOpen,
onclose: wsClose,
};
ws.connect(options);
}
});
onBeforeUnmount(() => {
ws.close();
});
</script>
<template>
<div ref="terminalDom" :id="id" class="terminal"></div>
</template>
<style lang="css" scoped>
.terminal {
width: 100%;
height: 100%;
}
</style>