feat: 打开连接.
This commit is contained in:
@@ -1,12 +1,10 @@
|
|||||||
import type { TabItem, TerminalDisplaySetting, TerminalPreference, TerminalState, TerminalThemeSchema } from './types';
|
import type { TerminalDisplaySetting, TerminalPreference, TerminalState, TerminalThemeSchema } from './types';
|
||||||
import type { HostQueryResponse } from '@/api/asset/host';
|
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { getPreference, updatePreference } from '@/api/user/preference';
|
import { getPreference, updatePreference } from '@/api/user/preference';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
import { useDark } from '@vueuse/core';
|
import { useDark } from '@vueuse/core';
|
||||||
import { DEFAULT_SCHEMA } from '@/views/host/terminal/types/terminal.theme';
|
import { DEFAULT_SCHEMA } from '@/views/host/terminal/types/terminal.theme';
|
||||||
import { InnerTabs } from '@/views/host/terminal/types/terminal.const';
|
import TerminalDispatcher from '@/views/host/terminal/handler/TerminalDispatcher';
|
||||||
import { getHostTerminalAccessToken } from '@/api/asset/host-terminal';
|
|
||||||
|
|
||||||
// 暗色主题
|
// 暗色主题
|
||||||
export const DarkTheme = {
|
export const DarkTheme = {
|
||||||
@@ -31,11 +29,7 @@ export default defineStore('terminal', {
|
|||||||
displaySetting: {} as TerminalDisplaySetting,
|
displaySetting: {} as TerminalDisplaySetting,
|
||||||
themeSchema: {} as TerminalThemeSchema
|
themeSchema: {} as TerminalThemeSchema
|
||||||
},
|
},
|
||||||
tabs: {
|
dispatcher: new TerminalDispatcher()
|
||||||
active: InnerTabs.NEW_CONNECTION.key,
|
|
||||||
items: [InnerTabs.NEW_CONNECTION, InnerTabs.VIEW_SETTING]
|
|
||||||
},
|
|
||||||
access: undefined
|
|
||||||
}),
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
@@ -116,41 +110,6 @@ export default defineStore('terminal', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 点击 tab
|
|
||||||
clickTab(key: string) {
|
|
||||||
this.tabs.active = key;
|
|
||||||
},
|
|
||||||
|
|
||||||
// 删除 tab
|
|
||||||
deleteTab(key: string) {
|
|
||||||
const tabIndex = this.tabs.items.findIndex(s => s.key === key);
|
|
||||||
this.tabs.items.splice(tabIndex, 1);
|
|
||||||
if (key === this.tabs.active && this.tabs.items.length !== 0) {
|
|
||||||
// 切换为前一个 tab
|
|
||||||
this.tabs.active = this.tabs.items[Math.max(tabIndex - 1, 0)].key;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 切换 tab
|
|
||||||
switchTab(tab: TabItem) {
|
|
||||||
// 不存在则创建tab
|
|
||||||
if (!this.tabs.items.find(s => s.key === tab.key)) {
|
|
||||||
this.tabs.items.push(tab);
|
|
||||||
}
|
|
||||||
this.tabs.active = tab.key;
|
|
||||||
},
|
|
||||||
|
|
||||||
// 打开终端
|
|
||||||
async openTerminal(record: HostQueryResponse) {
|
|
||||||
// 获取 access
|
|
||||||
if (!this.access) {
|
|
||||||
const { data } = await getHostTerminalAccessToken();
|
|
||||||
this.access = data;
|
|
||||||
}
|
|
||||||
console.log(this.access);
|
|
||||||
console.log(record);
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import type { Ref } from 'vue';
|
import type { Ref } from 'vue';
|
||||||
import type { HostTerminalAccessResponse } from '@/api/asset/host-terminal';
|
|
||||||
|
|
||||||
export interface TerminalState {
|
export interface TerminalState {
|
||||||
isDarkTheme: Ref<boolean>;
|
isDarkTheme: Ref<boolean>;
|
||||||
preference: TerminalPreference;
|
preference: TerminalPreference;
|
||||||
tabs: TerminalTabs;
|
dispatcher: ITerminalDispatcher;
|
||||||
access?: HostTerminalAccessResponse;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 终端配置
|
// 终端配置
|
||||||
@@ -58,17 +56,33 @@ export interface TerminalThemeSchema {
|
|||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 终端 tab
|
// 终端 tab 元素
|
||||||
export interface TerminalTabs {
|
export interface TerminalTabItem {
|
||||||
active: string;
|
|
||||||
items: Array<TabItem>;
|
|
||||||
}
|
|
||||||
|
|
||||||
// tab 元素
|
|
||||||
export interface TabItem {
|
|
||||||
key: string;
|
key: string;
|
||||||
title: string;
|
title: string;
|
||||||
type: string;
|
type: string;
|
||||||
|
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 终端调度器
|
||||||
|
export interface ITerminalDispatcher {
|
||||||
|
// 当前活跃 tab
|
||||||
|
active: string;
|
||||||
|
// 所有 tab
|
||||||
|
items: Array<TerminalTabItem>;
|
||||||
|
|
||||||
|
// 点击 tab
|
||||||
|
clickTab: (key: string) => void;
|
||||||
|
// 删除 tab
|
||||||
|
deleteTab: (key: string) => void;
|
||||||
|
// 打开 tab
|
||||||
|
openTab: (tab: TerminalTabItem) => void;
|
||||||
|
// 打开终端
|
||||||
|
openTerminal: (record: any) => void;
|
||||||
|
// 注册终端钩子
|
||||||
|
registerTerminalHook: (tab: TerminalTabItem) => void;
|
||||||
|
|
||||||
|
// 重置
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="terminal-content">
|
<div class="terminal-content">
|
||||||
<!-- 内容 tabs -->
|
<!-- 内容 tabs -->
|
||||||
<a-tabs v-model:active-key="terminalStore.tabs.active">
|
<a-tabs v-model:active-key="terminalStore.dispatcher.active">
|
||||||
<a-tab-pane v-for="tab in terminalStore.tabs.items"
|
<a-tab-pane v-for="tab in terminalStore.dispatcher.items"
|
||||||
:key="tab.key"
|
:key="tab.key"
|
||||||
:title="tab.title">
|
:title="tab.title">
|
||||||
<!-- 设置 -->
|
<!-- 设置 -->
|
||||||
@@ -14,13 +14,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<!-- 终端 -->
|
<!-- 终端 -->
|
||||||
<template v-else-if="tab.type === TabType.TERMINAL">
|
<template v-else-if="tab.type === TabType.TERMINAL">
|
||||||
<terminal-view>
|
<terminal-view :tab="tab" />
|
||||||
|
|
||||||
</terminal-view>
|
|
||||||
终端 {{ tab.key }}
|
|
||||||
<div v-for="i in 1000" :key="i">
|
|
||||||
{{ tab.title }}
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
|
|||||||
@@ -10,13 +10,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 左侧 tabs -->
|
<!-- 左侧 tabs -->
|
||||||
<div class="terminal-header-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"
|
:editable="true"
|
||||||
:hide-content="true"
|
:hide-content="true"
|
||||||
:auto-switch="true"
|
:auto-switch="true"
|
||||||
@tab-click="terminalStore.clickTab"
|
@tab-click="k => terminalStore.dispatcher.clickTab(k as string)"
|
||||||
@delete="terminalStore.deleteTab">
|
@delete="k => terminalStore.dispatcher.deleteTab(k as string)">
|
||||||
<a-tab-pane v-for="tab in terminalStore.tabs.items"
|
<a-tab-pane v-for="tab in terminalStore.dispatcher.items"
|
||||||
:key="tab.key"
|
:key="tab.key"
|
||||||
:title="tab.title" />
|
:title="tab.title" />
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
{
|
{
|
||||||
icon: 'icon-plus',
|
icon: 'icon-plus',
|
||||||
content: '新建连接',
|
content: '新建连接',
|
||||||
click: () => terminalStore.switchTab(InnerTabs.NEW_CONNECTION)
|
click: () => terminalStore.dispatcher.openTab(InnerTabs.NEW_CONNECTION)
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -39,12 +39,12 @@
|
|||||||
{
|
{
|
||||||
icon: 'icon-command',
|
icon: 'icon-command',
|
||||||
content: '快捷键设置',
|
content: '快捷键设置',
|
||||||
click: () => terminalStore.switchTab(InnerTabs.SHORTCUT_SETTING)
|
click: () => terminalStore.dispatcher.openTab(InnerTabs.SHORTCUT_SETTING)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'icon-palette',
|
icon: 'icon-palette',
|
||||||
content: '外观设置',
|
content: '外观设置',
|
||||||
click: () => terminalStore.switchTab(InnerTabs.VIEW_SETTING)
|
click: () => terminalStore.dispatcher.openTab(InnerTabs.VIEW_SETTING)
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<!-- 左侧图标-名称 -->
|
<!-- 左侧图标-名称 -->
|
||||||
<div class="flex-center host-item-left">
|
<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 />
|
<icon-desktop />
|
||||||
</span>
|
</span>
|
||||||
<!-- 名称 -->
|
<!-- 名称 -->
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
arrow-class="terminal-tooltip-content"
|
arrow-class="terminal-tooltip-content"
|
||||||
content="连接主机">
|
content="连接主机">
|
||||||
<div class="terminal-sidebar-icon-wrapper">
|
<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 />
|
<icon-thunderbolt />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
<template>
|
<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>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
@@ -9,9 +18,70 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<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>
|
</style>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { CSSProperties } from 'vue';
|
import type { CSSProperties } from 'vue';
|
||||||
import type { TabItem } from '@/store/modules/terminal/types';
|
|
||||||
|
|
||||||
// sidebar 操作类型
|
// sidebar 操作类型
|
||||||
export interface SidebarAction {
|
export interface SidebarAction {
|
||||||
@@ -16,7 +15,7 @@ export const TabType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 内置 tab
|
// 内置 tab
|
||||||
export const InnerTabs: Record<string, TabItem> = {
|
export const InnerTabs = {
|
||||||
NEW_CONNECTION: {
|
NEW_CONNECTION: {
|
||||||
key: 'newConnection',
|
key: 'newConnection',
|
||||||
title: '新建连接',
|
title: '新建连接',
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export interface Payload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 输入协议
|
// 输入协议
|
||||||
export const InputProtocol: Record<string, Protocol> = {
|
export const InputProtocol = {
|
||||||
// 主机连接检查
|
// 主机连接检查
|
||||||
CHECK: {
|
CHECK: {
|
||||||
type: 'ck',
|
type: 'ck',
|
||||||
@@ -52,7 +52,7 @@ export const InputProtocol: Record<string, Protocol> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 输出协议
|
// 输出协议
|
||||||
export const OutputProtocol: Record<string, Protocol> = {
|
export const OutputProtocol = {
|
||||||
// 主机连接检查
|
// 主机连接检查
|
||||||
CHECK: {
|
CHECK: {
|
||||||
type: 'ck',
|
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;
|
payload.type = protocol.type;
|
||||||
return protocol.template
|
return protocol.template
|
||||||
.map(i => payload[i] || '')
|
.map(i => payload[i] || '')
|
||||||
|
|||||||
Reference in New Issue
Block a user