refactor: 修改 tabs 存储方式.

This commit is contained in:
lijiahang
2024-01-04 17:10:07 +08:00
parent acc165b4c6
commit 685e68a47f
17 changed files with 258 additions and 173 deletions

View File

@@ -1,2 +1,3 @@
VITE_API_BASE_URL= 'http://127.0.0.1:9200/orion-api'
VITE_WS_BASE_URL= 'ws://127.0.0.1:9200/orion/keep-alive'
VITE_APP_VERSION= '1.0.0'

View File

@@ -1,2 +1,3 @@
VITE_API_BASE_URL= 'http://127.0.0.1:9200/orion-api'
VITE_WS_BASE_URL= 'ws://127.0.0.1:9200/orion/keep-alive'
VITE_APP_VERSION= '1.0.0'

View File

@@ -0,0 +1,16 @@
import axios from 'axios';
/**
* 主机终端访问响应
*/
export interface HostTerminalAccessResponse {
accessToken: string;
sessionInitial: string;
}
/**
* 获取主机终端 accessToken
*/
export function getHostTerminalAccessToken() {
return axios.get<HostTerminalAccessResponse>('/asset/host-terminal/access');
}

View File

@@ -9,6 +9,7 @@ declare module '*.vue' {
interface ImportMetaEnv {
readonly VITE_API_BASE_URL: string;
readonly VITE_WS_BASE_URL: string;
readonly VITE_APP_VERSION: string;
}

View File

@@ -1,9 +1,11 @@
import type { TerminalDisplaySetting, TerminalPreference, TerminalState, TerminalThemeSchema } from './types';
import type { TabItem, TerminalDisplaySetting, TerminalPreference, TerminalState, TerminalThemeSchema } from './types';
import type { HostQueryResponse } from '@/api/asset/host';
import { defineStore } from 'pinia';
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-ops/terminal/types/terminal.theme';
import { InnerTabs } from '@/views/host-ops/terminal/types/terminal.const';
// 暗色主题
export const DarkTheme = {
@@ -28,6 +30,10 @@ export default defineStore('terminal', {
displaySetting: {} as TerminalDisplaySetting,
themeSchema: {} as TerminalThemeSchema
},
tabs: {
active: InnerTabs.NEW_CONNECTION.key,
items: [InnerTabs.NEW_CONNECTION, InnerTabs.VIEW_SETTING]
}
}),
actions: {
@@ -94,7 +100,7 @@ export default defineStore('terminal', {
await this.updateTerminalPreference('newConnectionType', newConnectionType);
},
// 更新终端偏好-防抖
// 更新终端偏好
async updateTerminalPreference(item: string, value: any) {
try {
// 修改配置
@@ -106,7 +112,37 @@ export default defineStore('terminal', {
} catch (e) {
Message.error('同步失败');
}
},
// 点击 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;
},
// 打开终端
openTerminal(record: HostQueryResponse) {
console.log(record);
}
},
});

View File

@@ -3,6 +3,7 @@ import type { Ref } from 'vue';
export interface TerminalState {
isDarkTheme: Ref<boolean>;
preference: TerminalPreference;
tabs: TerminalTabs;
}
// 终端配置
@@ -54,3 +55,18 @@ export interface TerminalThemeSchema {
[key: string]: unknown;
}
// 终端 tab
export interface TerminalTabs {
active: string;
items: Array<TabItem>;
}
// tab 元素
export interface TabItem {
key: string;
title: string;
type: string;
[key: string]: unknown;
}

View File

@@ -7,8 +7,7 @@
content-class="terminal-tooltip-content"
arrow-class="terminal-tooltip-arrow"
:content="action.content">
<div class="terminal-sidebar-icon-wrapper"
v-if="action.visible !== false">
<div class="terminal-sidebar-icon-wrapper">
<div class="terminal-sidebar-icon"
:class="iconClass"
@click="action.click">

View File

@@ -1,8 +1,8 @@
<template>
<div class="terminal-content">
<!-- 内容 tabs -->
<a-tabs v-model:active-key="activeKey">
<a-tab-pane v-for="tab in tabs"
<a-tabs v-model:active-key="terminalStore.tabs.active">
<a-tab-pane v-for="tab in terminalStore.tabs.items"
:key="tab.key"
:title="tab.title">
<!-- 设置 -->
@@ -18,6 +18,9 @@
</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 }}
@@ -35,31 +38,13 @@
</script>
<script lang="ts" setup>
import type { PropType } from 'vue';
import type { TabItem } from '../../types/terminal.const';
import { computed } from 'vue';
import { TabType, InnerTabs } from '../../types/terminal.const';
import { useTerminalStore } from '@/store';
import TerminalViewSetting from '../view-setting/terminal-view-setting.vue';
import NewConnectionView from '@/views/host-ops/terminal/components/new-connection/new-connection-view.vue';
import NewConnectionView from '../new-connection/new-connection-view.vue';
import TerminalView from '../xterm/terminal-view.vue';
const props = defineProps({
modelValue: {
type: String,
required: true
},
tabs: {
type: Array as PropType<Array<TabItem>>,
required: true
},
});
const activeKey = computed<String>({
get() {
return props.modelValue;
},
set() {
}
});
const terminalStore = useTerminalStore();
</script>

View File

@@ -10,30 +10,19 @@
</div>
<!-- 左侧 tabs -->
<div class="terminal-header-tabs">
<a-tabs v-model:active-key="activeKey"
<a-tabs v-model:active-key="terminalStore.tabs.active"
:editable="true"
:hide-content="true"
:auto-switch="true"
@tab-click="e => emits('clickTab', e)"
@delete="e => emits('deleteTab', e)">
<a-tab-pane v-for="tab in tabs"
@tab-click="terminalStore.clickTab"
@delete="terminalStore.deleteTab">
<a-tab-pane v-for="tab in terminalStore.tabs.items"
:key="tab.key"
:title="tab.title" />
</a-tabs>
</div>
<!-- 右侧操作 -->
<div class="terminal-header-right">
<!-- 分享用户 -->
<a-avatar-group v-if="false"
class="terminal-header-right-avatar-group"
:size="28"
:max-count="4"
:max-style="{background: '#168CFF'}">
<a-avatar v-for="i in 8" :key="i"
:style="{background: '#168CFF'}">
{{ i }}
</a-avatar>
</a-avatar-group>
<!-- 操作按钮 -->
<icon-actions class="terminal-header-right-actions"
:actions="actions"
@@ -50,62 +39,29 @@
</script>
<script lang="ts" setup>
import type { SidebarAction, TabItem } from '../../types/terminal.const';
import type { PropType } from 'vue';
import type { SidebarAction } from '../../types/terminal.const';
import { useFullscreen } from '@vueuse/core';
import { computed } from 'vue';
import { useTerminalStore } from '@/store';
import IconActions from '../layout/icon-actions.vue';
const props = defineProps({
modelValue: {
type: String,
required: true
},
tabs: {
type: Array as PropType<Array<TabItem>>,
required: true
}
});
const emits = defineEmits(['update:modelValue', 'clickTab', 'deleteTab', 'share']);
const { isFullscreen, toggle: toggleFullScreen } = useFullscreen();
const terminalStore = useTerminalStore();
// 顶部操作
const actions = computed<Array<SidebarAction>>(() => [
{
icon: 'icon-share-alt',
content: '分享链接',
visible: false,
click: () => emits('share')
},
{
icon: isFullscreen.value ? 'icon-fullscreen-exit' : 'icon-fullscreen',
content: isFullscreen.value ? '点击退出全屏模式' : '点击切换全屏模式',
click: () => toggleFullScreen()
click: toggleFullScreen
},
]);
const activeKey = computed<String>({
get() {
return props.modelValue;
},
set(e) {
if (e) {
emits('update:modelValue', e);
} else {
emits('update:modelValue', null);
}
}
});
</script>
<style lang="less" scoped>
.terminal-header {
--logo-width: 168px;
--right-avatar-width: calc(28px * 5 - 7px * 4);
--right-action-width: calc(var(--sidebar-icon-wrapper-size) * 2);
}
.terminal-header {
@@ -137,24 +93,17 @@
}
&-tabs {
width: calc(100% - var(--logo-width) - var(--right-avatar-width) - var(--right-action-width));
width: calc(100% - var(--logo-width) - var(--sidebar-icon-wrapper-size));
display: flex;
}
&-right {
width: calc(var(--right-avatar-width) + var(--right-action-width));
width: var(--sidebar-icon-wrapper-size);
display: flex;
justify-content: flex-end;
&-avatar-group {
width: var(--right-avatar-width);
display: flex;
align-items: center;
justify-content: flex-end;
}
&-actions {
width: var(--right-action-width);
width: var(--sidebar-icon-wrapper-size);
display: flex;
justify-content: flex-end;
}

View File

@@ -20,16 +20,17 @@
<script lang="ts" setup>
import type { SidebarAction } from '../../types/terminal.const';
import { InnerTabs } from '../../types/terminal.const';
import { useTerminalStore } from '@/store';
import IconActions from './icon-actions.vue';
const emits = defineEmits(['switchTab']);
const terminalStore = useTerminalStore();
// 顶部操作
const topActions: Array<SidebarAction> = [
{
icon: 'icon-plus',
content: '新建连接',
click: () => emits('switchTab', InnerTabs.NEW_CONNECTION)
click: () => terminalStore.switchTab(InnerTabs.NEW_CONNECTION)
},
];
@@ -38,12 +39,12 @@
{
icon: 'icon-command',
content: '快捷键设置',
click: () => emits('switchTab', InnerTabs.SHORTCUT_SETTING)
click: () => terminalStore.switchTab(InnerTabs.SHORTCUT_SETTING)
},
{
icon: 'icon-palette',
content: '外观设置',
click: () => emits('switchTab', InnerTabs.VIEW_SETTING)
click: () => terminalStore.switchTab(InnerTabs.VIEW_SETTING)
},
];

View File

@@ -24,7 +24,7 @@
import { useTerminalStore } from '@/store';
import { DarkTheme } from '@/store/modules/terminal';
const emits = defineEmits(['openSnippet', 'openSftp', 'openTransfer', 'openHistory', 'screenshot']);
const emits = defineEmits(['openSnippet', 'openSftp', 'openTransfer', 'screenshot']);
const terminalStore = useTerminalStore();
@@ -48,12 +48,6 @@
},
click: () => emits('openTransfer')
},
{
icon: 'icon-history',
content: '历史命令',
visible: false,
click: () => emits('openHistory')
},
{
icon: terminalStore.isDarkTheme ? 'icon-sun-fill' : 'icon-moon-fill',
content: terminalStore.isDarkTheme ? '点击切换为亮色模式' : '点击切换为暗色模式',

View File

@@ -15,12 +15,12 @@
</template>
<!-- 数据 -->
<template #item="{ item }">
<a-list-item class="host-item-wrapper" @click="openTerminal(item)">
<a-list-item class="host-item-wrapper">
<div class="host-item">
<!-- 左侧图标-名称 -->
<div class="flex-center host-item-left">
<!-- 图标 -->
<span class="host-item-left-icon">
<span class="host-item-left-icon" @click="terminalStore.openTerminal(item)">
<icon-desktop />
</span>
<!-- 名称 -->
@@ -49,7 +49,7 @@
arrow-class="terminal-tooltip-content"
content="修改别名">
<icon-edit class="host-item-left-name-edit"
@click.stop="clickEditAlias(item)" />
@click="clickEditAlias(item)" />
</a-tooltip>
</template>
<!-- 名称输入框 -->
@@ -63,8 +63,7 @@
:placeholder="`${item.name} (${item.code})`"
@blur="saveAlias(item)"
@pressEnter="saveAlias(item)"
@change="saveAlias(item)"
@click.stop>
@change="saveAlias(item)">
<template #suffix>
<!-- 加载中 -->
<icon-loading v-if="item.loading" />
@@ -117,7 +116,7 @@
arrow-class="terminal-tooltip-content"
content="连接主机">
<div class="terminal-sidebar-icon-wrapper">
<div class="terminal-sidebar-icon" @click.stop="openTerminal(item)">
<div class="terminal-sidebar-icon" @click="terminalStore.openTerminal(item)">
<icon-thunderbolt />
</div>
</div>
@@ -129,7 +128,7 @@
arrow-class="terminal-tooltip-content"
content="连接设置">
<div class="terminal-sidebar-icon-wrapper">
<div class="terminal-sidebar-icon" @click.stop="openSetting(item)">
<div class="terminal-sidebar-icon" @click="openSetting(item)">
<icon-settings />
</div>
</div>
@@ -141,7 +140,7 @@
arrow-class="terminal-tooltip-content"
content="收藏">
<div class="terminal-sidebar-icon-wrapper">
<div class="terminal-sidebar-icon" @click.stop="setFavorite(item)">
<div class="terminal-sidebar-icon" @click="setFavorite(item)">
<icon-star-fill class="favorite" v-if="item.favorite" />
<icon-star v-else />
</div>
@@ -169,13 +168,15 @@
import { dataColor } from '@/utils';
import { tagColor } from '@/views/asset/host-list/types/const';
import { updateHostAlias } from '@/api/asset/host-extra';
import { sshModalKey } from '../../types/terminal.const';
import { openSshModalKey } from '../../types/terminal.const';
import { useTerminalStore } from '@/store';
const props = defineProps<{
hostList: Array<HostQueryResponse>,
emptyValue: string
}>();
const terminalStore = useTerminalStore();
const { toggle: toggleFavorite, loading: favoriteLoading } = useFavorite('HOST');
const aliasNameInput = ref();
@@ -212,13 +213,8 @@
}
};
// 打开终端
const openTerminal = (item: HostQueryResponse) => {
console.log('ter', item);
};
// 打开配置
const openSetting = inject<(record: HostQueryResponse) => void>(sshModalKey);
const openSetting = inject<(record: HostQueryResponse) => void>(openSshModalKey);
// 设置收藏
const setFavorite = async (item: HostQueryResponse) => {
@@ -257,7 +253,6 @@
.host-item-wrapper {
padding: 0 !important;
height: @host-item-height;
cursor: pointer;
font-size: 12px;
color: var(--color-content-text-2);
@@ -304,6 +299,7 @@
border-radius: 32px;
margin-right: 10px;
font-size: 16px;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;

View File

@@ -34,7 +34,7 @@
<script lang="ts" setup>
import { onMounted, provide, ref, watch } from 'vue';
import { NewConnectionType, sshModalKey } from '../../types/terminal.const';
import { NewConnectionType, openSshModalKey } from '../../types/terminal.const';
import { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data';
import { HostQueryResponse } from '@/api/asset/host';
import HostGroupView from './host-group-view.vue';
@@ -57,7 +57,7 @@
const sshModal = ref();
// 暴露打开 ssh 配置模态框
provide(sshModalKey, (record: any) => {
provide(openSshModalKey, (record: any) => {
sshModal.value?.open(record);
});

View File

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

View File

@@ -2,21 +2,17 @@
<div class="host-layout" v-if="render">
<!-- 头部区域 -->
<header class="host-layout-header">
<terminal-header v-model="activeKey"
:tabs="tabs"
@click-tab="clickTab"
@deleteTab="deleteTab" />
<terminal-header />
</header>
<!-- 主体区域 -->
<main class="host-layout-main">
<!-- 左侧操作栏 -->
<div class="host-layout-left">
<terminal-left-sidebar @switch-tab="switchTab" />
<terminal-left-sidebar />
</div>
<!-- 内容区域 -->
<div class="host-layout-content">
<terminal-content v-model="activeKey"
:tabs="tabs" />
<terminal-content />
</div>
<!-- 右侧操作栏 -->
<div class="host-layout-right">
@@ -33,9 +29,8 @@
</script>
<script lang="ts" setup>
import type { TabItem } from './types/terminal.const';
import { ref, onBeforeMount, onUnmounted } from 'vue';
import { TabType, InnerTabs, dictKeys } from './types/terminal.const';
import { dictKeys } from './types/terminal.const';
import { useCacheStore, useDictStore, useTerminalStore } from '@/store';
import TerminalHeader from './components/layout/terminal-header.vue';
import TerminalLeftSidebar from './components/layout/terminal-left-sidebar.vue';
@@ -49,39 +44,6 @@
const cacheStore = useCacheStore();
const render = ref(false);
const activeKey = ref(InnerTabs.NEW_CONNECTION.key);
const tabs = ref<Array<TabItem>>([InnerTabs.NEW_CONNECTION]);
for (let i = 0; i < 3; i++) {
tabs.value.push({
key: `host${i}`,
title: `主机name ${i}`,
type: TabType.TERMINAL
});
}
// 点击 tab
const clickTab = (key: string) => {
activeKey.value = key;
};
// 删除 tab
const deleteTab = (key: string) => {
const tabIndex = tabs.value.findIndex(s => s.key === key);
tabs.value.splice(tabIndex, 1);
if (key === activeKey.value && tabs.value.length !== 0) {
// 切换为前一个 tab
activeKey.value = tabs.value[Math.max(tabIndex - 1, 0)].key;
}
};
// 切换 tab
const switchTab = (tab: TabItem) => {
// 不存在则创建tab
if (!tabs.value.find(s => s.key === tab.key)) {
tabs.value.push(tab);
}
activeKey.value = tab.key;
};
// 加载用户终端偏好
onBeforeMount(async () => {

View File

@@ -1,23 +1,14 @@
import type { CSSProperties } from 'vue';
import type { TabItem } from '@/store/modules/terminal/types';
// sidebar 操作类型
export interface SidebarAction {
icon: string;
content: string;
iconStyle?: CSSProperties;
visible?: boolean;
click: () => void;
}
// tab 元素
export interface TabItem {
key: string;
title: string;
type: string;
[key: string]: unknown;
}
// tab 类型
export const TabType = {
SETTING: 'setting',
@@ -25,7 +16,7 @@ export const TabType = {
};
// 内置 tab
export const InnerTabs = {
export const InnerTabs: Record<string, TabItem> = {
NEW_CONNECTION: {
key: 'newConnection',
title: '新建连接',
@@ -70,7 +61,7 @@ export const ExtraSshAuthType = {
};
// 打开 sshModal key
export const sshModalKey = Symbol();
export const openSshModalKey = Symbol();
// 字体后缀 兜底
export const fontFamilySuffix = ',courier-new, courier, monospace';

View File

@@ -0,0 +1,120 @@
// 终端协议
export interface Protocol {
type: string;
template: string[];
}
// 终端内容
export interface Payload {
type?: string;
session: string;
[key: string]: unknown;
}
// 输入协议
export const InputProtocol: Record<string, Protocol> = {
// 主机连接检查
CHECK: {
type: 'ck',
template: ['type', 'session', 'hostId']
},
// 连接主机
CONNECT: {
type: 'co',
template: ['type', 'session', 'cols', 'rows']
},
// 关闭连接
CLOSE: {
type: 'cl',
template: ['type', 'session']
},
// ping
PING: {
type: 'p',
template: ['type']
},
// 修改大小
RESIZE: {
type: 'rs',
template: ['type', 'session', 'cols', 'rows']
},
// 执行
EXEC: {
type: 'e',
template: ['type', 'session', 'command']
},
// 输入
INPUT: {
type: 'i',
template: ['type', 'session', 'command']
}
};
// 输出协议
export const OutputProtocol: Record<string, Protocol> = {
// 主机连接检查
CHECK: {
type: 'ck',
template: ['type', 'session', 'result', 'errorMessage']
},
// 主机连接
CONNECT: {
type: 'co',
template: ['type', 'session', 'result', 'errorMessage']
},
// pong
PONG: {
type: 'p',
template: ['type']
},
// 输出
OUTPUT: {
type: 'o',
template: ['type', 'session', 'body']
},
};
// 分隔符
export const SEPARATOR = '|';
// 解析参数
export const parse: Record<string, any> = (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: Record<string, any> = {};
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 = (payload: Payload, protocol: Protocol) => {
payload.type = protocol.type;
return protocol.template
.map(i => payload[i] || '')
.join(SEPARATOR);
};