mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2025-01-19 16:29:17 +08:00
feat: 抽象出终端为独立组件
This commit is contained in:
parent
750a2a445e
commit
fa83199d7b
224
frontend/src/components/terminal/index.vue
Normal file
224
frontend/src/components/terminal/index.vue
Normal file
@ -0,0 +1,224 @@
|
|||||||
|
<template>
|
||||||
|
<div ref="terminalElement" @wheel="onTermWheel"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, watch, onBeforeUnmount, nextTick } from 'vue';
|
||||||
|
import { Terminal } from 'xterm';
|
||||||
|
import 'xterm/css/xterm.css';
|
||||||
|
import { FitAddon } from 'xterm-addon-fit';
|
||||||
|
import { Base64 } from 'js-base64';
|
||||||
|
|
||||||
|
const terminalElement = ref<HTMLDivElement | null>(null);
|
||||||
|
const fitAddon = new FitAddon();
|
||||||
|
const termReady = ref(false);
|
||||||
|
const webSocketReady = ref(false);
|
||||||
|
const term = ref(
|
||||||
|
new Terminal({
|
||||||
|
lineHeight: 1.2,
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: "Monaco, Menlo, Consolas, 'Courier New', monospace",
|
||||||
|
theme: {
|
||||||
|
background: '#000000',
|
||||||
|
},
|
||||||
|
cursorBlink: true,
|
||||||
|
cursorStyle: 'underline',
|
||||||
|
scrollback: 100,
|
||||||
|
tabStopWidth: 4,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const terminalSocket = ref<WebSocket>();
|
||||||
|
const heartbeatTimer = ref<number>();
|
||||||
|
const latency = ref(0);
|
||||||
|
|
||||||
|
const readyWatcher = watch(
|
||||||
|
() => webSocketReady.value && termReady.value,
|
||||||
|
(ready) => {
|
||||||
|
if (ready) {
|
||||||
|
changeTerminalSize();
|
||||||
|
readyWatcher(); // unwatch self
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
interface WsProps {
|
||||||
|
endpoint: string;
|
||||||
|
args: string;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
const acceptParams = (props: WsProps) => {
|
||||||
|
nextTick(() => {
|
||||||
|
if (props.error.length !== 0) {
|
||||||
|
initError(props.error);
|
||||||
|
} else {
|
||||||
|
init(props.endpoint, props.args);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const init = (endpoint: string, args: string) => {
|
||||||
|
if (initTerminal(true)) {
|
||||||
|
initWebSocket(endpoint, args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initError = (errorInfo: string) => {
|
||||||
|
if (initTerminal(false)) {
|
||||||
|
term.value.write(errorInfo);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function onClose() {
|
||||||
|
window.removeEventListener('resize', changeTerminalSize);
|
||||||
|
try {
|
||||||
|
terminalSocket.value?.close();
|
||||||
|
} catch {}
|
||||||
|
try {
|
||||||
|
term.value.dispose();
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// terminal 相关代码 start
|
||||||
|
|
||||||
|
const initTerminal = (online: boolean = false): boolean => {
|
||||||
|
if (terminalElement.value) {
|
||||||
|
term.value.open(terminalElement.value);
|
||||||
|
term.value.loadAddon(fitAddon);
|
||||||
|
window.addEventListener('resize', changeTerminalSize);
|
||||||
|
if (online) {
|
||||||
|
term.value.onData((data) => sendMsg(data));
|
||||||
|
}
|
||||||
|
termReady.value = true;
|
||||||
|
}
|
||||||
|
return termReady.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
function changeTerminalSize() {
|
||||||
|
fitAddon.fit();
|
||||||
|
if (isWsOpen()) {
|
||||||
|
const { cols, rows } = term.value;
|
||||||
|
terminalSocket.value!.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'resize',
|
||||||
|
cols: cols,
|
||||||
|
rows: rows,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Support for Ctrl+MouseWheel to scaling fonts
|
||||||
|
* @param event WheelEvent
|
||||||
|
*/
|
||||||
|
const onTermWheel = (event: WheelEvent) => {
|
||||||
|
if (event.ctrlKey) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (event.deltaY > 0) {
|
||||||
|
// web font-size mini 12px
|
||||||
|
if (term.value.options.fontSize > 12) {
|
||||||
|
term.value.options.fontSize = term.value.options.fontSize - 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
term.value.options.fontSize = term.value.options.fontSize + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// terminal 相关代码 end
|
||||||
|
|
||||||
|
// websocket 相关代码 start
|
||||||
|
|
||||||
|
const initWebSocket = (endpoint_: string, args: string = '') => {
|
||||||
|
const href = window.location.href;
|
||||||
|
const protocol = href.split('//')[0] === 'http:' ? 'ws' : 'wss';
|
||||||
|
const host = href.split('//')[1].split('/')[0];
|
||||||
|
const endpoint = endpoint_.replace(/^\/+/, '');
|
||||||
|
terminalSocket.value = new WebSocket(
|
||||||
|
`${protocol}://${host}/${endpoint}?cols=${term.value.cols}&rows=${term.value.rows}&${args}`,
|
||||||
|
);
|
||||||
|
terminalSocket.value.onopen = runRealTerminal;
|
||||||
|
terminalSocket.value.onmessage = onWSReceive;
|
||||||
|
terminalSocket.value.onclose = closeRealTerminal;
|
||||||
|
terminalSocket.value.onerror = errorRealTerminal;
|
||||||
|
heartbeatTimer.value = setInterval(() => {
|
||||||
|
if (isWsOpen()) {
|
||||||
|
terminalSocket.value!.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'heartbeat',
|
||||||
|
timestamp: `${new Date().getTime()}`,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, 1000 * 10);
|
||||||
|
};
|
||||||
|
|
||||||
|
const runRealTerminal = () => {
|
||||||
|
webSocketReady.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onWSReceive = (message: MessageEvent) => {
|
||||||
|
const wsMsg = JSON.parse(message.data);
|
||||||
|
switch (wsMsg.type) {
|
||||||
|
case 'cmd': {
|
||||||
|
term.value.element && term.value.focus();
|
||||||
|
term.value.write(Base64.decode(wsMsg.data));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'heartbeat': {
|
||||||
|
latency.value = new Date().getTime() - wsMsg.timestamp;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const errorRealTerminal = (ex: any) => {
|
||||||
|
let message = ex.message;
|
||||||
|
if (!message) message = 'disconnected';
|
||||||
|
term.value.write(`\x1b[31m${message}\x1b[m\r\n`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeRealTerminal = (ev: CloseEvent) => {
|
||||||
|
if (heartbeatTimer.value) {
|
||||||
|
clearInterval(heartbeatTimer.value);
|
||||||
|
}
|
||||||
|
term.value.write(ev.reason);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isWsOpen = () => {
|
||||||
|
const readyState = terminalSocket.value && terminalSocket.value.readyState;
|
||||||
|
return readyState === 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
function sendMsg(data: string) {
|
||||||
|
if (isWsOpen()) {
|
||||||
|
terminalSocket.value!.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'cmd',
|
||||||
|
data: Base64.encode(data),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// websocket 相关代码 end
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
acceptParams,
|
||||||
|
onClose,
|
||||||
|
isWsOpen,
|
||||||
|
sendMsg,
|
||||||
|
getLatency: () => latency.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
onClose();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
#terminal {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
@ -131,7 +131,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, getCurrentInstance, watch, nextTick } from 'vue';
|
import { ref, getCurrentInstance, watch, nextTick } from 'vue';
|
||||||
import Terminal from '@/views/host/terminal/terminal/terminal.vue';
|
import Terminal from '@/components/terminal/index.vue';
|
||||||
import HostDialog from '@/views/host/terminal/terminal/host-create.vue';
|
import HostDialog from '@/views/host/terminal/terminal/host-create.vue';
|
||||||
import type Node from 'element-plus/es/components/tree/src/model/node';
|
import type Node from 'element-plus/es/components/tree/src/model/node';
|
||||||
import { ElTree } from 'element-plus';
|
import { ElTree } from 'element-plus';
|
||||||
@ -260,11 +260,11 @@ function quickInput(val: any) {
|
|||||||
if (val !== '' && ctx) {
|
if (val !== '' && ctx) {
|
||||||
if (isBatch.value) {
|
if (isBatch.value) {
|
||||||
for (const tab of terminalTabs.value) {
|
for (const tab of terminalTabs.value) {
|
||||||
ctx.refs[`t-${tab.index}`] && ctx.refs[`t-${tab.index}`][0].onSendMsg(val + '\n');
|
ctx.refs[`t-${tab.index}`] && ctx.refs[`t-${tab.index}`][0].sendMsg(val + '\n');
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ctx.refs[`t-${terminalValue.value}`] && ctx.refs[`t-${terminalValue.value}`][0].onSendMsg(val + '\n');
|
ctx.refs[`t-${terminalValue.value}`] && ctx.refs[`t-${terminalValue.value}`][0].sendMsg(val + '\n');
|
||||||
quickCmd.value = '';
|
quickCmd.value = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -275,12 +275,12 @@ function batchInput() {
|
|||||||
}
|
}
|
||||||
if (isBatch.value) {
|
if (isBatch.value) {
|
||||||
for (const tab of terminalTabs.value) {
|
for (const tab of terminalTabs.value) {
|
||||||
ctx.refs[`t-${tab.index}`] && ctx.refs[`t-${tab.index}`][0].onSendMsg(batchVal.value + '\n');
|
ctx.refs[`t-${tab.index}`] && ctx.refs[`t-${tab.index}`][0].sendMsg(batchVal.value + '\n');
|
||||||
}
|
}
|
||||||
batchVal.value = '';
|
batchVal.value = '';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ctx.refs[`t-${terminalValue.value}`] && ctx.refs[`t-${terminalValue.value}`][0].onSendMsg(batchVal.value + '\n');
|
ctx.refs[`t-${terminalValue.value}`] && ctx.refs[`t-${terminalValue.value}`][0].sendMsg(batchVal.value + '\n');
|
||||||
batchVal.value = '';
|
batchVal.value = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -313,8 +313,8 @@ const onReconnect = async (item: any) => {
|
|||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
ctx.refs[`t-${item.index}`] &&
|
ctx.refs[`t-${item.index}`] &&
|
||||||
ctx.refs[`t-${item.index}`][0].acceptParams({
|
ctx.refs[`t-${item.index}`][0].acceptParams({
|
||||||
wsID: item.wsID,
|
endpoint: '/api/v1/terminals',
|
||||||
terminalID: item.index,
|
args: `id=${item.wsID}`,
|
||||||
error: res.data ? '' : 'Failed to set up the connection. Please check the host information',
|
error: res.data ? '' : 'Failed to set up the connection. Please check the host information',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -345,8 +345,8 @@ const onConnTerminal = async (title: string, wsID: number, isLocal?: boolean) =>
|
|||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
ctx.refs[`t-${terminalValue.value}`] &&
|
ctx.refs[`t-${terminalValue.value}`] &&
|
||||||
ctx.refs[`t-${terminalValue.value}`][0].acceptParams({
|
ctx.refs[`t-${terminalValue.value}`][0].acceptParams({
|
||||||
wsID: wsID,
|
endpoint: '/api/v1/terminals',
|
||||||
terminalID: terminalValue.value,
|
args: `id=${wsID}`,
|
||||||
error: res.data ? '' : 'Authentication failed. Please check the host information !',
|
error: res.data ? '' : 'Authentication failed. Please check the host information !',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,241 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div :id="'terminal-' + terminalID" @wheel="onTermWheel"></div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, nextTick, onBeforeUnmount, watch } from 'vue';
|
|
||||||
import { Terminal } from 'xterm';
|
|
||||||
import { Base64 } from 'js-base64';
|
|
||||||
import 'xterm/css/xterm.css';
|
|
||||||
import { FitAddon } from 'xterm-addon-fit';
|
|
||||||
|
|
||||||
const terminalID = ref();
|
|
||||||
const wsID = ref();
|
|
||||||
interface WsProps {
|
|
||||||
terminalID: string;
|
|
||||||
wsID: number;
|
|
||||||
error: string;
|
|
||||||
}
|
|
||||||
const acceptParams = (props: WsProps) => {
|
|
||||||
terminalID.value = props.terminalID;
|
|
||||||
wsID.value = props.wsID;
|
|
||||||
nextTick(() => {
|
|
||||||
if (props.error.length !== 0) {
|
|
||||||
initErrorTerm(props.error);
|
|
||||||
} else {
|
|
||||||
initTerm();
|
|
||||||
window.addEventListener('resize', changeTerminalSize);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const fitAddon = new FitAddon();
|
|
||||||
const webSocketReady = ref(false);
|
|
||||||
const termReady = ref(false);
|
|
||||||
const terminalSocket = ref<WebSocket>();
|
|
||||||
const term = ref<Terminal>();
|
|
||||||
const heartbeatTimer = ref<number>();
|
|
||||||
const latency = ref(0);
|
|
||||||
|
|
||||||
const readyWatcher = watch(
|
|
||||||
() => webSocketReady.value && termReady.value,
|
|
||||||
(ready) => {
|
|
||||||
if (ready) {
|
|
||||||
changeTerminalSize();
|
|
||||||
readyWatcher(); // unwatch self
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const runRealTerminal = () => {
|
|
||||||
webSocketReady.value = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onWSReceive = (message: MessageEvent) => {
|
|
||||||
const wsMsg = JSON.parse(message.data);
|
|
||||||
switch (wsMsg.type) {
|
|
||||||
case 'cmd': {
|
|
||||||
if (term.value) {
|
|
||||||
term.value.element && term.value.focus();
|
|
||||||
term.value.write(Base64.decode(wsMsg.data));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'heartbeat': {
|
|
||||||
latency.value = new Date().getTime() - wsMsg.timestamp;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const errorRealTerminal = (ex: any) => {
|
|
||||||
let message = ex.message;
|
|
||||||
if (!message) message = 'disconnected';
|
|
||||||
if (term.value) {
|
|
||||||
term.value.write(`\x1b[31m${message}\x1b[m\r\n`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeRealTerminal = (ev: CloseEvent) => {
|
|
||||||
if (heartbeatTimer.value) {
|
|
||||||
clearInterval(heartbeatTimer.value);
|
|
||||||
}
|
|
||||||
if (term.value) {
|
|
||||||
term.value.write(ev.reason);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const initErrorTerm = (errorInfo: string) => {
|
|
||||||
let ifm = document.getElementById('terminal-' + terminalID.value) as HTMLInputElement | null;
|
|
||||||
term.value = new Terminal({
|
|
||||||
lineHeight: 1.2,
|
|
||||||
fontSize: 12,
|
|
||||||
fontFamily: "Monaco, Menlo, Consolas, 'Courier New', monospace",
|
|
||||||
theme: {
|
|
||||||
background: '#000000',
|
|
||||||
},
|
|
||||||
cursorBlink: true,
|
|
||||||
cursorStyle: 'underline',
|
|
||||||
scrollback: 100,
|
|
||||||
tabStopWidth: 4,
|
|
||||||
});
|
|
||||||
if (ifm) {
|
|
||||||
term.value.open(ifm);
|
|
||||||
term.value.write(errorInfo);
|
|
||||||
term.value.loadAddon(fitAddon);
|
|
||||||
fitAddon.fit();
|
|
||||||
termReady.value = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const initTerm = () => {
|
|
||||||
let ifm = document.getElementById('terminal-' + terminalID.value) as HTMLInputElement | null;
|
|
||||||
let href = window.location.href;
|
|
||||||
let protocol = href.split('//')[0] === 'http:' ? 'ws' : 'wss';
|
|
||||||
let ipLocal = href.split('//')[1].split('/')[0];
|
|
||||||
term.value = new Terminal({
|
|
||||||
lineHeight: 1.2,
|
|
||||||
fontSize: 12,
|
|
||||||
fontFamily: "Monaco, Menlo, Consolas, 'Courier New', monospace",
|
|
||||||
theme: {
|
|
||||||
background: '#000000',
|
|
||||||
},
|
|
||||||
cursorBlink: true,
|
|
||||||
cursorStyle: 'underline',
|
|
||||||
scrollback: 100,
|
|
||||||
tabStopWidth: 4,
|
|
||||||
});
|
|
||||||
if (ifm) {
|
|
||||||
term.value.open(ifm);
|
|
||||||
terminalSocket.value = new WebSocket(
|
|
||||||
`${protocol}://${ipLocal}/api/v1/terminals?id=${wsID.value}&cols=${term.value.cols}&rows=${term.value.rows}`,
|
|
||||||
);
|
|
||||||
terminalSocket.value.onopen = runRealTerminal;
|
|
||||||
terminalSocket.value.onmessage = onWSReceive;
|
|
||||||
terminalSocket.value.onclose = closeRealTerminal;
|
|
||||||
terminalSocket.value.onerror = errorRealTerminal;
|
|
||||||
heartbeatTimer.value = setInterval(() => {
|
|
||||||
if (isWsOpen()) {
|
|
||||||
terminalSocket.value!.send(
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'heartbeat',
|
|
||||||
timestamp: `${new Date().getTime()}`,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, 1000 * 10);
|
|
||||||
term.value.onData((data: any) => {
|
|
||||||
if (isWsOpen()) {
|
|
||||||
terminalSocket.value!.send(
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'cmd',
|
|
||||||
data: Base64.encode(data),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
term.value.loadAddon(fitAddon);
|
|
||||||
termReady.value = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const fitTerm = () => {
|
|
||||||
fitAddon.fit();
|
|
||||||
};
|
|
||||||
|
|
||||||
const isWsOpen = () => {
|
|
||||||
const readyState = terminalSocket.value && terminalSocket.value.readyState;
|
|
||||||
return readyState === 1;
|
|
||||||
};
|
|
||||||
|
|
||||||
function onClose() {
|
|
||||||
window.removeEventListener('resize', changeTerminalSize);
|
|
||||||
try {
|
|
||||||
terminalSocket.value?.close();
|
|
||||||
} catch {}
|
|
||||||
try {
|
|
||||||
term.value?.dispose();
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSendMsg(command: string) {
|
|
||||||
terminalSocket.value?.send(
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'cmd',
|
|
||||||
data: Base64.encode(command),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function changeTerminalSize() {
|
|
||||||
fitTerm();
|
|
||||||
const { cols, rows } = term.value!;
|
|
||||||
if (isWsOpen()) {
|
|
||||||
terminalSocket.value!.send(
|
|
||||||
JSON.stringify({
|
|
||||||
type: 'resize',
|
|
||||||
cols: cols,
|
|
||||||
rows: rows,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Support for Ctrl+MouseWheel to scaling fonts
|
|
||||||
* @param event WheelEvent
|
|
||||||
*/
|
|
||||||
const onTermWheel = (event: WheelEvent) => {
|
|
||||||
if (event.ctrlKey) {
|
|
||||||
event.preventDefault();
|
|
||||||
if (term.value) {
|
|
||||||
if (event.deltaY > 0) {
|
|
||||||
// web font-size mini 12px
|
|
||||||
if (term.value.options.fontSize > 12) {
|
|
||||||
term.value.options.fontSize = term.value.options.fontSize - 1;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
term.value.options.fontSize = term.value.options.fontSize + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
defineExpose({
|
|
||||||
acceptParams,
|
|
||||||
onClose,
|
|
||||||
isWsOpen,
|
|
||||||
onSendMsg,
|
|
||||||
getLatency: () => latency.value,
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
onClose();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
#terminal {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
Loading…
x
Reference in New Issue
Block a user