写的不好? 那就推翻重来.
This commit is contained in:
@@ -191,14 +191,14 @@ body[host-space-theme='dark'] .arco-modal-container {
|
||||
}
|
||||
|
||||
// 侧栏图标
|
||||
.terminal-sidebar-icon-wrapper {
|
||||
.host-space-sidebar-icon-wrapper {
|
||||
width: var(--sidebar-icon-wrapper-size);
|
||||
height: var(--sidebar-icon-wrapper-size);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.terminal-sidebar-icon {
|
||||
.host-space-sidebar-icon {
|
||||
width: var(--sidebar-icon-size);
|
||||
height: var(--sidebar-icon-size);
|
||||
font-size: var(--sidebar-icon-font-size);
|
||||
@@ -226,44 +226,44 @@ body[host-space-theme='dark'] .arco-modal-container {
|
||||
}
|
||||
|
||||
// 终端设置容器
|
||||
.terminal-setting-container {
|
||||
.host-space-setting-container {
|
||||
padding: 32px 16px 16px 16px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.terminal-setting-wrapper {
|
||||
.host-space-setting-wrapper {
|
||||
min-width: 932px;
|
||||
max-width: 90%;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.terminal-setting-title {
|
||||
.host-space-setting-title {
|
||||
margin: 0 0 24px 0;
|
||||
user-select: none;
|
||||
font-size: 1.65em;
|
||||
color: var(--color-content-text-3);
|
||||
}
|
||||
|
||||
.terminal-setting-block {
|
||||
.host-space-setting-block {
|
||||
color: var(--color-content-text-2);
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.terminal-setting-subtitle-wrapper {
|
||||
.host-space-setting-subtitle-wrapper {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.terminal-setting-subtitle {
|
||||
.host-space-setting-subtitle {
|
||||
margin: 0 0 16px 0;
|
||||
user-select: none;
|
||||
color: var(--color-content-text-3);
|
||||
}
|
||||
|
||||
.terminal-setting-body {
|
||||
.host-space-setting-body {
|
||||
display: flex;
|
||||
|
||||
&.block-body {
|
||||
@@ -277,13 +277,13 @@ body[host-space-theme='dark'] .arco-modal-container {
|
||||
}
|
||||
|
||||
// tooltip 内容
|
||||
.terminal-tooltip-content {
|
||||
.host-space-tooltip-content {
|
||||
color: var(--color-sidebar-tooltip-text);
|
||||
background: var(--color-sidebar-tooltip-bg);
|
||||
}
|
||||
|
||||
// 终端右键菜单
|
||||
.terminal-context-menu {
|
||||
// 右键菜单
|
||||
.host-space-context-menu {
|
||||
.arco-dropdown-option {
|
||||
padding: 0 6px;
|
||||
line-height: 32px;
|
||||
|
||||
@@ -6,6 +6,7 @@ import useTabBarStore from './modules/tab-bar';
|
||||
import useCacheStore from './modules/cache';
|
||||
import useTipsStore from './modules/tips';
|
||||
import useDictStore from './modules/dict';
|
||||
import useHostSpaceStore from './modules/host-space';
|
||||
import useTerminalStore from './modules/terminal';
|
||||
|
||||
const pinia = createPinia();
|
||||
@@ -19,6 +20,7 @@ export {
|
||||
useTipsStore,
|
||||
useDictStore,
|
||||
useTerminalStore,
|
||||
useHostSpaceStore,
|
||||
};
|
||||
|
||||
export default pinia;
|
||||
|
||||
179
orion-ops-ui/src/store/modules/host-space/index.ts
Normal file
179
orion-ops-ui/src/store/modules/host-space/index.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import type {
|
||||
TerminalActionBarSetting,
|
||||
TerminalDisplaySetting,
|
||||
TerminalInteractSetting,
|
||||
TerminalPluginsSetting,
|
||||
TerminalPreference,
|
||||
TerminalSessionSetting,
|
||||
TerminalShortcutSetting,
|
||||
TerminalState
|
||||
} from './types';
|
||||
import type { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data';
|
||||
import type { HostQueryResponse } from '@/api/asset/host';
|
||||
import type { TerminalTheme, TerminalThemeSchema } from '@/api/asset/host-terminal';
|
||||
import { getCurrentAuthorizedHost } from '@/api/asset/asset-authorized-data';
|
||||
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 TerminalPreferenceItem = {
|
||||
// 新建连接类型
|
||||
NEW_CONNECTION_TYPE: 'newConnectionType',
|
||||
// 终端主题
|
||||
THEME: 'theme',
|
||||
// 显示设置
|
||||
DISPLAY_SETTING: 'displaySetting',
|
||||
// 操作栏设置
|
||||
ACTION_BAR_SETTING: 'actionBarSetting',
|
||||
// 右键菜单设置
|
||||
RIGHT_MENU_SETTING: 'rightMenuSetting',
|
||||
// 交互设置
|
||||
INTERACT_SETTING: 'interactSetting',
|
||||
// 插件设置
|
||||
PLUGINS_SETTING: 'pluginsSetting',
|
||||
// 会话设置
|
||||
SESSION_SETTING: 'sessionSetting',
|
||||
// 快捷键设置
|
||||
SHORTCUT_SETTING: 'shortcutSetting',
|
||||
};
|
||||
|
||||
export default defineStore('hostSpace', {
|
||||
state: (): TerminalState => ({
|
||||
preference: {
|
||||
newConnectionType: 'group',
|
||||
theme: {
|
||||
schema: {} as TerminalThemeSchema
|
||||
} as TerminalTheme,
|
||||
displaySetting: {} as TerminalDisplaySetting,
|
||||
actionBarSetting: {} as TerminalActionBarSetting,
|
||||
rightMenuSetting: [],
|
||||
interactSetting: {} as TerminalInteractSetting,
|
||||
pluginsSetting: {} as TerminalPluginsSetting,
|
||||
sessionSetting: {} as TerminalSessionSetting,
|
||||
shortcutSetting: {
|
||||
enabled: false,
|
||||
keys: []
|
||||
} as TerminalShortcutSetting,
|
||||
},
|
||||
hosts: {} as AuthorizedHostQueryResponse,
|
||||
tabManager: new TerminalTabManager(),
|
||||
sessionManager: new TerminalSessionManager()
|
||||
}),
|
||||
|
||||
actions: {
|
||||
// 加载终端偏好
|
||||
async fetchPreference() {
|
||||
try {
|
||||
// 加载偏好
|
||||
const { data } = await getPreference<TerminalPreference>('TERMINAL');
|
||||
// theme 不存在则默认加载第一个
|
||||
if (!data.theme?.name) {
|
||||
const { data: themes } = await getTerminalThemes();
|
||||
data.theme = themes[0];
|
||||
// 更新默认主题偏好
|
||||
await this.updateTerminalPreference(TerminalPreferenceItem.THEME, data.theme);
|
||||
}
|
||||
// 移除禁用的快捷键
|
||||
if (data.shortcutSetting?.enabled) {
|
||||
data.shortcutSetting.keys = data.shortcutSetting.keys.filter(s => s.enabled);
|
||||
} else {
|
||||
data.shortcutSetting = {
|
||||
enabled: false,
|
||||
keys: []
|
||||
};
|
||||
}
|
||||
// 选择赋值 (不能修改引用)
|
||||
const keys = Object.keys(this.preference);
|
||||
keys.forEach(key => {
|
||||
const item = data[key as keyof TerminalPreference];
|
||||
if (item) {
|
||||
this.preference[key as keyof TerminalPreference] = item as any;
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
Message.error('配置加载失败');
|
||||
}
|
||||
},
|
||||
|
||||
// 更新终端偏好
|
||||
async updateTerminalPreference(item: string, value: any, setLocal = false) {
|
||||
if (setLocal) {
|
||||
this.preference[item as keyof TerminalPreference] = value;
|
||||
}
|
||||
try {
|
||||
// 修改配置
|
||||
await updatePreference({
|
||||
type: 'TERMINAL',
|
||||
item,
|
||||
value
|
||||
});
|
||||
} catch (e) {
|
||||
Message.error('同步失败');
|
||||
}
|
||||
},
|
||||
|
||||
// 加载主机列表
|
||||
async loadHosts() {
|
||||
if (this.hosts.hostList?.length) {
|
||||
return;
|
||||
}
|
||||
const { data } = await getCurrentAuthorizedHost();
|
||||
Object.keys(data).forEach(k => {
|
||||
this.hosts[k as keyof AuthorizedHostQueryResponse] = data[k as keyof AuthorizedHostQueryResponse] as any;
|
||||
});
|
||||
},
|
||||
|
||||
// 打开终端
|
||||
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
|
||||
});
|
||||
},
|
||||
|
||||
// 复制并且打开终端
|
||||
openCopyTerminal(hostId: number) {
|
||||
const host = this.hosts.hostList
|
||||
.find(s => s.id === hostId);
|
||||
if (host) {
|
||||
this.openTerminal(host);
|
||||
}
|
||||
},
|
||||
|
||||
// 获取当前终端会话
|
||||
getCurrentTerminalSession(tips: boolean = true) {
|
||||
const tab = this.tabManager.getCurrentTab();
|
||||
if (!tab || tab.type !== TerminalTabType.TERMINAL) {
|
||||
if (tips) {
|
||||
Message.warning('请切换到终端标签页');
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 获取处理器并截图
|
||||
return this.sessionManager.getSession(tab.key);
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
});
|
||||
97
orion-ops-ui/src/store/modules/host-space/types.ts
Normal file
97
orion-ops-ui/src/store/modules/host-space/types.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import type { ITerminalSessionManager, ITerminalTabManager } from '@/views/host/terminal/types/terminal.type';
|
||||
import type { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data';
|
||||
import type { TerminalTheme } from '@/api/asset/host-terminal';
|
||||
|
||||
export interface TerminalState {
|
||||
preference: TerminalPreference;
|
||||
hosts: AuthorizedHostQueryResponse;
|
||||
tabManager: ITerminalTabManager;
|
||||
sessionManager: ITerminalSessionManager;
|
||||
}
|
||||
|
||||
// 终端配置
|
||||
export interface TerminalPreference {
|
||||
newConnectionType: string;
|
||||
theme: TerminalTheme;
|
||||
displaySetting: TerminalDisplaySetting;
|
||||
actionBarSetting: TerminalActionBarSetting;
|
||||
rightMenuSetting: Array<string>,
|
||||
interactSetting: TerminalInteractSetting;
|
||||
pluginsSetting: TerminalPluginsSetting;
|
||||
sessionSetting: TerminalSessionSetting;
|
||||
shortcutSetting: TerminalShortcutSetting;
|
||||
}
|
||||
|
||||
// 显示设置
|
||||
export interface TerminalDisplaySetting {
|
||||
fontFamily?: string;
|
||||
fontSize?: number;
|
||||
lineHeight?: number;
|
||||
fontWeight?: string | number;
|
||||
fontWeightBold?: string | number;
|
||||
cursorStyle?: string;
|
||||
cursorBlink?: boolean;
|
||||
}
|
||||
|
||||
// 操作栏设置
|
||||
export interface TerminalActionBarSetting {
|
||||
commandInput?: boolean;
|
||||
connectStatus?: boolean;
|
||||
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// 交互设置
|
||||
export interface TerminalInteractSetting {
|
||||
fastScrollModifier: boolean;
|
||||
altClickMovesCursor: boolean;
|
||||
rightClickSelectsWord: boolean;
|
||||
selectionChangeCopy: boolean;
|
||||
copyAutoTrim: boolean;
|
||||
pasteAutoTrim: boolean;
|
||||
rightClickPaste: boolean;
|
||||
enableRightClickMenu: boolean;
|
||||
enableBell: boolean;
|
||||
wordSeparator: string;
|
||||
}
|
||||
|
||||
// 插件设置
|
||||
export interface TerminalPluginsSetting {
|
||||
enableWeblinkPlugin: boolean;
|
||||
enableWebglPlugin: boolean;
|
||||
enableImagePlugin: boolean;
|
||||
}
|
||||
|
||||
// 会话设置
|
||||
export interface TerminalSessionSetting {
|
||||
terminalEmulationType: string;
|
||||
scrollBackLine: number;
|
||||
}
|
||||
|
||||
// 终端快捷键设置
|
||||
export interface TerminalShortcutSetting {
|
||||
enabled: boolean;
|
||||
keys: Array<TerminalShortcutKey>;
|
||||
}
|
||||
|
||||
// 终端快捷键
|
||||
export interface ShortcutKey {
|
||||
ctrlKey: boolean;
|
||||
shiftKey: boolean;
|
||||
altKey: boolean;
|
||||
code: string;
|
||||
}
|
||||
|
||||
// 终端快捷键
|
||||
export interface TerminalShortcutKey extends ShortcutKey {
|
||||
item: string;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
// 终端快捷键编辑
|
||||
export interface TerminalShortcutKeyEditable extends TerminalShortcutKey {
|
||||
editable: boolean;
|
||||
content: string;
|
||||
type: number;
|
||||
shortcutKey?: string;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<a-dropdown class="terminal-context-menu"
|
||||
<a-dropdown class="host-space-context-menu"
|
||||
:popup-max-height="false"
|
||||
trigger="contextMenu"
|
||||
position="bl"
|
||||
@@ -52,35 +52,35 @@
|
||||
<template #content>
|
||||
<!-- 复制 -->
|
||||
<a-doption @click="copyCommand">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<div class="host-space-context-menu-icon">
|
||||
<icon-copy />
|
||||
</div>
|
||||
<div>复制</div>
|
||||
</a-doption>
|
||||
<!-- 粘贴 -->
|
||||
<a-doption @click="paste">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<div class="host-space-context-menu-icon">
|
||||
<icon-paste />
|
||||
</div>
|
||||
<div>粘贴</div>
|
||||
</a-doption>
|
||||
<!-- 执行 -->
|
||||
<a-doption @click="exec">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<div class="host-space-context-menu-icon">
|
||||
<icon-thunderbolt />
|
||||
</div>
|
||||
<div>执行</div>
|
||||
</a-doption>
|
||||
<!-- 修改 -->
|
||||
<a-doption @click="openUpdateSnippet(item)">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<div class="host-space-context-menu-icon">
|
||||
<icon-edit />
|
||||
</div>
|
||||
<div>修改</div>
|
||||
</a-doption>
|
||||
<!-- 删除 -->
|
||||
<a-doption @click="removeSnippet(item.id)">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<div class="host-space-context-menu-icon">
|
||||
<icon-delete />
|
||||
</div>
|
||||
<div>删除</div>
|
||||
@@ -88,7 +88,7 @@
|
||||
<!-- 展开 -->
|
||||
<a-doption v-if="!item.expand"
|
||||
@click="() => item.expand = true">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<div class="host-space-context-menu-icon">
|
||||
<icon-expand />
|
||||
</div>
|
||||
<div>展开</div>
|
||||
@@ -96,7 +96,7 @@
|
||||
<!-- 收起 -->
|
||||
<a-doption v-else
|
||||
@click="() => item.expand = false">
|
||||
<div class="terminal-context-menu-icon">
|
||||
<div class="host-space-context-menu-icon">
|
||||
<icon-shrink />
|
||||
</div>
|
||||
<div>收起</div>
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<div class="host-space-setting-container">
|
||||
<div class="host-space-setting-wrapper">
|
||||
<!-- 组合容器 -->
|
||||
<div class="combined-container">
|
||||
<!-- 新建连接 -->
|
||||
<div class="combined-handler" v-for="(handler, index) in combinedHandlers"
|
||||
:key="index"
|
||||
@click="clickHandlerItem(handler)">
|
||||
<!-- 图标 -->
|
||||
<div class="combined-handler-icon">
|
||||
<component :is="handler.icon" />
|
||||
</div>
|
||||
<!-- 内容 -->
|
||||
<div class="combined-handler-text">
|
||||
{{ handler.title }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'emptyRecommend'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TabItem, CombinedHandlerItem } from '../../types/type';
|
||||
import type { HostQueryResponse } from '@/api/asset/host';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { useHostSpaceStore } from '@/store';
|
||||
import { InnerTabs, TabType } from '../../types/const';
|
||||
import { get } from 'lodash';
|
||||
|
||||
const totalCount = 7;
|
||||
const { tabManager, hosts, openTerminal } = useHostSpaceStore();
|
||||
|
||||
const combinedHandlers = ref<Array<CombinedHandlerItem>>([{
|
||||
title: InnerTabs.NEW_CONNECTION.title,
|
||||
settingTab: InnerTabs.NEW_CONNECTION,
|
||||
type: TabType.SETTING,
|
||||
icon: InnerTabs.NEW_CONNECTION.icon
|
||||
}]);
|
||||
|
||||
// 点击组合操作元素
|
||||
const clickHandlerItem = (item: CombinedHandlerItem) => {
|
||||
if (item.type === TabType.SETTING) {
|
||||
// 打开内置 tab
|
||||
tabManager.openTab(item.settingTab as TabItem);
|
||||
} 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: TabType.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 TabItem)
|
||||
.slice(0, totalCount - 1 - combinedHosts.length)
|
||||
.map(s => {
|
||||
return {
|
||||
title: s.title,
|
||||
settingTab: s,
|
||||
type: TabType.SETTING,
|
||||
icon: s.icon as string
|
||||
};
|
||||
});
|
||||
combinedHandlers.value.push(...fillTabs);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@transform-x: 8px;
|
||||
@container-width: 406px;
|
||||
@container-height: 448px;
|
||||
@handler-height: 44px;
|
||||
|
||||
.combined-container {
|
||||
padding: 12px;
|
||||
margin: 64px auto;
|
||||
width: @container-width;
|
||||
height: @container-height;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-sizing: content-box;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.combined-handler {
|
||||
width: calc(@container-width - @transform-x);
|
||||
height: @handler-height;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 6px;
|
||||
color: var(--color-content-text-1);
|
||||
background-color: var(--color-fill-2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
width: @container-width;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
width: @handler-height;
|
||||
height: @handler-height;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
&-text {
|
||||
height: 100%;
|
||||
width: calc(100% - @handler-height - 12px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
|
||||
&-wrapper {
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
white-space: pre;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -5,11 +5,11 @@
|
||||
:position="position as any"
|
||||
:mini="true"
|
||||
:show-arrow="false"
|
||||
content-class="terminal-tooltip-content"
|
||||
content-class="host-space-tooltip-content"
|
||||
:auto-fix-position="false"
|
||||
:content="action.content">
|
||||
<div class="terminal-sidebar-icon-wrapper" v-if="action.visible !== false">
|
||||
<div class="terminal-sidebar-icon"
|
||||
<div class="host-space-sidebar-icon-wrapper" v-if="action.visible !== false">
|
||||
<div class="host-space-sidebar-icon"
|
||||
:class="[
|
||||
iconClass,
|
||||
action.disabled === true ? 'disabled-item' : '',
|
||||
@@ -30,7 +30,7 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SidebarAction } from '../../terminal/types/terminal.type';
|
||||
import type { SidebarAction } from '../../types/type';
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
defineProps({
|
||||
@@ -40,14 +40,14 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SidebarAction } from '../../terminal/types/terminal.type';
|
||||
import type { SidebarAction } from '../../types/type';
|
||||
import { useFullscreen } from '@vueuse/core';
|
||||
import { computed } from 'vue';
|
||||
import { useTerminalStore } from '@/store';
|
||||
import { useHostSpaceStore } from '@/store';
|
||||
import IconActions from './icon-actions.vue';
|
||||
|
||||
const { isFullscreen, toggle: toggleFullScreen } = useFullscreen();
|
||||
const { tabManager } = useTerminalStore();
|
||||
const { tabManager } = useHostSpaceStore();
|
||||
|
||||
// 顶部操作
|
||||
const actions = computed<Array<SidebarAction>>(() => [
|
||||
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<div class="layout-main">
|
||||
<!-- 内容 tabs -->
|
||||
<a-tabs v-if="tabManager.active"
|
||||
v-model:active-key="tabManager.active">
|
||||
<a-tab-pane v-for="tab in tabManager.items"
|
||||
:key="tab.key"
|
||||
:title="tab.title">
|
||||
<!-- 设置 -->
|
||||
<template v-if="tab.type === TabType.SETTING">
|
||||
<!-- 新建连接 -->
|
||||
<new-connection-view v-if="tab.key === InnerTabs.NEW_CONNECTION.key" />
|
||||
<!-- 显示设置 -->
|
||||
<terminal-display-setting v-else-if="tab.key === InnerTabs.DISPLAY_SETTING.key" />
|
||||
<!-- 主题设置 -->
|
||||
<terminal-theme-setting v-else-if="tab.key === InnerTabs.THEME_SETTING.key" />
|
||||
<!-- 终端设置 -->
|
||||
<terminal-general-setting v-else-if="tab.key === InnerTabs.TERMINAL_SETTING.key" />
|
||||
<!-- 快捷键设置 -->
|
||||
<terminal-shortcut-setting v-else-if="tab.key === InnerTabs.SHORTCUT_SETTING.key" />
|
||||
</template>
|
||||
<!-- 终端 -->
|
||||
<template v-else-if="tab.type === TabType.TERMINAL">
|
||||
<!-- <terminal-view :tab="tab" />-->
|
||||
</template>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
<!-- 承载页推荐 -->
|
||||
<empty-recommend v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'layoutMain'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { TabType, InnerTabs, TabShortcutKeys } from '../../types/const';
|
||||
import { useHostSpaceStore } from '@/store';
|
||||
import { onMounted, onUnmounted, watch } from 'vue';
|
||||
import { addEventListen, removeEventListen } from '@/utils/event';
|
||||
import EmptyRecommend from './empty-recommend.vue';
|
||||
import NewConnectionView from '../new-connection/new-connection-view.vue';
|
||||
import TerminalDisplaySetting from '../setting/terminal-display-setting.vue';
|
||||
import TerminalThemeSetting from '../setting/terminal-theme-setting.vue';
|
||||
import TerminalGeneralSetting from '../setting/terminal-general-setting.vue';
|
||||
import TerminalShortcutSetting from '../setting/terminal-shortcut-setting.vue';
|
||||
// import TerminalView from '../xterm/terminal-view.vue';
|
||||
|
||||
const { preference, tabManager, sessionManager } = useHostSpaceStore();
|
||||
|
||||
// 监听 tab 修改
|
||||
watch(() => tabManager.active, (active, before) => {
|
||||
if (before) {
|
||||
// 失焦已经切换的终端
|
||||
const beforeTab = tabManager.items.find(s => s.key === before);
|
||||
if (beforeTab && beforeTab?.type === TabType.TERMINAL) {
|
||||
sessionManager.getSession(before)?.blur();
|
||||
}
|
||||
}
|
||||
if (active) {
|
||||
// 获取 activeTab
|
||||
const activeTab = tabManager.items.find(s => s.key === active);
|
||||
if (!activeTab) {
|
||||
return;
|
||||
}
|
||||
// 修改标题
|
||||
document.title = activeTab.title;
|
||||
// 终端自动聚焦
|
||||
if (activeTab?.type === TabType.TERMINAL) {
|
||||
sessionManager.getSession(active)?.focus();
|
||||
}
|
||||
} else {
|
||||
// 修改标题
|
||||
document.title = '主机终端';
|
||||
}
|
||||
});
|
||||
|
||||
// 处理快捷键逻辑
|
||||
const handlerKeyboard = (event: Event) => {
|
||||
// 当前页面非 terminal 的时候再触发快捷键 (terminal 有内置逻辑)
|
||||
if (tabManager.active
|
||||
&& tabManager.items.find(s => s.key === tabManager.active)?.type === TabType.TERMINAL) {
|
||||
return;
|
||||
}
|
||||
const e = event as KeyboardEvent;
|
||||
// 检测触发的快捷键
|
||||
const key = preference.shortcutSetting.keys.find(key => {
|
||||
return key.code === e.code
|
||||
&& key.altKey === e.altKey
|
||||
&& key.shiftKey === e.shiftKey
|
||||
&& key.ctrlKey === e.ctrlKey;
|
||||
});
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
// 触发逻辑
|
||||
switch (key.item) {
|
||||
case TabShortcutKeys.CLOSE_TAB:
|
||||
// 关闭 tab
|
||||
if (tabManager.active) {
|
||||
tabManager.deleteTab(tabManager.active);
|
||||
}
|
||||
break;
|
||||
case TabShortcutKeys.CHANGE_TO_PREV_TAB:
|
||||
// 切换至前一个 tab
|
||||
tabManager.changeToPrevTab();
|
||||
break;
|
||||
case TabShortcutKeys.CHANGE_TO_NEXT_TAB:
|
||||
// 切换至后一个 tab
|
||||
tabManager.changeToNextTab();
|
||||
break;
|
||||
case TabShortcutKeys.OPEN_NEW_CONNECT_TAB:
|
||||
// 切换到新建连接 tab
|
||||
tabManager.openTab(InnerTabs.NEW_CONNECTION);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 监听键盘事件
|
||||
onMounted(() => {
|
||||
if (preference.shortcutSetting.enabled) {
|
||||
addEventListen(window, 'keydown', handlerKeyboard);
|
||||
}
|
||||
});
|
||||
|
||||
// 移除键盘事件
|
||||
onUnmounted(() => {
|
||||
if (preference.shortcutSetting.enabled) {
|
||||
removeEventListen(window, 'keydown', handlerKeyboard);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.layout-main {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
:deep(.arco-tabs) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.arco-tabs-nav {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.arco-tabs-content {
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<div class="layout-left-sidebar">
|
||||
<!-- 顶部操作按钮 -->
|
||||
<icon-actions class="top-actions"
|
||||
:actions="topActions"
|
||||
position="left" />
|
||||
<!-- 底部操作按钮 -->
|
||||
<icon-actions class="bottom-actions"
|
||||
:actions="bottomActions"
|
||||
position="right" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'leftSidebar'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SidebarAction } from '../../types/type';
|
||||
import { InnerTabs } from '../../types/const';
|
||||
import { useHostSpaceStore } from '@/store';
|
||||
import IconActions from './icon-actions.vue';
|
||||
|
||||
const { tabManager } = useHostSpaceStore();
|
||||
|
||||
// 顶部操作
|
||||
const topActions: Array<SidebarAction> = [
|
||||
{
|
||||
icon: InnerTabs.NEW_CONNECTION.icon,
|
||||
content: InnerTabs.NEW_CONNECTION.title,
|
||||
click: () => tabManager.openTab(InnerTabs.NEW_CONNECTION)
|
||||
},
|
||||
];
|
||||
|
||||
// 底部操作
|
||||
const bottomActions: Array<SidebarAction> = [
|
||||
{
|
||||
icon: InnerTabs.SHORTCUT_SETTING.icon,
|
||||
content: InnerTabs.SHORTCUT_SETTING.title,
|
||||
click: () => tabManager.openTab(InnerTabs.SHORTCUT_SETTING)
|
||||
},
|
||||
{
|
||||
icon: InnerTabs.DISPLAY_SETTING.icon,
|
||||
content: InnerTabs.DISPLAY_SETTING.title,
|
||||
click: () => tabManager.openTab(InnerTabs.DISPLAY_SETTING)
|
||||
},
|
||||
{
|
||||
icon: InnerTabs.THEME_SETTING.icon,
|
||||
content: InnerTabs.THEME_SETTING.title,
|
||||
click: () => tabManager.openTab(InnerTabs.THEME_SETTING)
|
||||
},
|
||||
{
|
||||
icon: InnerTabs.TERMINAL_SETTING.icon,
|
||||
content: InnerTabs.TERMINAL_SETTING.title,
|
||||
click: () => tabManager.openTab(InnerTabs.TERMINAL_SETTING)
|
||||
},
|
||||
];
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.layout-left-sidebar {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div class="host-space-setting-container">
|
||||
<div class="host-space-setting-wrapper loading-skeleton">
|
||||
<!-- 加载骨架 -->
|
||||
<a-skeleton class="full"
|
||||
:animation="true">
|
||||
<a-skeleton-line :rows="6"
|
||||
:line-height="42"
|
||||
:line-spacing="24" />
|
||||
</a-skeleton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'loadingSkeleton'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.loading-skeleton {
|
||||
margin: 32px auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div class="layout-right-sidebar">
|
||||
<!-- 顶部操作按钮 -->
|
||||
<icon-actions class="top-actions"
|
||||
:actions="topActions"
|
||||
position="left" />
|
||||
<!-- 底部操作按钮 -->
|
||||
<icon-actions class="bottom-actions"
|
||||
:actions="bottomActions"
|
||||
position="left" />
|
||||
<!-- 命令片段列表抽屉 -->
|
||||
<command-snippet-list-drawer ref="snippetRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'rightSidebar'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SidebarAction } from '../../types/type';
|
||||
import { useHostSpaceStore } from '@/store';
|
||||
import { ref } from 'vue';
|
||||
import IconActions from './icon-actions.vue';
|
||||
import CommandSnippetListDrawer from '../../../command-snippet/components/command-snippet-list-drawer.vue';
|
||||
|
||||
const emits = defineEmits(['openSftp', 'openTransfer']);
|
||||
|
||||
const { getCurrentTerminalSession } = useHostSpaceStore();
|
||||
|
||||
const snippetRef = ref();
|
||||
|
||||
// 顶部操作
|
||||
const topActions = [
|
||||
{
|
||||
icon: 'icon-code',
|
||||
content: '打开命令片段',
|
||||
click: () => snippetRef.value.open()
|
||||
},
|
||||
{
|
||||
icon: 'icon-folder',
|
||||
content: '打开 SFTP',
|
||||
click: () => emits('openSftp')
|
||||
},
|
||||
{
|
||||
icon: 'icon-swap',
|
||||
content: '文件传输列表',
|
||||
iconStyle: {
|
||||
transform: 'rotate(90deg)'
|
||||
},
|
||||
click: () => emits('openTransfer')
|
||||
},
|
||||
];
|
||||
|
||||
// 底部操作
|
||||
const bottomActions: Array<SidebarAction> = [
|
||||
{
|
||||
icon: 'icon-camera',
|
||||
content: '截图',
|
||||
click: () => screenshot()
|
||||
},
|
||||
];
|
||||
|
||||
// 终端截屏
|
||||
const screenshot = () => {
|
||||
const handler = getCurrentTerminalSession()?.handler;
|
||||
if (handler && handler.enabledStatus('screenshot')) {
|
||||
handler.screenshot();
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.layout-right-sidebar {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="group-view-container">
|
||||
<!-- 主机分组 -->
|
||||
<div class="host-group-container">
|
||||
<a-scrollbar>
|
||||
<a-tree v-model:selected-keys="selectedGroup"
|
||||
class="host-tree block-tree"
|
||||
:data="groupTree"
|
||||
:blockNode="true">
|
||||
<!-- 组内数量 -->
|
||||
<template #extra="node">
|
||||
<span class="node-host-count span-blue">{{ treeNodes[node.key]?.length || 0 }}</span>
|
||||
</template>
|
||||
</a-tree>
|
||||
</a-scrollbar>
|
||||
</div>
|
||||
<!-- 主机列表 -->
|
||||
<host-list-view class="host-list"
|
||||
:hostList="hostList"
|
||||
empty-value="当前分组内无授权主机!" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostGroupView'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { HostQueryResponse } from '@/api/asset/host';
|
||||
import { HostGroupQueryResponse } from '@/api/asset/host-group';
|
||||
import HostListView from './host-list-view.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: number,
|
||||
groupTree: Array<HostGroupQueryResponse>;
|
||||
hostList: Array<HostQueryResponse>;
|
||||
treeNodes: Record<string, Array<number>>;
|
||||
}>();
|
||||
|
||||
const emits = defineEmits(['update:modelValue']);
|
||||
|
||||
const selectedGroup = computed({
|
||||
get() {
|
||||
return [props.modelValue];
|
||||
},
|
||||
set(e) {
|
||||
emits('update:modelValue', e[0]);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@tree-width: 298px;
|
||||
@tree-gap: 32px;
|
||||
|
||||
.group-view-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
max-height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.host-group-container {
|
||||
:deep(.arco-scrollbar) {
|
||||
width: @tree-width;
|
||||
height: 100%;
|
||||
margin-right: @tree-gap;
|
||||
border-radius: 4px;
|
||||
|
||||
&-container {
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.host-tree {
|
||||
min-width: 100%;
|
||||
width: max-content;
|
||||
user-select: none;
|
||||
overflow: hidden;
|
||||
|
||||
.node-host-count {
|
||||
margin-right: 10px;
|
||||
font-size: 13px;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.host-list {
|
||||
width: calc(100% - @tree-width - @tree-gap);
|
||||
border-radius: 4px;
|
||||
max-height: 100%;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,376 @@
|
||||
<template>
|
||||
<div class="hosts-list-container">
|
||||
<a-list size="large"
|
||||
max-height="100%"
|
||||
:hoverable="true"
|
||||
:data="hostList">
|
||||
<!-- 空数据 -->
|
||||
<template #empty>
|
||||
<a-empty>
|
||||
<template #image>
|
||||
<icon-desktop />
|
||||
</template>
|
||||
{{ emptyValue }}
|
||||
</a-empty>
|
||||
</template>
|
||||
<!-- 数据 -->
|
||||
<template #item="{ 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" @click="openTerminal(item)">
|
||||
<icon-desktop />
|
||||
</span>
|
||||
<!-- 名称 -->
|
||||
<span class="host-item-left-name">
|
||||
<!-- 名称文本 -->
|
||||
<template v-if="!item.editable">
|
||||
<!-- 文本 -->
|
||||
<a-tooltip position="top"
|
||||
:mini="true"
|
||||
content-class="host-space-tooltip-content"
|
||||
arrow-class="host-space-tooltip-content"
|
||||
:content="item.alias || `${item.name} (${item.code})`">
|
||||
<span class="host-item-text host-item-left-name-text">
|
||||
<template v-if="item.alias">
|
||||
{{ item.alias }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ `${item.name} (${item.code})` }}
|
||||
</template>
|
||||
</span>
|
||||
</a-tooltip>
|
||||
<!-- 修改别名 -->
|
||||
<a-tooltip position="top"
|
||||
:mini="true"
|
||||
:auto-fix-position="false"
|
||||
content-class="host-space-tooltip-content"
|
||||
arrow-class="host-space-tooltip-content"
|
||||
content="修改别名">
|
||||
<icon-edit class="host-item-left-name-edit"
|
||||
@click="clickEditAlias(item)" />
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<!-- 名称输入框 -->
|
||||
<template v-else>
|
||||
<a-input v-model="item.alias"
|
||||
ref="aliasNameInput"
|
||||
class="host-item-left-name-input"
|
||||
:max-length="32"
|
||||
:disabled="item.loading"
|
||||
size="mini"
|
||||
:placeholder="item.name"
|
||||
@blur="saveAlias(item)"
|
||||
@pressEnter="saveAlias(item)"
|
||||
@change="saveAlias(item)">
|
||||
<template #suffix>
|
||||
<!-- 加载中 -->
|
||||
<icon-loading v-if="item.loading" />
|
||||
<!-- 保存 -->
|
||||
<icon-check v-else
|
||||
class="pointer"
|
||||
title="保存"
|
||||
@click="saveAlias(item)" />
|
||||
</template>
|
||||
</a-input>
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
<!-- 中间ip -->
|
||||
<div class="flex-center host-item-center">
|
||||
<!-- ip -->
|
||||
<a-tooltip position="top"
|
||||
:mini="true"
|
||||
:auto-fix-position="false"
|
||||
content-class="host-space-tooltip-content"
|
||||
arrow-class="host-space-tooltip-content"
|
||||
:content="item.address">
|
||||
<span class="host-item-text host-item-center-address">
|
||||
{{ item.address }}
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
<!-- 右侧tag-操作 -->
|
||||
<div class="flex-center host-item-right">
|
||||
<!-- tags -->
|
||||
<div class="host-item-right-tags">
|
||||
<template v-if="item.tags?.length">
|
||||
<a-tag v-for="(tag, i) in item.tags"
|
||||
class="host-item-text"
|
||||
:key="tag.id"
|
||||
:style="{
|
||||
maxWidth: `calc(${100 / item.tags.length}% - ${i !== item.tags.length - 1 ? '8px' : '0px'})`,
|
||||
marginRight: `${i !== item.tags.length - 1 ? '8px' : '0'}`,
|
||||
}"
|
||||
:color="dataColor(tag.name, tagColor)">
|
||||
{{ tag.name }}
|
||||
</a-tag>
|
||||
</template>
|
||||
</div>
|
||||
<!-- 操作 -->
|
||||
<div class="host-item-right-actions">
|
||||
<!-- 连接主机 -->
|
||||
<a-tooltip position="top"
|
||||
:mini="true"
|
||||
:auto-fix-position="false"
|
||||
content-class="host-space-tooltip-content"
|
||||
arrow-class="host-space-tooltip-content"
|
||||
content="连接主机">
|
||||
<div class="host-space-sidebar-icon-wrapper">
|
||||
<div class="host-space-sidebar-icon" @click="openTerminal(item)">
|
||||
<icon-thunderbolt />
|
||||
</div>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<!-- 连接设置 -->
|
||||
<a-tooltip position="top"
|
||||
:mini="true"
|
||||
:auto-fix-position="false"
|
||||
content-class="host-space-tooltip-content"
|
||||
arrow-class="host-space-tooltip-content"
|
||||
content="连接设置">
|
||||
<div class="host-space-sidebar-icon-wrapper">
|
||||
<div class="host-space-sidebar-icon" @click="openSetting(item)">
|
||||
<icon-settings />
|
||||
</div>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
<!-- 收藏 -->
|
||||
<a-tooltip position="top"
|
||||
:mini="true"
|
||||
:auto-fix-position="false"
|
||||
content-class="host-space-tooltip-content"
|
||||
arrow-class="host-space-tooltip-content"
|
||||
content="收藏">
|
||||
<div class="host-space-sidebar-icon-wrapper">
|
||||
<div class="host-space-sidebar-icon" @click="setFavorite(item)">
|
||||
<icon-star-fill class="favorite" v-if="item.favorite" />
|
||||
<icon-star v-else />
|
||||
</div>
|
||||
</div>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-list-item>
|
||||
</template>
|
||||
</a-list>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostListView'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { HostQueryResponse } from '@/api/asset/host';
|
||||
import { ref, nextTick, inject } from 'vue';
|
||||
import useFavorite from '@/hooks/favorite';
|
||||
import { dataColor } from '@/utils';
|
||||
import { tagColor } from '@/views/asset/host-list/types/const';
|
||||
import { updateHostAlias } from '@/api/asset/host-extra';
|
||||
import { openSshModalKey } from '../../types/const';
|
||||
import { useHostSpaceStore } from '@/store';
|
||||
|
||||
const props = defineProps<{
|
||||
hostList: Array<HostQueryResponse>,
|
||||
emptyValue: string
|
||||
}>();
|
||||
|
||||
const { openTerminal } = useHostSpaceStore();
|
||||
const { toggle: toggleFavorite, loading: favoriteLoading } = useFavorite('HOST');
|
||||
|
||||
const aliasNameInput = ref();
|
||||
|
||||
// 点击修改别名
|
||||
const clickEditAlias = (item: HostQueryResponse) => {
|
||||
item.editable = true;
|
||||
if (!item.alias) {
|
||||
item.alias = '';
|
||||
}
|
||||
nextTick(() => {
|
||||
aliasNameInput.value?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
// 保存别名
|
||||
const saveAlias = async (item: HostQueryResponse) => {
|
||||
item.loading = true;
|
||||
item.modCount = (item.modCount || 0) + 1;
|
||||
if (item.modCount != 1) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 修改别名
|
||||
await updateHostAlias({
|
||||
id: item.id,
|
||||
name: item.alias || ''
|
||||
});
|
||||
item.editable = false;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
item.loading = false;
|
||||
item.modCount = 0;
|
||||
}
|
||||
};
|
||||
|
||||
// 打开配置
|
||||
const openSetting = inject(openSshModalKey) as (record: HostQueryResponse) => void;
|
||||
|
||||
// 设置收藏
|
||||
const setFavorite = async (item: HostQueryResponse) => {
|
||||
if (favoriteLoading.value) {
|
||||
return;
|
||||
}
|
||||
await toggleFavorite(item, item.id);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@host-item-height: 56px;
|
||||
|
||||
:deep(.arco-list-bordered) {
|
||||
border: 1px solid var(--color-fill-3);
|
||||
|
||||
.arco-empty {
|
||||
padding: 16px 0;
|
||||
flex-direction: column;
|
||||
|
||||
.arco-empty-image {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.arco-list-item:not(:last-child) {
|
||||
border-bottom: 1px solid var(--color-fill-3);
|
||||
}
|
||||
|
||||
.arco-list-item:hover {
|
||||
background-color: var(--color-fill-2);
|
||||
}
|
||||
}
|
||||
|
||||
.host-item-wrapper {
|
||||
padding: 0 !important;
|
||||
height: @host-item-height;
|
||||
font-size: 12px;
|
||||
color: var(--color-content-text-2);
|
||||
|
||||
.host-item {
|
||||
width: 100%;
|
||||
height: @host-item-height;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
&-text {
|
||||
display: inline-block;
|
||||
white-space: pre;
|
||||
word-break: break-all;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.host-item-left-name-edit {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.host-item-right-tags {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.host-item-right-actions {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.host-item-left {
|
||||
width: 35%;
|
||||
height: 100%;
|
||||
padding-left: 18px;
|
||||
position: absolute;
|
||||
|
||||
&-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 32px;
|
||||
margin-right: 10px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--color-text-3);
|
||||
background: var(--color-fill-3);
|
||||
}
|
||||
|
||||
&-name {
|
||||
// 100% - icon-width - icon-margin-right
|
||||
width: calc(100% - 42px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&-text {
|
||||
// 100% - edit-margin-left - edit-font-size
|
||||
max-width: calc(100% - 18px);
|
||||
}
|
||||
|
||||
&-edit {
|
||||
display: none;
|
||||
margin-left: 4px;
|
||||
cursor: pointer;
|
||||
color: rgb(var(--blue-6));
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&-input {
|
||||
width: 80%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.host-item-center {
|
||||
width: 25%;
|
||||
height: 100%;
|
||||
left: 35%;
|
||||
padding: 0 8px;
|
||||
position: absolute;
|
||||
|
||||
&-address {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.host-item-right {
|
||||
width: 40%;
|
||||
height: 100%;
|
||||
left: 60%;
|
||||
padding-right: 18px;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
|
||||
&-tags {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&-actions {
|
||||
display: none;
|
||||
width: 100%;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.favorite {
|
||||
color: rgb(var(--yellow-6));
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div>
|
||||
<!-- 分组视图列表 -->
|
||||
<host-group-view v-if="NewConnectionType.GROUP === newConnectionType"
|
||||
v-model="selectedGroup"
|
||||
:group-tree="hosts.groupTree"
|
||||
:tree-nodes="treeNodes"
|
||||
:host-list="hostList"
|
||||
:filter-value="filterValue" />
|
||||
<!-- 列表视图 -->
|
||||
<host-list-view v-if="NewConnectionType.LIST === newConnectionType"
|
||||
:hostList="hostList"
|
||||
empty-value="无授权主机!" />
|
||||
<!-- 我的收藏 -->
|
||||
<host-list-view v-if="NewConnectionType.FAVORITE === newConnectionType"
|
||||
class="list-view-container"
|
||||
:hostList="hostList"
|
||||
empty-value="无收藏记录, 快去点击主机右侧的⭐进行收藏吧!" />
|
||||
<!-- 最近连接 -->
|
||||
<host-list-view v-if="NewConnectionType.LATEST === newConnectionType"
|
||||
class="list-view-container"
|
||||
:hostList="hostList"
|
||||
empty-value="暂无连接记录, 快去体验吧!" />
|
||||
<!-- 修改主机设置模态框 -->
|
||||
<ssh-extra-modal ref="sshModal" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'hostsView'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, provide, ref, watch } from 'vue';
|
||||
import { NewConnectionType, openSshModalKey } from '../../types/const';
|
||||
import { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data';
|
||||
import { HostQueryResponse } from '@/api/asset/host';
|
||||
import HostGroupView from './host-group-view.vue';
|
||||
import HostListView from './host-list-view.vue';
|
||||
import SshExtraModal from '../setting/ssh-extra-modal.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
hosts: AuthorizedHostQueryResponse,
|
||||
filterValue: string,
|
||||
newConnectionType: string
|
||||
}>();
|
||||
|
||||
const hostList = ref<Array<HostQueryResponse>>([]);
|
||||
const treeNodes = ref<Record<string, Array<number>>>({});
|
||||
const selectedGroup = ref(
|
||||
props.hosts?.groupTree?.length
|
||||
? props.hosts.groupTree[0].key
|
||||
: 0
|
||||
);
|
||||
const sshModal = ref();
|
||||
|
||||
// 暴露打开 ssh 配置模态框
|
||||
provide(openSshModalKey, (record: any) => {
|
||||
sshModal.value?.open(record);
|
||||
});
|
||||
|
||||
// 主机数据处理
|
||||
const shuffleHosts = () => {
|
||||
let list = [...props.hosts?.hostList];
|
||||
// 过滤
|
||||
const filterVal = props.filterValue.toLowerCase();
|
||||
if (filterVal) {
|
||||
list = filterVal.startsWith('@')
|
||||
// tag 过滤
|
||||
? list.filter(item => item.tags.some(tag => (tag.name as string).toLowerCase().startsWith(filterVal.substring(1, filterVal.length))))
|
||||
// 名称/编码/地址 过滤
|
||||
: list.filter(item => {
|
||||
return (item.name as string)?.toLowerCase().indexOf(filterVal) > -1
|
||||
|| (item.code as string)?.toLowerCase().indexOf(filterVal) > -1
|
||||
|| (item.alias as string)?.toLowerCase().indexOf(filterVal) > -1
|
||||
|| (item.address as string)?.toLowerCase().indexOf(filterVal) > -1;
|
||||
});
|
||||
}
|
||||
// 判断类型
|
||||
if (NewConnectionType.GROUP === props.newConnectionType) {
|
||||
// 过滤-分组
|
||||
const groupNodes = { ...props.hosts.treeNodes };
|
||||
Object.keys(groupNodes).forEach(k => {
|
||||
groupNodes[k] = (groupNodes[k] || []).filter(item => list.some(host => host.id === item));
|
||||
});
|
||||
treeNodes.value = groupNodes;
|
||||
// 当前组内数据
|
||||
list = list.filter(item => groupNodes[selectedGroup.value]?.some(id => id === item.id));
|
||||
} else if (NewConnectionType.FAVORITE === props.newConnectionType) {
|
||||
// 过滤-个人收藏
|
||||
list = list.filter(item => item.favorite);
|
||||
} else if (NewConnectionType.LATEST === props.newConnectionType) {
|
||||
// 过滤-最近连接
|
||||
list = props.hosts.latestHosts
|
||||
.map(s => list.find(item => item.id === s) as HostQueryResponse)
|
||||
.filter(Boolean);
|
||||
}
|
||||
// 非最近连接排序
|
||||
if (NewConnectionType.LATEST !== props.newConnectionType) {
|
||||
hostList.value = list.sort((o1, o2) => {
|
||||
if (o1.favorite || o2.favorite) {
|
||||
if (o1.favorite && o2.favorite) {
|
||||
return o2.id < o1.id ? 1 : -1;
|
||||
}
|
||||
return o2.favorite ? 1 : -1;
|
||||
} else {
|
||||
return o2.id < o1.id ? 1 : -1;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 最近连接不排序
|
||||
hostList.value = list;
|
||||
}
|
||||
};
|
||||
|
||||
// 监听搜索值变化
|
||||
watch(() => props.filterValue, shuffleHosts);
|
||||
|
||||
// 监听类型变化
|
||||
watch(() => props.newConnectionType, shuffleHosts);
|
||||
|
||||
// 监听分组变化
|
||||
watch(selectedGroup, shuffleHosts);
|
||||
|
||||
// 初始化 加载主机
|
||||
onMounted(shuffleHosts);
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.list-view-container {
|
||||
max-height: 100%;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<div class="host-space-setting-container">
|
||||
<div class="host-space-setting-wrapper">
|
||||
<!-- 主标题 -->
|
||||
<h2 class="host-space-setting-title">新建连接</h2>
|
||||
<!-- 操作栏 -->
|
||||
<div class="host-space-setting-block header-actions">
|
||||
<!-- 视图类型 -->
|
||||
<a-radio-group v-model="newConnectionType"
|
||||
type="button"
|
||||
class="usn"
|
||||
:options="toRadioOptions(newConnectionTypeKey)"
|
||||
@change="s => updateTerminalPreference(TerminalPreferenceItem.NEW_CONNECTION_TYPE, s as string, true)" />
|
||||
<!-- 过滤 -->
|
||||
<a-auto-complete v-model="filterValue"
|
||||
class="host-filter"
|
||||
placeholder="别名/名称/编码/IP @标签"
|
||||
:allow-clear="true"
|
||||
:data="filterOptions"
|
||||
:filter-option="searchFilter">
|
||||
<template #option="{ data: { raw: { label, isTag} } }">
|
||||
<!-- tag -->
|
||||
<a-tag v-if="isTag" :color="dataColor(label, tagColor)">
|
||||
{{ label }}
|
||||
</a-tag>
|
||||
<!-- 文本 -->
|
||||
<template v-else>
|
||||
{{ label }}
|
||||
</template>
|
||||
</template>
|
||||
</a-auto-complete>
|
||||
</div>
|
||||
<!-- 授权主机 -->
|
||||
<div class="host-space-setting-block" style="margin: 0;">
|
||||
<!-- 顶部 -->
|
||||
<div class="host-space-setting-subtitle-wrapper">
|
||||
<h3 class="host-space-setting-subtitle">
|
||||
授权主机
|
||||
</h3>
|
||||
</div>
|
||||
<!-- 内容区域 -->
|
||||
<div class="host-space-setting-body body-container">
|
||||
<!-- 无数据 -->
|
||||
<a-empty v-if="!hosts.hostList?.length">
|
||||
<template #image>
|
||||
<icon-desktop />
|
||||
</template>
|
||||
Oops! 无授权主机 请联系管理员授权后重试!
|
||||
</a-empty>
|
||||
<!-- 主机列表 -->
|
||||
<hosts-view v-else
|
||||
class="host-view-container"
|
||||
:hosts="hosts"
|
||||
:filter-value="filterValue"
|
||||
:new-connection-type="newConnectionType" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'newConnectionView'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { SelectOptionData } from '@arco-design/web-vue';
|
||||
import { onBeforeMount, ref } from 'vue';
|
||||
import { NewConnectionType, newConnectionTypeKey } from '../../types/const';
|
||||
import { useDictStore, useHostSpaceStore } from '@/store';
|
||||
import { TerminalPreferenceItem } from '@/store/modules/host-space';
|
||||
import { dataColor } from '@/utils';
|
||||
import { tagColor } from '@/views/asset/host-list/types/const';
|
||||
import HostsView from './hosts-view.vue';
|
||||
|
||||
const { toRadioOptions } = useDictStore();
|
||||
const { preference, updateTerminalPreference, hosts } = useHostSpaceStore();
|
||||
|
||||
const newConnectionType = ref(preference.newConnectionType || NewConnectionType.GROUP);
|
||||
const filterValue = ref('');
|
||||
const filterOptions = ref<Array<SelectOptionData>>([]);
|
||||
|
||||
// 过滤输入
|
||||
const searchFilter = (searchValue: string, option: SelectOptionData) => {
|
||||
if (searchValue.startsWith('@')) {
|
||||
// tag 过滤
|
||||
return option.isTag && (option.label as string).toLowerCase().startsWith(searchValue.substring(1, searchValue.length).toLowerCase());
|
||||
} else {
|
||||
// 文本过滤
|
||||
return !option.isTag && (option.label as string).toLowerCase().indexOf(searchValue.toLowerCase()) > -1;
|
||||
}
|
||||
};
|
||||
|
||||
// 初始化过滤器项
|
||||
const initFilterOptions = () => {
|
||||
// 添加 tags
|
||||
const tagNames = hosts.hostList?.map(s => s.tags)
|
||||
.filter(s => s?.length)
|
||||
.flat(1)
|
||||
.sort((o1, o2) => o1.id - o2.id)
|
||||
.map(s => s.name);
|
||||
[...new Set(tagNames)].map(value => {
|
||||
return { label: value, value: `@${value}`, isTag: true };
|
||||
}).forEach(s => filterOptions.value.push(s));
|
||||
// 添加主机信息
|
||||
const hostMeta = hosts.hostList?.map(s => {
|
||||
return [s.name, s.code, s.address, s.alias];
|
||||
}).filter(Boolean).flat(1);
|
||||
[...new Set(hostMeta)].map(value => {
|
||||
return { label: value, value };
|
||||
}).forEach(s => filterOptions.value.push(s));
|
||||
};
|
||||
|
||||
// 初始化过滤器项
|
||||
onBeforeMount(initFilterOptions);
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
:deep(.host-filter) {
|
||||
width: 36%;
|
||||
}
|
||||
}
|
||||
|
||||
.body-container {
|
||||
justify-content: space-between;
|
||||
|
||||
.host-view-container {
|
||||
width: 100%;
|
||||
height: calc(100vh - 240px);
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<a-col :span="12">
|
||||
<div class="block-form-item-wrapper">
|
||||
<div class="block-form-item-header">
|
||||
<!-- label -->
|
||||
<div class="block-form-item-label">
|
||||
{{ label }}
|
||||
</div>
|
||||
<!-- item -->
|
||||
<div class="block-form-item-value">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 描述 -->
|
||||
<div class="block-form-item-desc">
|
||||
{{ desc }}
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'blockSettingItem'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
defineProps<{
|
||||
label: string,
|
||||
desc: string,
|
||||
}>();
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.block-form-item-wrapper {
|
||||
height: 100%;
|
||||
min-height: 64px;
|
||||
border-radius: 4px;
|
||||
background: var(--color-fill-2);
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.block-form-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.block-form-item-label {
|
||||
color: var(--color-content-text-3);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.block-form-item-desc {
|
||||
color: var(--color-text-2);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.arco-input-wrapper) {
|
||||
background-color: var(--color-fill-3)
|
||||
}
|
||||
|
||||
:deep(.arco-select) {
|
||||
background-color: var(--color-fill-3)
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,146 @@
|
||||
<template>
|
||||
<a-modal v-model:visible="visible"
|
||||
body-class="modal-form"
|
||||
title-align="start"
|
||||
:title="title"
|
||||
:top="80"
|
||||
:align-center="false"
|
||||
:draggable="true"
|
||||
:mask-closable="false"
|
||||
:unmount-on-close="true"
|
||||
ok-text="保存"
|
||||
:ok-button-props="{ disabled: loading }"
|
||||
:cancel-button-props="{ disabled: loading }"
|
||||
:on-before-ok="handlerOk"
|
||||
@close="handleClose">
|
||||
<a-spin class="full" :loading="loading">
|
||||
<a-form :model="formModel"
|
||||
ref="formRef"
|
||||
label-align="right"
|
||||
:style="{ width: '460px' }"
|
||||
:label-col-props="{ span: 6 }"
|
||||
:wrapper-col-props="{ span: 18 }"
|
||||
:rules="{}">
|
||||
<!-- 验证方式 -->
|
||||
<a-form-item field="authType" label="验证方式">
|
||||
<a-radio-group type="button"
|
||||
v-model="formModel.authType"
|
||||
:options="toRadioOptions(extraSshAuthTypeKey)" />
|
||||
</a-form-item>
|
||||
<!-- 用户名 -->
|
||||
<a-form-item v-if="formModel.authType === ExtraSshAuthType.CUSTOM_KEY"
|
||||
field="username"
|
||||
label="用户名">
|
||||
<a-input v-model="formModel.username" placeholder="请输入用户名" />
|
||||
</a-form-item>
|
||||
<!-- 主机秘钥 -->
|
||||
<a-form-item v-if="formModel.authType === ExtraSshAuthType.CUSTOM_KEY"
|
||||
field="keyId"
|
||||
label="主机秘钥"
|
||||
:rules="{ required: true, message: '请选择主机秘钥' }">
|
||||
<host-key-selector v-model="formModel.keyId"
|
||||
:authorized="true" />
|
||||
</a-form-item>
|
||||
<!-- 主机身份 -->
|
||||
<a-form-item v-if="formModel.authType === ExtraSshAuthType.CUSTOM_IDENTITY"
|
||||
field="identityId"
|
||||
label="主机身份"
|
||||
:rules="{ required: true, message: '请选择主机身份' }">
|
||||
<host-identity-selector v-model="formModel.identityId"
|
||||
:authorized="true" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-spin>
|
||||
</a-modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'sshExtraModal'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { HostQueryResponse } from '@/api/asset/host';
|
||||
import type { SshExtraModel } from '../../types/type';
|
||||
import { ref } from 'vue';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import useVisible from '@/hooks/visible';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { getHostExtraItem, updateHostExtra } from '@/api/asset/host-extra';
|
||||
import { ExtraSshAuthType, extraSshAuthTypeKey } from '../../types/const';
|
||||
import { useDictStore } from '@/store';
|
||||
import HostIdentitySelector from '@/components/asset/host-identity/host-identity-selector.vue';
|
||||
import HostKeySelector from '@/components/asset/host-key/host-key-selector.vue';
|
||||
|
||||
const { toRadioOptions } = useDictStore();
|
||||
const { visible, setVisible } = useVisible();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const title = ref<string>();
|
||||
const hostId = ref<number>();
|
||||
const formRef = ref();
|
||||
const formModel = ref<SshExtraModel>({});
|
||||
|
||||
// 打开配置
|
||||
const open = (record: HostQueryResponse) => {
|
||||
hostId.value = record.id;
|
||||
title.value = record.alias || `${record.name} (${record.code})`;
|
||||
renderForm();
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
defineExpose({ open });
|
||||
|
||||
// 渲染表单
|
||||
const renderForm = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const { data } = await getHostExtraItem<SshExtraModel>({ hostId: hostId.value, item: 'ssh' });
|
||||
formModel.value = data;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 确定
|
||||
const handlerOk = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 验证参数
|
||||
const error = await formRef.value.validate();
|
||||
if (error) {
|
||||
return false;
|
||||
}
|
||||
// 修改
|
||||
await updateHostExtra({
|
||||
hostId: hostId.value,
|
||||
item: 'ssh',
|
||||
extra: JSON.stringify(formModel.value)
|
||||
});
|
||||
Message.success('保存成功');
|
||||
// 清空
|
||||
handlerClear();
|
||||
} catch (e) {
|
||||
return false;
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 关闭
|
||||
const handleClose = () => {
|
||||
handlerClear();
|
||||
};
|
||||
|
||||
// 清空
|
||||
const handlerClear = () => {
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div class="host-space-setting-block">
|
||||
<!-- 顶部 -->
|
||||
<div class="host-space-setting-subtitle-wrapper">
|
||||
<h3 class="host-space-setting-subtitle">
|
||||
操作栏设置
|
||||
</h3>
|
||||
</div>
|
||||
<!-- 提示 -->
|
||||
<a-alert class="mb16">修改后会立刻保存, 立即生效 (无需刷新页面)</a-alert>
|
||||
<!-- 内容区域 -->
|
||||
<div class="host-space-setting-body block-body setting-body">
|
||||
<a-form class="host-space-setting-form"
|
||||
:model="formModel"
|
||||
layout="vertical">
|
||||
<a-space>
|
||||
<!-- 顶部操作按钮 -->
|
||||
<a-form-item field="actions" label="顶部操作按钮">
|
||||
<icon-actions class="form-item-actions"
|
||||
:actions="actions"
|
||||
position="bottom" />
|
||||
</a-form-item>
|
||||
<!-- 命令输入框 -->
|
||||
<a-form-item field="commandInput" label="命令输入框">
|
||||
<a-switch v-model="formModel.commandInput"
|
||||
class="form-item-command-input"
|
||||
:default-checked="true"
|
||||
type="round" />
|
||||
</a-form-item>
|
||||
<!-- 终端连接状态 -->
|
||||
<a-form-item field="showStatus" label="终端连接状态">
|
||||
<a-switch v-model="formModel.connectStatus"
|
||||
:default-checked="true"
|
||||
type="round" />
|
||||
</a-form-item>
|
||||
</a-space>
|
||||
</a-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TerminalActionBarBlock'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TerminalActionBarSetting } from '@/store/modules/host-space/types';
|
||||
import type { SidebarAction } from '../../types/type';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useHostSpaceStore } from '@/store';
|
||||
import { TerminalPreferenceItem } from '@/store/modules/host-space';
|
||||
import { ActionBarItems } from '../../types/const';
|
||||
import IconActions from '../layout/icon-actions.vue';
|
||||
|
||||
const { preference, updateTerminalPreference } = useHostSpaceStore();
|
||||
|
||||
const formModel = ref<TerminalActionBarSetting>({ ...preference.actionBarSetting });
|
||||
|
||||
// 监听同步
|
||||
watch(formModel, (v) => {
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
// 同步
|
||||
updateTerminalPreference(TerminalPreferenceItem.ACTION_BAR_SETTING, formModel.value, true);
|
||||
}, { deep: true });
|
||||
|
||||
// 右侧操作
|
||||
const actions = computed<Array<SidebarAction>>(() => {
|
||||
return ActionBarItems.map(s => {
|
||||
return {
|
||||
icon: s.icon,
|
||||
content: (formModel.value[s.item] === false ? '显示 ' : '隐藏 ') + s.content,
|
||||
checked: formModel.value[s.item] !== false,
|
||||
click: () => {
|
||||
formModel.value[s.item] = formModel.value[s.item] === false;
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.form-item-actions {
|
||||
display: flex;
|
||||
background-color: var(--color-fill-2);
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
|
||||
:deep(.host-space-sidebar-icon-wrapper) {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
:deep(.host-space-sidebar-icon) {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.form-item-actions, .form-item-command-input {
|
||||
margin-right: 48px;
|
||||
}
|
||||
|
||||
:deep(.arco-form) {
|
||||
.arco-form-item-label {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.arco-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,195 @@
|
||||
<template>
|
||||
<div class="host-space-setting-block">
|
||||
<!-- 顶部 -->
|
||||
<div class="host-space-setting-subtitle-wrapper">
|
||||
<h3 class="host-space-setting-subtitle">
|
||||
显示偏好
|
||||
</h3>
|
||||
</div>
|
||||
<!-- 提示 -->
|
||||
<a-alert class="mb16">修改后会立刻保存, 重新打开终端后生效 (无需刷新页面)</a-alert>
|
||||
<!-- 内容区域 -->
|
||||
<div class="host-space-setting-body block-body setting-body">
|
||||
<a-form class="host-space-setting-form"
|
||||
:model="formModel"
|
||||
layout="vertical">
|
||||
<a-space>
|
||||
<!-- 字体样式 -->
|
||||
<a-form-item field="fontFamily" label="字体样式">
|
||||
<a-select v-model="formModel.fontFamily"
|
||||
class="form-item-font-family"
|
||||
placeholder="请选择字体样式"
|
||||
:options="toOptions(fontFamilyKey)"
|
||||
:allow-create="true"
|
||||
:filter-option="labelFilter">
|
||||
<template #option="{ data }">
|
||||
<span :style="{ fontFamily: data.value }">{{ data.label }}</span>
|
||||
</template>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<!-- 字体大小 -->
|
||||
<a-form-item field="fontSize" label="字体大小">
|
||||
<a-select v-model="formModel.fontSize"
|
||||
class="form-item-font-size"
|
||||
placeholder="请选择字体大小"
|
||||
:options="toOptions(fontSizeKey)" />
|
||||
</a-form-item>
|
||||
<!-- 行高 -->
|
||||
<a-form-item field="lineHeight" label="行高">
|
||||
<a-input-number v-model="formModel.lineHeight"
|
||||
class="form-item-line-height"
|
||||
placeholder="请输入行高"
|
||||
:precision="2"
|
||||
:min="1"
|
||||
:max="2"
|
||||
hide-button />
|
||||
</a-form-item>
|
||||
</a-space>
|
||||
<a-space>
|
||||
<!-- 普通文本字重 -->
|
||||
<a-form-item field="fontWeight" label="普通文本字重">
|
||||
<a-select v-model="formModel.fontWeight"
|
||||
class="form-item-font-weight"
|
||||
placeholder="请选择字重"
|
||||
:options="toOptions(fontWeightKey)" />
|
||||
</a-form-item>
|
||||
<!-- 加粗文本字重 -->
|
||||
<a-form-item field="fontWeightBold" label="加粗文本字重">
|
||||
<a-select v-model="formModel.fontWeightBold"
|
||||
class="form-item-font-bold-weight"
|
||||
placeholder="请选择字重"
|
||||
:options="toOptions(fontWeightKey)" />
|
||||
</a-form-item>
|
||||
</a-space>
|
||||
<a-space>
|
||||
<!-- 光标样式 -->
|
||||
<a-form-item field="cursorStyle" label="光标样式">
|
||||
<a-radio-group type="button"
|
||||
v-model="formModel.cursorStyle"
|
||||
class="form-item-cursor-style usn"
|
||||
:options="toRadioOptions(cursorStyleKey)" />
|
||||
</a-form-item>
|
||||
<!-- 光标闪烁 -->
|
||||
<a-form-item field="cursorBlink" label="光标是否闪烁">
|
||||
<a-switch v-model="formModel.cursorBlink"
|
||||
type="round"
|
||||
class="form-item-cursor-blink" />
|
||||
</a-form-item>
|
||||
</a-space>
|
||||
</a-form>
|
||||
<!-- 预览区域 -->
|
||||
<div class="terminal-example">
|
||||
<span class="vertical-form-label">预览效果</span>
|
||||
<div class="terminal-example-wrapper"
|
||||
:style="{ background: preference.theme.schema.background }">
|
||||
<terminal-example :schema="preference.theme.schema"
|
||||
ref="previewTerminal" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TerminalDisplayBlock'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TerminalDisplaySetting } from '@/store/modules/host-space/types';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useDictStore, useHostSpaceStore } from '@/store';
|
||||
import { fontFamilyKey, fontSizeKey, fontWeightKey, fontFamilySuffix, cursorStyleKey } from '../../types/const';
|
||||
import { labelFilter } from '@/types/form';
|
||||
import { TerminalPreferenceItem } from '@/store/modules/host-space';
|
||||
import TerminalExample from '../setting/terminal-example.vue';
|
||||
|
||||
const { toOptions, toRadioOptions } = useDictStore();
|
||||
const { preference, updateTerminalPreference } = useHostSpaceStore();
|
||||
|
||||
const previewTerminal = ref();
|
||||
const formModel = ref<TerminalDisplaySetting>({ ...preference.displaySetting });
|
||||
|
||||
// 监听内容变化
|
||||
watch(formModel, (v) => {
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
const options = previewTerminal.value?.term?.options;
|
||||
if (!options) {
|
||||
return;
|
||||
}
|
||||
// 修改预览终端配置
|
||||
Object.keys(v).forEach(key => {
|
||||
if (key === 'fontFamily') {
|
||||
options[key] = (formModel.value as any)[key] + fontFamilySuffix;
|
||||
} else {
|
||||
options[key] = (formModel.value as any)[key];
|
||||
}
|
||||
});
|
||||
// 同步
|
||||
updateTerminalPreference(TerminalPreferenceItem.DISPLAY_SETTING, formModel.value, true);
|
||||
// 聚焦
|
||||
previewTerminal.value.term.focus();
|
||||
}, { deep: true });
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@terminal-width: 458px;
|
||||
|
||||
.setting-body {
|
||||
height: 248px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
:deep(.arco-form) {
|
||||
.arco-form-item-label {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.arco-form-item {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.form-item-font-family {
|
||||
width: 158px;
|
||||
}
|
||||
|
||||
.form-item-font-size {
|
||||
width: 148px;
|
||||
}
|
||||
|
||||
.form-item-line-height {
|
||||
width: 114px;
|
||||
}
|
||||
|
||||
.form-item-font-weight, .form-item-font-bold-weight {
|
||||
width: 178px;
|
||||
}
|
||||
|
||||
.form-item-font-weight {
|
||||
margin-right: 70px;
|
||||
}
|
||||
|
||||
.form-item-cursor-style {
|
||||
margin-right: 90px;
|
||||
|
||||
.arco-radio-button-content {
|
||||
padding: 0 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.terminal-example {
|
||||
height: 100%;
|
||||
|
||||
&-wrapper {
|
||||
border-radius: 4px;
|
||||
width: calc(@terminal-width - 16px);
|
||||
height: calc(100% - 16px - 12px);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="host-space-setting-container">
|
||||
<div class="host-space-setting-wrapper">
|
||||
<!-- 主标题 -->
|
||||
<h2 class="host-space-setting-title">显示设置</h2>
|
||||
<!-- 显示偏好 -->
|
||||
<terminal-display-block />
|
||||
<!-- 顶部工具栏 -->
|
||||
<terminal-action-bar-block />
|
||||
<!-- 右键菜单 -->
|
||||
<terminal-right-menu-block />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TerminalDisplaySetting'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import TerminalDisplayBlock from './terminal-display-block.vue';
|
||||
import TerminalActionBarBlock from './terminal-action-bar-block.vue';
|
||||
import TerminalRightMenuBlock from './terminal-right-menu-block.vue';
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div class="terminal-example" ref="terminal"></div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'terminalExample'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TerminalThemeSchema } from '@/api/asset/host-terminal';
|
||||
import { Terminal } from 'xterm';
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
schema: TerminalThemeSchema | Record<string, any>
|
||||
}>();
|
||||
|
||||
const terminal = ref();
|
||||
const term = ref();
|
||||
|
||||
onMounted(() => {
|
||||
term.value = new Terminal({
|
||||
theme: { ...props.schema, cursor: props.schema.background },
|
||||
cols: 42,
|
||||
rows: 6,
|
||||
fontSize: 15,
|
||||
cursorInactiveStyle: 'none',
|
||||
});
|
||||
term.value.open(terminal.value);
|
||||
term.value.write(
|
||||
'[1;94m[root[0m@[1;96mOrionServer usr]#[0m\r\n' +
|
||||
'dr-xr-xr-x. 2 root root [0m[01;34mbin[0m\r\n' +
|
||||
'dr-xr-xr-x. 2 root root [01;34msbin[0m\r\n' +
|
||||
'drwxr-xr-x. 4 root root [01;34msrc[0m\r\n' +
|
||||
'lrwxrwxrwx. 1 root root [01;36mtmp[0m -> [30;42m../var/tmp[0m '
|
||||
);
|
||||
});
|
||||
|
||||
defineExpose({ term });
|
||||
|
||||
onUnmounted(() => {
|
||||
term.value?.dispose();
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.terminal-example {
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.xterm-viewport) {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<div class="host-space-setting-container">
|
||||
<div class="host-space-setting-wrapper">
|
||||
<!-- 主标题 -->
|
||||
<h2 class="host-space-setting-title">终端设置</h2>
|
||||
<!-- 交互设置 -->
|
||||
<terminal-interact-block />
|
||||
<!-- 插件设置 -->
|
||||
<terminal-plugins-block />
|
||||
<!-- 会话设置 -->
|
||||
<terminal-session-block />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TerminalGeneralSetting'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import TerminalInteractBlock from './terminal-interact-block.vue';
|
||||
import TerminalPluginsBlock from './terminal-plugins-block.vue';
|
||||
import TerminalSessionBlock from './terminal-session-block.vue';
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,112 @@
|
||||
<template>
|
||||
<div class="host-space-setting-block">
|
||||
<!-- 顶部 -->
|
||||
<div class="host-space-setting-subtitle-wrapper">
|
||||
<h3 class="host-space-setting-subtitle">
|
||||
交互设置
|
||||
</h3>
|
||||
</div>
|
||||
<!-- 提示 -->
|
||||
<a-alert class="mb16">修改后会立刻保存, 刷新页面后生效</a-alert>
|
||||
<!-- 内容区域 -->
|
||||
<div class="host-space-setting-body setting-body">
|
||||
<a-row class="mb16" align="stretch" :gutter="16">
|
||||
<!-- 快速滚动 -->
|
||||
<block-setting-item label="快速滚动" desc="alt + 鼠标滚轮快速滚动">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.fastScrollModifier" />
|
||||
</block-setting-item>
|
||||
<!-- 点击移动光标 -->
|
||||
<block-setting-item label="点击移动光标" desc="alt + 鼠标左键可以切换光标位置">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.altClickMovesCursor" />
|
||||
</block-setting-item>
|
||||
</a-row>
|
||||
<a-row class="mb16" align="stretch" :gutter="16">
|
||||
<!-- 右键选中词条 -->
|
||||
<block-setting-item label="右键选中词条" desc="右键文本后会根据单词分隔符自动选中词条">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.rightClickSelectsWord" />
|
||||
</block-setting-item>
|
||||
<!-- 选中自动复制 -->
|
||||
<block-setting-item label="选中自动复制" desc="自动将选中的文本复制到剪切板">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.selectionChangeCopy" />
|
||||
</block-setting-item>
|
||||
</a-row>
|
||||
<a-row class="mb16" align="stretch" :gutter="16">
|
||||
<!-- 复制去除空格 -->
|
||||
<block-setting-item label="复制去除空格" desc="复制文本后自动删除尾部空格">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.copyAutoTrim" />
|
||||
</block-setting-item>
|
||||
<!-- 粘贴去除空格 -->
|
||||
<block-setting-item label="粘贴去除空格" desc="粘贴文本前自动删除尾部空格 如: 命令输入框, 命令编辑器, 右键粘贴, 粘贴按钮, 右键菜单粘贴, 自定义粘贴快捷键">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.pasteAutoTrim" />
|
||||
</block-setting-item>
|
||||
</a-row>
|
||||
<a-row class="mb16" align="stretch" :gutter="16">
|
||||
<!-- 右键粘贴 -->
|
||||
<block-setting-item label="右键粘贴" desc="右键自动粘贴, 启用后需要关闭右键菜单 (若开启了右键选中词条, 有选中的文本时, 右键粘贴无效)">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.rightClickPaste" />
|
||||
</block-setting-item>
|
||||
<!-- 启用右键菜单 -->
|
||||
<block-setting-item label="启用右键菜单" desc="右键终端将打开自定义菜单, 启用后需要关闭右键粘贴">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.enableRightClickMenu" />
|
||||
</block-setting-item>
|
||||
</a-row>
|
||||
<a-row class="mb16" align="stretch" :gutter="16">
|
||||
<!-- 启用响铃 -->
|
||||
<block-setting-item label="启用响铃" desc="系统接收到 \a 时发出响铃 (一般不用开启)">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.enableBell" />
|
||||
</block-setting-item>
|
||||
<!-- 单词分隔符 -->
|
||||
<block-setting-item label="单词分隔符" desc="在终端中双击文本将使用该分隔符进行分割 (一般不用修改)">
|
||||
<a-input size="small"
|
||||
v-model="formModel.wordSeparator"
|
||||
placeholder="单词分隔符"
|
||||
allow-clear />
|
||||
</block-setting-item>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TerminalInteractBlock'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TerminalInteractSetting } from '@/store/modules/host-space/types';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useHostSpaceStore } from '@/store';
|
||||
import { TerminalPreferenceItem } from '@/store/modules/host-space';
|
||||
import BlockSettingItem from './block-setting-item.vue';
|
||||
|
||||
const { preference, updateTerminalPreference } = useHostSpaceStore();
|
||||
|
||||
const formModel = ref<TerminalInteractSetting>({ ...preference.interactSetting });
|
||||
|
||||
// 监听内容变化
|
||||
watch(formModel, (v) => {
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
// 同步
|
||||
updateTerminalPreference(TerminalPreferenceItem.INTERACT_SETTING, formModel.value);
|
||||
}, { deep: true });
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.setting-body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<div class="host-space-setting-block">
|
||||
<!-- 顶部 -->
|
||||
<div class="host-space-setting-subtitle-wrapper">
|
||||
<h3 class="host-space-setting-subtitle">
|
||||
插件设置
|
||||
</h3>
|
||||
</div>
|
||||
<!-- 内容区域 -->
|
||||
<div class="host-space-setting-body setting-body">
|
||||
<a-row class="mb16" align="stretch" :gutter="16">
|
||||
<!-- 超链接插件 -->
|
||||
<block-setting-item label="超链接插件" desc="自动检测 http(https) url 并可以点击">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.enableWeblinkPlugin" />
|
||||
</block-setting-item>
|
||||
<!-- WebGL 渲染插件 -->
|
||||
<block-setting-item label="WebGL 渲染插件" desc="使用 WebGL 加速渲染终端 (建议开启, 若无法开启终端请关闭)">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.enableWebglPlugin" />
|
||||
</block-setting-item>
|
||||
</a-row>
|
||||
<a-row class="mb16" align="stretch" :gutter="16">
|
||||
<!-- 图片渲染插件 -->
|
||||
<block-setting-item label="图片渲染插件" desc="支持使用 sixel 打开图片 (一般不需要开启)">
|
||||
<a-switch type="round"
|
||||
v-model="formModel.enableImagePlugin" />
|
||||
</block-setting-item>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TerminalPluginsBlock'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TerminalPluginsSetting } from '@/store/modules/host-space/types';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useHostSpaceStore } from '@/store';
|
||||
import { TerminalPreferenceItem } from '@/store/modules/host-space';
|
||||
import BlockSettingItem from './block-setting-item.vue';
|
||||
|
||||
const { preference, updateTerminalPreference } = useHostSpaceStore();
|
||||
|
||||
const formModel = ref<TerminalPluginsSetting>({ ...preference.pluginsSetting });
|
||||
|
||||
// 监听内容变化
|
||||
watch(formModel, (v) => {
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
// 同步
|
||||
updateTerminalPreference(TerminalPreferenceItem.PLUGINS_SETTING, formModel.value);
|
||||
}, { deep: true });
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.setting-body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,234 @@
|
||||
<template>
|
||||
<div class="host-space-setting-block">
|
||||
<!-- 顶部 -->
|
||||
<div class="host-space-setting-subtitle-wrapper">
|
||||
<h3 class="host-space-setting-subtitle">
|
||||
右键菜单设置
|
||||
</h3>
|
||||
</div>
|
||||
<!-- 提示 -->
|
||||
<a-alert class="mb16">修改后会立刻保存, 重新打开终端后生效 (无需刷新页面)</a-alert>
|
||||
<!-- 内容区域 -->
|
||||
<div class="host-space-setting-body block-body setting-body">
|
||||
<!-- 功能项 -->
|
||||
<div class="actions-container">
|
||||
<div class="vertical-form-label">功能</div>
|
||||
<!-- 功能项列表 -->
|
||||
<div class="actions-wrapper">
|
||||
<a-row :gutter="[8, 8]">
|
||||
<a-col :span="12"
|
||||
class="action-item-wrapper"
|
||||
v-for="(action, index) in ActionBarItems"
|
||||
:key="index">
|
||||
<div class="action-item" @click="clickAction(action.item)">
|
||||
<!-- 图标 -->
|
||||
<div class="action-icon">
|
||||
<component :is="action.icon" />
|
||||
</div>
|
||||
<!-- 描述 -->
|
||||
<div class="action-desc">
|
||||
{{ action.content }}
|
||||
</div>
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 菜单预览容器 -->
|
||||
<div class="preview-container">
|
||||
<div class="vertical-form-label">菜单预览</div>
|
||||
<div ref="popupContainer" />
|
||||
</div>
|
||||
<!-- 预览下拉菜单 -->
|
||||
<a-dropdown v-if="popupContainer"
|
||||
:popup-visible="true"
|
||||
:popup-container="popupContainer"
|
||||
:popup-max-height="false">
|
||||
<template #content v-if="rightActions.length">
|
||||
<a-doption v-for="(action, index) in rightActions"
|
||||
:key="index">
|
||||
<div class="preview-action">
|
||||
<!-- 图标 -->
|
||||
<div class="preview-icon">
|
||||
<component :is="action.icon" />
|
||||
</div>
|
||||
<!-- 文本 -->
|
||||
<div>{{ action.content }}</div>
|
||||
</div>
|
||||
<!-- 关闭按钮 -->
|
||||
<div class="close-icon" @click="clickAction(action.item)">
|
||||
<icon-close />
|
||||
</div>
|
||||
</a-doption>
|
||||
</template>
|
||||
<!-- 空数据 -->
|
||||
<template #content v-else>
|
||||
<a-doption>
|
||||
点击左侧功能添加
|
||||
</a-doption>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'terminalRightMenuBlock'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ContextMenuItem } from '../../types/type';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useHostSpaceStore } from '@/store';
|
||||
import { TerminalPreferenceItem } from '@/store/modules/host-space';
|
||||
import { ActionBarItems } from '../../types/const';
|
||||
|
||||
const { preference, updateTerminalPreference } = useHostSpaceStore();
|
||||
|
||||
const popupContainer = ref();
|
||||
const rightActionItems = ref<Array<string>>([...preference.rightMenuSetting]);
|
||||
|
||||
// // 监听同步
|
||||
watch(rightActionItems, (v) => {
|
||||
// 同步
|
||||
updateTerminalPreference(TerminalPreferenceItem.RIGHT_MENU_SETTING, v, true);
|
||||
}, { deep: true });
|
||||
|
||||
// 实际操作项
|
||||
const rightActions = computed<Array<ContextMenuItem>>(() => {
|
||||
return rightActionItems.value
|
||||
.map(s => ActionBarItems.find(i => i.item === s) as ContextMenuItem)
|
||||
.filter(Boolean);
|
||||
});
|
||||
|
||||
// 添加操作项
|
||||
const clickAction = (item: string) => {
|
||||
if (rightActionItems.value.includes(item)) {
|
||||
// 移除
|
||||
rightActionItems.value.splice(rightActionItems.value.indexOf(item), 1);
|
||||
} else {
|
||||
// 添加
|
||||
rightActionItems.value.push(item);
|
||||
}
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@container-width: 418px;
|
||||
@wrapper-margin-r: 32px;
|
||||
@transform-x: 8px;
|
||||
@item-width: (@container-width - @wrapper-margin-r) / 2;
|
||||
|
||||
.setting-body {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.actions-container {
|
||||
width: @container-width;
|
||||
height: auto;
|
||||
|
||||
.actions-wrapper {
|
||||
padding-right: 8px;
|
||||
margin-right: @wrapper-margin-r;
|
||||
}
|
||||
|
||||
.action-item-wrapper {
|
||||
transition: all 0.2s;
|
||||
border-radius: 4px;
|
||||
width: calc((@item-width) - @transform-x);
|
||||
|
||||
&:hover {
|
||||
width: calc(@item-width);
|
||||
padding: 4px 0 !important;
|
||||
|
||||
.action-item {
|
||||
background: var(--color-fill-3);
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
background: var(--color-fill-4);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
padding: 6px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
width: inherit;
|
||||
background: var(--color-fill-2);
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
border-radius: 4px;
|
||||
margin-right: 8px;
|
||||
background-color: var(--color-fill-3);
|
||||
}
|
||||
|
||||
.action-desc {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
user-select: none;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-container {
|
||||
width: 242px;
|
||||
height: auto;
|
||||
|
||||
:deep(.arco-dropdown-option-content) {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
&:hover {
|
||||
.close-icon {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.preview-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.preview-icon {
|
||||
font-size: 18px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
display: none;
|
||||
padding: 3px;
|
||||
border-radius: 12px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: .2s;
|
||||
font-size: 15px;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-fill-3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-trigger-popup) {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="host-space-setting-block">
|
||||
<!-- 顶部 -->
|
||||
<div class="host-space-setting-subtitle-wrapper">
|
||||
<h3 class="host-space-setting-subtitle">
|
||||
会话设置
|
||||
</h3>
|
||||
</div>
|
||||
<!-- 内容区域 -->
|
||||
<div class="host-space-setting-body setting-body">
|
||||
<a-row class="mb16" align="stretch" :gutter="16">
|
||||
<!-- 终端类型 -->
|
||||
<block-setting-item label="终端类型" desc="若显示异常请尝试切换此选项 兼容性 vt100 > xterm > 16color > 256color">
|
||||
<a-select style="width: 160px;"
|
||||
v-model="formModel.terminalEmulationType"
|
||||
size="small"
|
||||
:options="toOptions(terminalEmulationTypeKey)" />
|
||||
</block-setting-item>
|
||||
<!-- 缓冲区行数 -->
|
||||
<block-setting-item label="缓冲区行数" desc="保存在缓冲区的行数, 多出的行数会被忽略, 此值越大占用内存的内存会更多">
|
||||
<a-input-number v-model="formModel.scrollBackLine"
|
||||
size="small"
|
||||
:min="1"
|
||||
:max="10000"
|
||||
placeholder="缓冲区行数 默认 1000 行"
|
||||
allow-clear
|
||||
hide-button />
|
||||
</block-setting-item>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TerminalSessionBlock'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TerminalSessionSetting } from '@/store/modules/host-space/types';
|
||||
import { ref, watch } from 'vue';
|
||||
import { useDictStore, useHostSpaceStore } from '@/store';
|
||||
import { TerminalPreferenceItem } from '@/store/modules/host-space';
|
||||
import { terminalEmulationTypeKey } from '../../types/const';
|
||||
import BlockSettingItem from './block-setting-item.vue';
|
||||
|
||||
const { toOptions } = useDictStore();
|
||||
const { preference, updateTerminalPreference } = useHostSpaceStore();
|
||||
|
||||
const formModel = ref<TerminalSessionSetting>({ ...preference.sessionSetting });
|
||||
|
||||
// 监听内容变化
|
||||
watch(formModel, (v) => {
|
||||
if (!v) {
|
||||
return;
|
||||
}
|
||||
// 同步
|
||||
updateTerminalPreference(TerminalPreferenceItem.SESSION_SETTING, formModel.value);
|
||||
}, { deep: true });
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.setting-body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="host-space-setting-block">
|
||||
<!-- 顶部 -->
|
||||
<div class="host-space-setting-subtitle-wrapper">
|
||||
<h3 class="host-space-setting-subtitle">
|
||||
快捷键操作
|
||||
</h3>
|
||||
</div>
|
||||
<!-- 内容区域 -->
|
||||
<div class="host-space-setting-body setting-body">
|
||||
<!-- 提示 -->
|
||||
<a-alert class="mb16">点击保存按钮后需要刷新页面生效 (恢复默认配置后也需要点击保存按钮) (设置时需要避免与浏览器内置快捷键冲突)</a-alert>
|
||||
<a-space class="action-container" size="mini">
|
||||
<!-- 是否启用 -->
|
||||
<a-switch v-model="value"
|
||||
checked-text="启用"
|
||||
unchecked-text="禁用"
|
||||
type="round" />
|
||||
<a-button size="small"
|
||||
type="text"
|
||||
@click="emits('save')">
|
||||
保存
|
||||
</a-button>
|
||||
<a-button size="small"
|
||||
type="text"
|
||||
@click="emits('reset')">
|
||||
恢复默认配置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'terminalShortcutActionBlock'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
enabled: boolean
|
||||
}>();
|
||||
|
||||
const emits = defineEmits(['update:enabled', 'save', 'reset']);
|
||||
|
||||
const value = computed<boolean>({
|
||||
get() {
|
||||
return props.enabled;
|
||||
},
|
||||
set(e) {
|
||||
emits('update:enabled', e);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.setting-body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,157 @@
|
||||
<template>
|
||||
<div class="host-space-setting-block">
|
||||
<!-- 顶部 -->
|
||||
<div class="host-space-setting-subtitle-wrapper">
|
||||
<h3 class="host-space-setting-subtitle">
|
||||
{{ title }}
|
||||
</h3>
|
||||
</div>
|
||||
<!-- 内容区域 -->
|
||||
<div class="host-space-setting-body terminal-shortcut-container">
|
||||
<template v-for="item in items">
|
||||
<div class="shortcut-row" v-if="item.type === type">
|
||||
<!-- 名称 -->
|
||||
<span class="shortcut-name">{{ item.content }}</span>
|
||||
<!-- 快捷键 -->
|
||||
<div class="shortcut-key-container">
|
||||
<!-- 启用-修改中 -->
|
||||
<a-input v-if="item.editable && item.enabled"
|
||||
v-model="item.shortcutKey"
|
||||
:ref="setEditRef as unknown as VNodeRef"
|
||||
class="trigger-input"
|
||||
size="small"
|
||||
placeholder="请按下快捷键"
|
||||
readonly
|
||||
@blur="clearEditableStatus" />
|
||||
<!-- 启用-未修改 -->
|
||||
<span v-else-if="item.enabled">{{ item.shortcutKey }}</span>
|
||||
<!-- 禁用 -->
|
||||
<span v-else />
|
||||
</div>
|
||||
<!-- 操作 -->
|
||||
<a-space class="shortcut-actions-container">
|
||||
<!-- 屏蔽 -->
|
||||
<div class="click-icon-wrapper"
|
||||
v-if="item.enabled"
|
||||
title="屏蔽"
|
||||
@click="updateEnabledStatus(item, false)">
|
||||
<icon-message-banned />
|
||||
</div>
|
||||
<!-- 恢复 -->
|
||||
<div class="click-icon-wrapper"
|
||||
v-if="!item.enabled"
|
||||
title="恢复"
|
||||
@click="updateEnabledStatus(item, true)">
|
||||
<icon-message />
|
||||
</div>
|
||||
<!-- 设置 -->
|
||||
<div class="click-icon-wrapper"
|
||||
v-if="!item.editable && item.enabled"
|
||||
title="设置"
|
||||
@click="setEditableStatus(item)">
|
||||
<icon-settings />
|
||||
</div>
|
||||
</a-space>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TerminalShortcutKeysBlock'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TerminalShortcutKeyEditable } from '@/store/modules/host-space/types';
|
||||
import type { VNodeRef } from 'vue';
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
defineProps<{
|
||||
title: string;
|
||||
type: number;
|
||||
items: Array<TerminalShortcutKeyEditable>
|
||||
}>();
|
||||
|
||||
const emits = defineEmits(['setEditable', 'clearEditable', 'updateEnabled']);
|
||||
|
||||
// 设置 ref
|
||||
const setEditRef = (el: HTMLElement) => {
|
||||
// 自动聚焦
|
||||
nextTick(() => {
|
||||
el && el.focus();
|
||||
});
|
||||
};
|
||||
|
||||
// 修改启用状态
|
||||
const updateEnabledStatus = (item: TerminalShortcutKeyEditable, enabled: boolean) => {
|
||||
emits('updateEnabled', item, enabled);
|
||||
};
|
||||
|
||||
// 设置可编辑状态
|
||||
const setEditableStatus = (item: TerminalShortcutKeyEditable) => {
|
||||
emits('setEditable', item);
|
||||
};
|
||||
|
||||
// 清除可编辑状态
|
||||
const clearEditableStatus = () => {
|
||||
emits('clearEditable');
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
.terminal-shortcut-container {
|
||||
flex-direction: column;
|
||||
|
||||
.shortcut-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
background: var(--color-neutral-2);
|
||||
height: 42px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
user-select: none;
|
||||
transition: .3s;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-neutral-3);
|
||||
|
||||
.shortcut-actions-container {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-name {
|
||||
font-size: 14px;
|
||||
width: 238px;
|
||||
}
|
||||
|
||||
.shortcut-key-container {
|
||||
width: 268px;
|
||||
|
||||
.trigger-input {
|
||||
width: 188px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.shortcut-actions-container {
|
||||
display: none;
|
||||
|
||||
.click-icon-wrapper {
|
||||
font-size: 18px;
|
||||
padding: 4px;
|
||||
|
||||
&:hover {
|
||||
background: var(--color-neutral-4);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,248 @@
|
||||
<template>
|
||||
<div class="host-space-setting-container">
|
||||
<div class="host-space-setting-wrapper">
|
||||
<!-- 主标题 -->
|
||||
<h2 class="host-space-setting-title">快捷键设置</h2>
|
||||
<!-- 加载中 -->
|
||||
<a-skeleton v-if="!render"
|
||||
class="skeleton-wrapper"
|
||||
:animation="true">
|
||||
<a-skeleton-line :rows="8"
|
||||
:line-height="42"
|
||||
:line-spacing="12" />
|
||||
</a-skeleton>
|
||||
<!-- 设置 -->
|
||||
<a-spin v-else
|
||||
class="full"
|
||||
:loading="loading">
|
||||
<!-- 快捷键操作 -->
|
||||
<terminal-shortcut-action-block v-model:enabled="enabled"
|
||||
@reset="loadDefaultPreference"
|
||||
@save="savePreference" />
|
||||
<!-- 系统快捷键 -->
|
||||
<terminal-shortcut-keys-block title="系统快捷键"
|
||||
:type="ShortcutType.SYSTEM"
|
||||
:items="shortcutKeys"
|
||||
@set-editable="setEditableStatus"
|
||||
@clear-editable="clearEditableStatus"
|
||||
@update-enabled="updateEnabledStatus" />
|
||||
<!-- 终端快捷键 -->
|
||||
<terminal-shortcut-keys-block title="终端快捷键"
|
||||
:type="ShortcutType.TERMINAL"
|
||||
:items="shortcutKeys"
|
||||
@set-editable="setEditableStatus"
|
||||
@clear-editable="clearEditableStatus"
|
||||
@update-enabled="updateEnabledStatus" />
|
||||
</a-spin>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'terminalShortcutSetting'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TerminalShortcutKeyEditable, TerminalShortcutSetting } from '@/store/modules/host-space/types';
|
||||
import { ref, onMounted, onUnmounted } from 'vue';
|
||||
import { getDefaultPreference, getPreference } from '@/api/user/preference';
|
||||
import { TerminalPreferenceItem } from '@/store/modules/host-space';
|
||||
import { TerminalShortcutItems, ShortcutType } from '../../types/const';
|
||||
import { useHostSpaceStore } from '@/store';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import { useDebounceFn } from '@vueuse/core';
|
||||
import { addEventListen, removeEventListen } from '@/utils/event';
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import TerminalShortcutKeysBlock from './terminal-shortcut-keys-block.vue';
|
||||
import TerminalShortcutActionBlock from './terminal-shortcut-action-block.vue';
|
||||
|
||||
const { updateTerminalPreference } = useHostSpaceStore();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const render = ref(false);
|
||||
const enabled = ref(false);
|
||||
const editable = ref(false);
|
||||
const currentItem = ref<TerminalShortcutKeyEditable>();
|
||||
const shortcutKeys = ref<Array<TerminalShortcutKeyEditable>>([]);
|
||||
|
||||
// 修改快捷键状态
|
||||
const updateEnabledStatus = (item: TerminalShortcutKeyEditable, enabled: boolean) => {
|
||||
clearEditableStatus();
|
||||
item.editable = false;
|
||||
item.enabled = enabled;
|
||||
};
|
||||
|
||||
// 设置可编辑
|
||||
const setEditableStatus = (item: TerminalShortcutKeyEditable) => {
|
||||
item.editable = true;
|
||||
editable.value = true;
|
||||
currentItem.value = item;
|
||||
};
|
||||
|
||||
// 清除可编辑状态
|
||||
const clearEditableStatus = () => {
|
||||
editable.value = false;
|
||||
if (currentItem.value) {
|
||||
currentItem.value.editable = false;
|
||||
currentItem.value = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
// 计算显示的快捷键
|
||||
const computeShortcutKey = (item: TerminalShortcutKeyEditable): string => {
|
||||
const keys = [];
|
||||
if (item.ctrlKey) {
|
||||
keys.push('Ctrl');
|
||||
}
|
||||
if (item.altKey) {
|
||||
keys.push('Alt');
|
||||
}
|
||||
if (item.shiftKey) {
|
||||
keys.push('Shift');
|
||||
}
|
||||
let code = item.code;
|
||||
if (code) {
|
||||
if (code.startsWith('Key')) {
|
||||
code = code.substring(3);
|
||||
} else if (code.startsWith('Digit')) {
|
||||
code = code.substring(5);
|
||||
} else {
|
||||
const keyMap: Record<string, any> = {
|
||||
'Backquote': '`',
|
||||
'Minus': '-',
|
||||
'Equal': '=',
|
||||
'BracketLeft': '[',
|
||||
'BracketRight': ']',
|
||||
'Backslash': '\\',
|
||||
'Semicolon': ';',
|
||||
'Quote': '\'',
|
||||
'Comma': ',',
|
||||
'Period': '.',
|
||||
'Slash': '/',
|
||||
'ArrowUp': '↑',
|
||||
'ArrowDown': '↓',
|
||||
'ArrowLeft': '←',
|
||||
'ArrowRight': '→',
|
||||
};
|
||||
if (Object.keys(keyMap).includes(code)) {
|
||||
code = keyMap[code] as string;
|
||||
}
|
||||
}
|
||||
keys.push(code);
|
||||
}
|
||||
return keys.join(' + ');
|
||||
};
|
||||
|
||||
// 处理快捷键逻辑 防抖函数
|
||||
const handlerKeyboardFn = useDebounceFn((e: KeyboardEvent, item: TerminalShortcutKeyEditable) => {
|
||||
item.ctrlKey = e.ctrlKey;
|
||||
item.shiftKey = e.shiftKey;
|
||||
item.altKey = e.altKey;
|
||||
if (e.key !== 'Control' && e.key !== 'Shift' && e.key !== 'Alt') {
|
||||
item.code = e.code;
|
||||
} else {
|
||||
item.code = '';
|
||||
}
|
||||
item.shortcutKey = computeShortcutKey(item);
|
||||
});
|
||||
|
||||
// 处理快捷键逻辑
|
||||
const handlerKeyboard = (event: Event) => {
|
||||
if (editable.value && !!currentItem.value) {
|
||||
const e = event as KeyboardEvent;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
// 修改快捷键
|
||||
handlerKeyboardFn(e, currentItem.value);
|
||||
}
|
||||
};
|
||||
|
||||
// 保存
|
||||
const savePreference = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await updateTerminalPreference(TerminalPreferenceItem.SHORTCUT_SETTING, {
|
||||
enabled: enabled.value,
|
||||
keys: shortcutKeys.value
|
||||
} as TerminalShortcutSetting);
|
||||
Message.success('保存成功');
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 恢复默认设置
|
||||
const loadDefaultPreference = async () => {
|
||||
const { data } = await getDefaultPreference<Record<string, any>>('TERMINAL', [TerminalPreferenceItem.SHORTCUT_SETTING]);
|
||||
const setting = data[TerminalPreferenceItem.SHORTCUT_SETTING] as TerminalShortcutSetting;
|
||||
renderShortcutKeys(setting);
|
||||
};
|
||||
|
||||
// 加载用户设置
|
||||
const loadUserPreference = async () => {
|
||||
// 加载偏好
|
||||
const { data } = await getPreference<Record<string, any>>('TERMINAL', [TerminalPreferenceItem.SHORTCUT_SETTING]);
|
||||
const setting = data[TerminalPreferenceItem.SHORTCUT_SETTING] as TerminalShortcutSetting;
|
||||
renderShortcutKeys(setting);
|
||||
};
|
||||
|
||||
// 渲染快捷键
|
||||
const renderShortcutKeys = (setting: TerminalShortcutSetting) => {
|
||||
// 设置快捷键
|
||||
const keys: Array<TerminalShortcutKeyEditable> = [];
|
||||
for (const shortcutItem of TerminalShortcutItems) {
|
||||
const shortcutKey = setting.keys?.find(s => s.item === shortcutItem.item);
|
||||
if (shortcutKey) {
|
||||
// 存在
|
||||
keys.push({
|
||||
...shortcutItem,
|
||||
...shortcutKey,
|
||||
editable: false,
|
||||
});
|
||||
} else {
|
||||
// 不存在
|
||||
keys.push({
|
||||
...shortcutItem,
|
||||
ctrlKey: false,
|
||||
shiftKey: false,
|
||||
altKey: false,
|
||||
code: '',
|
||||
enabled: false,
|
||||
editable: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
// 计算快捷键
|
||||
keys.forEach(key => key.shortcutKey = computeShortcutKey(key));
|
||||
shortcutKeys.value = keys;
|
||||
enabled.value = setting.enabled;
|
||||
};
|
||||
|
||||
// 加载用户快捷键
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await loadUserPreference();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
render.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
// 监听键盘事件
|
||||
onMounted(() => {
|
||||
addEventListen(window, 'keydown', handlerKeyboard, true);
|
||||
});
|
||||
|
||||
// 移除键盘事件
|
||||
onUnmounted(() => {
|
||||
removeEventListen(window, 'keydown', handlerKeyboard, true);
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,165 @@
|
||||
<template>
|
||||
<div class="host-space-setting-block">
|
||||
<!-- 顶部 -->
|
||||
<div class="host-space-setting-subtitle-wrapper">
|
||||
<h3 class="host-space-setting-subtitle">
|
||||
主题设置
|
||||
</h3>
|
||||
</div>
|
||||
<!-- 加载中 -->
|
||||
<a-skeleton v-if="loading"
|
||||
class="skeleton-wrapper"
|
||||
:animation="true">
|
||||
<a-skeleton-line :rows="8" />
|
||||
</a-skeleton>
|
||||
<!-- 内容区域 -->
|
||||
<div v-else class="host-space-setting-body terminal-theme-container">
|
||||
<!-- 提示 -->
|
||||
<a-alert class="mb16">选择后会立刻保存, 刷新页面后生效</a-alert>
|
||||
<!-- 终端主题 -->
|
||||
<div class="theme-row"
|
||||
v-for="rowIndex in themes.length / 2"
|
||||
:key="rowIndex">
|
||||
<a-card v-for="(theme, colIndex) in [themes[(rowIndex - 1) * 2], themes[(rowIndex - 1) * 2 + 1]]"
|
||||
:key="theme.name"
|
||||
class="terminal-theme-card simple-card"
|
||||
:class="{
|
||||
'terminal-theme-card-check': theme.name === currentThemeName
|
||||
}"
|
||||
:title="theme.name"
|
||||
:style="{
|
||||
background: theme.schema.background,
|
||||
marginRight: colIndex === 0 ? '16px' : 0
|
||||
}"
|
||||
:header-style="{
|
||||
color: theme.dark ? 'rgba(255, 255, 255, .8)' : 'rgba(0, 0, 0, .8)',
|
||||
userSelect: 'none'
|
||||
}"
|
||||
@click="selectTheme(theme)">
|
||||
<!-- 样例 -->
|
||||
<terminal-example :schema="theme.schema" />
|
||||
<!-- 选中按钮 -->
|
||||
<icon-check class="theme-check-icon"
|
||||
v-show="theme.name === currentThemeName" />
|
||||
</a-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TerminalThemeBlock'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { TerminalTheme } from '@/api/asset/host-terminal';
|
||||
import { TerminalPreferenceItem } from '@/store/modules/host-space';
|
||||
import { useHostSpaceStore } from '@/store';
|
||||
import { onMounted, ref } from 'vue';
|
||||
import { getTerminalThemes } from '@/api/asset/host-terminal';
|
||||
import { getPreference } from '@/api/user/preference';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import TerminalExample from './terminal-example.vue';
|
||||
|
||||
const { updateTerminalPreference } = useHostSpaceStore();
|
||||
const { loading, setLoading } = useLoading();
|
||||
|
||||
const currentThemeName = ref();
|
||||
const themes = ref<Array<TerminalTheme>>([]);
|
||||
|
||||
// 选择主题
|
||||
const selectTheme = async (theme: TerminalTheme) => {
|
||||
currentThemeName.value = theme.name;
|
||||
await updateTerminalPreference(TerminalPreferenceItem.THEME, theme);
|
||||
};
|
||||
|
||||
// 加载用户主题
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const { data } = await getPreference<Record<string, any>>('TERMINAL', [TerminalPreferenceItem.THEME]);
|
||||
currentThemeName.value = data[TerminalPreferenceItem.THEME]?.name;
|
||||
} catch (e) {
|
||||
}
|
||||
});
|
||||
|
||||
// 加载主题列表
|
||||
onMounted(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// 加载全部主题
|
||||
const { data } = await getTerminalThemes();
|
||||
themes.value = data;
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@terminal-width: 458px;
|
||||
@terminal-height: 138px;
|
||||
|
||||
.terminal-theme-container {
|
||||
flex-direction: column;
|
||||
|
||||
.theme-row {
|
||||
display: flex;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.terminal-theme-card {
|
||||
width: @terminal-width;
|
||||
height: calc(@terminal-height + 44px);
|
||||
border: 2px solid var(--color-border);
|
||||
cursor: pointer;
|
||||
|
||||
:deep(.arco-card-header) {
|
||||
padding: 4px 16px;
|
||||
height: 40px;
|
||||
border-bottom: .5px solid var(--color-border-2);
|
||||
|
||||
&-title {
|
||||
color: unset;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.arco-card-body) {
|
||||
height: @terminal-height;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
.theme-check-icon {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
color: #FFF;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
}
|
||||
}
|
||||
|
||||
&-check, &:hover {
|
||||
border: 2px solid rgb(var(--blue-6));
|
||||
}
|
||||
|
||||
&-check::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-bottom: 28px solid rgb(var(--blue-6));
|
||||
border-left: 28px solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div class="host-space-setting-container">
|
||||
<div class="host-space-setting-wrapper">
|
||||
<!-- 主标题 -->
|
||||
<h2 class="host-space-setting-title">主题设置</h2>
|
||||
<!-- 主题设置 -->
|
||||
<terminal-theme-block />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'TerminalThemeSetting'
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import TerminalThemeBlock from './terminal-theme-block.vue';
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
</style>
|
||||
@@ -8,18 +8,18 @@
|
||||
<main class="host-space-layout-main">
|
||||
<!-- 左侧操作栏 -->
|
||||
<div class="host-space-layout-left">
|
||||
<terminal-left-sidebar />
|
||||
<left-sidebar />
|
||||
</div>
|
||||
<!-- 内容区域 -->
|
||||
<div class="host-space-layout-content">
|
||||
<!-- 主机加载中骨架 -->
|
||||
<loading-skeleton v-if="contentLoading" />
|
||||
<!-- 终端内容区域 -->
|
||||
<terminal-content v-else />
|
||||
<!-- 内容区域 -->
|
||||
<layout-main v-else />
|
||||
</div>
|
||||
<!-- 右侧操作栏 -->
|
||||
<div class="host-space-layout-right">
|
||||
<terminal-right-sidebar />
|
||||
<right-sidebar />
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
@@ -33,18 +33,18 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onBeforeMount, onUnmounted, onMounted } from 'vue';
|
||||
import { dictKeys, InnerTabs } from '../terminal/types/terminal.const';
|
||||
import { useCacheStore, useDictStore, useTerminalStore } from '@/store';
|
||||
import { dictKeys, InnerTabs } from './types/const';
|
||||
import { useCacheStore, useDictStore, useHostSpaceStore } from '@/store';
|
||||
import useLoading from '@/hooks/loading';
|
||||
import TerminalLeftSidebar from '../terminal/components/layout/terminal-left-sidebar.vue';
|
||||
import TerminalRightSidebar from '../terminal/components/layout/terminal-right-sidebar.vue';
|
||||
import TerminalContent from '../terminal/components/layout/terminal-content.vue';
|
||||
import LoadingSkeleton from '../terminal/components/layout/loading-skeleton.vue';
|
||||
import LayoutHeader from './components/layout/layout-header.vue';
|
||||
import LeftSidebar from './components/layout/left-sidebar.vue';
|
||||
import RightSidebar from './components/layout/right-sidebar.vue';
|
||||
import LoadingSkeleton from './components/layout/loading-skeleton.vue';
|
||||
import LayoutMain from './components/layout/layout-main.vue';
|
||||
import '@/assets/style/host-space-layout.less';
|
||||
import 'xterm/css/xterm.css';
|
||||
import LayoutHeader from './components/layout-header.vue';
|
||||
|
||||
const terminalStore = useTerminalStore();
|
||||
const hostSpaceStore = useHostSpaceStore();
|
||||
const dictStore = useDictStore();
|
||||
const cacheStore = useCacheStore();
|
||||
const { loading: contentLoading, setLoading: setContentLoading } = useLoading(true);
|
||||
@@ -60,9 +60,9 @@
|
||||
|
||||
// 加载用户终端偏好
|
||||
onBeforeMount(async () => {
|
||||
await terminalStore.fetchPreference();
|
||||
await hostSpaceStore.fetchPreference();
|
||||
// 设置系统主题配色
|
||||
const dark = terminalStore.preference.theme.dark;
|
||||
const dark = hostSpaceStore.preference.theme.dark;
|
||||
document.body.setAttribute('host-space-theme', dark ? 'dark' : 'light');
|
||||
render.value = true;
|
||||
});
|
||||
@@ -75,7 +75,7 @@
|
||||
// 加载主机信息
|
||||
onMounted(async () => {
|
||||
try {
|
||||
await terminalStore.loadHosts();
|
||||
await hostSpaceStore.loadHosts();
|
||||
} catch (e) {
|
||||
} finally {
|
||||
setContentLoading(false);
|
||||
|
||||
@@ -0,0 +1,247 @@
|
||||
import { ShortcutKeyItem } from './type';
|
||||
|
||||
// tab 类型
|
||||
export const TabType = {
|
||||
SETTING: 'setting',
|
||||
TERMINAL: 'terminal',
|
||||
};
|
||||
|
||||
// 内置 tab
|
||||
export const InnerTabs = {
|
||||
NEW_CONNECTION: {
|
||||
key: 'newConnection',
|
||||
title: '新建连接',
|
||||
icon: 'icon-plus',
|
||||
type: TabType.SETTING
|
||||
},
|
||||
SHORTCUT_SETTING: {
|
||||
key: 'shortcutSetting',
|
||||
title: '快捷键设置',
|
||||
icon: 'icon-command',
|
||||
type: TabType.SETTING
|
||||
},
|
||||
DISPLAY_SETTING: {
|
||||
key: 'displaySetting',
|
||||
title: '显示设置',
|
||||
icon: 'icon-stamp',
|
||||
type: TabType.SETTING
|
||||
},
|
||||
THEME_SETTING: {
|
||||
key: 'themeSetting',
|
||||
title: '主题设置',
|
||||
icon: 'icon-palette',
|
||||
type: TabType.SETTING
|
||||
},
|
||||
TERMINAL_SETTING: {
|
||||
key: 'terminalSetting',
|
||||
title: '终端设置',
|
||||
icon: 'icon-settings',
|
||||
type: TabType.SETTING
|
||||
},
|
||||
};
|
||||
|
||||
// 新建连接类型
|
||||
export const NewConnectionType = {
|
||||
GROUP: 'group',
|
||||
LIST: 'list',
|
||||
FAVORITE: 'favorite',
|
||||
LATEST: 'latest'
|
||||
};
|
||||
|
||||
// 主机额外配置 ssh 认证方式
|
||||
export const ExtraSshAuthType = {
|
||||
// 使用默认认证方式
|
||||
DEFAULT: 'DEFAULT',
|
||||
// 自定义秘钥
|
||||
CUSTOM_KEY: 'CUSTOM_KEY',
|
||||
// 自定义身份
|
||||
CUSTOM_IDENTITY: 'CUSTOM_IDENTITY',
|
||||
};
|
||||
|
||||
// 终端状态
|
||||
export const TerminalStatus = {
|
||||
// 连接中
|
||||
CONNECTING: 0,
|
||||
// 已连接
|
||||
CONNECTED: 1,
|
||||
// 已断开
|
||||
CLOSED: 2
|
||||
};
|
||||
|
||||
// 终端操作栏-操作项
|
||||
export const ActionBarItems = [
|
||||
{
|
||||
item: 'toTop',
|
||||
icon: 'icon-up',
|
||||
content: '去顶部',
|
||||
}, {
|
||||
item: 'toBottom',
|
||||
icon: 'icon-down',
|
||||
content: '去底部',
|
||||
}, {
|
||||
item: 'selectAll',
|
||||
icon: 'icon-expand',
|
||||
content: '全选',
|
||||
}, {
|
||||
item: 'search',
|
||||
icon: 'icon-find-replace',
|
||||
content: '搜索',
|
||||
}, {
|
||||
item: 'copy',
|
||||
icon: 'icon-copy',
|
||||
content: '复制',
|
||||
}, {
|
||||
item: 'paste',
|
||||
icon: 'icon-paste',
|
||||
content: '粘贴',
|
||||
}, {
|
||||
item: 'interrupt',
|
||||
icon: 'icon-formula',
|
||||
content: 'ctrl + c',
|
||||
}, {
|
||||
item: 'enter',
|
||||
icon: 'icon-play-arrow-fill',
|
||||
content: '回车',
|
||||
}, {
|
||||
item: 'fontSizePlus',
|
||||
icon: 'icon-zoom-in',
|
||||
content: '增大字号',
|
||||
}, {
|
||||
item: 'fontSizeSubtract',
|
||||
icon: 'icon-zoom-out',
|
||||
content: '减小字号',
|
||||
}, {
|
||||
item: 'commandEditor',
|
||||
icon: 'icon-code-square',
|
||||
content: '命令编辑器',
|
||||
}, {
|
||||
item: 'clear',
|
||||
icon: 'icon-delete',
|
||||
content: '清空',
|
||||
}, {
|
||||
item: 'disconnect',
|
||||
icon: 'icon-poweroff',
|
||||
content: '断开连接',
|
||||
}, {
|
||||
item: 'closeTab',
|
||||
icon: 'icon-close',
|
||||
content: '关闭终端',
|
||||
}
|
||||
];
|
||||
|
||||
// 快捷键操作类型
|
||||
export const ShortcutType = {
|
||||
SYSTEM: 1,
|
||||
TERMINAL: 2
|
||||
};
|
||||
|
||||
// 终端操作快捷键 key
|
||||
export const TabShortcutKeys = {
|
||||
// 切换为前一个 tab
|
||||
CHANGE_TO_PREV_TAB: 'changeToPrevTab',
|
||||
// 切换为后一个 tab
|
||||
CHANGE_TO_NEXT_TAB: 'changeToNextTab',
|
||||
// 关闭 tab
|
||||
CLOSE_TAB: 'closeTab',
|
||||
// 打开新建连接 tab
|
||||
OPEN_NEW_CONNECT_TAB: 'openNewConnectTab',
|
||||
};
|
||||
|
||||
// 终端操作快捷键
|
||||
export const TerminalShortcutItems: Array<ShortcutKeyItem> = [
|
||||
{
|
||||
item: TabShortcutKeys.CHANGE_TO_PREV_TAB,
|
||||
content: '切换为前一个 tab',
|
||||
type: ShortcutType.SYSTEM
|
||||
}, {
|
||||
item: TabShortcutKeys.CHANGE_TO_NEXT_TAB,
|
||||
content: '切换为后一个 tab',
|
||||
type: ShortcutType.SYSTEM
|
||||
}, {
|
||||
item: TabShortcutKeys.CLOSE_TAB,
|
||||
content: '关闭当前 tab',
|
||||
type: ShortcutType.SYSTEM
|
||||
}, {
|
||||
item: TabShortcutKeys.OPEN_NEW_CONNECT_TAB,
|
||||
content: '打开新建连接 tab',
|
||||
type: ShortcutType.SYSTEM
|
||||
}, {
|
||||
item: 'openCopyTerminalTab',
|
||||
content: '复制当前终端 tab',
|
||||
type: ShortcutType.TERMINAL
|
||||
}, {
|
||||
item: 'copy',
|
||||
content: '复制',
|
||||
type: ShortcutType.TERMINAL
|
||||
}, {
|
||||
item: 'paste',
|
||||
content: '粘贴',
|
||||
type: ShortcutType.TERMINAL
|
||||
}, {
|
||||
item: 'toTop',
|
||||
content: '去顶部',
|
||||
type: ShortcutType.TERMINAL
|
||||
}, {
|
||||
item: 'toBottom',
|
||||
content: '去底部',
|
||||
type: ShortcutType.TERMINAL
|
||||
}, {
|
||||
item: 'selectAll',
|
||||
content: '全选',
|
||||
type: ShortcutType.TERMINAL
|
||||
}, {
|
||||
item: 'search',
|
||||
content: '搜索',
|
||||
type: ShortcutType.TERMINAL
|
||||
}, {
|
||||
item: 'fontSizePlus',
|
||||
content: '增大字号',
|
||||
type: ShortcutType.TERMINAL
|
||||
}, {
|
||||
item: 'fontSizeSubtract',
|
||||
content: '减小字号',
|
||||
type: ShortcutType.TERMINAL
|
||||
}, {
|
||||
item: 'commandEditor',
|
||||
content: '命令编辑器',
|
||||
type: ShortcutType.TERMINAL
|
||||
},
|
||||
];
|
||||
|
||||
// 打开 sshModal key
|
||||
export const openSshModalKey = Symbol();
|
||||
|
||||
// 字体后缀 兜底
|
||||
export const fontFamilySuffix = ',courier-new, courier, monospace';
|
||||
|
||||
// 终端字体样式
|
||||
export const fontFamilyKey = 'terminalFontFamily';
|
||||
|
||||
// 终端字体大小
|
||||
export const fontSizeKey = 'terminalFontSize';
|
||||
|
||||
// 终端字体字重
|
||||
export const fontWeightKey = 'terminalFontWeight';
|
||||
|
||||
// 终端光标样式
|
||||
export const cursorStyleKey = 'terminalCursorStyle';
|
||||
|
||||
// 主机新建连接类型
|
||||
export const newConnectionTypeKey = 'hostNewConnectionType';
|
||||
|
||||
// 终端新建连接类型
|
||||
export const extraSshAuthTypeKey = 'hostExtraSshAuthType';
|
||||
|
||||
// 终端状态
|
||||
export const connectStatusKey = 'terminalConnectStatus';
|
||||
|
||||
// 终端类型
|
||||
export const terminalEmulationTypeKey = 'terminalEmulationType';
|
||||
|
||||
// 加载的字典值
|
||||
export const dictKeys = [
|
||||
fontFamilyKey, fontSizeKey,
|
||||
fontWeightKey, cursorStyleKey,
|
||||
newConnectionTypeKey, extraSshAuthTypeKey,
|
||||
connectStatusKey, terminalEmulationTypeKey
|
||||
];
|
||||
|
||||
55
orion-ops-ui/src/views/host/space/types/type.ts
Normal file
55
orion-ops-ui/src/views/host/space/types/type.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { CSSProperties } from 'vue';
|
||||
import { HostQueryResponse } from '@/api/asset/host';
|
||||
import { TerminalTabItem } from '@/views/host/terminal/types/terminal.type';
|
||||
|
||||
// tab 元素
|
||||
export interface TabItem {
|
||||
key: string;
|
||||
title: string;
|
||||
type: string;
|
||||
icon?: string;
|
||||
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// sidebar 操作类型
|
||||
export interface SidebarAction {
|
||||
icon: string;
|
||||
content: string;
|
||||
visible?: boolean;
|
||||
disabled?: boolean;
|
||||
checked?: boolean;
|
||||
iconStyle?: CSSProperties;
|
||||
click: () => void;
|
||||
}
|
||||
|
||||
// 组合操作元素
|
||||
export interface CombinedHandlerItem {
|
||||
icon: string,
|
||||
type: string,
|
||||
title: string;
|
||||
settingTab?: TerminalTabItem;
|
||||
host?: HostQueryResponse;
|
||||
}
|
||||
|
||||
// 右键菜单元素
|
||||
export interface ContextMenuItem {
|
||||
item: string;
|
||||
icon: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
// 快捷键元素
|
||||
export interface ShortcutKeyItem {
|
||||
item: string;
|
||||
content: string;
|
||||
type: number;
|
||||
}
|
||||
|
||||
// ssh 额外配置
|
||||
export interface SshExtraModel {
|
||||
authType?: string;
|
||||
username?: string;
|
||||
keyId?: number;
|
||||
identityId?: number;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<!-- 终端右键菜单 -->
|
||||
<a-dropdown class="terminal-context-menu"
|
||||
<a-dropdown class="host-space-context-menu"
|
||||
:popup-max-height="false"
|
||||
trigger="contextMenu"
|
||||
position="bl"
|
||||
@@ -14,7 +14,7 @@
|
||||
:disabled="!session.handler.enabledStatus(action.item)"
|
||||
@click="emits('click', action.item)">
|
||||
<!-- 图标 -->
|
||||
<div class="terminal-context-menu-icon">
|
||||
<div class="host-space-context-menu-icon">
|
||||
<component :is="action.icon" />
|
||||
</div>
|
||||
<!-- 文本 -->
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { ITerminalSession, TerminalTabItem, SidebarAction, ITerminalSessionHandler } from '../../types/terminal.type';
|
||||
import type { ITerminalSession, TerminalTabItem, SidebarAction } from '../../types/terminal.type';
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { useDictStore, useTerminalStore } from '@/store';
|
||||
import useCopy from '@/hooks/copy';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// tab 类型
|
||||
import { ShortcutKeyItem } from '@/views/host/space/types/terminal.type';
|
||||
import { ShortcutKeyItem } from './terminal.type';
|
||||
|
||||
export const TerminalTabType = {
|
||||
SETTING: 'setting',
|
||||
@@ -226,8 +226,8 @@ export const fontWeightKey = 'terminalFontWeight';
|
||||
// 终端光标样式
|
||||
export const cursorStyleKey = 'terminalCursorStyle';
|
||||
|
||||
// 终端新建连接类型
|
||||
export const newConnectionTypeKey = 'terminalNewConnectionType';
|
||||
// 主机新建连接类型
|
||||
export const newConnectionTypeKey = 'hostNewConnectionType';
|
||||
|
||||
// 终端新建连接类型
|
||||
export const extraSshAuthTypeKey = 'hostExtraSshAuthType';
|
||||
|
||||
Reference in New Issue
Block a user