feat: 添加未选择页面的空承载页.

This commit is contained in:
lijiahangmax
2024-01-12 23:28:32 +08:00
parent 720c2247c8
commit c16a461e5c
16 changed files with 176 additions and 96 deletions

View File

@@ -9,16 +9,19 @@ import type {
} from './types';
import type { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data';
import { getCurrentAuthorizedHost } from '@/api/asset/asset-authorized-data';
import type { HostQueryResponse } from '@/api/asset/host';
import type { TerminalTheme } from '@/api/asset/host-terminal';
import { getTerminalThemes } from '@/api/asset/host-terminal';
import { defineStore } from 'pinia';
import { getPreference, updatePreference } from '@/api/user/preference';
import { nextSessionId } from '@/utils';
import { Message } from '@arco-design/web-vue';
import { TerminalTabType } 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';
// 偏好项
export const PreferenceItem = {
// 终端偏好项
export const TerminalPreferenceItem = {
// 新建连接类型
NEW_CONNECTION_TYPE: 'newConnectionType',
// 终端主题
@@ -62,7 +65,7 @@ export default defineStore('terminal', {
const { data: themes } = await getTerminalThemes();
data.theme = themes[0];
// 更新默认主题偏好
await this.updateTerminalPreference(PreferenceItem.THEME, data.theme);
await this.updateTerminalPreference(TerminalPreferenceItem.THEME, data.theme);
}
// 选择赋值
const keys = Object.keys(this.preference);
@@ -105,10 +108,28 @@ export default defineStore('terminal', {
});
},
// 添加到最近连接列表
addToLatestConnect(hostId: number) {
this.hosts.latestHosts = [...new Set([hostId, ...this.hosts.latestHosts])];
}
// 打开终端
openTerminal(record: HostQueryResponse) {
// 添加到最近连接
this.hosts.latestHosts = [...new Set([record.id, ...this.hosts.latestHosts])];
// 获取 seq
const tabSeqArr = this.tabManager.items
.map(s => s.seq)
.filter(Boolean)
.map(Number);
const nextSeq = tabSeqArr.length
? Math.max(...tabSeqArr) + 1
: 1;
// 打开 tab
this.tabManager.openTab({
type: TerminalTabType.TERMINAL,
key: nextSessionId(10),
seq: nextSeq,
title: `(${nextSeq}) ${record.alias || record.name}`,
hostId: record.id,
address: record.address
});
},
},

View File

@@ -163,7 +163,7 @@ export const resetObject = (obj: any, ignore: string[] = []) => {
export const objectTruthKeyCount = (obj: any, ignore: string[] = []) => {
return Object.keys(obj)
.filter(s => !ignore.includes(s))
.reduce(function(acc, curr) {
.reduce(function (acc, curr) {
const currVal = obj[curr];
return acc + ~~(currVal !== undefined && currVal !== null && currVal?.length !== 0 && currVal !== '');
}, 0);
@@ -196,13 +196,20 @@ export function detectZoom() {
* 获取唯一的 UUID
*/
export function getUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
/**
* 获取会话id
*/
export const nextSessionId = (len: number): string => {
return getUUID().replaceAll('-', '').substring(0, len);
};
/**
* 清除 xss
*/

View File

@@ -4,25 +4,16 @@
<!-- 组合容器 -->
<div class="combined-container">
<!-- 新建连接 -->
<div class="combined-handler">
<div class="combined-handler" v-for="(handler, index) in combinedHandlers"
:key="index"
@click="clickHandlerItem(handler)">
<!-- 图标 -->
<div class="combined-handler-icon">
<icon-plus />
<component :is="handler.icon" />
</div>
<!-- 内容 -->
<div class="combined-handler-text">
新建连接
</div>
</div>
<!-- 主机列表 -->
<div class="combined-handler">
<!-- 图标 -->
<div class="combined-handler-icon">
<icon-desktop />
</div>
<!-- 内容 -->
<div class="combined-handler-text">
历史1
{{ handler.title }}
</div>
</div>
</div>
@@ -37,20 +28,83 @@
</script>
<script lang="ts" setup>
import type { TerminalTabItem, CombinedHandlerItem } from '../../types/terminal.type';
import type { HostQueryResponse } from '@/api/asset/host';
import { onMounted, ref } from 'vue';
import { useTerminalStore } from '@/store';
import { InnerTabs, TerminalTabType } from '../../types/terminal.const';
import { get } from 'lodash';
const { tabManager, hosts } = useTerminalStore();
const totalCount = 8;
const { tabManager, hosts, openTerminal } = useTerminalStore();
const combinedHandlers = ref<Array<CombinedHandlerItem>>([{
title: InnerTabs.NEW_CONNECTION.title,
settingTab: InnerTabs.NEW_CONNECTION,
type: TerminalTabType.SETTING,
icon: InnerTabs.NEW_CONNECTION.icon
}]);
// 点击组合操作元素
const clickHandlerItem = (item: CombinedHandlerItem) => {
if (item.type === TerminalTabType.SETTING) {
// 打开内置 tab
tabManager.openTab(item.settingTab as TerminalTabItem);
} else {
// 打开终端
openTerminal(item.host as HostQueryResponse);
}
};
// 组合主机列表
onMounted(() => {
// 推荐的主机 tab
const combinedHosts = [
...new Set([
...hosts.latestHosts,
...hosts.hostList.filter(s => s.favorite).map(s => s.id),
...hosts.hostList.map(s => s.id)
])
].slice(0, totalCount - 1)
.map(s => hosts.hostList.find(t => t.id === s) as HostQueryResponse)
.filter(Boolean)
.map(s => {
return {
title: `${s.alias || s.name} (${s.address})`,
type: TerminalTabType.TERMINAL,
host: s,
icon: 'icon-desktop'
};
});
// 插入主机列表
combinedHandlers.value.push(...combinedHosts);
// 不足显示的行数用设置补充
if (totalCount - 1 - combinedHosts.length > 0) {
const fillTabs = Object.keys(InnerTabs)
.filter(s => s !== 'NEW_CONNECTION')
.map(s => get(InnerTabs, s) as TerminalTabItem)
.slice(0, totalCount - 1 - combinedHosts.length)
.map(s => {
return {
title: s.title,
settingTab: s,
type: TerminalTabType.SETTING,
icon: s.icon as string
};
});
combinedHandlers.value.push(...fillTabs);
}
});
</script>
<style lang="less" scoped>
@handler-height: 48px;
@handler-height: 44px;
.combined-container {
padding: 8px;
padding: 12px;
margin: 64px auto;
width: 448px;
width: 424px;
height: 448px;
display: flex;
flex-direction: column;
@@ -70,6 +124,12 @@
display: flex;
align-items: center;
color: var(--color-content-text-1);
cursor: pointer;
transition: transform 0.3s ease;
&:hover {
transform: scale(1.04);
}
&-icon {
width: @handler-height;
@@ -77,14 +137,22 @@
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
font-size: 16px;
}
&-text {
height: 100%;
width: calc(100% - @handler-height - 12px);
display: flex;
align-items: center;
font-size: 14px;
&-wrapper {
display: block;
overflow: hidden;
white-space: pre;
text-overflow: ellipsis;
}
}
}
</style>

View File

@@ -7,7 +7,7 @@
:key="tab.key"
:title="tab.title">
<!-- 设置 -->
<template v-if="tab.type === TabType.SETTING">
<template v-if="tab.type === TerminalTabType.SETTING">
<!-- 新建连接 -->
<new-connection-view v-if="tab.key === InnerTabs.NEW_CONNECTION.key" />
<!-- 显示设置 -->
@@ -18,7 +18,7 @@
<terminal-general-setting v-else-if="tab.key === InnerTabs.TERMINAL_SETTING.key" />
</template>
<!-- 终端 -->
<template v-else-if="tab.type === TabType.TERMINAL">
<template v-else-if="tab.type === TerminalTabType.TERMINAL">
<terminal-view :tab="tab" />
</template>
</a-tab-pane>
@@ -35,7 +35,7 @@
</script>
<script lang="ts" setup>
import { TabType, InnerTabs } from '../../types/terminal.const';
import { TerminalTabType, InnerTabs } from '../../types/terminal.const';
import { useTerminalStore } from '@/store';
import { watch } from 'vue';
import EmptyRecommend from './empty-recommend.vue';
@@ -62,7 +62,7 @@
// 修改标题
document.title = tab.title;
// terminal 自动聚焦
if (tab?.type === TabType.TERMINAL) {
if (tab?.type === TerminalTabType.TERMINAL) {
sessionManager.getSession(active)?.focus();
}
});

View File

@@ -28,7 +28,7 @@
// 顶部操作
const topActions: Array<SidebarAction> = [
{
icon: 'icon-plus',
icon: InnerTabs.NEW_CONNECTION.icon,
content: InnerTabs.NEW_CONNECTION.title,
click: () => tabManager.openTab(InnerTabs.NEW_CONNECTION)
},
@@ -37,22 +37,22 @@
// 底部操作
const bottomActions: Array<SidebarAction> = [
{
icon: 'icon-command',
icon: InnerTabs.SHORTCUT_SETTING.icon,
content: InnerTabs.SHORTCUT_SETTING.title,
click: () => tabManager.openTab(InnerTabs.SHORTCUT_SETTING)
},
{
icon: 'icon-desktop',
icon: InnerTabs.DISPLAY_SETTING.icon,
content: InnerTabs.DISPLAY_SETTING.title,
click: () => tabManager.openTab(InnerTabs.DISPLAY_SETTING)
},
{
icon: 'icon-palette',
icon: InnerTabs.THEME_SETTING.icon,
content: InnerTabs.THEME_SETTING.title,
click: () => tabManager.openTab(InnerTabs.THEME_SETTING)
},
{
icon: 'icon-settings',
icon: InnerTabs.TERMINAL_SETTING.icon,
content: InnerTabs.TERMINAL_SETTING.title,
click: () => tabManager.openTab(InnerTabs.TERMINAL_SETTING)
},

View File

@@ -168,7 +168,7 @@
import { dataColor } from '@/utils';
import { tagColor } from '@/views/asset/host-list/types/const';
import { updateHostAlias } from '@/api/asset/host-extra';
import { nextSessionId, openSshModalKey, TabType } from '../../types/terminal.const';
import { openSshModalKey } from '../../types/terminal.const';
import { useTerminalStore } from '@/store';
const props = defineProps<{
@@ -176,7 +176,7 @@
emptyValue: string
}>();
const { tabManager, addToLatestConnect } = useTerminalStore();
const { openTerminal } = useTerminalStore();
const { toggle: toggleFavorite, loading: favoriteLoading } = useFavorite('HOST');
const aliasNameInput = ref();
@@ -213,29 +213,6 @@
}
};
// 打开终端
const openTerminal = (record: HostQueryResponse) => {
// 添加到最近连接
addToLatestConnect(record.id);
// 获取 seq
const tabSeqArr = tabManager.items
.map(s => s.seq)
.filter(Boolean)
.map(Number);
const nextSeq = tabSeqArr.length
? Math.max(...tabSeqArr) + 1
: 1;
// 打开 tab
tabManager.openTab({
type: TabType.TERMINAL,
key: nextSessionId(),
seq: nextSeq,
title: `(${nextSeq}) ${record.alias || record.name}`,
hostId: record.id,
address: record.address
});
};
// 打开配置
const openSetting = inject<(record: HostQueryResponse) => void>(openSshModalKey) as any;

View File

@@ -10,7 +10,7 @@
type="button"
class="usn"
:options="toRadioOptions(newConnectionTypeKey)"
@change="s => updateTerminalPreference(PreferenceItem.NEW_CONNECTION_TYPE, s as string, true)" />
@change="s => updateTerminalPreference(TerminalPreferenceItem.NEW_CONNECTION_TYPE, s as string, true)" />
<!-- 过滤 -->
<a-auto-complete v-model="filterValue"
class="host-filter"
@@ -70,7 +70,7 @@
import { onBeforeMount, ref } from 'vue';
import { NewConnectionType, newConnectionTypeKey } from '../../types/terminal.const';
import { useDictStore, useTerminalStore } from '@/store';
import { PreferenceItem } from '@/store/modules/terminal';
import { TerminalPreferenceItem } from '@/store/modules/terminal';
import { dataColor } from '@/utils';
import { tagColor } from '@/views/asset/host-list/types/const';
import HostsView from './hosts-view.vue';

View File

@@ -50,7 +50,7 @@
import type { SidebarAction } from '../../types/terminal.type';
import { computed, ref, watch } from 'vue';
import { useTerminalStore } from '@/store';
import { PreferenceItem } from '@/store/modules/terminal';
import { TerminalPreferenceItem } from '@/store/modules/terminal';
import { ActionBarItems } from '../../types/terminal.const';
import IconActions from '../layout/icon-actions.vue';
@@ -64,7 +64,7 @@
return;
}
// 同步
updateTerminalPreference(PreferenceItem.ACTION_BAR_SETTING, formModel.value, true);
updateTerminalPreference(TerminalPreferenceItem.ACTION_BAR_SETTING, formModel.value, true);
}, { deep: true });
// 右侧操作

View File

@@ -102,7 +102,7 @@
import { useDictStore, useTerminalStore } from '@/store';
import { fontFamilyKey, fontSizeKey, fontWeightKey, fontFamilySuffix, cursorStyleKey } from '../../types/terminal.const';
import { labelFilter } from '@/types/form';
import { PreferenceItem } from '@/store/modules/terminal';
import { TerminalPreferenceItem } from '@/store/modules/terminal';
import TerminalExample from '../view-setting/terminal-example.vue';
const { toOptions, toRadioOptions } = useDictStore();
@@ -129,7 +129,7 @@
}
});
// 同步
updateTerminalPreference(PreferenceItem.DISPLAY_SETTING, formModel.value, true);
updateTerminalPreference(TerminalPreferenceItem.DISPLAY_SETTING, formModel.value, true);
// 聚焦
previewTerminal.value.term.focus();
}, { deep: true });

View File

@@ -86,7 +86,7 @@
import type { TerminalInteractSetting } from '@/store/modules/terminal/types';
import { ref, watch } from 'vue';
import { useTerminalStore } from '@/store';
import { PreferenceItem } from '@/store/modules/terminal';
import { TerminalPreferenceItem } from '@/store/modules/terminal';
import BlockSettingItem from './block-setting-item.vue';
const { preference, updateTerminalPreference } = useTerminalStore();
@@ -99,7 +99,7 @@
return;
}
// 同步
updateTerminalPreference(PreferenceItem.INTERACT_SETTING, formModel.value, true);
updateTerminalPreference(TerminalPreferenceItem.INTERACT_SETTING, formModel.value, true);
}, { deep: true });
</script>

View File

@@ -41,7 +41,7 @@
import type { TerminalPluginsSetting } from '@/store/modules/terminal/types';
import { ref, watch } from 'vue';
import { useTerminalStore } from '@/store';
import { PreferenceItem } from '@/store/modules/terminal';
import { TerminalPreferenceItem } from '@/store/modules/terminal';
import BlockSettingItem from './block-setting-item.vue';
const { preference, updateTerminalPreference } = useTerminalStore();
@@ -54,7 +54,7 @@
return;
}
// 同步
updateTerminalPreference(PreferenceItem.PLUGINS_SETTING, formModel.value, true);
updateTerminalPreference(TerminalPreferenceItem.PLUGINS_SETTING, formModel.value, true);
}, { deep: true });
</script>

View File

@@ -41,7 +41,7 @@
import type { TerminalSessionSetting } from '@/store/modules/terminal/types';
import { ref, watch } from 'vue';
import { useDictStore, useTerminalStore } from '@/store';
import { PreferenceItem } from '@/store/modules/terminal';
import { TerminalPreferenceItem } from '@/store/modules/terminal';
import { terminalEmulationTypeKey } from '../../types/terminal.const';
import BlockSettingItem from './block-setting-item.vue';
@@ -56,7 +56,7 @@
return;
}
// 同步
updateTerminalPreference(PreferenceItem.SESSION_SETTING, formModel.value, true);
updateTerminalPreference(TerminalPreferenceItem.SESSION_SETTING, formModel.value, true);
}, { deep: true });
</script>

View File

@@ -56,12 +56,12 @@
<script lang="ts" setup>
import type { TerminalTheme } from '@/api/asset/host-terminal';
import { useTerminalStore } from '@/store';
import { PreferenceItem } from '@/store/modules/terminal';
import { TerminalPreferenceItem } from '@/store/modules/terminal';
import { onMounted, ref } from 'vue';
import { getTerminalThemes } from '@/api/asset/host-terminal';
import TerminalExample from './terminal-example.vue';
import { getPreference } from '@/api/user/preference';
import useLoading from '@/hooks/loading';
import TerminalExample from './terminal-example.vue';
const { updateTerminalPreference } = useTerminalStore();
const { loading, setLoading } = useLoading();
@@ -72,14 +72,14 @@
// 选择主题
const selectTheme = async (theme: TerminalTheme) => {
currentThemeName.value = theme.name;
await updateTerminalPreference(PreferenceItem.THEME, theme);
await updateTerminalPreference(TerminalPreferenceItem.THEME, theme);
};
// 加载用户主题
onMounted(async () => {
try {
const { data } = await getPreference<Record<string, any>>('TERMINAL', [PreferenceItem.THEME]);
currentThemeName.value = data[PreferenceItem.THEME]?.name;
const { data } = await getPreference<Record<string, any>>('TERMINAL', [TerminalPreferenceItem.THEME]);
currentThemeName.value = data[TerminalPreferenceItem.THEME]?.name;
} catch (e) {
}
});

View File

@@ -9,9 +9,7 @@ export default class TerminalTabManager implements ITerminalTabManager {
public items: Array<TerminalTabItem>;
constructor() {
// fixme
// this.active = InnerTabs.NEW_CONNECTION.key;
this.active = undefined as unknown as string;
this.active = InnerTabs.NEW_CONNECTION.key;
this.items = [InnerTabs.NEW_CONNECTION];
}

View File

@@ -1,7 +1,5 @@
import { getUUID } from '@/utils';
// tab 类型
export const TabType = {
export const TerminalTabType = {
SETTING: 'setting',
TERMINAL: 'terminal',
};
@@ -11,27 +9,32 @@ export const InnerTabs = {
NEW_CONNECTION: {
key: 'newConnection',
title: '新建连接',
type: TabType.SETTING
icon: 'icon-plus',
type: TerminalTabType.SETTING
},
SHORTCUT_SETTING: {
key: 'shortcutSetting',
title: '快捷键设置',
type: TabType.SETTING
icon: 'icon-command',
type: TerminalTabType.SETTING
},
DISPLAY_SETTING: {
key: 'displaySetting',
title: '显示设置',
type: TabType.SETTING
icon: 'icon-dice',
type: TerminalTabType.SETTING
},
THEME_SETTING: {
key: 'themeSetting',
title: '主题设置',
type: TabType.SETTING
icon: 'icon-palette',
type: TerminalTabType.SETTING
},
TERMINAL_SETTING: {
key: 'terminalSetting',
title: '终端设置',
type: TabType.SETTING
icon: 'icon-settings',
type: TerminalTabType.SETTING
},
};
@@ -124,11 +127,6 @@ export const ActionBarItems = [
}
];
// 获取会话id
export const nextSessionId = (): string => {
return getUUID().replaceAll('-', '').substring(0, 10);
};
// 打开 sshModal key
export const openSshModalKey = Symbol();

View File

@@ -6,12 +6,14 @@ import type { WebLinksAddon } from 'xterm-addon-web-links';
import type { SearchAddon } from 'xterm-addon-search';
import type { ImageAddon } from 'xterm-addon-image';
import type { CSSProperties } from 'vue';
import type { HostQueryResponse } from '@/api/asset/host';
// 终端 tab 元素
export interface TerminalTabItem {
key: string;
title: string;
type: string;
icon?: string;
[key: string]: unknown;
}
@@ -27,6 +29,15 @@ export interface SidebarAction {
click: () => void;
}
// 组合操作元素
export interface CombinedHandlerItem {
icon: string,
type: string,
title: string;
settingTab?: TerminalTabItem;
host?: HostQueryResponse;
}
// ssh 额外配置
export interface SshExtraModel {
authType?: string;