🔨 重构终端前端逻辑
This commit is contained in:
@@ -75,4 +75,6 @@ public interface ErrorMessage {
|
||||
|
||||
String SESSION_ABSENT = "会话不存在";
|
||||
|
||||
String CONNECT_ERROR = "连接失败";
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.orion.ops.module.asset.handler.host.terminal.handler;
|
||||
|
||||
import com.orion.lang.exception.argument.InvalidArgumentException;
|
||||
import com.orion.lang.utils.Exceptions;
|
||||
import com.orion.lang.utils.collect.Maps;
|
||||
import com.orion.ops.framework.biz.operator.log.core.service.OperatorLogFrameworkService;
|
||||
import com.orion.ops.framework.biz.operator.log.core.uitls.OperatorLogFiller;
|
||||
@@ -79,9 +81,12 @@ public class TerminalCheckHandler extends AbstractTerminalHandler<TerminalCheckR
|
||||
// 设置到缓存中
|
||||
channel.getAttributes().put(sessionId, connect);
|
||||
log.info("TerminalCheckHandler-handle success userId: {}, hostId: {}, sessionId: {}", userId, hostId, sessionId);
|
||||
} catch (Exception e) {
|
||||
} catch (InvalidArgumentException e) {
|
||||
ex = e;
|
||||
log.error("TerminalCheckHandler-handle error userId: {}, hostId: {}, sessionId: {}", userId, hostId, sessionId, e);
|
||||
} catch (Exception e) {
|
||||
ex = Exceptions.runtime(ErrorMessage.CONNECT_ERROR);
|
||||
log.error("TerminalCheckHandler-handle exception userId: {}, hostId: {}, sessionId: {}", userId, hostId, sessionId, e);
|
||||
}
|
||||
// 记录主机日志
|
||||
this.saveTerminalLog(channel, userId, host, startTime, ex, sessionId);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
{
|
||||
"name": "catppuccin-mocha",
|
||||
"dark": true,
|
||||
"headerBackgroundColor": "#121222",
|
||||
"schema": {
|
||||
"background": "#1E1E2E",
|
||||
"foreground": "#CDD6F4",
|
||||
@@ -27,7 +26,6 @@
|
||||
{
|
||||
"name": "MaterialDesignColors",
|
||||
"dark": true,
|
||||
"headerBackgroundColor": "#111A1E",
|
||||
"schema": {
|
||||
"background": "#1D262A",
|
||||
"foreground": "#E7EBED",
|
||||
@@ -52,7 +50,6 @@
|
||||
{
|
||||
"name": "catppuccin-macchiato",
|
||||
"dark": true,
|
||||
"headerBackgroundColor": "#181B2E",
|
||||
"schema": {
|
||||
"background": "#24273A",
|
||||
"foreground": "#CAD3F5",
|
||||
@@ -77,7 +74,6 @@
|
||||
{
|
||||
"name": "OneHalfDark",
|
||||
"dark": true,
|
||||
"headerBackgroundColor": "#1C2028",
|
||||
"schema": {
|
||||
"background": "#282C34",
|
||||
"foreground": "#DCDFE4",
|
||||
@@ -102,7 +98,6 @@
|
||||
{
|
||||
"name": "Dracula",
|
||||
"dark": true,
|
||||
"headerBackgroundColor": "#12131D",
|
||||
"schema": {
|
||||
"background": "#1E1F29",
|
||||
"foreground": "#F8F8F2",
|
||||
@@ -127,7 +122,6 @@
|
||||
{
|
||||
"name": "Atom",
|
||||
"dark": true,
|
||||
"headerBackgroundColor": "#0A0B0D",
|
||||
"schema": {
|
||||
"background": "#161719",
|
||||
"foreground": "#C5C8C6",
|
||||
@@ -152,7 +146,6 @@
|
||||
{
|
||||
"name": "Apple System Colors",
|
||||
"dark": true,
|
||||
"headerBackgroundColor": "#121212",
|
||||
"schema": {
|
||||
"background": "#1E1E1E",
|
||||
"foreground": "#FFFFFF",
|
||||
@@ -177,7 +170,6 @@
|
||||
{
|
||||
"name": "Builtin Tango Light",
|
||||
"dark": false,
|
||||
"headerBackgroundColor": "#F3F3F3",
|
||||
"schema": {
|
||||
"background": "#FFFFFF",
|
||||
"foreground": "#000000",
|
||||
@@ -202,7 +194,6 @@
|
||||
{
|
||||
"name": "Duotone Dark",
|
||||
"dark": true,
|
||||
"headerBackgroundColor": "#13111B",
|
||||
"schema": {
|
||||
"background": "#1F1D27",
|
||||
"foreground": "#B7A1FF",
|
||||
@@ -227,7 +218,6 @@
|
||||
{
|
||||
"name": "BlulocoLight",
|
||||
"dark": false,
|
||||
"headerBackgroundColor": "#EDEDED",
|
||||
"schema": {
|
||||
"background": "#F9F9F9",
|
||||
"foreground": "#373A41",
|
||||
@@ -252,7 +242,6 @@
|
||||
{
|
||||
"name": "Chester",
|
||||
"dark": true,
|
||||
"headerBackgroundColor": "#202A37",
|
||||
"schema": {
|
||||
"background": "#2C3643",
|
||||
"foreground": "#FFFFFF",
|
||||
@@ -277,7 +266,6 @@
|
||||
{
|
||||
"name": "CLRS",
|
||||
"dark": false,
|
||||
"headerBackgroundColor": "#F3F3F3",
|
||||
"schema": {
|
||||
"background": "#FFFFFF",
|
||||
"foreground": "#262626",
|
||||
@@ -302,7 +290,6 @@
|
||||
{
|
||||
"name": "Calamity",
|
||||
"dark": true,
|
||||
"headerBackgroundColor": "#231C27",
|
||||
"schema": {
|
||||
"background": "#2F2833",
|
||||
"foreground": "#D5CED9",
|
||||
@@ -327,7 +314,6 @@
|
||||
{
|
||||
"name": "Tomorrow",
|
||||
"dark": false,
|
||||
"headerBackgroundColor": "#F3F3F3",
|
||||
"schema": {
|
||||
"background": "#FFFFFF",
|
||||
"foreground": "#4D4D4C",
|
||||
|
||||
@@ -5,7 +5,6 @@ import com.alibaba.fastjson.JSONObject;
|
||||
import com.alibaba.fastjson.annotation.JSONField;
|
||||
import com.alibaba.fastjson.serializer.ValueFilter;
|
||||
import com.orion.lang.utils.Colors;
|
||||
import com.orion.lang.utils.Strings;
|
||||
import com.orion.lang.utils.collect.Lists;
|
||||
import com.orion.lang.utils.io.FileReaders;
|
||||
import com.orion.lang.utils.io.Files1;
|
||||
@@ -59,8 +58,6 @@ public class TerminalThemeGenerator {
|
||||
TerminalTheme theme = new TerminalTheme();
|
||||
theme.setName(schema.getString("name"));
|
||||
theme.setDark(Colors.isDarkColor(background));
|
||||
// header 颜色为深 12
|
||||
theme.setHeaderBackgroundColor(adjustColor(background, -12));
|
||||
theme.setSchema(JSON.parseObject(JSON.toJSONString(schema), TerminalThemeSchema.class));
|
||||
return theme;
|
||||
}).collect(Collectors.toList());
|
||||
@@ -113,8 +110,6 @@ public class TerminalThemeGenerator {
|
||||
@JSONField(ordinal = 1)
|
||||
private Boolean dark;
|
||||
@JSONField(ordinal = 2)
|
||||
private String headerBackgroundColor;
|
||||
@JSONField(ordinal = 3)
|
||||
private TerminalThemeSchema schema;
|
||||
}
|
||||
|
||||
@@ -162,26 +157,4 @@ public class TerminalThemeGenerator {
|
||||
private String brightWhite;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整颜色
|
||||
*
|
||||
* @param color color
|
||||
* @param range 正数越浅 负数越深
|
||||
* @return color
|
||||
*/
|
||||
private static String adjustColor(String color, int range) {
|
||||
StringBuilder newColor = new StringBuilder("#");
|
||||
for (int i = 0; i < 3; i++) {
|
||||
int c = Integer.parseInt(color.substring(i * 2 + 1, i * 2 + 3), 16);
|
||||
c += range;
|
||||
if (c < 0) {
|
||||
c = 0;
|
||||
} else if (c > 255) {
|
||||
c = 255;
|
||||
}
|
||||
newColor.append(Strings.leftPad(Integer.toString(c, 16), 2, "0"));
|
||||
}
|
||||
return newColor.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import axios from 'axios';
|
||||
export interface TerminalTheme {
|
||||
name: string;
|
||||
dark: boolean;
|
||||
headerBackgroundColor: string;
|
||||
schema: TerminalThemeSchema;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// 亮色主题配色常量
|
||||
body {
|
||||
--color-bg-header: #232323;
|
||||
--color-bg-sidebar: #F2F3F4;
|
||||
--color-bg-sidebar: #EBECED;
|
||||
--color-bg-panel: var(--color-bg-sidebar);
|
||||
--color-bg-content: #FEFEFE;
|
||||
--color-sidebar-icon: #737070;
|
||||
--color-sidebar-icon-bg: #D7D8DB;
|
||||
@@ -11,6 +12,14 @@ body {
|
||||
--color-content-text-1: rgba(0, 0, 0, .8);
|
||||
--color-content-text-2: rgba(0, 0, 0, .85);
|
||||
--color-content-text-3: rgba(0, 0, 0, .95);
|
||||
--color-bg-panel-tabs: var(--color-bg-panel);
|
||||
--color-bg-panel-tabs-active: #F9F9F9;
|
||||
--color-bg-panel-icon-1: #F5F5F5;
|
||||
--color-bg-panel-bar: #F3F4F5;
|
||||
--color-panel-text-1: var(--color-content-text-1);
|
||||
--color-panel-text-2: var(--color-content-text-3);
|
||||
--color-panel-gradient-start: rgba(218, 218, 218, 1);
|
||||
--color-panel-gradient-end: rgba(218, 218, 218, 0);
|
||||
--search-bg-focus: rgba(234, 234, 234, .75);
|
||||
--search-bg: rgba(234, 234, 234, .95);
|
||||
--search-color-text: #0E0E0E;
|
||||
@@ -25,6 +34,7 @@ body {
|
||||
body[terminal-theme='dark'] {
|
||||
--color-bg-header: #232323;
|
||||
--color-bg-sidebar: #2C2E31;
|
||||
--color-bg-panel: var(--color-bg-sidebar);
|
||||
--color-bg-content: #1A1B1C;
|
||||
--color-sidebar-icon: #C3C6C9;
|
||||
--color-sidebar-icon-bg: #3D3E3F;
|
||||
@@ -34,6 +44,14 @@ body[terminal-theme='dark'] {
|
||||
--color-content-text-1: rgba(255, 255, 255, .8);
|
||||
--color-content-text-2: rgba(255, 255, 255, .85);
|
||||
--color-content-text-3: rgba(255, 255, 255, .95);
|
||||
--color-bg-panel-tabs: var(--color-bg-panel);
|
||||
--color-bg-panel-tabs-active: #434343;
|
||||
--color-bg-panel-icon-1: var(--color-bg-panel-tabs-active);
|
||||
--color-bg-panel-bar: #323538;
|
||||
--color-panel-text-1: var(--color-content-text-1);
|
||||
--color-panel-text-2: var(--color-content-text-3);
|
||||
--color-panel-gradient-start: rgba(38, 38, 38, 1);
|
||||
--color-panel-gradient-end: rgba(38, 38, 38, 0);
|
||||
--search-bg: rgba(12, 12, 12, .75);
|
||||
--search-bg-focus: rgba(12, 12, 12, .95);
|
||||
--search-color-text: #E0E0E0;
|
||||
@@ -47,14 +65,14 @@ body[terminal-theme='dark'] {
|
||||
// 布局常量
|
||||
.host-terminal-layout {
|
||||
--header-height: 44px;
|
||||
--panel-nav-height: 40px;
|
||||
--sidebar-width: 44px;
|
||||
--sidebar-icon-wrapper-size: var(--header-height);
|
||||
--sidebar-icon-size: 32px;
|
||||
--sidebar-icon-font-size: 22px;
|
||||
|
||||
--color-bg-header-icon-1: #434343;
|
||||
--color-header-tabs-bg: var(--color-bg-header);
|
||||
--color-header-tabs-bg-hover: #434343;
|
||||
--color-bg-header-tabs: var(--color-bg-header);
|
||||
--color-bg-header-tabs-active: #434343;
|
||||
--color-bg-header-icon-1: var(--color-bg-header-tabs-active);
|
||||
--color-header-text-1: rgba(255, 255, 255, .75);
|
||||
--color-header-text-2: rgba(255, 255, 255, .9);
|
||||
--color-gradient-start: rgba(38, 38, 38, 1);
|
||||
|
||||
@@ -17,9 +17,10 @@ import { defineStore } from 'pinia';
|
||||
import { getPreference, updatePreference } from '@/api/user/preference';
|
||||
import { nextSessionId } from '@/utils';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { TerminalTabs } from '@/views/host/terminal/types/terminal.const';
|
||||
import { TerminalPanelTabType, TerminalTabs } from '@/views/host/terminal/types/terminal.const';
|
||||
import TerminalTabManager from '@/views/host/terminal/handler/terminal-tab-manager';
|
||||
import TerminalSessionManager from '@/views/host/terminal/handler/terminal-session-manager';
|
||||
import TerminalPanelManager from '@/views/host/terminal/handler/terminal-panel-manager';
|
||||
|
||||
// 终端偏好项
|
||||
export const TerminalPreferenceItem = {
|
||||
@@ -63,7 +64,7 @@ export default defineStore('terminal', {
|
||||
},
|
||||
hosts: {} as AuthorizedHostQueryResponse,
|
||||
tabManager: new TerminalTabManager(TerminalTabs.NEW_CONNECTION),
|
||||
routerTabManager: [new TerminalTabManager()],
|
||||
panelManager: new TerminalPanelManager(),
|
||||
sessionManager: new TerminalSessionManager()
|
||||
}),
|
||||
|
||||
@@ -137,43 +138,57 @@ export default defineStore('terminal', {
|
||||
// 切换到终端面板页面
|
||||
this.tabManager.openTab(TerminalTabs.TERMINAL_PANEL);
|
||||
// 获取 seq
|
||||
const tabSeqArr = this.tabManager.items
|
||||
const seqArr = this.panelManager
|
||||
.getPanel(panelIndex)
|
||||
.items
|
||||
.map(s => s.seq)
|
||||
.filter(Boolean)
|
||||
.map(Number);
|
||||
const nextSeq = tabSeqArr.length
|
||||
? Math.max(...tabSeqArr) + 1
|
||||
const nextSeq = seqArr.length
|
||||
? Math.max(...seqArr) + 1
|
||||
: 1;
|
||||
// FIXME
|
||||
// 打开 tab
|
||||
this.tabManager.openTab({
|
||||
this.panelManager.getPanel(panelIndex).openTab({
|
||||
key: nextSessionId(10),
|
||||
seq: nextSeq,
|
||||
title: `(${nextSeq}) ${record.alias || record.name}`,
|
||||
hostId: record.id,
|
||||
address: record.address
|
||||
address: record.address,
|
||||
icon: 'icon-desktop',
|
||||
type: TerminalPanelTabType.TERMINAL
|
||||
});
|
||||
},
|
||||
|
||||
// 复制并且打开终端
|
||||
openCopyTerminal(hostId: number) {
|
||||
openCopyTerminal(hostId: number, panelIndex: number = 0) {
|
||||
const host = this.hosts.hostList
|
||||
.find(s => s.id === hostId);
|
||||
if (host) {
|
||||
this.openTerminal(host);
|
||||
this.openTerminal(host, panelIndex);
|
||||
}
|
||||
},
|
||||
|
||||
// 获取当前终端会话
|
||||
getCurrentTerminalSession(tips: boolean = true) {
|
||||
// 获取当前 tab
|
||||
const tab = this.tabManager.getCurrentTab();
|
||||
// FIXME
|
||||
if (!tab || tab.type !== 'TERMINAL') {
|
||||
if (!tab || tab.key !== TerminalTabs.TERMINAL_PANEL.key) {
|
||||
if (tips) {
|
||||
Message.warning('请切换到终端标签页');
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 获取面板会话
|
||||
const activeTab = this.panelManager
|
||||
.getCurrentPanel()
|
||||
.getCurrentTab();
|
||||
if (!activeTab || activeTab.type !== TerminalPanelTabType.TERMINAL) {
|
||||
if (tips) {
|
||||
Message.warning('请打开终端');
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 获取会话
|
||||
return this.sessionManager.getSession(tab.key);
|
||||
},
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ITerminalSessionManager, ITerminalTabManager } from '@/views/host/terminal/types/terminal.type';
|
||||
import type { ITerminalPanelManager, ITerminalSessionManager, ITerminalTabManager } from '@/views/host/terminal/types/terminal.type';
|
||||
import type { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data';
|
||||
import type { TerminalTheme } from '@/api/asset/host-terminal';
|
||||
|
||||
@@ -6,7 +6,7 @@ export interface TerminalState {
|
||||
preference: TerminalPreference;
|
||||
hosts: AuthorizedHostQueryResponse;
|
||||
tabManager: ITerminalTabManager;
|
||||
routerTabManager: Array<ITerminalTabManager>;
|
||||
panelManager: ITerminalPanelManager;
|
||||
sessionManager: ITerminalSessionManager;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,17 @@
|
||||
@tab-click="k => tabManager.clickTab(k as string)"
|
||||
@delete="k => tabManager.deleteTab(k as string)">
|
||||
<a-tab-pane v-for="tab in tabManager.items"
|
||||
:key="tab.key"
|
||||
:title="tab.title" />
|
||||
:key="tab.key">
|
||||
<!-- 标题 -->
|
||||
<template #title>
|
||||
<span class="tab-title-wrapper">
|
||||
<span class="tab-title-icon">
|
||||
<component :is="tab.icon" />
|
||||
</span>
|
||||
{{ tab.title }}
|
||||
</span>
|
||||
</template>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
<!-- 右侧操作 -->
|
||||
@@ -45,6 +54,7 @@
|
||||
import { computed } from 'vue';
|
||||
import { useTerminalStore } from '@/store';
|
||||
import IconActions from '../layout/icon-actions.vue';
|
||||
import DictKeySelector from '@/components/system/dict-key/dict-key-selector.vue';
|
||||
|
||||
const { isFullscreen, toggle: toggleFullScreen } = useFullscreen();
|
||||
const { tabManager } = useTerminalStore();
|
||||
@@ -118,6 +128,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.tab-title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.tab-title-icon {
|
||||
font-size: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-nav) {
|
||||
height: 100%;
|
||||
|
||||
@@ -151,7 +171,7 @@
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: var(--color-header-text-1);
|
||||
background: var(--color-header-tabs-bg);
|
||||
background: var(--color-bg-header-tabs);
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
@@ -177,8 +197,9 @@
|
||||
|
||||
.arco-tabs-tab-title {
|
||||
padding: 11px 18px;
|
||||
background: var(--color-header-tabs-bg);
|
||||
font-size: 12px;
|
||||
background: var(--color-bg-header-tabs);
|
||||
font-size: 14px;
|
||||
height: var(--header-height);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -213,11 +234,11 @@
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-tab-active) {
|
||||
background: var(--color-header-tabs-bg-hover);
|
||||
background: var(--color-bg-header-tabs-active);
|
||||
color: var(--color-header-text-2) !important;
|
||||
|
||||
.arco-tabs-tab-title {
|
||||
background: var(--color-header-tabs-bg-hover);
|
||||
background: var(--color-bg-header-tabs-active);
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
<div class="terminal-content">
|
||||
<!-- 内容 tabs -->
|
||||
<a-tabs v-if="tabManager.active"
|
||||
v-model:active-key="tabManager.active">
|
||||
v-model:active-key="tabManager.active"
|
||||
class="main-tabs">
|
||||
<a-tab-pane v-for="tab in tabManager.items"
|
||||
:key="tab.key"
|
||||
:title="tab.title">
|
||||
@@ -16,8 +17,8 @@
|
||||
<terminal-theme-setting v-else-if="tab.key === TerminalTabs.THEME_SETTING.key" />
|
||||
<!-- 终端设置 -->
|
||||
<terminal-general-setting v-else-if="tab.key === TerminalTabs.TERMINAL_SETTING.key" />
|
||||
<!-- 主机终端 -->
|
||||
<terminal-panel-view v-else-if="tab.key === TerminalTabs.TERMINAL_PANEL.key" />
|
||||
<!-- 终端面板 -->
|
||||
<terminal-panels-view v-else-if="tab.key === TerminalTabs.TERMINAL_PANEL.key" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
<!-- 承载页推荐 -->
|
||||
@@ -42,11 +43,11 @@
|
||||
import TerminalThemeSetting from '../setting/theme/terminal-theme-setting.vue';
|
||||
import TerminalGeneralSetting from '../setting/general/terminal-general-setting.vue';
|
||||
import TerminalShortcutSetting from '../setting/shortcut/terminal-shortcut-setting.vue';
|
||||
import TerminalPanelView from '@/views/host/terminal/components/layout/terminal-panel-view.vue';
|
||||
import TerminalPanelsView from '@/views/host/terminal/components/layout/terminal-panels-view.vue';
|
||||
|
||||
const { preference, tabManager, sessionManager } = useTerminalStore();
|
||||
|
||||
// fixme TerminalTabType.TERMINAL
|
||||
// fixme title逻辑 失焦逻辑
|
||||
// 监听 tab 修改
|
||||
watch(() => tabManager.active, (active, before) => {
|
||||
if (before) {
|
||||
@@ -58,11 +59,10 @@
|
||||
}
|
||||
if (active) {
|
||||
// 获取 activeTab
|
||||
const activeTab = tabManager.items.find(s => s.key === active);
|
||||
const activeTab = tabManager.getCurrentTab();
|
||||
if (!activeTab) {
|
||||
return;
|
||||
}
|
||||
console.log(activeTab.title);
|
||||
// 修改标题
|
||||
document.title = activeTab.title;
|
||||
// 终端自动聚焦
|
||||
@@ -78,8 +78,8 @@
|
||||
// 处理快捷键逻辑
|
||||
const handlerKeyboard = (event: Event) => {
|
||||
// 当前页面非 terminal 的时候再触发快捷键 (terminal 有内置逻辑)
|
||||
if (tabManager.active
|
||||
&& tabManager.items.find(s => s.key === tabManager.active)?.type === 'TerminalTabType.TERMINAL') {
|
||||
// fixme panel 无数据继续触发
|
||||
if (tabManager.getCurrentTab()?.key === TerminalTabs.TERMINAL_PANEL.key) {
|
||||
return;
|
||||
}
|
||||
const e = event as KeyboardEvent;
|
||||
@@ -140,15 +140,15 @@
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
:deep(.arco-tabs) {
|
||||
:deep(.main-tabs) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.arco-tabs-nav {
|
||||
> .arco-tabs-nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.arco-tabs-content {
|
||||
> .arco-tabs-content {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'terminalPanelView'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<div class="terminal-panel-container">
|
||||
<a-tabs v-model:active-key="panel.active"
|
||||
:editable="true"
|
||||
:auto-switch="true"
|
||||
@tab-click="k => panel.clickTab(k as string)"
|
||||
@delete="k => panel.deleteTab(k as string)">
|
||||
<a-tab-pane v-for="tab in panel.items"
|
||||
:key="tab.key">
|
||||
<!-- 标题 -->
|
||||
<template #title>
|
||||
<span class="tab-title-wrapper">
|
||||
<span class="tab-title-icon">
|
||||
<component :is="tab.icon" />
|
||||
</span>
|
||||
{{ tab.title }}
|
||||
</span>
|
||||
</template>
|
||||
<!-- 终端 -->
|
||||
<terminal-view :tab="tab" />
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'terminalPanel'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ITerminalTabManager } from '../../types/terminal.type';
|
||||
import TerminalView from '@/views/host/terminal/components/xterm/terminal-view.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
panel: ITerminalTabManager
|
||||
}>();
|
||||
|
||||
// FIXME 全部关闭则关闭
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.terminal-panel-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tab-title-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.tab-title-icon {
|
||||
font-size: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-tabs) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.arco-tabs-content {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: calc(100% - var(--panel-nav-height));
|
||||
overflow: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-nav) {
|
||||
height: var(--panel-nav-height);
|
||||
background: var(--color-bg-panel);
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-tab {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
&-ink {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&-button .arco-icon-hover:hover {
|
||||
color: var(--color-panel-text-2);
|
||||
|
||||
&::before {
|
||||
background: var(--color-bg-panel-icon-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-nav-type-line .arco-tabs-tab:hover .arco-tabs-tab-title::before) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-tab) {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: var(--color-panel-text-1);
|
||||
background: var(--color-bg-panel-tabs);
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-panel-text-2);
|
||||
transition: .2s;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
width: 54px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
background: linear-gradient(270deg, var(--color-panel-gradient-start) 45%, var(--color-panel-gradient-end) 120%);
|
||||
}
|
||||
|
||||
.arco-tabs-tab-title {
|
||||
padding: 11px 18px;
|
||||
background: var(--color-bg-panel-tabs);
|
||||
font-size: 14px;
|
||||
height: var(--panel-nav-height);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .arco-tabs-tab-close-btn {
|
||||
display: unset;
|
||||
}
|
||||
|
||||
&-close-btn {
|
||||
margin: 0 8px 0 0;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
z-index: 4;
|
||||
display: none;
|
||||
color: var(--color-panel-text-2);
|
||||
|
||||
&:hover {
|
||||
transition: .2s;
|
||||
background: var(--color-bg-panel-icon-1);
|
||||
}
|
||||
|
||||
&::before {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-tabs-tab-active) {
|
||||
background: var(--color-bg-panel-tabs-active);
|
||||
color: var(--color-panel-text-2) !important;
|
||||
|
||||
.arco-tabs-tab-title {
|
||||
background: var(--color-bg-panel-tabs-active);
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
background: linear-gradient(270deg, var(--color-panel-gradient-start) 45%, var(--color-panel-gradient-end) 120%);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="terminal-panels-container">
|
||||
<!-- 面板 -->
|
||||
<terminal-panel v-for="panelIndex in panelManager.panels.length"
|
||||
:key="panelIndex"
|
||||
:panel="panelManager.panels[panelIndex - 1]" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'terminalPanelView'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useTerminalStore } from '@/store';
|
||||
import TerminalPanel from './terminal-panel.vue';
|
||||
import { onUnmounted } from 'vue';
|
||||
|
||||
const { panelManager } = useTerminalStore();
|
||||
// FIXME 全部关闭则关闭
|
||||
|
||||
// 卸载清空
|
||||
onUnmounted(() => {
|
||||
panelManager.reset();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.terminal-panels-container {
|
||||
width: 100%;
|
||||
height: calc(100vh - var(--header-height));
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
@@ -21,7 +21,7 @@
|
||||
@save="savePreference" />
|
||||
<!-- 系统快捷键 -->
|
||||
<terminal-shortcut-keys-block title="系统快捷键"
|
||||
:type="TerminalShortcutType.SYSTEM"
|
||||
:type="TerminalShortcutType.TAB"
|
||||
:items="shortcutKeys"
|
||||
@set-editable="setEditableStatus"
|
||||
@clear-editable="clearEditableStatus"
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
<template>
|
||||
<div class="terminal-container">
|
||||
<!-- 头部 -->
|
||||
<div class="terminal-header"
|
||||
:style="{
|
||||
background: preference.theme.headerBackgroundColor
|
||||
}">
|
||||
<div class="terminal-header">
|
||||
<!-- 左侧操作 -->
|
||||
<div class="terminal-header-left">
|
||||
<!-- 主机地址 -->
|
||||
@@ -161,7 +158,7 @@
|
||||
|
||||
.terminal-container {
|
||||
width: 100%;
|
||||
height: calc(100vh - var(--header-height));
|
||||
height: calc(100vh - var(--header-height) - var(--panel-nav-height));
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -172,6 +169,7 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: var(--color-bg-panel-bar);
|
||||
|
||||
&-left, &-right {
|
||||
display: flex;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { ITerminalPanelManager, TerminalPanelTabItem } from '../types/terminal.type';
|
||||
import TerminalTabManager from '../handler/terminal-tab-manager';
|
||||
|
||||
// 终端面板管理器实现
|
||||
export default class TerminalPanelManager<T extends TerminalPanelTabItem = TerminalPanelTabItem> implements ITerminalPanelManager<T> {
|
||||
|
||||
// 当前面板
|
||||
active: number;
|
||||
// 面板列表
|
||||
panels: Array<TerminalTabManager<T>>;
|
||||
|
||||
constructor() {
|
||||
this.active = 0;
|
||||
this.panels = [new TerminalTabManager()];
|
||||
}
|
||||
|
||||
// 获取当前面板
|
||||
getCurrentPanel(): TerminalTabManager<T> {
|
||||
return this.panels[this.active];
|
||||
}
|
||||
|
||||
// 设置当前面板
|
||||
setCurrentPanel(active: number): void {
|
||||
this.active = active;
|
||||
};
|
||||
|
||||
// 获取面板
|
||||
getPanel(index: number): TerminalTabManager<T> {
|
||||
return this.panels[index];
|
||||
};
|
||||
|
||||
// 重置
|
||||
reset() {
|
||||
for (let panel of this.panels) {
|
||||
panel.clear();
|
||||
}
|
||||
this.active = 0;
|
||||
this.panels = [new TerminalTabManager()];
|
||||
};
|
||||
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { ITerminalTabManager, TerminalTabItem } from '../types/terminal.type';
|
||||
|
||||
// 终端 tab 管理器实现
|
||||
export default class TerminalTabManager implements ITerminalTabManager {
|
||||
export default class TerminalTabManagerm<T extends TerminalTabItem = TerminalTabItem> implements ITerminalTabManager<T> {
|
||||
|
||||
public active: string;
|
||||
|
||||
public items: Array<TerminalTabItem>;
|
||||
public items: Array<T>;
|
||||
|
||||
constructor(def: TerminalTabItem | undefined = undefined) {
|
||||
constructor(def: T | undefined = undefined) {
|
||||
if (def) {
|
||||
this.active = def.key;
|
||||
this.items = [def];
|
||||
@@ -45,7 +45,7 @@ export default class TerminalTabManager implements ITerminalTabManager {
|
||||
}
|
||||
|
||||
// 打开 tab
|
||||
openTab(tab: TerminalTabItem): void {
|
||||
openTab(tab: T): void {
|
||||
// 不存在则创建 tab
|
||||
if (!this.items.find(s => s.key === tab.key)) {
|
||||
this.items.push(tab);
|
||||
|
||||
@@ -130,7 +130,6 @@
|
||||
width: var(--sidebar-width);
|
||||
height: 100%;
|
||||
background: var(--color-bg-sidebar);
|
||||
border-top: 1px solid var(--color-bg-content);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -52,6 +52,12 @@ export const ExtraSshAuthType = {
|
||||
CUSTOM_IDENTITY: 'CUSTOM_IDENTITY',
|
||||
};
|
||||
|
||||
// 终端面板 tab 类型
|
||||
export const TerminalPanelTabType = {
|
||||
TERMINAL: 'terminal',
|
||||
SFTP: 'sftp',
|
||||
};
|
||||
|
||||
// 终端状态
|
||||
export const TerminalStatus = {
|
||||
// 连接中
|
||||
@@ -125,7 +131,7 @@ export const ActionBarItems = [
|
||||
|
||||
// 终端快捷键操作类型
|
||||
export const TerminalShortcutType = {
|
||||
SYSTEM: 1,
|
||||
TAB: 1,
|
||||
TERMINAL: 2
|
||||
};
|
||||
|
||||
@@ -146,19 +152,19 @@ export const TerminalShortcutItems: Array<ShortcutKeyItem> = [
|
||||
{
|
||||
item: TerminalShortcutKeys.CHANGE_TO_PREV_TAB,
|
||||
content: '切换为前一个 tab',
|
||||
type: TerminalShortcutType.SYSTEM
|
||||
type: TerminalShortcutType.TAB
|
||||
}, {
|
||||
item: TerminalShortcutKeys.CHANGE_TO_NEXT_TAB,
|
||||
content: '切换为后一个 tab',
|
||||
type: TerminalShortcutType.SYSTEM
|
||||
type: TerminalShortcutType.TAB
|
||||
}, {
|
||||
item: TerminalShortcutKeys.CLOSE_TAB,
|
||||
content: '关闭当前 tab',
|
||||
type: TerminalShortcutType.SYSTEM
|
||||
type: TerminalShortcutType.TAB
|
||||
}, {
|
||||
item: TerminalShortcutKeys.OPEN_NEW_CONNECT_TAB,
|
||||
content: '打开新建连接 tab',
|
||||
type: TerminalShortcutType.SYSTEM
|
||||
type: TerminalShortcutType.TAB
|
||||
}, {
|
||||
item: 'openCopyTerminalTab',
|
||||
content: '复制当前终端 tab',
|
||||
|
||||
@@ -12,11 +12,19 @@ import type { HostQueryResponse } from '@/api/asset/host';
|
||||
export interface TerminalTabItem {
|
||||
key: string;
|
||||
title: string;
|
||||
icon?: string;
|
||||
icon: string;
|
||||
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// 终端面板 tab 元素
|
||||
export interface TerminalPanelTabItem extends TerminalTabItem {
|
||||
seq: number;
|
||||
hostId: number;
|
||||
address: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
// sidebar 操作类型
|
||||
export interface SidebarAction {
|
||||
icon: string;
|
||||
@@ -90,20 +98,20 @@ export interface TerminalDomRef {
|
||||
}
|
||||
|
||||
// 终端 tab 管理器定义
|
||||
export interface ITerminalTabManager {
|
||||
export interface ITerminalTabManager<T extends TerminalTabItem = TerminalTabItem> {
|
||||
// 当前 tab
|
||||
active: string;
|
||||
// 全部 tab
|
||||
items: Array<TerminalTabItem>;
|
||||
items: Array<T>;
|
||||
|
||||
// 获取当前 tab
|
||||
getCurrentTab: () => TerminalTabItem | undefined;
|
||||
getCurrentTab: () => T | undefined;
|
||||
// 点击 tab
|
||||
clickTab: (key: string) => void;
|
||||
// 删除 tab
|
||||
deleteTab: (key: string) => void;
|
||||
// 打开 tab
|
||||
openTab: (tab: TerminalTabItem) => void;
|
||||
openTab: (tab: T) => void;
|
||||
// 切换到前一个 tab
|
||||
changeToPrevTab: () => void;
|
||||
// 切换到后一个 tab
|
||||
@@ -114,6 +122,23 @@ export interface ITerminalTabManager {
|
||||
clear: () => void;
|
||||
}
|
||||
|
||||
// 终端面板管理器定义
|
||||
export interface ITerminalPanelManager<T extends TerminalPanelTabItem = TerminalPanelTabItem> {
|
||||
// 当前面板
|
||||
active: number;
|
||||
// 面板列表
|
||||
panels: Array<ITerminalTabManager<T>>;
|
||||
|
||||
// 获取当前面板
|
||||
getCurrentPanel: () => ITerminalTabManager<T>;
|
||||
// 设置当前面板
|
||||
setCurrentPanel: (active: number) => void;
|
||||
// 获取面板
|
||||
getPanel: (index: number) => ITerminalTabManager<T>;
|
||||
// 重置
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
// 终端会话管理器定义
|
||||
export interface ITerminalSessionManager {
|
||||
// 打开终端会话
|
||||
|
||||
Reference in New Issue
Block a user