refactor: 修改 tabs 存储方式.
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
16
orion-ops-ui/src/api/asset/host-terminal.ts
Normal file
16
orion-ops-ui/src/api/asset/host-terminal.ts
Normal 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');
|
||||
}
|
||||
1
orion-ops-ui/src/env.d.ts
vendored
1
orion-ops-ui/src/env.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -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 ? '点击切换为亮色模式' : '点击切换为暗色模式',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<template>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'terminalView'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
Reference in New Issue
Block a user