refactor: 连接终端前端重构.
This commit is contained in:
@@ -4,7 +4,8 @@ import { getPreference, updatePreference } from '@/api/user/preference';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { useDark } from '@vueuse/core';
|
||||
import { DEFAULT_SCHEMA } from '@/views/host/terminal/types/terminal.theme';
|
||||
import TerminalDispatcher from '@/views/host/terminal/handler/TerminalDispatcher';
|
||||
import TerminalDispatcher from '@/views/host/terminal/handler/terminal-dispatcher';
|
||||
import TerminalTabManager from '@/views/host/terminal/handler/terminal-tab-manager';
|
||||
|
||||
// 暗色主题
|
||||
export const DarkTheme = {
|
||||
@@ -29,6 +30,7 @@ export default defineStore('terminal', {
|
||||
displaySetting: {} as TerminalDisplaySetting,
|
||||
themeSchema: {} as TerminalThemeSchema
|
||||
},
|
||||
tabs: new TerminalTabManager(),
|
||||
dispatcher: new TerminalDispatcher()
|
||||
}),
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Terminal } from 'xterm';
|
||||
export interface TerminalState {
|
||||
isDarkTheme: Ref<boolean>;
|
||||
preference: TerminalPreference;
|
||||
tabs: ITerminalTabManager;
|
||||
dispatcher: ITerminalDispatcher;
|
||||
}
|
||||
|
||||
@@ -66,11 +67,11 @@ export interface TerminalTabItem {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// 终端调度器
|
||||
export interface ITerminalDispatcher {
|
||||
// 当前活跃 tab
|
||||
// 终端 tab 管理器定义
|
||||
export interface ITerminalTabManager {
|
||||
// 当前 tab
|
||||
active: string;
|
||||
// 所有 tab
|
||||
// 全部 tab
|
||||
items: Array<TerminalTabItem>;
|
||||
|
||||
// 点击 tab
|
||||
@@ -79,6 +80,12 @@ export interface ITerminalDispatcher {
|
||||
deleteTab: (key: string) => void;
|
||||
// 打开 tab
|
||||
openTab: (tab: TerminalTabItem) => void;
|
||||
// 清空
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
// 终端调度器
|
||||
export interface ITerminalDispatcher {
|
||||
// 打开终端
|
||||
openTerminal: (record: any) => void;
|
||||
// 注册终端处理器
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<div class="terminal-content">
|
||||
<!-- 内容 tabs -->
|
||||
<a-tabs v-model:active-key="terminalStore.dispatcher.active">
|
||||
<a-tab-pane v-for="tab in terminalStore.dispatcher.items"
|
||||
<a-tabs v-model:active-key="tabs.active">
|
||||
<a-tab-pane v-for="tab in tabs.items"
|
||||
:key="tab.key"
|
||||
:title="tab.title">
|
||||
<!-- 设置 -->
|
||||
@@ -34,7 +34,7 @@
|
||||
import NewConnectionView from '../new-connection/new-connection-view.vue';
|
||||
import TerminalView from '../xterm/terminal-view.vue';
|
||||
|
||||
const terminalStore = useTerminalStore();
|
||||
const { tabs } = useTerminalStore();
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
</div>
|
||||
<!-- 左侧 tabs -->
|
||||
<div class="terminal-header-tabs">
|
||||
<a-tabs v-model:active-key="terminalStore.dispatcher.active"
|
||||
<a-tabs v-model:active-key="tabs.active"
|
||||
:editable="true"
|
||||
:hide-content="true"
|
||||
:auto-switch="true"
|
||||
@tab-click="k => terminalStore.dispatcher.clickTab(k as string)"
|
||||
@delete="k => terminalStore.dispatcher.deleteTab(k as string)">
|
||||
<a-tab-pane v-for="tab in terminalStore.dispatcher.items"
|
||||
@tab-click="k => tabs.clickTab(k as string)"
|
||||
@delete="k => tabs.deleteTab(k as string)">
|
||||
<a-tab-pane v-for="tab in tabs.items"
|
||||
:key="tab.key"
|
||||
:title="tab.title" />
|
||||
</a-tabs>
|
||||
@@ -46,7 +46,7 @@
|
||||
import IconActions from '../layout/icon-actions.vue';
|
||||
|
||||
const { isFullscreen, toggle: toggleFullScreen } = useFullscreen();
|
||||
const terminalStore = useTerminalStore();
|
||||
const { tabs } = useTerminalStore();
|
||||
|
||||
// 顶部操作
|
||||
const actions = computed<Array<SidebarAction>>(() => [
|
||||
|
||||
@@ -23,14 +23,14 @@
|
||||
import { useTerminalStore } from '@/store';
|
||||
import IconActions from './icon-actions.vue';
|
||||
|
||||
const terminalStore = useTerminalStore();
|
||||
const { tabs } = useTerminalStore();
|
||||
|
||||
// 顶部操作
|
||||
const topActions: Array<SidebarAction> = [
|
||||
{
|
||||
icon: 'icon-plus',
|
||||
content: '新建连接',
|
||||
click: () => terminalStore.dispatcher.openTab(InnerTabs.NEW_CONNECTION)
|
||||
click: () => tabs.openTab(InnerTabs.NEW_CONNECTION)
|
||||
},
|
||||
];
|
||||
|
||||
@@ -39,12 +39,12 @@
|
||||
{
|
||||
icon: 'icon-command',
|
||||
content: '快捷键设置',
|
||||
click: () => terminalStore.dispatcher.openTab(InnerTabs.SHORTCUT_SETTING)
|
||||
click: () => tabs.openTab(InnerTabs.SHORTCUT_SETTING)
|
||||
},
|
||||
{
|
||||
icon: 'icon-palette',
|
||||
content: '外观设置',
|
||||
click: () => terminalStore.dispatcher.openTab(InnerTabs.VIEW_SETTING)
|
||||
click: () => tabs.openTab(InnerTabs.VIEW_SETTING)
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<!-- 左侧图标-名称 -->
|
||||
<div class="flex-center host-item-left">
|
||||
<!-- 图标 -->
|
||||
<span class="host-item-left-icon" @click="terminalStore.dispatcher.openTerminal(item)">
|
||||
<span class="host-item-left-icon" @click="openTerminal(item)">
|
||||
<icon-desktop />
|
||||
</span>
|
||||
<!-- 名称 -->
|
||||
@@ -116,7 +116,7 @@
|
||||
arrow-class="terminal-tooltip-content"
|
||||
content="连接主机">
|
||||
<div class="terminal-sidebar-icon-wrapper">
|
||||
<div class="terminal-sidebar-icon" @click="terminalStore.dispatcher.openTerminal(item)">
|
||||
<div class="terminal-sidebar-icon" @click="openTerminal(item)">
|
||||
<icon-thunderbolt />
|
||||
</div>
|
||||
</div>
|
||||
@@ -168,7 +168,7 @@
|
||||
import { dataColor } from '@/utils';
|
||||
import { tagColor } from '@/views/asset/host-list/types/const';
|
||||
import { updateHostAlias } from '@/api/asset/host-extra';
|
||||
import { openSshModalKey } from '../../types/terminal.const';
|
||||
import { nextSessionId, openSshModalKey, TabType } from '../../types/terminal.const';
|
||||
import { useTerminalStore } from '@/store';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -176,7 +176,7 @@
|
||||
emptyValue: string
|
||||
}>();
|
||||
|
||||
const terminalStore = useTerminalStore();
|
||||
const { tabs } = useTerminalStore();
|
||||
const { toggle: toggleFavorite, loading: favoriteLoading } = useFavorite('HOST');
|
||||
|
||||
const aliasNameInput = ref();
|
||||
@@ -213,6 +213,17 @@
|
||||
}
|
||||
};
|
||||
|
||||
// 打开终端
|
||||
const openTerminal = (record: HostQueryResponse) => {
|
||||
tabs.openTab({
|
||||
type: TabType.TERMINAL,
|
||||
key: nextSessionId(),
|
||||
title: record.alias || (`${record.name} ${record.address}`),
|
||||
hostId: record.id,
|
||||
address: record.address
|
||||
});
|
||||
};
|
||||
|
||||
// 打开配置
|
||||
const openSetting = inject<(record: HostQueryResponse) => void>(openSshModalKey);
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TerminalTabItem } from '@/store/modules/terminal/types';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
import { useTerminalStore } from '@/store';
|
||||
import TerminalHandler from '@/views/host/terminal/handler/TerminalHandler';
|
||||
import { sleep } from '@/utils';
|
||||
@@ -44,6 +44,11 @@
|
||||
|
||||
onMounted(init);
|
||||
|
||||
onUnmounted(() => {
|
||||
// 发送关闭
|
||||
console.log('12312312');
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
102
orion-ops-ui/src/views/host/terminal/handler/terminal-channel.ts
Normal file
102
orion-ops-ui/src/views/host/terminal/handler/terminal-channel.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { OutputProtocol } from '@/views/host/terminal/types/terminal.protocol';
|
||||
import type { InputPayload, ITerminalChannel, ITerminalOutputProcessor, OutputPayload, Protocol, } from '@/views/host/terminal/types/terminal.type';
|
||||
|
||||
|
||||
export const wsBase = import.meta.env.VITE_WS_BASE_URL;
|
||||
|
||||
// 终端通信处理器 实现
|
||||
export default class TerminalChannel implements ITerminalChannel {
|
||||
|
||||
private readonly processor;
|
||||
|
||||
constructor(processor: ITerminalOutputProcessor) {
|
||||
this.processor = processor;
|
||||
}
|
||||
|
||||
send(protocol: Protocol, payload: InputPayload): void {
|
||||
|
||||
}
|
||||
|
||||
// 初始化
|
||||
async init() {
|
||||
}
|
||||
|
||||
// 处理消息
|
||||
handlerMessage({ data }: MessageEvent) {
|
||||
// 解析消息
|
||||
const payload = parse(data as string);
|
||||
if (!payload) {
|
||||
return;
|
||||
}
|
||||
// 消息调度
|
||||
switch (payload.type) {
|
||||
case OutputProtocol.CHECK.type:
|
||||
// 检查 回调
|
||||
this.processor.processCheck(payload);
|
||||
break;
|
||||
case OutputProtocol.CONNECT.type:
|
||||
// 连接 回调
|
||||
this.processor.processConnect(payload);
|
||||
break;
|
||||
case OutputProtocol.PONG.type:
|
||||
// pong 回调
|
||||
this.processor.processPong(payload);
|
||||
break;
|
||||
case OutputProtocol.OUTPUT.type:
|
||||
// 输出 回调
|
||||
this.processor.processOutput(payload);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// 分隔符
|
||||
export const SEPARATOR = '|';
|
||||
|
||||
// 解析参数
|
||||
export const parse = (payload: string) => {
|
||||
const protocols = Object.values(OutputProtocol);
|
||||
const useProtocol = protocols.find(p => payload.startsWith(p.type + SEPARATOR) || p.type === payload);
|
||||
if (!useProtocol) {
|
||||
return undefined;
|
||||
}
|
||||
const template = useProtocol.template;
|
||||
const res = {} as OutputPayload;
|
||||
let curr = 0;
|
||||
let len = payload.length;
|
||||
for (let i = 0, pl = template.length; i < pl; i++) {
|
||||
if (i == pl - 1) {
|
||||
// 最后一次
|
||||
res[template[i]] = payload.substring(curr, len);
|
||||
} else {
|
||||
// 非最后一次
|
||||
let tmp = '';
|
||||
for (; curr < len; curr++) {
|
||||
const c = payload.charAt(curr);
|
||||
if (c == SEPARATOR) {
|
||||
res[template[i]] = tmp;
|
||||
curr++;
|
||||
break;
|
||||
} else {
|
||||
tmp += c;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
};
|
||||
|
||||
// 格式化参数
|
||||
export const format = (protocol: Protocol, payload: InputPayload) => {
|
||||
payload.type = protocol.type;
|
||||
return protocol.template
|
||||
.map(i => payload[i] || '')
|
||||
.join(SEPARATOR);
|
||||
};
|
||||
@@ -2,12 +2,13 @@ import type { ITerminalDispatcher, ITerminalHandler, TerminalTabItem } from '@/s
|
||||
import type { HostQueryResponse } from '@/api/asset/host';
|
||||
import type { HostTerminalAccessResponse } from '@/api/asset/host-terminal';
|
||||
import { getHostTerminalAccessToken } from '@/api/asset/host-terminal';
|
||||
import { InnerTabs, TabType } from '@/views/host/terminal/types/terminal.const';
|
||||
import { TabType } from '@/views/host/terminal/types/terminal.const';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { sleep } from '@/utils';
|
||||
import { format, InputProtocol, OutputProtocol, parse } from '../types/terminal.protocol';
|
||||
import { format, InputProtocol, OutputProtocol, parse, Payload } from '../types/terminal.protocol';
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
import { addEventListen, removeEventListen } from '@/utils/event';
|
||||
import { useTerminalStore } from '@/store';
|
||||
|
||||
export const wsBase = import.meta.env.VITE_WS_BASE_URL;
|
||||
|
||||
@@ -20,10 +21,6 @@ export const wsBase = import.meta.env.VITE_WS_BASE_URL;
|
||||
*/
|
||||
export default class TerminalDispatcher implements ITerminalDispatcher {
|
||||
|
||||
public active: string;
|
||||
|
||||
public items: Array<TerminalTabItem>;
|
||||
|
||||
private access?: HostTerminalAccessResponse;
|
||||
|
||||
private client?: WebSocket;
|
||||
@@ -35,42 +32,10 @@ export default class TerminalDispatcher implements ITerminalDispatcher {
|
||||
private readonly dispatchResizeFn: () => {};
|
||||
|
||||
constructor() {
|
||||
this.active = InnerTabs.NEW_CONNECTION.key;
|
||||
this.items = [InnerTabs.NEW_CONNECTION];
|
||||
this.handlers = {};
|
||||
this.dispatchResizeFn = useDebounceFn(this.dispatchResize).bind(this);
|
||||
}
|
||||
|
||||
// 点击 tab
|
||||
clickTab(key: string): void {
|
||||
this.active = key;
|
||||
}
|
||||
|
||||
// 删除 tab
|
||||
deleteTab(key: string): void {
|
||||
// 获取当前 tab
|
||||
const tabIndex = this.items.findIndex(s => s.key === key);
|
||||
if (this.items[tabIndex]?.type === TabType.TERMINAL) {
|
||||
// 如果是 terminal 则需要关闭
|
||||
this.closeTerminal(key);
|
||||
}
|
||||
// 删除 tab
|
||||
this.items.splice(tabIndex, 1);
|
||||
if (key === this.active && this.items.length !== 0) {
|
||||
// 切换为前一个 tab
|
||||
this.active = this.items[Math.max(tabIndex - 1, 0)].key;
|
||||
}
|
||||
// fixme 关闭 socket
|
||||
}
|
||||
|
||||
// 打开 tab
|
||||
openTab(tab: TerminalTabItem): void {
|
||||
// 不存在则创建 tab
|
||||
if (!this.items.find(s => s.key === tab.key)) {
|
||||
this.items.push(tab);
|
||||
}
|
||||
this.active = tab.key;
|
||||
}
|
||||
|
||||
// 初始化客户端
|
||||
async initClient() {
|
||||
@@ -92,7 +57,7 @@ export default class TerminalDispatcher implements ITerminalDispatcher {
|
||||
this.client.onmessage = this.handlerMessage.bind(this);
|
||||
// 注册 ping 事件
|
||||
this.pingTask = setInterval(() => {
|
||||
this.client?.send(format(InputProtocol.PING, {}));
|
||||
this.client?.send(format(InputProtocol.PING, {} as Payload));
|
||||
}, 150000);
|
||||
// 注册 resize 事件
|
||||
addEventListen(window, 'resize', this.dispatchResizeFn);
|
||||
@@ -140,7 +105,7 @@ export default class TerminalDispatcher implements ITerminalDispatcher {
|
||||
}
|
||||
const session = this.access.sessionInitial = (parseInt(this.access.sessionInitial as string, 32) + 1).toString(32);
|
||||
// 打开会话
|
||||
this.openTab({
|
||||
useTerminalStore().tabs.openTab({
|
||||
type: TabType.TERMINAL,
|
||||
key: session,
|
||||
title: record.alias || (`${record.name} ${record.address}`),
|
||||
@@ -211,8 +176,6 @@ export default class TerminalDispatcher implements ITerminalDispatcher {
|
||||
|
||||
// 重置
|
||||
reset(): void {
|
||||
this.active = undefined as unknown as string;
|
||||
this.items = [];
|
||||
this.access = undefined;
|
||||
this.handlers = {};
|
||||
// 关闭 client
|
||||
@@ -0,0 +1,49 @@
|
||||
import { ITerminalChannel, ITerminalOutputProcessor, OutputPayload, } from '@/views/host/terminal/types/terminal.type';
|
||||
import TerminalChannel from '@/views/host/terminal/handler/terminal-channel';
|
||||
|
||||
// 终端调度器实现
|
||||
export default class TerminalOutputProcessor implements ITerminalOutputProcessor {
|
||||
|
||||
private readonly channel: ITerminalChannel;
|
||||
|
||||
constructor() {
|
||||
this.channel = new TerminalChannel(this);
|
||||
}
|
||||
|
||||
// 处理检查消息
|
||||
processCheck(payload: OutputPayload): void {
|
||||
// const success = !!Number.parseInt(payload.result);
|
||||
// const handler = this.handlers[session];
|
||||
// // 未成功展示错误信息
|
||||
// if (!success) {
|
||||
// handler.write('[91m' + errormessage + '[0m');
|
||||
// return;
|
||||
// }
|
||||
// // 发送 connect 命令
|
||||
// this.channel.send(InputProtocol.CONNECT, { session, cols: handler.inst.cols, rows: handler.inst.rows });
|
||||
}
|
||||
|
||||
// 处理连接消息
|
||||
processConnect(payload: OutputPayload): void {
|
||||
const success = !!Number.parseInt(payload.result);
|
||||
// const handler = this.handlers[session];
|
||||
// // 未成功展示错误信息
|
||||
// if (!success) {
|
||||
// handler.write('[91m' + errormessage + '[0m');
|
||||
// return;
|
||||
// }
|
||||
// // 设置可写
|
||||
// handler.setCanWrite(true);
|
||||
// handler.connect();
|
||||
}
|
||||
|
||||
// 处理 pong 消息
|
||||
processPong(payload: OutputPayload): void {
|
||||
}
|
||||
|
||||
// 处理输出消息
|
||||
processOutput(payload: OutputPayload): void {
|
||||
// this.handlers[session].write(body);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { ITerminalChannel, ITerminalSession } from '../types/terminal.type';
|
||||
import type { TerminalTabItem } from '@/store/modules/terminal/types';
|
||||
import { sleep } from '@/utils';
|
||||
import TerminalSession from './terminal-session';
|
||||
|
||||
// 终端会话管理器定义
|
||||
export interface ITerminalSessionManager {
|
||||
// 打开终端
|
||||
openSession: (tab: TerminalTabItem, dom: HTMLElement) => void;
|
||||
// 获取终端会话
|
||||
getSession: (sessionId: string) => ITerminalSession;
|
||||
// 重置
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
// FIXME 去除 TOKEN 起始量
|
||||
|
||||
// 终端会话管理器实现
|
||||
export default class TerminalSessionManager implements ITerminalSessionManager {
|
||||
|
||||
private readonly channel: ITerminalChannel;
|
||||
|
||||
private sessions: Record<string, ITerminalSession>;
|
||||
|
||||
constructor(channel: ITerminalChannel) {
|
||||
this.channel = channel;
|
||||
this.sessions = {};
|
||||
}
|
||||
|
||||
// 打开终端会话
|
||||
async openSession(tab: TerminalTabItem, dom: HTMLElement) {
|
||||
// 初始化客户端
|
||||
await this.channel.init();
|
||||
// 新建会话
|
||||
const session = new TerminalSession(
|
||||
tab.hostId as number,
|
||||
tab.key,
|
||||
this.channel
|
||||
);
|
||||
// 初始化
|
||||
session.init(dom);
|
||||
// 等待前端渲染完成
|
||||
await sleep(100);
|
||||
// 添加会话
|
||||
this.sessions[tab.key] = session;
|
||||
}
|
||||
|
||||
// 获取终端会话
|
||||
getSession(sessionId: string): ITerminalSession {
|
||||
return this.sessions[sessionId];
|
||||
}
|
||||
|
||||
// 重置
|
||||
reset(): void {
|
||||
this.sessions = {};
|
||||
}
|
||||
|
||||
}
|
||||
119
orion-ops-ui/src/views/host/terminal/handler/terminal-session.ts
Normal file
119
orion-ops-ui/src/views/host/terminal/handler/terminal-session.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import type { ITerminalChannel, ITerminalSession } from '../types/terminal.type';
|
||||
import { useTerminalStore } from '@/store';
|
||||
import { fontFamilySuffix } from '@/views/host/terminal/types/terminal.const';
|
||||
import { InputProtocol } from '@/views/host/terminal/types/terminal.protocol';
|
||||
import { Terminal } from 'xterm';
|
||||
import { FitAddon } from 'xterm-addon-fit';
|
||||
import { WebglAddon } from 'xterm-addon-webgl';
|
||||
|
||||
// 终端插件
|
||||
export interface TerminalAddons {
|
||||
fit: FitAddon;
|
||||
webgl: WebglAddon;
|
||||
}
|
||||
|
||||
// 终端会话实现
|
||||
export default class TerminalSession implements ITerminalSession {
|
||||
|
||||
public hostId: number;
|
||||
|
||||
public inst: Terminal;
|
||||
|
||||
public connected: boolean;
|
||||
|
||||
private canWrite: boolean;
|
||||
|
||||
private readonly sessionId: string;
|
||||
|
||||
private readonly channel: ITerminalChannel;
|
||||
|
||||
private readonly addons: TerminalAddons;
|
||||
|
||||
constructor(hostId: number,
|
||||
sessionId: string,
|
||||
channel: ITerminalChannel) {
|
||||
this.hostId = hostId;
|
||||
this.sessionId = sessionId;
|
||||
this.channel = channel;
|
||||
this.connected = false;
|
||||
this.canWrite = false;
|
||||
this.inst = undefined as unknown as Terminal;
|
||||
this.addons = {} as TerminalAddons;
|
||||
}
|
||||
|
||||
// 初始化
|
||||
init(dom: HTMLElement): void {
|
||||
const { preference } = useTerminalStore();
|
||||
// 初始化实例
|
||||
this.inst = new Terminal({
|
||||
...(preference.displaySetting as any),
|
||||
theme: preference.themeSchema,
|
||||
fastScrollModifier: 'shift',
|
||||
fontFamily: preference.displaySetting.fontFamily + fontFamilySuffix,
|
||||
});
|
||||
// 注册插件
|
||||
this.addons.fit = new FitAddon();
|
||||
this.addons.webgl = new WebglAddon();
|
||||
// TODO check
|
||||
const inst = this.inst;
|
||||
Object.values(this.addons).forEach(s => inst.loadAddon(s));
|
||||
// 打开终端
|
||||
this.inst.open(dom);
|
||||
// 自适应
|
||||
this.addons.fit.fit();
|
||||
// TODO sendCheck
|
||||
}
|
||||
|
||||
// 设置已连接
|
||||
connect(): void {
|
||||
this.connected = true;
|
||||
// 注册输入事件
|
||||
this.inst.onData(s => {
|
||||
if (!this.canWrite) {
|
||||
return;
|
||||
}
|
||||
// 输入
|
||||
this.channel.send(InputProtocol.INPUT, {
|
||||
session: this.sessionId,
|
||||
command: s
|
||||
});
|
||||
});
|
||||
// 注册 resize 事件
|
||||
this.inst.onResize(({ cols, rows }) => {
|
||||
this.channel.send(InputProtocol.RESIZE, {
|
||||
session: this.sessionId,
|
||||
cols,
|
||||
rows
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 设置是否可写
|
||||
setCanWrite(canWrite: boolean): void {
|
||||
this.canWrite = canWrite;
|
||||
}
|
||||
|
||||
// 写入数据
|
||||
write(value: string): void {
|
||||
this.inst.write(value);
|
||||
}
|
||||
|
||||
// 自适应
|
||||
fit(): void {
|
||||
this.addons.fit?.fit();
|
||||
}
|
||||
|
||||
// 关闭
|
||||
close(): void {
|
||||
try {
|
||||
// 卸载插件
|
||||
Object.values(this.addons)
|
||||
.filter(Boolean)
|
||||
.forEach(s => s.dispose());
|
||||
// 卸载实体
|
||||
this.inst.dispose();
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import type { ITerminalTabManager, TerminalTabItem } from '@/store/modules/terminal/types';
|
||||
import { InnerTabs } from '@/views/host/terminal/types/terminal.const';
|
||||
|
||||
// 终端 tab 管理器实现
|
||||
export default class TerminalTabManager implements ITerminalTabManager {
|
||||
|
||||
public active: string;
|
||||
|
||||
public items: Array<TerminalTabItem>;
|
||||
|
||||
constructor() {
|
||||
this.active = InnerTabs.NEW_CONNECTION.key;
|
||||
this.items = [InnerTabs.NEW_CONNECTION];
|
||||
}
|
||||
|
||||
// 点击 tab
|
||||
clickTab(key: string): void {
|
||||
this.active = key;
|
||||
}
|
||||
|
||||
// 删除 tab
|
||||
deleteTab(key: string): void {
|
||||
// 获取当前 tab
|
||||
const tabIndex = this.items.findIndex(s => s.key === key);
|
||||
// 删除 tab
|
||||
this.items.splice(tabIndex, 1);
|
||||
if (key === this.active && this.items.length !== 0) {
|
||||
// 切换为前一个 tab
|
||||
this.active = this.items[Math.max(tabIndex - 1, 0)].key;
|
||||
}
|
||||
// fixme 关闭 ws
|
||||
}
|
||||
|
||||
// 打开 tab
|
||||
openTab(tab: TerminalTabItem): void {
|
||||
// 不存在则创建 tab
|
||||
if (!this.items.find(s => s.key === tab.key)) {
|
||||
this.items.push(tab);
|
||||
}
|
||||
this.active = tab.key;
|
||||
}
|
||||
|
||||
// 清空
|
||||
clear() {
|
||||
this.active = undefined as unknown as string;
|
||||
this.items = [];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { CSSProperties } from 'vue';
|
||||
import { getUUID } from '@/utils';
|
||||
|
||||
// sidebar 操作类型
|
||||
export interface SidebarAction {
|
||||
@@ -59,6 +60,11 @@ export const ExtraSshAuthType = {
|
||||
CUSTOM_IDENTITY: 'CUSTOM_IDENTITY',
|
||||
};
|
||||
|
||||
// 获取会话id
|
||||
export const nextSessionId = (): string => {
|
||||
return getUUID().replaceAll('-', '').substring(0, 10);
|
||||
};
|
||||
|
||||
// 打开 sshModal key
|
||||
export const openSshModalKey = Symbol();
|
||||
|
||||
|
||||
68
orion-ops-ui/src/views/host/terminal/types/terminal.type.ts
Normal file
68
orion-ops-ui/src/views/host/terminal/types/terminal.type.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
// 终端协议
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
export interface Protocol {
|
||||
type: string;
|
||||
template: string[];
|
||||
}
|
||||
|
||||
// 终端输入消息内容
|
||||
export interface InputPayload {
|
||||
type?: string;
|
||||
session?: string;
|
||||
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// 终端输出消息内容
|
||||
export interface OutputPayload {
|
||||
type: string;
|
||||
session: string;
|
||||
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
// 终端通信处理器 定义
|
||||
export interface ITerminalChannel {
|
||||
// 初始化
|
||||
init: () => Promise<void>;
|
||||
// 发送消息
|
||||
send: (protocol: Protocol, payload: InputPayload) => void;
|
||||
|
||||
// 关闭
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
// 终端输出消息体处理器定义
|
||||
export interface ITerminalOutputProcessor {
|
||||
// 处理检查消息
|
||||
processCheck: (payload: OutputPayload) => void;
|
||||
// 处理连接消息
|
||||
processConnect: (payload: OutputPayload) => void;
|
||||
// 处理 pong 消息
|
||||
processPong: (payload: OutputPayload) => void;
|
||||
// 处理输出消息
|
||||
processOutput: (payload: OutputPayload) => void;
|
||||
}
|
||||
|
||||
// 终端会话定义
|
||||
export interface ITerminalSession {
|
||||
hostId: number;
|
||||
// terminal 实例
|
||||
inst: Terminal;
|
||||
// 是否已连接
|
||||
connected: boolean;
|
||||
|
||||
// 初始化
|
||||
init: (dom: HTMLElement) => void;
|
||||
// 连接
|
||||
connect: () => void;
|
||||
// 设置是否可写
|
||||
setCanWrite: (canWrite: boolean) => void;
|
||||
// 写入数据
|
||||
write: (value: string) => void;
|
||||
// 自适应
|
||||
fit: () => void;
|
||||
// 关闭
|
||||
close: () => void;
|
||||
}
|
||||
Reference in New Issue
Block a user