feat: 新增终端主机页面
This commit is contained in:
225
src/components/TerminalSSH/index.vue
Normal file
225
src/components/TerminalSSH/index.vue
Normal 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>
|
||||
274
src/components/TerminalTelnet/index.vue
Normal file
274
src/components/TerminalTelnet/index.vue
Normal file
@@ -0,0 +1,274 @@
|
||||
<script lang="ts" setup>
|
||||
import { message } from 'ant-design-vue/lib';
|
||||
import { ref, reactive, 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,
|
||||
},
|
||||
/**初始发送命令 */
|
||||
initCmd: {
|
||||
type: [String, Boolean],
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
/**终端输入DOM节点实例对象 */
|
||||
const terminalDom = ref<HTMLElement | undefined>(undefined);
|
||||
|
||||
/**终端输入实例对象 */
|
||||
const terminal = ref<any>(null);
|
||||
|
||||
/**终端输入文字状态 */
|
||||
const terminalState = reactive<{
|
||||
/**输入值 */
|
||||
text: string;
|
||||
/**历史 */
|
||||
history: {
|
||||
label?: string;
|
||||
value: string;
|
||||
}[];
|
||||
}>({
|
||||
text: '',
|
||||
history: [
|
||||
{
|
||||
value: 'help',
|
||||
},
|
||||
{
|
||||
value: 'quit',
|
||||
},
|
||||
{
|
||||
value: 'list ver',
|
||||
},
|
||||
{
|
||||
value: 'list lic',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
/**自动完成根据输入项进行筛选 */
|
||||
function fnAutoCompleteFilter(input: string, option: any) {
|
||||
return option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0;
|
||||
}
|
||||
|
||||
/**自动完成按键触发 */
|
||||
function fnAutoCompleteKeydown(evt: any) {
|
||||
if (evt.key === 'Enter') {
|
||||
// 阻止默认的换行行为
|
||||
evt.preventDefault();
|
||||
// 按下 Shift + Enter 键时换行
|
||||
if (evt.shiftKey) {
|
||||
// 插入换行符
|
||||
const textarea = evt.target;
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const text = textarea.value;
|
||||
textarea.value = text.substring(0, start) + '\n' + text.substring(end);
|
||||
terminalState.text = textarea.value;
|
||||
// 更新光标位置
|
||||
textarea.selectionStart = textarea.selectionEnd = start + 1;
|
||||
} else {
|
||||
// ws未连接
|
||||
if (ws.state() !== WebSocket.OPEN) {
|
||||
message.error('disconnected');
|
||||
return;
|
||||
}
|
||||
|
||||
// 输入历史
|
||||
const cmdStr = terminalState.text.trim().replace(/\n/g, '\r\n');
|
||||
const hisIndex = terminalState.history.findIndex(
|
||||
item => item.value === cmdStr
|
||||
);
|
||||
if (hisIndex === -1) {
|
||||
terminalState.history.push({
|
||||
value: cmdStr,
|
||||
});
|
||||
}
|
||||
|
||||
// 发送文本
|
||||
terminal.value.scrollToBottom();
|
||||
terminal.value.writeln(cmdStr);
|
||||
ws.send({
|
||||
requestId: `telnet_${props.hostId}`,
|
||||
type: 'telnet',
|
||||
data: `${cmdStr}\r\n'`,
|
||||
});
|
||||
terminalState.text = ' ';
|
||||
|
||||
// 退出登录
|
||||
if (['q', 'quit', 'exit'].includes(cmdStr)) {
|
||||
setTimeout(() => {
|
||||
ws.close();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**终端输入渲染 */
|
||||
function handleRanderXterm(container: HTMLElement | undefined) {
|
||||
if (!container) return;
|
||||
const xterm = new Terminal({
|
||||
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: true, // 禁止输入
|
||||
});
|
||||
// 挂载
|
||||
xterm.open(container);
|
||||
// 自适应尺寸
|
||||
const fitAddon = new FitAddon();
|
||||
xterm.loadAddon(fitAddon);
|
||||
|
||||
// 创建 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: `telnet_${props.hostId}`,
|
||||
type: 'telnet',
|
||||
data: `${props.initCmd}\r\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>) {
|
||||
// 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.trim().replace(/\n/g, "\r\n"));
|
||||
// 是否n结尾
|
||||
if (/[\r\n]$/.test(data)) {
|
||||
terminal.value.writeln(data.trim().replace(/\n/g, '\r\n'));
|
||||
} else {
|
||||
terminal.value.write(data.replace(/\n/g, '\r\n'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.hostId) {
|
||||
// 建立链接
|
||||
const options: OptionsType = {
|
||||
url: '/ws/telnet',
|
||||
params: {
|
||||
hostId: props.hostId,
|
||||
},
|
||||
onmessage: wsMessage,
|
||||
onerror: wsError,
|
||||
onopen: wsOpen,
|
||||
onclose: wsClose,
|
||||
};
|
||||
ws.connect(options);
|
||||
}
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
ws.close();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="terminal">
|
||||
<div ref="terminalDom" style="height: 78%" :id="id"></div>
|
||||
<a-auto-complete
|
||||
v-model:value="terminalState.text"
|
||||
:dropdown-match-select-width="500"
|
||||
style="width: 100%"
|
||||
:options="terminalState.history"
|
||||
:filter-option="fnAutoCompleteFilter"
|
||||
@keydown="fnAutoCompleteKeydown"
|
||||
>
|
||||
<a-textarea
|
||||
:auto-size="{ minRows: 1, maxRows: 6 }"
|
||||
placeholder="Execute command. Shift+Enter to line feed, Enter to send"
|
||||
/>
|
||||
</a-auto-complete>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.terminal {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user