refactor: 修改 tabs 存储方式.
This commit is contained in:
@@ -1,2 +1,3 @@
|
|||||||
VITE_API_BASE_URL= 'http://127.0.0.1:9200/orion-api'
|
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'
|
VITE_APP_VERSION= '1.0.0'
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
VITE_API_BASE_URL= 'http://127.0.0.1:9200/orion-api'
|
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'
|
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 {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_API_BASE_URL: string;
|
readonly VITE_API_BASE_URL: string;
|
||||||
|
readonly VITE_WS_BASE_URL: string;
|
||||||
readonly VITE_APP_VERSION: 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 { defineStore } from 'pinia';
|
||||||
import { getPreference, updatePreference } from '@/api/user/preference';
|
import { getPreference, updatePreference } from '@/api/user/preference';
|
||||||
import { Message } from '@arco-design/web-vue';
|
import { Message } from '@arco-design/web-vue';
|
||||||
import { useDark } from '@vueuse/core';
|
import { useDark } from '@vueuse/core';
|
||||||
import { DEFAULT_SCHEMA } from '@/views/host-ops/terminal/types/terminal.theme';
|
import { DEFAULT_SCHEMA } from '@/views/host-ops/terminal/types/terminal.theme';
|
||||||
|
import { InnerTabs } from '@/views/host-ops/terminal/types/terminal.const';
|
||||||
|
|
||||||
// 暗色主题
|
// 暗色主题
|
||||||
export const DarkTheme = {
|
export const DarkTheme = {
|
||||||
@@ -28,6 +30,10 @@ export default defineStore('terminal', {
|
|||||||
displaySetting: {} as TerminalDisplaySetting,
|
displaySetting: {} as TerminalDisplaySetting,
|
||||||
themeSchema: {} as TerminalThemeSchema
|
themeSchema: {} as TerminalThemeSchema
|
||||||
},
|
},
|
||||||
|
tabs: {
|
||||||
|
active: InnerTabs.NEW_CONNECTION.key,
|
||||||
|
items: [InnerTabs.NEW_CONNECTION, InnerTabs.VIEW_SETTING]
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
actions: {
|
actions: {
|
||||||
@@ -94,7 +100,7 @@ export default defineStore('terminal', {
|
|||||||
await this.updateTerminalPreference('newConnectionType', newConnectionType);
|
await this.updateTerminalPreference('newConnectionType', newConnectionType);
|
||||||
},
|
},
|
||||||
|
|
||||||
// 更新终端偏好-防抖
|
// 更新终端偏好
|
||||||
async updateTerminalPreference(item: string, value: any) {
|
async updateTerminalPreference(item: string, value: any) {
|
||||||
try {
|
try {
|
||||||
// 修改配置
|
// 修改配置
|
||||||
@@ -106,7 +112,37 @@ export default defineStore('terminal', {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
Message.error('同步失败');
|
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 {
|
export interface TerminalState {
|
||||||
isDarkTheme: Ref<boolean>;
|
isDarkTheme: Ref<boolean>;
|
||||||
preference: TerminalPreference;
|
preference: TerminalPreference;
|
||||||
|
tabs: TerminalTabs;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 终端配置
|
// 终端配置
|
||||||
@@ -54,3 +55,18 @@ export interface TerminalThemeSchema {
|
|||||||
|
|
||||||
[key: string]: unknown;
|
[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"
|
content-class="terminal-tooltip-content"
|
||||||
arrow-class="terminal-tooltip-arrow"
|
arrow-class="terminal-tooltip-arrow"
|
||||||
:content="action.content">
|
:content="action.content">
|
||||||
<div class="terminal-sidebar-icon-wrapper"
|
<div class="terminal-sidebar-icon-wrapper">
|
||||||
v-if="action.visible !== false">
|
|
||||||
<div class="terminal-sidebar-icon"
|
<div class="terminal-sidebar-icon"
|
||||||
:class="iconClass"
|
:class="iconClass"
|
||||||
@click="action.click">
|
@click="action.click">
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="terminal-content">
|
<div class="terminal-content">
|
||||||
<!-- 内容 tabs -->
|
<!-- 内容 tabs -->
|
||||||
<a-tabs v-model:active-key="activeKey">
|
<a-tabs v-model:active-key="terminalStore.tabs.active">
|
||||||
<a-tab-pane v-for="tab in tabs"
|
<a-tab-pane v-for="tab in terminalStore.tabs.items"
|
||||||
:key="tab.key"
|
:key="tab.key"
|
||||||
:title="tab.title">
|
:title="tab.title">
|
||||||
<!-- 设置 -->
|
<!-- 设置 -->
|
||||||
@@ -18,6 +18,9 @@
|
|||||||
</template>
|
</template>
|
||||||
<!-- 终端 -->
|
<!-- 终端 -->
|
||||||
<template v-else-if="tab.type === TabType.TERMINAL">
|
<template v-else-if="tab.type === TabType.TERMINAL">
|
||||||
|
<terminal-view>
|
||||||
|
|
||||||
|
</terminal-view>
|
||||||
终端 {{ tab.key }}
|
终端 {{ tab.key }}
|
||||||
<div v-for="i in 1000" :key="i">
|
<div v-for="i in 1000" :key="i">
|
||||||
{{ tab.title }}
|
{{ tab.title }}
|
||||||
@@ -35,31 +38,13 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<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 { TabType, InnerTabs } from '../../types/terminal.const';
|
||||||
|
import { useTerminalStore } from '@/store';
|
||||||
import TerminalViewSetting from '../view-setting/terminal-view-setting.vue';
|
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({
|
const terminalStore = useTerminalStore();
|
||||||
modelValue: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
tabs: {
|
|
||||||
type: Array as PropType<Array<TabItem>>,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const activeKey = computed<String>({
|
|
||||||
get() {
|
|
||||||
return props.modelValue;
|
|
||||||
},
|
|
||||||
set() {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -10,30 +10,19 @@
|
|||||||
</div>
|
</div>
|
||||||
<!-- 左侧 tabs -->
|
<!-- 左侧 tabs -->
|
||||||
<div class="terminal-header-tabs">
|
<div class="terminal-header-tabs">
|
||||||
<a-tabs v-model:active-key="activeKey"
|
<a-tabs v-model:active-key="terminalStore.tabs.active"
|
||||||
:editable="true"
|
:editable="true"
|
||||||
:hide-content="true"
|
:hide-content="true"
|
||||||
:auto-switch="true"
|
:auto-switch="true"
|
||||||
@tab-click="e => emits('clickTab', e)"
|
@tab-click="terminalStore.clickTab"
|
||||||
@delete="e => emits('deleteTab', e)">
|
@delete="terminalStore.deleteTab">
|
||||||
<a-tab-pane v-for="tab in tabs"
|
<a-tab-pane v-for="tab in terminalStore.tabs.items"
|
||||||
:key="tab.key"
|
:key="tab.key"
|
||||||
:title="tab.title" />
|
:title="tab.title" />
|
||||||
</a-tabs>
|
</a-tabs>
|
||||||
</div>
|
</div>
|
||||||
<!-- 右侧操作 -->
|
<!-- 右侧操作 -->
|
||||||
<div class="terminal-header-right">
|
<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"
|
<icon-actions class="terminal-header-right-actions"
|
||||||
:actions="actions"
|
:actions="actions"
|
||||||
@@ -50,62 +39,29 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { SidebarAction, TabItem } from '../../types/terminal.const';
|
import type { SidebarAction } from '../../types/terminal.const';
|
||||||
import type { PropType } from 'vue';
|
|
||||||
import { useFullscreen } from '@vueuse/core';
|
import { useFullscreen } from '@vueuse/core';
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
import { useTerminalStore } from '@/store';
|
||||||
import IconActions from '../layout/icon-actions.vue';
|
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 { isFullscreen, toggle: toggleFullScreen } = useFullscreen();
|
||||||
|
const terminalStore = useTerminalStore();
|
||||||
|
|
||||||
// 顶部操作
|
// 顶部操作
|
||||||
const actions = computed<Array<SidebarAction>>(() => [
|
const actions = computed<Array<SidebarAction>>(() => [
|
||||||
{
|
|
||||||
icon: 'icon-share-alt',
|
|
||||||
content: '分享链接',
|
|
||||||
visible: false,
|
|
||||||
click: () => emits('share')
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: isFullscreen.value ? 'icon-fullscreen-exit' : 'icon-fullscreen',
|
icon: isFullscreen.value ? 'icon-fullscreen-exit' : 'icon-fullscreen',
|
||||||
content: isFullscreen.value ? '点击退出全屏模式' : '点击切换全屏模式',
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="less" scoped>
|
<style lang="less" scoped>
|
||||||
.terminal-header {
|
.terminal-header {
|
||||||
--logo-width: 168px;
|
--logo-width: 168px;
|
||||||
--right-avatar-width: calc(28px * 5 - 7px * 4);
|
|
||||||
--right-action-width: calc(var(--sidebar-icon-wrapper-size) * 2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-header {
|
.terminal-header {
|
||||||
@@ -137,24 +93,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
&-tabs {
|
&-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;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
&-right {
|
&-right {
|
||||||
width: calc(var(--right-avatar-width) + var(--right-action-width));
|
width: var(--sidebar-icon-wrapper-size);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
|
||||||
&-avatar-group {
|
|
||||||
width: var(--right-avatar-width);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
|
|
||||||
&-actions {
|
&-actions {
|
||||||
width: var(--right-action-width);
|
width: var(--sidebar-icon-wrapper-size);
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,16 +20,17 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { SidebarAction } from '../../types/terminal.const';
|
import type { SidebarAction } from '../../types/terminal.const';
|
||||||
import { InnerTabs } from '../../types/terminal.const';
|
import { InnerTabs } from '../../types/terminal.const';
|
||||||
|
import { useTerminalStore } from '@/store';
|
||||||
import IconActions from './icon-actions.vue';
|
import IconActions from './icon-actions.vue';
|
||||||
|
|
||||||
const emits = defineEmits(['switchTab']);
|
const terminalStore = useTerminalStore();
|
||||||
|
|
||||||
// 顶部操作
|
// 顶部操作
|
||||||
const topActions: Array<SidebarAction> = [
|
const topActions: Array<SidebarAction> = [
|
||||||
{
|
{
|
||||||
icon: 'icon-plus',
|
icon: 'icon-plus',
|
||||||
content: '新建连接',
|
content: '新建连接',
|
||||||
click: () => emits('switchTab', InnerTabs.NEW_CONNECTION)
|
click: () => terminalStore.switchTab(InnerTabs.NEW_CONNECTION)
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -38,12 +39,12 @@
|
|||||||
{
|
{
|
||||||
icon: 'icon-command',
|
icon: 'icon-command',
|
||||||
content: '快捷键设置',
|
content: '快捷键设置',
|
||||||
click: () => emits('switchTab', InnerTabs.SHORTCUT_SETTING)
|
click: () => terminalStore.switchTab(InnerTabs.SHORTCUT_SETTING)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'icon-palette',
|
icon: 'icon-palette',
|
||||||
content: '外观设置',
|
content: '外观设置',
|
||||||
click: () => emits('switchTab', InnerTabs.VIEW_SETTING)
|
click: () => terminalStore.switchTab(InnerTabs.VIEW_SETTING)
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@
|
|||||||
import { useTerminalStore } from '@/store';
|
import { useTerminalStore } from '@/store';
|
||||||
import { DarkTheme } from '@/store/modules/terminal';
|
import { DarkTheme } from '@/store/modules/terminal';
|
||||||
|
|
||||||
const emits = defineEmits(['openSnippet', 'openSftp', 'openTransfer', 'openHistory', 'screenshot']);
|
const emits = defineEmits(['openSnippet', 'openSftp', 'openTransfer', 'screenshot']);
|
||||||
|
|
||||||
const terminalStore = useTerminalStore();
|
const terminalStore = useTerminalStore();
|
||||||
|
|
||||||
@@ -48,12 +48,6 @@
|
|||||||
},
|
},
|
||||||
click: () => emits('openTransfer')
|
click: () => emits('openTransfer')
|
||||||
},
|
},
|
||||||
{
|
|
||||||
icon: 'icon-history',
|
|
||||||
content: '历史命令',
|
|
||||||
visible: false,
|
|
||||||
click: () => emits('openHistory')
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: terminalStore.isDarkTheme ? 'icon-sun-fill' : 'icon-moon-fill',
|
icon: terminalStore.isDarkTheme ? 'icon-sun-fill' : 'icon-moon-fill',
|
||||||
content: terminalStore.isDarkTheme ? '点击切换为亮色模式' : '点击切换为暗色模式',
|
content: terminalStore.isDarkTheme ? '点击切换为亮色模式' : '点击切换为暗色模式',
|
||||||
|
|||||||
@@ -15,12 +15,12 @@
|
|||||||
</template>
|
</template>
|
||||||
<!-- 数据 -->
|
<!-- 数据 -->
|
||||||
<template #item="{ item }">
|
<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="host-item">
|
||||||
<!-- 左侧图标-名称 -->
|
<!-- 左侧图标-名称 -->
|
||||||
<div class="flex-center host-item-left">
|
<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 />
|
<icon-desktop />
|
||||||
</span>
|
</span>
|
||||||
<!-- 名称 -->
|
<!-- 名称 -->
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
arrow-class="terminal-tooltip-content"
|
arrow-class="terminal-tooltip-content"
|
||||||
content="修改别名">
|
content="修改别名">
|
||||||
<icon-edit class="host-item-left-name-edit"
|
<icon-edit class="host-item-left-name-edit"
|
||||||
@click.stop="clickEditAlias(item)" />
|
@click="clickEditAlias(item)" />
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<!-- 名称输入框 -->
|
<!-- 名称输入框 -->
|
||||||
@@ -63,8 +63,7 @@
|
|||||||
:placeholder="`${item.name} (${item.code})`"
|
:placeholder="`${item.name} (${item.code})`"
|
||||||
@blur="saveAlias(item)"
|
@blur="saveAlias(item)"
|
||||||
@pressEnter="saveAlias(item)"
|
@pressEnter="saveAlias(item)"
|
||||||
@change="saveAlias(item)"
|
@change="saveAlias(item)">
|
||||||
@click.stop>
|
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<!-- 加载中 -->
|
<!-- 加载中 -->
|
||||||
<icon-loading v-if="item.loading" />
|
<icon-loading v-if="item.loading" />
|
||||||
@@ -117,7 +116,7 @@
|
|||||||
arrow-class="terminal-tooltip-content"
|
arrow-class="terminal-tooltip-content"
|
||||||
content="连接主机">
|
content="连接主机">
|
||||||
<div class="terminal-sidebar-icon-wrapper">
|
<div class="terminal-sidebar-icon-wrapper">
|
||||||
<div class="terminal-sidebar-icon" @click.stop="openTerminal(item)">
|
<div class="terminal-sidebar-icon" @click="terminalStore.openTerminal(item)">
|
||||||
<icon-thunderbolt />
|
<icon-thunderbolt />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,7 +128,7 @@
|
|||||||
arrow-class="terminal-tooltip-content"
|
arrow-class="terminal-tooltip-content"
|
||||||
content="连接设置">
|
content="连接设置">
|
||||||
<div class="terminal-sidebar-icon-wrapper">
|
<div class="terminal-sidebar-icon-wrapper">
|
||||||
<div class="terminal-sidebar-icon" @click.stop="openSetting(item)">
|
<div class="terminal-sidebar-icon" @click="openSetting(item)">
|
||||||
<icon-settings />
|
<icon-settings />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -141,7 +140,7 @@
|
|||||||
arrow-class="terminal-tooltip-content"
|
arrow-class="terminal-tooltip-content"
|
||||||
content="收藏">
|
content="收藏">
|
||||||
<div class="terminal-sidebar-icon-wrapper">
|
<div class="terminal-sidebar-icon-wrapper">
|
||||||
<div class="terminal-sidebar-icon" @click.stop="setFavorite(item)">
|
<div class="terminal-sidebar-icon" @click="setFavorite(item)">
|
||||||
<icon-star-fill class="favorite" v-if="item.favorite" />
|
<icon-star-fill class="favorite" v-if="item.favorite" />
|
||||||
<icon-star v-else />
|
<icon-star v-else />
|
||||||
</div>
|
</div>
|
||||||
@@ -169,13 +168,15 @@
|
|||||||
import { dataColor } from '@/utils';
|
import { dataColor } from '@/utils';
|
||||||
import { tagColor } from '@/views/asset/host-list/types/const';
|
import { tagColor } from '@/views/asset/host-list/types/const';
|
||||||
import { updateHostAlias } from '@/api/asset/host-extra';
|
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<{
|
const props = defineProps<{
|
||||||
hostList: Array<HostQueryResponse>,
|
hostList: Array<HostQueryResponse>,
|
||||||
emptyValue: string
|
emptyValue: string
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
const terminalStore = useTerminalStore();
|
||||||
const { toggle: toggleFavorite, loading: favoriteLoading } = useFavorite('HOST');
|
const { toggle: toggleFavorite, loading: favoriteLoading } = useFavorite('HOST');
|
||||||
|
|
||||||
const aliasNameInput = ref();
|
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) => {
|
const setFavorite = async (item: HostQueryResponse) => {
|
||||||
@@ -257,7 +253,6 @@
|
|||||||
.host-item-wrapper {
|
.host-item-wrapper {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
height: @host-item-height;
|
height: @host-item-height;
|
||||||
cursor: pointer;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--color-content-text-2);
|
color: var(--color-content-text-2);
|
||||||
|
|
||||||
@@ -304,6 +299,7 @@
|
|||||||
border-radius: 32px;
|
border-radius: 32px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, provide, ref, watch } from 'vue';
|
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 { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data';
|
||||||
import { HostQueryResponse } from '@/api/asset/host';
|
import { HostQueryResponse } from '@/api/asset/host';
|
||||||
import HostGroupView from './host-group-view.vue';
|
import HostGroupView from './host-group-view.vue';
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
const sshModal = ref();
|
const sshModal = ref();
|
||||||
|
|
||||||
// 暴露打开 ssh 配置模态框
|
// 暴露打开 ssh 配置模态框
|
||||||
provide(sshModalKey, (record: any) => {
|
provide(openSshModalKey, (record: any) => {
|
||||||
sshModal.value?.open(record);
|
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">
|
<div class="host-layout" v-if="render">
|
||||||
<!-- 头部区域 -->
|
<!-- 头部区域 -->
|
||||||
<header class="host-layout-header">
|
<header class="host-layout-header">
|
||||||
<terminal-header v-model="activeKey"
|
<terminal-header />
|
||||||
:tabs="tabs"
|
|
||||||
@click-tab="clickTab"
|
|
||||||
@deleteTab="deleteTab" />
|
|
||||||
</header>
|
</header>
|
||||||
<!-- 主体区域 -->
|
<!-- 主体区域 -->
|
||||||
<main class="host-layout-main">
|
<main class="host-layout-main">
|
||||||
<!-- 左侧操作栏 -->
|
<!-- 左侧操作栏 -->
|
||||||
<div class="host-layout-left">
|
<div class="host-layout-left">
|
||||||
<terminal-left-sidebar @switch-tab="switchTab" />
|
<terminal-left-sidebar />
|
||||||
</div>
|
</div>
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<div class="host-layout-content">
|
<div class="host-layout-content">
|
||||||
<terminal-content v-model="activeKey"
|
<terminal-content />
|
||||||
:tabs="tabs" />
|
|
||||||
</div>
|
</div>
|
||||||
<!-- 右侧操作栏 -->
|
<!-- 右侧操作栏 -->
|
||||||
<div class="host-layout-right">
|
<div class="host-layout-right">
|
||||||
@@ -33,9 +29,8 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { TabItem } from './types/terminal.const';
|
|
||||||
import { ref, onBeforeMount, onUnmounted } from 'vue';
|
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 { useCacheStore, useDictStore, useTerminalStore } from '@/store';
|
||||||
import TerminalHeader from './components/layout/terminal-header.vue';
|
import TerminalHeader from './components/layout/terminal-header.vue';
|
||||||
import TerminalLeftSidebar from './components/layout/terminal-left-sidebar.vue';
|
import TerminalLeftSidebar from './components/layout/terminal-left-sidebar.vue';
|
||||||
@@ -49,39 +44,6 @@
|
|||||||
const cacheStore = useCacheStore();
|
const cacheStore = useCacheStore();
|
||||||
|
|
||||||
const render = ref(false);
|
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 () => {
|
onBeforeMount(async () => {
|
||||||
|
|||||||
@@ -1,23 +1,14 @@
|
|||||||
import type { CSSProperties } from 'vue';
|
import type { CSSProperties } from 'vue';
|
||||||
|
import type { TabItem } from '@/store/modules/terminal/types';
|
||||||
|
|
||||||
// sidebar 操作类型
|
// sidebar 操作类型
|
||||||
export interface SidebarAction {
|
export interface SidebarAction {
|
||||||
icon: string;
|
icon: string;
|
||||||
content: string;
|
content: string;
|
||||||
iconStyle?: CSSProperties;
|
iconStyle?: CSSProperties;
|
||||||
visible?: boolean;
|
|
||||||
click: () => void;
|
click: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// tab 元素
|
|
||||||
export interface TabItem {
|
|
||||||
key: string;
|
|
||||||
title: string;
|
|
||||||
type: string;
|
|
||||||
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
|
||||||
|
|
||||||
// tab 类型
|
// tab 类型
|
||||||
export const TabType = {
|
export const TabType = {
|
||||||
SETTING: 'setting',
|
SETTING: 'setting',
|
||||||
@@ -25,7 +16,7 @@ export const TabType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 内置 tab
|
// 内置 tab
|
||||||
export const InnerTabs = {
|
export const InnerTabs: Record<string, TabItem> = {
|
||||||
NEW_CONNECTION: {
|
NEW_CONNECTION: {
|
||||||
key: 'newConnection',
|
key: 'newConnection',
|
||||||
title: '新建连接',
|
title: '新建连接',
|
||||||
@@ -70,7 +61,7 @@ export const ExtraSshAuthType = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 打开 sshModal key
|
// 打开 sshModal key
|
||||||
export const sshModalKey = Symbol();
|
export const openSshModalKey = Symbol();
|
||||||
|
|
||||||
// 字体后缀 兜底
|
// 字体后缀 兜底
|
||||||
export const fontFamilySuffix = ',courier-new, courier, monospace';
|
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