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>
|
||||
@@ -1688,7 +1688,24 @@ export default {
|
||||
download: "Download",
|
||||
pdfViewer: "In-browser preview",
|
||||
pdfViewerErr: "Sorry, your browser does not support PDF preview!",
|
||||
}
|
||||
},
|
||||
terminal: {
|
||||
start: "Start Page",
|
||||
new: "To Create",
|
||||
more: "More",
|
||||
reload: "Reload Current Tab",
|
||||
reloadTip: "Are you sure you want to refresh and reconnect the current [{num}] terminal link?",
|
||||
current: "Close Current Tab",
|
||||
other: "Close Other Tabs",
|
||||
otherTip: "Confirm that you want to close other terminal links?",
|
||||
all: "Close All Tabs",
|
||||
allTip: "Confirmed to close all terminal links?",
|
||||
closeTip: "Confirm that you want to close the [{num}] terminal link?",
|
||||
hostSelectTitle: "Select the created host to connect to",
|
||||
hostSelectShow: "Open Selection",
|
||||
hostSelectMore: "Load More {num}",
|
||||
hostSelectHeader: "Host List",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1688,7 +1688,24 @@ export default {
|
||||
download: "下载",
|
||||
pdfViewer: "浏览器内预览",
|
||||
pdfViewerErr: "很抱歉,您的浏览器不支持 PDF 预览!",
|
||||
}
|
||||
},
|
||||
terminal: {
|
||||
start: "开始页",
|
||||
new: "去创建",
|
||||
more: "更多",
|
||||
reload: "断开重连",
|
||||
reloadTip: "确认要刷新重连当前 【{num}】 终端链接?",
|
||||
current: "关闭当前",
|
||||
other: "关闭其他",
|
||||
otherTip: "确认要关闭其他终端链接?",
|
||||
all: "关闭全部",
|
||||
allTip: "确认要关闭全部终端链接?",
|
||||
closeTip: "确认要关闭 【{num}】 终端链接?",
|
||||
hostSelectTitle: "选择已创建的主机进行连接",
|
||||
hostSelectShow: "打开选择",
|
||||
hostSelectMore: "加载更多 {num}",
|
||||
hostSelectHeader: "主机列表",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
385
src/views/tool/terminal/index.vue
Normal file
385
src/views/tool/terminal/index.vue
Normal file
@@ -0,0 +1,385 @@
|
||||
<script lang="ts" setup>
|
||||
import { PageContainer } from 'antdv-pro-layout';
|
||||
import { Modal } from 'ant-design-vue/lib';
|
||||
import TerminalSSH from '@/components/TerminalSSH/index.vue';
|
||||
import TerminalTelnet from '@/components/TerminalTelnet/index.vue';
|
||||
import { reactive, toRaw } from 'vue';
|
||||
import { parseDuration } from '@/utils/date-utils';
|
||||
import { listNeHost } from '@/api/ne/neHost';
|
||||
import { RESULT_CODE_SUCCESS } from '@/constants/result-constants';
|
||||
import { useRouter } from 'vue-router';
|
||||
import useI18n from '@/hooks/useI18n';
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
/**主机对象信息状态类型 */
|
||||
type HostStateType = {
|
||||
/**显示主机列表 */
|
||||
show: boolean;
|
||||
/**加载等待 */
|
||||
loading: boolean;
|
||||
/**查询参数 */
|
||||
params: {
|
||||
pageNum: number;
|
||||
pageSize: number;
|
||||
};
|
||||
/**数据总数 */
|
||||
total: number;
|
||||
data: Record<string, any>[];
|
||||
};
|
||||
/**主机对象信息状态 */
|
||||
const hostState: HostStateType = reactive({
|
||||
show: false,
|
||||
loading: false,
|
||||
params: {
|
||||
pageNum: 1,
|
||||
pageSize: 20,
|
||||
},
|
||||
total: 0,
|
||||
data: [],
|
||||
});
|
||||
|
||||
/**查询主机信息列表, pageNum初始页数 */
|
||||
function fnGetHostList(pageNum?: number) {
|
||||
if (hostState.loading) return;
|
||||
hostState.loading = true;
|
||||
if (pageNum) {
|
||||
hostState.params.pageNum = pageNum;
|
||||
}
|
||||
// 超过总数不请求
|
||||
if (hostState.data.length >= hostState.total && hostState.total !== 0) {
|
||||
hostState.loading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
listNeHost(toRaw(hostState.params)).then(res => {
|
||||
if (res.code === RESULT_CODE_SUCCESS && Array.isArray(res.rows)) {
|
||||
hostState.total = res.total;
|
||||
hostState.data = hostState.data.concat(res.rows);
|
||||
// 页数+
|
||||
if (hostState.data.length < hostState.total) {
|
||||
hostState.params.pageNum += 1;
|
||||
}
|
||||
}
|
||||
hostState.loading = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**选择主机 */
|
||||
function fnSelectHost() {
|
||||
if (hostState.loading) return;
|
||||
hostState.show = true;
|
||||
fnGetHostList(1);
|
||||
}
|
||||
|
||||
/**连接主机 */
|
||||
function fnConnectHost(data: Record<string, any>) {
|
||||
const id = `${Date.now()}`;
|
||||
tabState.panes.push({
|
||||
id,
|
||||
status: false,
|
||||
host: data,
|
||||
});
|
||||
tabState.activeKey = id;
|
||||
}
|
||||
|
||||
/**标签对象信息状态类型 */
|
||||
type TabStateType = {
|
||||
/**激活选中 */
|
||||
activeKey: string;
|
||||
/**页签数据 */
|
||||
panes: {
|
||||
id: string;
|
||||
status: boolean;
|
||||
host: Record<string, any>;
|
||||
connectStamp?: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
/**标签对象信息状态 */
|
||||
const tabState: TabStateType = reactive({
|
||||
activeKey: '0',
|
||||
panes: [
|
||||
{
|
||||
id: '0',
|
||||
host: {
|
||||
hostId: '0',
|
||||
title: t('views.tool.terminal.start'),
|
||||
type: '0',
|
||||
},
|
||||
status: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
/**
|
||||
* 终端连接状态
|
||||
* @param data 主机连接结果
|
||||
*/
|
||||
function fnTerminalConnect(data: Record<string, any>) {
|
||||
const { id, timeStamp } = data;
|
||||
const seconds = timeStamp / 1000;
|
||||
// 获取当前项下标
|
||||
const tab = tabState.panes.find(item => item.id === id);
|
||||
if (tab) {
|
||||
tab.status = true;
|
||||
tab.connectStamp = parseDuration(seconds);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 终端关闭状态
|
||||
* @param data 主机连接结果
|
||||
*/
|
||||
function fnTerminalClose(data: Record<string, any>) {
|
||||
const { id } = data;
|
||||
// 获取当前项下标
|
||||
const tab = tabState.panes.find(item => item.id === id);
|
||||
if (tab) {
|
||||
tab.status = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签更多菜单项
|
||||
* @param key 菜单key
|
||||
*/
|
||||
function fnTabMenu(key: string | number) {
|
||||
// 跳转主机创建
|
||||
if (key === 'new') {
|
||||
router.push({ name: 'NeHost_2135' });
|
||||
}
|
||||
// 刷新当前
|
||||
if (key === 'reload') {
|
||||
const tabIndex = tabState.panes.findIndex(
|
||||
item => item.id === tabState.activeKey
|
||||
);
|
||||
if (tabIndex) {
|
||||
const tab = tabState.panes[tabIndex];
|
||||
Modal.confirm({
|
||||
title: t('common.tipTitle'),
|
||||
content: t('views.tool.terminal.reloadTip', {
|
||||
num: `${tab.host.hostType} - ${tab.host.title}`,
|
||||
}),
|
||||
onOk() {
|
||||
tabState.panes.splice(tabIndex, 1);
|
||||
tab.host && fnConnectHost(tab.host);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
// 关闭当前
|
||||
if (key === 'current') {
|
||||
fnTabClose(tabState.activeKey);
|
||||
}
|
||||
// 关闭其他
|
||||
if (key === 'other') {
|
||||
Modal.confirm({
|
||||
title: t('common.tipTitle'),
|
||||
content: t('views.tool.terminal.otherTip'),
|
||||
onOk() {
|
||||
hostState.show = false;
|
||||
tabState.panes = tabState.panes.filter(
|
||||
tab => tab.id === '0' || tab.id === tabState.activeKey
|
||||
);
|
||||
tabState.activeKey = tabState.activeKey;
|
||||
},
|
||||
});
|
||||
}
|
||||
// 关闭全部
|
||||
if (key === 'all') {
|
||||
Modal.confirm({
|
||||
title: t('common.tipTitle'),
|
||||
content: t('views.tool.terminal.allTip'),
|
||||
onOk() {
|
||||
hostState.show = false;
|
||||
tabState.panes.splice(1);
|
||||
tabState.activeKey = '0';
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导航标签关闭
|
||||
* @param id 标签的key
|
||||
*/
|
||||
function fnTabClose(id: string) {
|
||||
// 获取当前项下标
|
||||
const tabIndex = tabState.panes.findIndex(tab => tab.id === id);
|
||||
if (tabIndex === -1) return;
|
||||
const item = tabState.panes[tabIndex];
|
||||
Modal.confirm({
|
||||
title: t('common.tipTitle'),
|
||||
content: t('views.tool.terminal.closeTip', {
|
||||
num: `${item.host.hostType} - ${item.host.title}`,
|
||||
}),
|
||||
onOk() {
|
||||
tabState.panes.splice(tabIndex, 1);
|
||||
// 激活前一项标签
|
||||
tabState.activeKey = tabState.panes[tabIndex - 1].id;
|
||||
},
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<PageContainer>
|
||||
<a-card :bordered="false" :body-style="{ padding: '12px' }">
|
||||
<a-tabs
|
||||
class="terminal-tabs"
|
||||
hide-add
|
||||
tab-position="top"
|
||||
type="editable-card"
|
||||
:tab-bar-gutter="8"
|
||||
:tab-bar-style="{ margin: '0' }"
|
||||
v-model:activeKey="tabState.activeKey"
|
||||
@edit="(hostId:any) => fnTabClose(hostId as string)"
|
||||
>
|
||||
<a-tab-pane
|
||||
v-for="pane in tabState.panes"
|
||||
:key="pane.id"
|
||||
:closable="tabState.panes.length > 1"
|
||||
>
|
||||
<template #tab>
|
||||
<a-badge
|
||||
:status="pane.status ? 'success' : 'error'"
|
||||
:text="pane.host.title"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<div style="height: 650px">
|
||||
<TerminalSSH
|
||||
v-if="pane.id !== '0' && pane.host.hostType === 'ssh'"
|
||||
:id="pane.id"
|
||||
:hostId="pane.host.hostId"
|
||||
@connect="fnTerminalConnect"
|
||||
@close="fnTerminalClose"
|
||||
>
|
||||
</TerminalSSH>
|
||||
|
||||
<TerminalTelnet
|
||||
v-if="pane.id !== '0' && pane.host.hostType === 'telnet'"
|
||||
:id="pane.id"
|
||||
:hostId="pane.host.hostId"
|
||||
init-cmd="help"
|
||||
@connect="fnTerminalConnect"
|
||||
@close="fnTerminalClose"
|
||||
>
|
||||
</TerminalTelnet>
|
||||
|
||||
<div v-if="pane.id === '0'">
|
||||
<a-list
|
||||
v-show="tabState.activeKey === '0' && hostState.show"
|
||||
:header="t('views.tool.terminal.hostSelectHeader')"
|
||||
:grid="{ gutter: 16, column: 4 }"
|
||||
:data-source="hostState.data"
|
||||
style="height: 650px; overflow-x: hidden"
|
||||
>
|
||||
<template #loadMore>
|
||||
<div
|
||||
:style="{
|
||||
textAlign: 'center',
|
||||
marginTop: '12px',
|
||||
height: '32px',
|
||||
lineHeight: '32px',
|
||||
}"
|
||||
>
|
||||
<a-button
|
||||
@click="fnGetHostList()"
|
||||
:loading="hostState.loading"
|
||||
>
|
||||
{{
|
||||
t('views.tool.terminal.hostSelectMore', {
|
||||
num: hostState.total - hostState.data.length,
|
||||
})
|
||||
}}
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
<template #renderItem="{ item }">
|
||||
<a-list-item>
|
||||
<a-card size="small" :title="item.title">
|
||||
<template #extra>
|
||||
<a-button
|
||||
type="primary"
|
||||
shape="round"
|
||||
size="small"
|
||||
@click="fnConnectHost(item)"
|
||||
>
|
||||
<template #icon><LinkOutlined /></template>
|
||||
{{ item.hostType.toUpperCase() }}
|
||||
</a-button>
|
||||
</template>
|
||||
<div>{{ `${item.addr}:${item.port}` }}</div>
|
||||
</a-card>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
<a-result
|
||||
:title="t('views.tool.terminal.hostSelectTitle')"
|
||||
v-show="tabState.activeKey === '0' && !hostState.show"
|
||||
>
|
||||
<template #extra>
|
||||
<a-button
|
||||
key="hostState"
|
||||
type="primary"
|
||||
@click="fnSelectHost()"
|
||||
>
|
||||
{{ t('views.tool.terminal.hostSelectShow') }}
|
||||
</a-button>
|
||||
</template>
|
||||
</a-result>
|
||||
</div>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<template #rightExtra>
|
||||
<a-space :size="8" align="center">
|
||||
<a-tooltip placement="topRight">
|
||||
<template #title>
|
||||
{{ t('views.tool.terminal.new') }}
|
||||
</template>
|
||||
<a-button type="default" shape="circle" @click="fnTabMenu('new')">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<div v-show="tabState.activeKey !== '0'">
|
||||
<a-tooltip placement="topRight">
|
||||
<template #title>
|
||||
{{ t('views.tool.terminal.more') }}
|
||||
</template>
|
||||
<a-dropdown
|
||||
:trigger="['click', 'hover']"
|
||||
placement="bottomRight"
|
||||
>
|
||||
<a-button type="ghost" shape="circle">
|
||||
<template #icon><DashOutlined /></template>
|
||||
</a-button>
|
||||
<template #overlay>
|
||||
<a-menu @click="({ key }:any) => fnTabMenu(key)">
|
||||
<a-menu-item key="reload">
|
||||
{{ t('views.tool.terminal.reload') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="current">
|
||||
{{ t('views.tool.terminal.current') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="other">
|
||||
{{ t('views.tool.terminal.other') }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="all">
|
||||
{{ t('views.tool.terminal.all') }}
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-tabs>
|
||||
</a-card>
|
||||
</PageContainer>
|
||||
</template>
|
||||
|
||||
<style lang="less" scoped></style>
|
||||
Reference in New Issue
Block a user