265 lines
6.1 KiB
Vue
265 lines
6.1 KiB
Vue
<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,
|
||
},
|
||
/**ws连接地址,必传 如/ws/view */
|
||
url: {
|
||
type: String,
|
||
required: true,
|
||
},
|
||
/**网元类型,必传 */
|
||
neType: {
|
||
type: String,
|
||
required: true,
|
||
},
|
||
/**网元ID,必传 */
|
||
neId: {
|
||
type: String,
|
||
required: true,
|
||
},
|
||
/**窗口单行字符数 */
|
||
cols: {
|
||
type: Number,
|
||
default: 80,
|
||
},
|
||
/**窗口行数 */
|
||
rows: {
|
||
type: Number,
|
||
default: 40,
|
||
},
|
||
/**ws发送requestId前缀 如ssh_id */
|
||
prefix: {
|
||
type: String,
|
||
default: 'ssh',
|
||
},
|
||
/**消息处理函数 */
|
||
processMessages: {
|
||
type: Function,
|
||
default: undefined,
|
||
},
|
||
});
|
||
|
||
/**终端输入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, // 设置历史缓冲区大小为 1000 行
|
||
scrollSensitivity: 15,
|
||
tabStopWidth: 4,
|
||
disableStdin: true, // 禁止输入
|
||
});
|
||
// 挂载
|
||
xterm.open(container);
|
||
// 自适应尺寸
|
||
const fitAddon = new FitAddon();
|
||
xterm.loadAddon(fitAddon);
|
||
// 终端尺寸变化触发
|
||
xterm.onResize(({ cols, rows }) => {
|
||
// console.log('尺寸', cols, rows);
|
||
ws.send({
|
||
requestId: `resize_${props.id}`,
|
||
type: '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,
|
||
neType: props.neType,
|
||
neId: props.neId,
|
||
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 ' + code;
|
||
terminal.value.write(`\x1b[31m${message}\x1b[m\r\n`);
|
||
}
|
||
// 关闭事件
|
||
emit('close', {
|
||
code: code,
|
||
neType: props.neType,
|
||
neId: props.neId,
|
||
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) {
|
||
let text = '';
|
||
// 处理消息
|
||
if (props.processMessages) {
|
||
text = props.processMessages(data);
|
||
}else{
|
||
text = processMessage(data);
|
||
}
|
||
// 无消息是则不输出
|
||
if (text === '') {
|
||
return;
|
||
}
|
||
terminal.value.write(text);
|
||
}
|
||
}
|
||
|
||
/**终端消息处理*/
|
||
function processMessage(data: string): string {
|
||
// 查找的开始输出标记
|
||
const parts: string[] = data.split('\u001b[?2004l\r');
|
||
if (parts.length > 0) {
|
||
if (parts[0].startsWith('^C') || parts[0].startsWith('\r')) {
|
||
return '';
|
||
}
|
||
let text = parts[parts.length - 1];
|
||
// 找到最后输出标记
|
||
let lestIndex = text.lastIndexOf('\u001b[?2004h\u001b]0;');
|
||
if (lestIndex !== -1) {
|
||
text = text.substring(0, lestIndex);
|
||
}
|
||
if (text === '' || text === '\r\n' || text.startsWith('^C\r\n')) {
|
||
return '';
|
||
}
|
||
// 是否还有最后输出标记
|
||
lestIndex = text.lastIndexOf('\u001b[?2004h');
|
||
if (lestIndex !== -1) {
|
||
text = text.substring(0, lestIndex);
|
||
}
|
||
// console.log({ parts, text });
|
||
return text;
|
||
}
|
||
return data;
|
||
}
|
||
|
||
onMounted(() => {
|
||
if (props.neType && props.neId) {
|
||
// 建立链接
|
||
const options: OptionsType = {
|
||
url: props.url,
|
||
params: {
|
||
neType: props.neType,
|
||
neId: props.neId,
|
||
cols: props.cols,
|
||
rows: props.rows,
|
||
},
|
||
onmessage: wsMessage,
|
||
onerror: wsError,
|
||
onopen: wsOpen,
|
||
onclose: wsClose,
|
||
};
|
||
ws.connect(options);
|
||
}
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
if (ws.state() === WebSocket.OPEN) ws.close();
|
||
});
|
||
|
||
// 给组件设置属性 ref="xxxTerminal"
|
||
// setup内使用 const xxxTerminal = ref();
|
||
defineExpose({
|
||
/**清除 */
|
||
clear: () => {
|
||
if (terminal.value != null) {
|
||
terminal.value.clear();
|
||
}
|
||
},
|
||
/**发送命令 */
|
||
send: (type: string, data: Record<string, any>) => {
|
||
ws.send({
|
||
requestId: `${props.prefix}_${props.id}`,
|
||
type,
|
||
data,
|
||
});
|
||
},
|
||
/**模拟按下 Ctrl+C */
|
||
ctrlC: () => {
|
||
ws.send({
|
||
requestId: `${props.prefix}_${props.id}`,
|
||
type: 'ctrl-c',
|
||
});
|
||
},
|
||
});
|
||
</script>
|
||
|
||
<template>
|
||
<div ref="terminalDom" :id="id" class="terminal"></div>
|
||
</template>
|
||
|
||
<style lang="css" scoped>
|
||
.terminal {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
</style>
|