feat: 打开连接.

This commit is contained in:
lijiahangmax
2024-01-05 00:51:42 +08:00
parent c6b86f8346
commit c40a4fdf13
10 changed files with 251 additions and 79 deletions

View File

@@ -1,8 +1,8 @@
<template>
<div class="terminal-content">
<!-- 内容 tabs -->
<a-tabs v-model:active-key="terminalStore.tabs.active">
<a-tab-pane v-for="tab in terminalStore.tabs.items"
<a-tabs v-model:active-key="terminalStore.dispatcher.active">
<a-tab-pane v-for="tab in terminalStore.dispatcher.items"
:key="tab.key"
:title="tab.title">
<!-- 设置 -->
@@ -14,13 +14,7 @@
</template>
<!-- 终端 -->
<template v-else-if="tab.type === TabType.TERMINAL">
<terminal-view>
</terminal-view>
终端 {{ tab.key }}
<div v-for="i in 1000" :key="i">
{{ tab.title }}
</div>
<terminal-view :tab="tab" />
</template>
</a-tab-pane>
</a-tabs>

View File

@@ -10,13 +10,13 @@
</div>
<!-- 左侧 tabs -->
<div class="terminal-header-tabs">
<a-tabs v-model:active-key="terminalStore.tabs.active"
<a-tabs v-model:active-key="terminalStore.dispatcher.active"
:editable="true"
:hide-content="true"
:auto-switch="true"
@tab-click="terminalStore.clickTab"
@delete="terminalStore.deleteTab">
<a-tab-pane v-for="tab in terminalStore.tabs.items"
@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"
:key="tab.key"
:title="tab.title" />
</a-tabs>

View File

@@ -30,7 +30,7 @@
{
icon: 'icon-plus',
content: '新建连接',
click: () => terminalStore.switchTab(InnerTabs.NEW_CONNECTION)
click: () => terminalStore.dispatcher.openTab(InnerTabs.NEW_CONNECTION)
},
];
@@ -39,12 +39,12 @@
{
icon: 'icon-command',
content: '快捷键设置',
click: () => terminalStore.switchTab(InnerTabs.SHORTCUT_SETTING)
click: () => terminalStore.dispatcher.openTab(InnerTabs.SHORTCUT_SETTING)
},
{
icon: 'icon-palette',
content: '外观设置',
click: () => terminalStore.switchTab(InnerTabs.VIEW_SETTING)
click: () => terminalStore.dispatcher.openTab(InnerTabs.VIEW_SETTING)
},
];

View File

@@ -20,7 +20,7 @@
<!-- 左侧图标-名称 -->
<div class="flex-center host-item-left">
<!-- 图标 -->
<span class="host-item-left-icon" @click="terminalStore.openTerminal(item)">
<span class="host-item-left-icon" @click="terminalStore.dispatcher.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.openTerminal(item)">
<div class="terminal-sidebar-icon" @click="terminalStore.dispatcher.openTerminal(item)">
<icon-thunderbolt />
</div>
</div>

View File

@@ -1,5 +1,14 @@
<template>
<div class="terminal-container">
<!-- 头部 -->
<div class="terminal-header">
终端 {{ tab.key }} {{ tab.title }}
</div>
<!-- 终端 -->
<div class="terminal-wrapper">
<div class="terminal-inst" ref="terminalRef" />
</div>
</div>
</template>
<script lang="ts">
@@ -9,9 +18,70 @@
</script>
<script lang="ts" setup>
import type { TerminalTabItem } from '@/store/modules/terminal/types';
import { onMounted, ref } from 'vue';
import { useTerminalStore } from '@/store';
import { FitAddon } from 'xterm-addon-fit';
import { WebglAddon } from 'xterm-addon-webgl';
import { Terminal } from 'xterm';
const props = defineProps<{
tab: TerminalTabItem
}>();
const { preference, dispatcher } = useTerminalStore();
const terminalRef = ref();
// 初始化
const init = () => {
// FIXME fontfamily
// 初始化终端
const term = new Terminal({
theme: preference.themeSchema,
fastScrollModifier: 'shift',
...(preference.displaySetting as any),
});
// 注册插件
const fitAddon = new FitAddon();
const webglAddon = new WebglAddon();
term.loadAddon(fitAddon);
term.loadAddon(webglAddon);
// 打开终端
term.open(terminalRef.value);
// 自适应
fitAddon.fit();
// 注册钩子
dispatcher.registerTerminalHook(props.tab);
// 初始化终端
};
onMounted(init);
</script>
<style lang="less" scoped>
@terminal-header-height: 30px;
.terminal-container {
width: 100%;
height: calc(100vh - var(--header-height));
position: relative;
}
.terminal-header {
width: 100%;
height: @terminal-header-height;
}
.terminal-wrapper {
width: 100%;
height: calc(100% - @terminal-header-height);
position: relative;
}
.terminal-inst {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,136 @@
import type { ITerminalDispatcher, TerminalTabItem } from '@/store/modules/terminal/types';
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 { Message } from '@arco-design/web-vue';
import { sleep } from '@/utils';
import { InputProtocol, format } from '../types/terminal.protocol';
export const wsBase = import.meta.env.VITE_WS_BASE_URL;
/**
* 终端调度器
*/
export default class TerminalDispatcher implements ITerminalDispatcher {
private access?: HostTerminalAccessResponse;
private client?: WebSocket;
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);
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() {
if (this.client) {
return;
}
// 获取 access
const { data: accessData } = await getHostTerminalAccessToken();
this.access = accessData;
// 打开会话
this.client = new WebSocket(`${wsBase}/host/terminal/${accessData.accessToken}`);
this.client.onerror = event => {
Message.error('无法连接至服务器');
console.error('error', event);
};
this.client.onclose = event => {
console.warn('close', event);
};
this.client.onmessage = this.handlerMessage;
// 等待会话等待完成
for (let i = 0; i < 100; i++) {
await sleep(50);
if (this.client.readyState === WebSocket.OPEN) {
break;
}
}
}
// 处理消息
handlerMessage({ data }: MessageEvent) {
console.log(data);
}
// 打开终端
async openTerminal(record: HostQueryResponse) {
// 初始化客户端
await this.initClient();
// uncheck
if (!this.access) {
return;
}
const session = this.access.sessionInitial = (parseInt(this.access.sessionInitial as string, 32) + 1).toString(32);
// 打开会话
this.openTab({
type: TabType.TERMINAL,
key: session,
title: record.alias || (`${record.name} ${record.address}`),
hostId: record.id,
address: record.address,
checked: false,
connected: false
});
}
// 注册终端钩子
registerTerminalHook(tab: TerminalTabItem) {
if (!this.client) {
return;
}
// 发送 check 命令
this.client.send(format(InputProtocol.CHECK, { session: tab.key, hostId: tab.hostId }));
}
// 关闭终端
closeTerminal(key: string) {
}
// 重置
reset(): void {
this.active = undefined as unknown as string;
this.items = [];
this.access = undefined;
this.client = undefined;
}
}

View File

@@ -1,5 +1,4 @@
import type { CSSProperties } from 'vue';
import type { TabItem } from '@/store/modules/terminal/types';
// sidebar 操作类型
export interface SidebarAction {
@@ -16,7 +15,7 @@ export const TabType = {
};
// 内置 tab
export const InnerTabs: Record<string, TabItem> = {
export const InnerTabs = {
NEW_CONNECTION: {
key: 'newConnection',
title: '新建连接',

View File

@@ -13,7 +13,7 @@ export interface Payload {
}
// 输入协议
export const InputProtocol: Record<string, Protocol> = {
export const InputProtocol = {
// 主机连接检查
CHECK: {
type: 'ck',
@@ -52,7 +52,7 @@ export const InputProtocol: Record<string, Protocol> = {
};
// 输出协议
export const OutputProtocol: Record<string, Protocol> = {
export const OutputProtocol = {
// 主机连接检查
CHECK: {
type: 'ck',
@@ -112,7 +112,7 @@ export const parse: Record<string, any> = (payload: string) => {
};
// 格式化参数
export const format = (payload: Payload, protocol: Protocol) => {
export const format = (protocol: Protocol, payload: Payload) => {
payload.type = protocol.type;
return protocol.template
.map(i => payload[i] || '')