🔨 重构终端前端逻辑

This commit is contained in:
lijiahangmax
2024-02-02 01:07:03 +08:00
parent 697de97473
commit ae52a556d9
20 changed files with 413 additions and 119 deletions

View File

@@ -4,7 +4,6 @@ import axios from 'axios';
export interface TerminalTheme {
name: string;
dark: boolean;
headerBackgroundColor: string;
schema: TerminalThemeSchema;
}

View File

@@ -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);

View File

@@ -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);
},

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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%;

View File

@@ -1,17 +0,0 @@
<template>
</template>
<script lang="ts">
export default {
name: 'terminalPanelView'
};
</script>
<script lang="ts" setup>
</script>
<style lang="less" scoped>
</style>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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"

View File

@@ -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;

View File

@@ -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()];
};
}

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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',

View File

@@ -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 {
// 打开终端会话