2023-08-31 21:49:20 +08:00
|
|
|
|
<template>
|
2024-02-29 22:12:50 +08:00
|
|
|
|
<div id="terminal-body" :style="{ height }">
|
2023-08-31 21:49:20 +08:00
|
|
|
|
<div ref="terminalRef" class="terminal" />
|
|
|
|
|
|
|
|
|
|
|
|
<TerminalSearch ref="terminalSearchRef" :search-addon="state.addon.search" @close="focus" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
2025-02-27 19:40:31 +08:00
|
|
|
|
import '@xterm/xterm/css/xterm.css';
|
|
|
|
|
|
import { Terminal, ITheme } from '@xterm/xterm';
|
|
|
|
|
|
import { FitAddon } from '@xterm/addon-fit';
|
|
|
|
|
|
import { SearchAddon } from '@xterm/addon-search';
|
|
|
|
|
|
import { WebLinksAddon } from '@xterm/addon-web-links';
|
2023-08-31 21:49:20 +08:00
|
|
|
|
|
|
|
|
|
|
import { storeToRefs } from 'pinia';
|
|
|
|
|
|
import { useThemeConfig } from '@/store/themeConfig';
|
|
|
|
|
|
import { ref, nextTick, reactive, onMounted, onBeforeUnmount, watch } from 'vue';
|
|
|
|
|
|
import TerminalSearch from './TerminalSearch.vue';
|
|
|
|
|
|
import { TerminalStatus } from './common';
|
2025-04-15 21:42:31 +08:00
|
|
|
|
import { useDebounceFn, useEventListener } from '@vueuse/core';
|
2024-02-23 22:53:17 +08:00
|
|
|
|
import themes from './themes';
|
2024-04-12 07:53:42 +00:00
|
|
|
|
import { TrzszFilter } from 'trzsz';
|
2024-11-20 22:43:53 +08:00
|
|
|
|
import { useI18n } from 'vue-i18n';
|
|
|
|
|
|
|
|
|
|
|
|
const { t } = useI18n();
|
2023-08-31 21:49:20 +08:00
|
|
|
|
|
|
|
|
|
|
const props = defineProps({
|
2024-02-29 22:12:50 +08:00
|
|
|
|
// mounted时,是否执行init方法
|
|
|
|
|
|
mountInit: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: true,
|
|
|
|
|
|
},
|
2023-08-31 21:49:20 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 初始化执行命令
|
|
|
|
|
|
*/
|
|
|
|
|
|
cmd: { type: String },
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 连接url
|
|
|
|
|
|
*/
|
|
|
|
|
|
socketUrl: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
},
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 高度
|
|
|
|
|
|
*/
|
|
|
|
|
|
height: {
|
|
|
|
|
|
type: [String, Number],
|
|
|
|
|
|
default: '100%',
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
const emit = defineEmits(['statusChange']);
|
|
|
|
|
|
|
|
|
|
|
|
const terminalRef: any = ref(null);
|
|
|
|
|
|
const terminalSearchRef: any = ref(null);
|
|
|
|
|
|
|
|
|
|
|
|
const { themeConfig } = storeToRefs(useThemeConfig());
|
|
|
|
|
|
|
|
|
|
|
|
// 终端实例
|
|
|
|
|
|
let term: Terminal;
|
|
|
|
|
|
let socket: WebSocket;
|
|
|
|
|
|
let pingInterval: any;
|
|
|
|
|
|
|
|
|
|
|
|
const state = reactive({
|
|
|
|
|
|
// 插件
|
|
|
|
|
|
addon: {
|
|
|
|
|
|
fit: null as any,
|
|
|
|
|
|
search: null as any,
|
|
|
|
|
|
weblinks: null as any,
|
|
|
|
|
|
},
|
2024-05-21 12:34:26 +08:00
|
|
|
|
status: -11,
|
2023-08-31 21:49:20 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
2024-02-29 22:12:50 +08:00
|
|
|
|
if (props.mountInit) {
|
2023-08-31 21:49:20 +08:00
|
|
|
|
init();
|
2024-02-29 22:12:50 +08:00
|
|
|
|
}
|
2023-08-31 21:49:20 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => state.status,
|
|
|
|
|
|
() => {
|
|
|
|
|
|
emit('statusChange', state.status);
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2024-02-23 22:53:17 +08:00
|
|
|
|
// 监听 themeConfig terminalTheme配置的变化
|
|
|
|
|
|
watch(
|
|
|
|
|
|
() => themeConfig.value.terminalTheme,
|
|
|
|
|
|
() => {
|
|
|
|
|
|
term.options.theme = getTerminalTheme();
|
|
|
|
|
|
}
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2023-08-31 21:49:20 +08:00
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
|
close();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
function init() {
|
2024-05-21 12:34:26 +08:00
|
|
|
|
state.status = TerminalStatus.NoConnected;
|
2023-08-31 21:49:20 +08:00
|
|
|
|
if (term) {
|
|
|
|
|
|
console.log('重新连接...');
|
|
|
|
|
|
close();
|
|
|
|
|
|
}
|
2024-02-29 22:12:50 +08:00
|
|
|
|
nextTick(() => {
|
|
|
|
|
|
initTerm();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-05-21 12:34:26 +08:00
|
|
|
|
async function initTerm() {
|
2023-08-31 21:49:20 +08:00
|
|
|
|
term = new Terminal({
|
|
|
|
|
|
fontSize: themeConfig.value.terminalFontSize || 15,
|
|
|
|
|
|
fontWeight: themeConfig.value.terminalFontWeight || 'normal',
|
|
|
|
|
|
fontFamily: 'JetBrainsMono, monaco, Consolas, Lucida Console, monospace',
|
|
|
|
|
|
cursorBlink: true,
|
|
|
|
|
|
disableStdin: false,
|
|
|
|
|
|
allowProposedApi: true,
|
2024-02-07 06:37:59 +00:00
|
|
|
|
fastScrollModifier: 'ctrl',
|
2024-02-23 22:53:17 +08:00
|
|
|
|
theme: getTerminalTheme(),
|
2023-08-31 21:49:20 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2024-02-23 22:53:17 +08:00
|
|
|
|
term.open(terminalRef.value);
|
2023-08-31 21:49:20 +08:00
|
|
|
|
|
2024-04-12 17:07:28 +08:00
|
|
|
|
// 注册自适应组件
|
|
|
|
|
|
const fitAddon = new FitAddon();
|
|
|
|
|
|
state.addon.fit = fitAddon;
|
|
|
|
|
|
term.loadAddon(fitAddon);
|
|
|
|
|
|
fitTerminal();
|
|
|
|
|
|
// 注册窗口大小监听器
|
2025-04-15 21:42:31 +08:00
|
|
|
|
useEventListener('resize', useDebounceFn(fitTerminal, 400));
|
2023-08-31 21:49:20 +08:00
|
|
|
|
|
2024-04-12 17:07:28 +08:00
|
|
|
|
initSocket();
|
|
|
|
|
|
// 注册其他插件
|
2024-04-12 07:53:42 +00:00
|
|
|
|
loadAddon();
|
2023-08-31 21:49:20 +08:00
|
|
|
|
|
2024-02-29 22:12:50 +08:00
|
|
|
|
// 注册自定义快捷键
|
|
|
|
|
|
term.attachCustomKeyEventHandler((event: KeyboardEvent) => {
|
|
|
|
|
|
// 注册搜索键 ctrl + f
|
|
|
|
|
|
if (event.key === 'f' && (event.ctrlKey || event.metaKey) && event.type === 'keydown') {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
terminalSearchRef.value.open();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return true;
|
|
|
|
|
|
});
|
2024-02-23 22:53:17 +08:00
|
|
|
|
}
|
2023-08-31 21:49:20 +08:00
|
|
|
|
|
|
|
|
|
|
function initSocket() {
|
2024-03-28 22:20:39 +08:00
|
|
|
|
if (!props.socketUrl) {
|
|
|
|
|
|
return;
|
2023-08-31 21:49:20 +08:00
|
|
|
|
}
|
2024-03-28 22:20:39 +08:00
|
|
|
|
socket = new WebSocket(`${props.socketUrl}&rows=${term?.rows}&cols=${term?.cols}`);
|
2023-08-31 21:49:20 +08:00
|
|
|
|
// 监听socket连接
|
|
|
|
|
|
socket.onopen = () => {
|
2024-02-23 22:53:17 +08:00
|
|
|
|
// 注册心跳
|
|
|
|
|
|
pingInterval = setInterval(sendPing, 15000);
|
|
|
|
|
|
state.status = TerminalStatus.Connected;
|
|
|
|
|
|
|
|
|
|
|
|
focus();
|
2024-05-21 12:34:26 +08:00
|
|
|
|
fitTerminal();
|
2024-02-23 22:53:17 +08:00
|
|
|
|
|
|
|
|
|
|
// 如果有初始要执行的命令,则发送执行命令
|
|
|
|
|
|
if (props.cmd) {
|
|
|
|
|
|
sendCmd(props.cmd + ' \r');
|
|
|
|
|
|
}
|
2023-08-31 21:49:20 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 监听socket错误信息
|
|
|
|
|
|
socket.onerror = (e: Event) => {
|
2024-11-20 22:43:53 +08:00
|
|
|
|
term.writeln(`\r\n\x1b[31m${t('components.terminal.connErrMsg')}`);
|
2023-08-31 21:49:20 +08:00
|
|
|
|
state.status = TerminalStatus.Error;
|
|
|
|
|
|
console.log('连接错误', e);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
socket.onclose = (e: CloseEvent) => {
|
|
|
|
|
|
console.log('terminal socket close...', e.reason);
|
|
|
|
|
|
state.status = TerminalStatus.Disconnected;
|
|
|
|
|
|
};
|
2024-04-12 07:53:42 +00:00
|
|
|
|
}
|
2023-08-31 21:49:20 +08:00
|
|
|
|
|
2024-04-12 07:53:42 +00:00
|
|
|
|
function loadAddon() {
|
|
|
|
|
|
// 注册搜索组件
|
|
|
|
|
|
const searchAddon = new SearchAddon();
|
|
|
|
|
|
state.addon.search = searchAddon;
|
|
|
|
|
|
term.loadAddon(searchAddon);
|
|
|
|
|
|
|
|
|
|
|
|
// 注册 url link组件
|
|
|
|
|
|
const weblinks = new WebLinksAddon();
|
|
|
|
|
|
state.addon.weblinks = weblinks;
|
|
|
|
|
|
term.loadAddon(weblinks);
|
|
|
|
|
|
|
|
|
|
|
|
// 注册 trzsz
|
|
|
|
|
|
// initialize trzsz filter
|
|
|
|
|
|
const trzsz = new TrzszFilter({
|
|
|
|
|
|
// write the server output to the terminal
|
|
|
|
|
|
writeToTerminal: (data: any) => term.write(typeof data === 'string' ? data : new Uint8Array(data)),
|
|
|
|
|
|
// send the user input to the server
|
|
|
|
|
|
sendToServer: sendCmd,
|
|
|
|
|
|
// the terminal columns
|
|
|
|
|
|
terminalColumns: term.cols,
|
|
|
|
|
|
// there is a windows shell
|
|
|
|
|
|
isWindowsShell: false,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// let trzsz process the server output
|
|
|
|
|
|
socket?.addEventListener('message', (e) => trzsz.processServerOutput(e.data));
|
|
|
|
|
|
// let trzsz process the user input
|
|
|
|
|
|
term.onData((data) => trzsz.processTerminalInput(data));
|
|
|
|
|
|
term.onBinary((data) => trzsz.processBinaryInput(data));
|
|
|
|
|
|
term.onResize((size) => {
|
|
|
|
|
|
sendResize(size.cols, size.rows);
|
|
|
|
|
|
// tell trzsz the terminal columns has been changed
|
|
|
|
|
|
trzsz.setTerminalColumns(size.cols);
|
|
|
|
|
|
});
|
|
|
|
|
|
// enable drag files or directories to upload
|
|
|
|
|
|
terminalRef.value.addEventListener('dragover', (event: Event) => event.preventDefault());
|
|
|
|
|
|
terminalRef.value.addEventListener('drop', (event: any) => {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
trzsz
|
|
|
|
|
|
.uploadFiles(event.dataTransfer.items)
|
|
|
|
|
|
.then(() => console.log('upload success'))
|
2024-04-12 17:07:28 +08:00
|
|
|
|
.catch((err: any) => console.log(err));
|
2024-04-12 07:53:42 +00:00
|
|
|
|
});
|
2023-08-31 21:49:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-03-28 22:20:39 +08:00
|
|
|
|
// 写入内容至终端
|
|
|
|
|
|
const write2Term = (data: any) => {
|
|
|
|
|
|
term.write(data);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const writeln2Term = (data: any) => {
|
|
|
|
|
|
term.writeln(data);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2024-02-23 22:53:17 +08:00
|
|
|
|
const getTerminalTheme = () => {
|
|
|
|
|
|
const terminalTheme = themeConfig.value.terminalTheme;
|
|
|
|
|
|
// 如果不是自定义主题,则返回内置主题
|
|
|
|
|
|
if (terminalTheme != 'custom') {
|
2025-02-27 19:40:31 +08:00
|
|
|
|
return (themes as any)[terminalTheme];
|
2024-02-23 22:53:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 自定义主题
|
|
|
|
|
|
return {
|
|
|
|
|
|
foreground: themeConfig.value.terminalForeground || '#7e9192', //字体
|
|
|
|
|
|
background: themeConfig.value.terminalBackground || '#002833', //背景色
|
|
|
|
|
|
cursor: themeConfig.value.terminalCursor || '#268F81', //设置光标
|
|
|
|
|
|
// cursorAccent: "red", // 光标停止颜色
|
|
|
|
|
|
} as ITheme;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 自适应终端
|
|
|
|
|
|
const fitTerminal = () => {
|
|
|
|
|
|
state.addon.fit.fit();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const focus = () => {
|
|
|
|
|
|
setTimeout(() => term.focus(), 300);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const clear = () => {
|
|
|
|
|
|
term.clear();
|
|
|
|
|
|
term.clearSelection();
|
|
|
|
|
|
term.focus();
|
|
|
|
|
|
};
|
2023-08-31 21:49:20 +08:00
|
|
|
|
|
|
|
|
|
|
enum MsgType {
|
|
|
|
|
|
Resize = 1,
|
|
|
|
|
|
Data = 2,
|
|
|
|
|
|
Ping = 3,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const send = (msg: any) => {
|
2024-03-28 22:20:39 +08:00
|
|
|
|
state.status == TerminalStatus.Connected && socket?.send(msg);
|
2023-08-31 21:49:20 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const sendResize = (cols: number, rows: number) => {
|
2024-02-23 22:53:17 +08:00
|
|
|
|
send(`${MsgType.Resize}|${rows}|${cols}`);
|
2023-08-31 21:49:20 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const sendPing = () => {
|
2024-02-23 22:53:17 +08:00
|
|
|
|
send(`${MsgType.Ping}|ping`);
|
2023-08-31 21:49:20 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
function sendCmd(key: any) {
|
2024-02-23 22:53:17 +08:00
|
|
|
|
send(`${MsgType.Data}|${key}`);
|
2023-08-31 21:49:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function closeSocket() {
|
|
|
|
|
|
// 关闭 websocket
|
|
|
|
|
|
socket && socket.readyState === 1 && socket.close();
|
|
|
|
|
|
// 清除 ping
|
|
|
|
|
|
pingInterval && clearInterval(pingInterval);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function close() {
|
|
|
|
|
|
console.log('in terminal body close');
|
|
|
|
|
|
closeSocket();
|
|
|
|
|
|
if (term) {
|
|
|
|
|
|
state.addon.search.dispose();
|
|
|
|
|
|
state.addon.fit.dispose();
|
|
|
|
|
|
state.addon.weblinks.dispose();
|
|
|
|
|
|
term.dispose();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const getStatus = (): TerminalStatus => {
|
|
|
|
|
|
return state.status;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2024-03-28 22:20:39 +08:00
|
|
|
|
defineExpose({ init, fitTerminal, focus, clear, close, getStatus, sendResize, write2Term, writeln2Term });
|
2023-08-31 21:49:20 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
<style lang="scss">
|
|
|
|
|
|
#terminal-body {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
|
|
|
|
|
|
.terminal {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
|
2024-03-28 22:20:39 +08:00
|
|
|
|
// .xterm .xterm-viewport {
|
|
|
|
|
|
// overflow-y: hidden;
|
|
|
|
|
|
// }
|
2023-08-31 21:49:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|