Files
fe.ems.vue3/src/components/TerminalSSH/index.vue
2024-05-07 12:06:03 +08:00

259 lines
6.3 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script lang="ts" setup>
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
import { FitAddon } from '@xterm/addon-fit';
import { Terminal } from '@xterm/xterm';
import '@xterm/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', 'message']);
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,
},
/**禁止输入 */
disable: {
type: Boolean,
default: false,
},
/**初始发送命令 */
initCmd: {
type: [String, Boolean],
default: false,
},
});
/**终端输入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,
disableStdin: props.disable, // 禁止输入
});
// 挂载
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,
});
// 初始发送命令
if (typeof props.initCmd === 'string') {
ws.send({
requestId: `ssh_${props.hostId}`,
type: 'ssh',
data: `${props.initCmd}\n`,
});
}
});
}
/**连接错误后回调 */
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>) {
emit('message', res);
// 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();
});
// 给组件设置属性 ref="xxxTerminal"
// setup内使用 const xxxTerminal = ref();
defineExpose({
/**发送方法 */
send: (data: string) => {
ws.send({
requestId: `ssh_${props.hostId}`,
type: 'ssh',
data: `${data}\n`,
});
},
});
</script>
<template>
<div ref="terminalDom" :id="id" class="terminal"></div>
</template>
<style lang="css" scoped>
.terminal {
width: 100%;
height: 100%;
}
</style>