feat: 添加终端右键菜单逻辑.

This commit is contained in:
lijiahangmax
2024-01-13 15:45:29 +08:00
parent 57d06e7b05
commit 826907380b
15 changed files with 335 additions and 38 deletions

View File

@@ -8,6 +8,8 @@ import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
/**
* 终端偏好模型
*
@@ -33,6 +35,9 @@ public class TerminalPreferenceModel implements PreferenceModel {
@Schema(description = "操作栏设置")
private JSONObject actionBarSetting;
@Schema(description = "右键菜单设置")
private List<String> rightMenuSetting;
@Schema(description = "交互设置")
private JSONObject interactSetting;

View File

@@ -1,6 +1,7 @@
package com.orion.ops.module.infra.handler.preference.strategy;
import com.alibaba.fastjson.JSONObject;
import com.orion.lang.utils.collect.Lists;
import com.orion.net.host.ssh.TerminalType;
import com.orion.ops.module.infra.handler.preference.model.TerminalPreferenceModel;
import org.springframework.stereotype.Component;
@@ -63,6 +64,7 @@ public class TerminalPreferenceStrategy implements IPreferenceStrategy<TerminalP
.theme(new JSONObject())
.displaySetting(JSONObject.parseObject(defaultDisplaySetting))
.actionBarSetting(new JSONObject())
.rightMenuSetting(Lists.of("copy", "paste", "checkAll", "search", "clear"))
.interactSetting(JSONObject.parseObject(defaultInteractSetting))
.pluginsSetting(JSONObject.parseObject(defaultPluginsSetting))
.sessionSetting(JSONObject.parseObject(defaultSessionSetting))

View File

@@ -10,7 +10,7 @@ import type {
import type { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data';
import { getCurrentAuthorizedHost } from '@/api/asset/asset-authorized-data';
import type { HostQueryResponse } from '@/api/asset/host';
import type { TerminalTheme } from '@/api/asset/host-terminal';
import type { TerminalTheme, TerminalThemeSchema } from '@/api/asset/host-terminal';
import { getTerminalThemes } from '@/api/asset/host-terminal';
import { defineStore } from 'pinia';
import { getPreference, updatePreference } from '@/api/user/preference';
@@ -26,10 +26,14 @@ export const TerminalPreferenceItem = {
NEW_CONNECTION_TYPE: 'newConnectionType',
// 终端主题
THEME: 'theme',
// 快捷键设置
SHORTCUT_SETTING: 'shortcutSetting',
// 显示设置
DISPLAY_SETTING: 'displaySetting',
// 操作栏设置
ACTION_BAR_SETTING: 'actionBarSetting',
// 右键菜单设置
RIGHT_MENU_SETTING: 'rightMenuSetting',
// 交互设置
INTERACT_SETTING: 'interactSetting',
// 插件设置
@@ -42,9 +46,12 @@ export default defineStore('terminal', {
state: (): TerminalState => ({
preference: {
newConnectionType: 'group',
theme: {} as TerminalTheme,
theme: {
schema: {} as TerminalThemeSchema
} as TerminalTheme,
displaySetting: {} as TerminalDisplaySetting,
actionBarSetting: {} as TerminalActionBarSetting,
rightMenuSetting: [],
interactSetting: {} as TerminalInteractSetting,
pluginsSetting: {} as TerminalPluginsSetting,
sessionSetting: {} as TerminalSessionSetting,

View File

@@ -1,6 +1,6 @@
import type { ITerminalSessionManager, ITerminalTabManager } from '@/views/host/terminal/types/terminal.type';
import type { TerminalTheme } from '@/api/asset/host-terminal';
import type { AuthorizedHostQueryResponse } from '@/api/asset/asset-authorized-data';
import type { TerminalTheme } from '@/api/asset/host-terminal';
export interface TerminalState {
preference: TerminalPreference;
@@ -15,6 +15,7 @@ export interface TerminalPreference {
theme: TerminalTheme;
displaySetting: TerminalDisplaySetting;
actionBarSetting: TerminalActionBarSetting;
rightMenuSetting: Array<string>,
interactSetting: TerminalInteractSetting;
pluginsSetting: TerminalPluginsSetting;
sessionSetting: TerminalSessionSetting;

View File

@@ -131,11 +131,13 @@ body .host-layout, .arco-modal-container {
}
// arco 暗色配色
body[terminal-theme='dark'],
body[terminal-theme='dark'] .host-layout,
body[terminal-theme='dark'] .arco-modal-container {
--color-white: rgba(255, 255, 255, 0.9);
--color-black: #000000;
--color-border: #333335;
--color-bg-popup: var(--color-bg-5);
--color-bg-1: #17171a;
--color-bg-2: #232324;
--color-bg-3: #2a2a2b;
@@ -260,7 +262,6 @@ body[terminal-theme='dark'] .arco-modal-container {
display: flex;
&.block-body {
display: flex;
width: 100%;
padding: 16px;
border: 1px solid var(--color-fill-4);
@@ -280,3 +281,17 @@ body[terminal-theme='dark'] .arco-modal-container {
.terminal-tooltip-arrow {
display: none;
}
// 终端右键菜单
.terminal-context-menu {
.arco-dropdown-option {
padding: 0 6px;
line-height: 32px;
&-content {
width: 120px;
display: flex;
align-items: center;
}
}
}

View File

@@ -126,6 +126,7 @@
color: var(--color-content-text-1);
cursor: pointer;
transition: transform 0.3s ease;
will-change: transform;
&:hover {
transform: scale(1.04);

View File

@@ -11,10 +11,10 @@
<div class="terminal-sidebar-icon"
:class="[
iconClass,
action.disabled !== false ? '' : 'disabled-item',
action.disabled === true ? 'disabled-item' : '',
action.checked === true ? 'checked-item' : '',
]"
@click="action.disabled !== false ? action.click() : false">
@click="action.disabled === true ? false : action.click()">
<component :is="action.icon" :style="action?.iconStyle" />
</div>
</div>

View File

@@ -7,6 +7,8 @@
<terminal-display-block />
<!-- 顶部工具栏 -->
<terminal-action-bar-block />
<!-- 右键菜单 -->
<terminal-right-menu-block />
</div>
</div>
</template>
@@ -20,6 +22,7 @@
<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>

View File

@@ -0,0 +1,227 @@
<template>
<div class="terminal-setting-block">
<!-- 顶部 -->
<div class="terminal-setting-subtitle-wrapper">
<h3 class="terminal-setting-subtitle">
右键菜单设置
</h3>
</div>
<!-- 提示 -->
<a-alert class="mb16">修改后会立刻保存, 重新打开终端后生效 (无需刷新页面)</a-alert>
<!-- 内容区域 -->
<div class="terminal-setting-body block-body setting-body">
<!-- 功能项 -->
<div class="actions-container">
<div class="setting-label">功能</div>
<!-- 功能项列表 -->
<div class="actions-wrapper">
<a-row :gutter="[8, 8]">
<a-col :span="12"
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="setting-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/terminal.type';
import { computed, ref, watch } from 'vue';
import { useTerminalStore } from '@/store';
import { TerminalPreferenceItem } from '@/store/modules/terminal';
import { ActionBarItems } from '../../types/terminal.const';
const { preference, updateTerminalPreference } = useTerminalStore();
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>
.setting-body {
display: flex;
}
.setting-label {
display: flex;
max-width: 100%;
color: var(--color-text-2);
font-size: 14px;
margin-bottom: 8px;
padding: 0;
line-height: 1.5715;
white-space: normal;
user-select: none;
}
.actions-container {
width: 418px;
height: auto;
.actions-wrapper {
padding-right: 8px;
margin-right: 32px;
}
.action-item {
display: flex;
padding: 6px;
align-items: center;
cursor: pointer;
border-radius: 4px;
background-color: var(--color-fill-2);
transition: transform 0.3s ease;
will-change: transform;
&:hover {
transform: scale(1.04);
}
}
.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>

View File

@@ -1,15 +1,25 @@
<template>
<!-- 终端右键菜单 -->
<a-dropdown trigger="contextMenu"
<a-dropdown class="terminal-context-menu"
trigger="contextMenu"
:popup-max-height="false"
position="bl"
alignPoint>
<!-- 终端插槽 -->
<slot />
<!-- 右键菜单 -->
<template v-if="preference.interactSetting.enableRightClickMenu" #content>
<a-doption>Option 1</a-doption>
<a-doption>Option 2</a-doption>
<a-doption>Option 3</a-doption>
<a-doption v-for="(action, index) in actions"
:key="index"
:disabled="enabledStatus[action.item] === false"
@click="emits('click', action.item)">
<!-- 图标 -->
<div class="action-icon">
<component :is="action.icon" />
</div>
<!-- 文本 -->
<div>{{ action.content }}</div>
</a-doption>
</template>
</a-dropdown>
</template>
@@ -21,14 +31,31 @@
</script>
<script lang="ts" setup>
import type { ContextMenuItem } from '../../types/terminal.type';
import { ActionBarItems } from '../../types/terminal.const';
import { useTerminalStore } from '@/store';
defineProps<{
enabledStatus: Record<string, boolean | undefined>
}>();
const emits = defineEmits(['click']);
const { preference } = useTerminalStore();
// TODO 颜色 配置 触发事件
const actions: Array<ContextMenuItem> = !preference.interactSetting.enableRightClickMenu
? []
: preference.rightMenuSetting
.map(s => ActionBarItems.find(i => i.item === s) as ContextMenuItem)
.filter(Boolean);
</script>
<style lang="less" scoped>
.action-icon {
font-size: 16px;
margin: 0 8px 0 4px;
}
</style>

View File

@@ -37,7 +37,8 @@
</div>
</div>
<!-- 终端右键菜单 -->
<terminal-context-menu>
<terminal-context-menu :enabled-status="actionsEnabledStatus"
@click="action => actionsClickHandler[action] && actionsClickHandler[action]()">
<!-- 终端容器 -->
<div class="terminal-wrapper"
:style="{ background: preference.theme.schema.background }">
@@ -92,9 +93,7 @@
const session = ref<ITerminalSession>();
// TODO
// 右键菜单补充 enableRightClickMenu 粘贴逻辑
// 设置快捷键 粘贴逻辑
// 读取快捷键并且禁用快捷键
// 设置快捷键 粘贴逻辑 禁用
// 截屏
// sftp
@@ -124,14 +123,14 @@
session.value?.find(word, next, options);
};
// 操作用状态
const actionsDisableStatus = computed<Record<string, boolean | undefined>>(() => {
// 操作用状态
const actionsEnabledStatus = computed<Record<string, boolean | undefined>>(() => {
return {
paste: session.value?.canWrite,
interrupt: session.value?.canWrite,
enter: session.value?.canWrite,
commandEditor: session.value?.canWrite,
disconnect: session.value?.connected,
paste: !!session.value?.canWrite,
interrupt: !!session.value?.canWrite,
enter: !!session.value?.canWrite,
commandEditor: !!session.value?.canWrite,
disconnect: !!session.value?.connected,
};
});
@@ -143,18 +142,16 @@
toBottom: () => session.value?.toBottom(),
// 全选
checkAll: () => session.value?.selectAll(),
// 复制选中部分
copy: () => session.value?.copySelection(),
// 搜索
search: () => searchModal.value.toggle(),
// 复制选中部分
copy: () => session.value?.copySelection(),
// 粘贴
paste: async () => session.value?.pasteTrimEnd(await readText()),
// ctrl + c
interrupt: () => session.value?.paste(String.fromCharCode(3)),
// 回车
enter: () => session.value?.paste(String.fromCharCode(13)),
// 命令编辑器
commandEditor: () => editorModal.value.open('', ''),
// 增大字号
fontSizePlus: () => {
if (session.value) {
@@ -175,6 +172,8 @@
}
}
},
// 命令编辑器
commandEditor: () => editorModal.value.open('', ''),
// 清空
clear: () => session.value?.clear(),
// 断开连接
@@ -190,7 +189,7 @@
icon: s.icon,
content: s.content,
visible: preference.actionBarSetting[s.item] !== false,
disabled: actionsDisableStatus.value[s.item] !== false,
disabled: actionsEnabledStatus.value[s.item] === false,
click: () => {
actionsClickHandler[s.item] && actionsClickHandler[s.item]();
}

View File

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

View File

@@ -35,6 +35,7 @@
import { ref, onBeforeMount, onUnmounted, onMounted } from 'vue';
import { dictKeys, InnerTabs } from './types/terminal.const';
import { useCacheStore, useDictStore, useTerminalStore } from '@/store';
import useLoading from '@/hooks/loading';
import TerminalHeader from './components/layout/terminal-header.vue';
import TerminalLeftSidebar from './components/layout/terminal-left-sidebar.vue';
import TerminalRightSidebar from './components/layout/terminal-right-sidebar.vue';
@@ -42,7 +43,6 @@
import LoadingSkeleton from './components/layout/loading-skeleton.vue';
import './assets/styles/layout.less';
import 'xterm/css/xterm.css';
import useLoading from '@/hooks/loading';
const terminalStore = useTerminalStore();
const dictStore = useDictStore();

View File

@@ -80,18 +80,18 @@ export const ActionBarItems = [
item: 'checkAll',
icon: 'icon-expand',
content: '全选',
}, {
item: 'search',
icon: 'icon-find-replace',
content: '搜索',
}, {
item: 'copy',
icon: 'icon-copy',
content: '复制选中部分',
content: '复制',
}, {
item: 'paste',
icon: 'icon-paste',
content: '粘贴',
}, {
item: 'search',
icon: 'icon-find-replace',
content: '搜索',
}, {
item: 'interrupt',
icon: 'icon-formula',
@@ -100,10 +100,6 @@ export const ActionBarItems = [
item: 'enter',
icon: 'icon-play-arrow-fill',
content: '回车',
}, {
item: 'commandEditor',
icon: 'icon-code-square',
content: '命令编辑器',
}, {
item: 'fontSizePlus',
icon: 'icon-zoom-in',
@@ -112,6 +108,10 @@ export const ActionBarItems = [
item: 'fontSizeSubtract',
icon: 'icon-zoom-out',
content: '减小字号',
}, {
item: 'commandEditor',
icon: 'icon-code-square',
content: '命令编辑器',
}, {
item: 'clear',
icon: 'icon-delete',
@@ -123,7 +123,7 @@ export const ActionBarItems = [
}, {
item: 'close',
icon: 'icon-close',
content: '关闭',
content: '关闭终端',
}
];

View File

@@ -38,6 +38,13 @@ export interface CombinedHandlerItem {
host?: HostQueryResponse;
}
// 右键菜单元素
export interface ContextMenuItem {
item: string;
icon: string;
content: string;
}
// ssh 额外配置
export interface SshExtraModel {
authType?: string;