feat: 快捷键设置.

This commit is contained in:
lijiahang
2024-01-17 18:50:18 +08:00
parent 0eed20e54f
commit fc28391927
10 changed files with 474 additions and 241 deletions

View File

@@ -94,6 +94,19 @@ body {
border-color: rgb(var(--gray-2));
}
// 垂直 label
.vertical-form-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;
}
.full {
width: 100%;
height: 100%;

View File

@@ -82,8 +82,12 @@ export interface TerminalShortcutKey {
shiftKey: boolean;
altKey: boolean;
code: string;
// extra
edit: boolean;
}
// 终端快捷键编辑
export interface TerminalShortcutKeyEditable extends TerminalShortcutKey {
editable: boolean;
content: string;
type: number;
shortcutKey?: string;
}

View File

@@ -147,6 +147,7 @@ body[terminal-theme='dark'] .arco-modal-container {
--color-neutral-1: rgba(255, 255, 255, 0.04);
--color-neutral-2: rgba(255, 255, 255, 0.08);
--color-neutral-3: rgba(255, 255, 255, 0.12);
--color-neutral-4: rgba(255, 255, 255, 0.16);
--color-text-1: rgba(255, 255, 255, 0.9);
--color-text-2: rgba(255, 255, 255, 0.7);
--color-text-3: rgba(255, 255, 255, 0.5);

View File

@@ -79,7 +79,7 @@
</a-form>
<!-- 预览区域 -->
<div class="terminal-example">
<span class="terminal-example-label">预览效果</span>
<span class="vertical-form-label">预览效果</span>
<div class="terminal-example-wrapper"
:style="{ background: preference.theme.schema.background }">
<terminal-example :schema="preference.theme.schema"
@@ -185,14 +185,6 @@
.terminal-example {
height: 100%;
&-label {
color: var(--color-text-2);
display: block;
height: 16px;
margin-bottom: 12px;
user-select: none;
}
&-wrapper {
border-radius: 4px;
width: calc(@terminal-width - 16px);

View File

@@ -12,7 +12,7 @@
<div class="terminal-setting-body block-body setting-body">
<!-- 功能项 -->
<div class="actions-container">
<div class="setting-label">功能</div>
<div class="vertical-form-label">功能</div>
<!-- 功能项列表 -->
<div class="actions-wrapper">
<a-row :gutter="[8, 8]">
@@ -35,7 +35,7 @@
</div>
<!-- 菜单预览容器 -->
<div class="preview-container">
<div class="setting-label">菜单预览</div>
<div class="vertical-form-label">菜单预览</div>
<div ref="popupContainer" />
</div>
<!-- 预览下拉菜单 -->
@@ -121,18 +121,6 @@
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;

View File

@@ -0,0 +1,69 @@
<template>
<div class="terminal-setting-block">
<!-- 顶部 -->
<div class="terminal-setting-subtitle-wrapper">
<h3 class="terminal-setting-subtitle">
快捷键操作
</h3>
</div>
<!-- 内容区域 -->
<div class="terminal-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>

View File

@@ -1,208 +0,0 @@
<template>
<div class="terminal-setting-block">
<!-- 顶部 -->
<div class="terminal-setting-subtitle-wrapper">
<h3 class="terminal-setting-subtitle">
系统快捷键
</h3>
</div>
<!-- 加载中 -->
<a-skeleton v-if="loading"
class="skeleton-wrapper"
:animation="true">
<a-skeleton-line :rows="4" />
</a-skeleton>
<!-- 内容区域 -->
<div v-else class="terminal-setting-body terminal-shortcut-container">
<!-- 提示 -->
<a-alert class="mb16">刷新页面后生效 (设置时需要避免浏览器内置快捷键)</a-alert>
<template v-for="item in shortcutKeys">
<div class="shortcut-row" v-if="item.type === TerminalShortcutType.SYSTEM">
<!-- label -->
<span class="shortcut-label">{{ item.content }}</span>
<!-- 快捷键 -->
<a-space class="shortcut-key-container"
:class="[item.edit ? 'edit-container' : '']">
<a-tag v-if="item.edit || item.ctrlKey"
v-model:checked="item.ctrlKey"
:checkable="item.edit">
ctrl
</a-tag>
<a-tag v-if="item.edit || item.shiftKey"
v-model:checked="item.shiftKey"
:checkable="item.edit">
shift
</a-tag>
<a-tag v-if="item.edit || item.altKey"
v-model:checked="item.altKey"
:checkable="item.edit">
alt
</a-tag>
<!-- 触发按键-->
<a-tag v-if="item.code && !item.edit" :checkable="item.edit">{{ item.code }}</a-tag>
<a-input v-if="item.edit"
v-model="item.code"
:data-item="item.item"
:ref="setEditRef"
class="trigger-input"
size="small"
@keyup="e => item.code = e.code" />
</a-space>
<!-- 操作 -->
<a-space class="shortcut-actions-container">
<!-- 屏蔽 -->
<div class="click-icon-wrapper" title="屏蔽">
<icon-stop />
</div>
<!-- 设置 -->
<div class="click-icon-wrapper" v-if="!item.edit"
title="设置"
@click="() => item.edit = true">
<icon-settings />
</div>
<!-- 保存 -->
<div class="click-icon-wrapper" v-else
title="保存"
@click="() => item.edit = false">
<icon-check />
</div>
</a-space>
</div>
</template>
</div>
</div>
</template>
<script lang="ts">
export default {
name: 'TerminalShortcutBlock'
};
</script>
<script lang="ts" setup>
import type { TerminalShortcutSetting, TerminalShortcutKey } from '@/store/modules/terminal/types';
import { useTerminalStore } from '@/store';
import { TerminalPreferenceItem } from '@/store/modules/terminal';
import { nextTick, onMounted, ref } from 'vue';
import { getPreference } from '@/api/user/preference';
import useLoading from '@/hooks/loading';
import { TerminalShortcutType, TerminalShortcutItems } from '../../types/terminal.const';
const { updateTerminalPreference } = useTerminalStore();
const { loading, setLoading } = useLoading(true);
const shortcutKeys = ref<Array<TerminalShortcutKey>>([]);
// 设置 ref
const setEditRef = (el: HTMLElement) => {
// 自动聚焦
nextTick(() => {
el && el.focus();
});
};
// 加载用户快捷键
onMounted(async () => {
setLoading(true);
try {
// 加载偏好
const { data } = await getPreference<Record<string, any>>('TERMINAL', [TerminalPreferenceItem.SHORTCUT_SETTING]);
const setting = data[TerminalPreferenceItem.SHORTCUT_SETTING] as TerminalShortcutSetting;
// 设置快捷键
const keys: Array<TerminalShortcutKey> = [];
for (const shortcutItem of TerminalShortcutItems) {
const shortcutKey = setting.keys?.find(s => s.item === shortcutItem.item);
if (shortcutKey) {
// 存在
keys.push({
...shortcutItem,
...shortcutKey,
edit: false,
});
} else {
// 不存在
keys.push({
...shortcutItem,
ctrlKey: false,
shiftKey: false,
altKey: false,
code: '',
enabled: false,
edit: false,
});
}
}
shortcutKeys.value = keys;
} catch (e) {
} finally {
setLoading(false);
}
});
</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-1);
height: 42px;
padding: 8px 16px;
border-radius: 4px;
user-select: none;
transition: .3s;
&:hover {
background: var(--color-neutral-2);
.shortcut-actions-container {
display: flex;
}
}
}
.shortcut-label {
font-size: 14px;
width: 218px;
}
.shortcut-key-container {
width: 268px;
}
.trigger-input {
width: 68px;
}
.shortcut-actions-container {
display: none;
.click-icon-wrapper {
font-size: 16px;
padding: 4px;
}
}
:deep(.arco-tag) {
background: var(--color-neutral-2);
color: var(--color-text-3);
width: 44px;
display: flex;
justify-content: center;
height: 26px;
font-size: 14px;
}
:deep(.arco-tag-checked) {
background: var(--color-neutral-3);
color: var(--color-text-1);
}
}
</style>

View File

@@ -0,0 +1,156 @@
<template>
<div class="terminal-setting-block">
<!-- 顶部 -->
<div class="terminal-setting-subtitle-wrapper">
<h3 class="terminal-setting-subtitle">
{{ title }}
</h3>
</div>
<!-- 内容区域 -->
<div class="terminal-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"
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/terminal/types';
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>

View File

@@ -3,8 +3,37 @@
<div class="terminal-setting-wrapper">
<!-- 主标题 -->
<h2 class="terminal-setting-title">快捷键设置</h2>
<!-- 快捷键设置 -->
<terminal-shortcut-block />
<!-- 加载中 -->
<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="TerminalShortcutType.SYSTEM"
:items="shortcutKeys"
@set-editable="setEditableStatus"
@clear-editable="clearEditableStatus"
@update-enabled="updateEnabledStatus" />
<!-- 终端快捷键 -->
<terminal-shortcut-keys-block title="终端快捷键"
:type="TerminalShortcutType.TERMINAL"
:items="shortcutKeys"
@set-editable="setEditableStatus"
@clear-editable="clearEditableStatus"
@update-enabled="updateEnabledStatus" />
</a-spin>
</div>
</div>
</template>
@@ -16,7 +45,199 @@
</script>
<script lang="ts" setup>
import TerminalShortcutBlock from './terminal-shortcut-block.vue';
import type { TerminalShortcutKeyEditable, TerminalShortcutSetting } from '@/store/modules/terminal/types';
import { ref, onMounted, onUnmounted } from 'vue';
import { getDefaultPreference, getPreference } from '@/api/user/preference';
import { TerminalPreferenceItem } from '@/store/modules/terminal';
import { TerminalShortcutItems, TerminalShortcutType } from '../../types/terminal.const';
import { useTerminalStore } from '@/store';
import useLoading from '@/hooks/loading';
import { useDebounceFn } from '@vueuse/core';
import { addEventListen, removeEventListen } from '@/utils/event';
import TerminalShortcutKeysBlock from './terminal-shortcut-keys-block.vue';
import TerminalShortcutActionBlock from './terminal-shortcut-action-block.vue';
const { updateTerminalPreference } = useTerminalStore();
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);
} 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>

View File

@@ -9,11 +9,8 @@ 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];
this.active = InnerTabs.NEW_CONNECTION.key;
this.items = [InnerTabs.NEW_CONNECTION];
}
// 点击 tab