feat: 偏好设置的快捷键列表追加ESC快捷键的控制(关闭当前窗口) (#7947)

* feat: 快捷键追加ESC控制,关闭当前窗口

* feat: 偏好设置中,页面切换动画的颜色看不清的问题(使用当前主题色)

* feat: 三种弹出框支持快捷键ESC动作

* feat: 代码自动格式化(3个框架改动)

* feat: 代码自动格式化(3个框架改动)

* fix: 修正locale数据获取方式

* 单元测试问题修改

* 单元测试问题修改

* fix: 解决代码评论的问题

* fix: 解决代码评论的问题

* fix: 解决代码评论的问题

* fix: 解决代码评论的问题

* 单元测试问题修改

* fix: 解决评论问题

* fix: 解决代码格式导致pnpm run lint报错的问题

---------

Co-authored-by: PanFu <panfu@zhihuaai.com>
master^2
PanFu 2026-05-23 09:50:35 +08:00 committed by GitHub
parent e98f0b7558
commit f813245827
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 98 additions and 18 deletions

View File

@ -75,6 +75,7 @@ exports[`defaultPreferences immutability test > should not modify the config obj
},
"shortcutKeys": {
"enable": true,
"globalEscape": false,
"globalLockScreen": true,
"globalLogout": true,
"globalPreferences": true,

View File

@ -76,6 +76,7 @@ const defaultPreferences: Preferences = {
},
shortcutKeys: {
enable: true,
globalEscape: false,
globalLockScreen: true,
globalLogout: true,
globalPreferences: true,

View File

@ -135,7 +135,7 @@ class PreferenceManager {
const cachedPreferences = (await this.loadFromCache()) || {};
const mergedPreference = merge(
{},
cachedPreferences, // 用户缓存的设置优先
cachedPreferences, // 用户缓存的设置优先
this.initialPreferences, // 初始设置仅补齐缺失字段
);

View File

@ -277,6 +277,8 @@ interface SidebarPreferences {
interface ShortcutKeyPreferences {
/** 是否启用快捷键-全局 */
enable: boolean;
/** 是否启用全局关闭窗口快捷键 */
globalEscape: boolean;
/** 是否启用全局锁屏快捷键 */
globalLockScreen: boolean;
/** 是否启用全局注销快捷键 */

View File

@ -39,7 +39,7 @@ function usePreferences() {
});
const locale = computed(() => {
return preferences.app.locale;
return appPreferences.value.locale;
});
const isMobile = computed(() => {
@ -185,6 +185,14 @@ function usePreferences() {
return enable && globalLogout;
});
/**
* @zh_CN
*/
const globalEscapeShortcutKey = computed(() => {
const { enable, globalEscape } = shortcutKeysPreferences.value;
return enable && globalEscape;
});
const globalLockScreenShortcutKey = computed(() => {
const { enable, globalLockScreen } = shortcutKeysPreferences.value;
return enable && globalLockScreen;
@ -247,6 +255,7 @@ function usePreferences() {
diffCustomPreference,
globalLockScreenShortcutKey,
globalLogoutShortcutKey,
globalEscapeShortcutKey,
globalSearchShortcutKey,
isDark,
isFullContent,
@ -265,6 +274,7 @@ function usePreferences() {
preferencesButtonPosition,
sidebarCollapsed,
theme,
app: appPreferences.value,
};
}

View File

@ -42,6 +42,7 @@
"dependencies": {
"@vben-core/composables": "workspace:*",
"@vben-core/icons": "workspace:*",
"@vben-core/preferences": "workspace:*",
"@vben-core/shadcn-ui": "workspace:*",
"@vben-core/shared": "workspace:*",
"@vben-core/typings": "workspace:*",

View File

@ -36,6 +36,8 @@ export type AlertProps = {
contentClass?: string;
/** 执行beforeClose回调期间在内容区域显示一个loading遮罩*/
contentMasking?: boolean;
/** 按下Esc时是否关闭弹窗 */
escapeKeyClose?: boolean;
/** 弹窗底部内容(与按钮在同一个容器中) */
footer?: Component | string;
/** 弹窗的图标(在标题的前面) */

View File

@ -14,6 +14,7 @@ import {
Info,
X,
} from '@vben-core/icons';
import { usePreferences } from '@vben-core/preferences';
import {
AlertDialog,
AlertDialogAction,
@ -34,8 +35,10 @@ const props = withDefaults(defineProps<AlertProps>(), {
bordered: true,
buttonAlign: 'end',
centered: true,
escapeKeyClose: true,
});
const emits = defineEmits(['closed', 'confirm', 'opened']);
const { globalEscapeShortcutKey } = usePreferences();
const open = defineModel<boolean>('open', { default: false });
const { $t } = useSimpleLocale();
const components = globalShareState.getComponents();
@ -46,8 +49,14 @@ function onAlertClosed() {
isConfirm.value = false;
}
function onEscapeKeyDown() {
function onEscapeKeyDown(e: KeyboardEvent) {
// Esc isConfirm
isConfirm.value = false;
// falsetrueesc
if (!props.escapeKeyClose && !globalEscapeShortcutKey.value) {
e.preventDefault();
}
}
const getIconRender = computed(() => {
@ -143,7 +152,7 @@ async function handleOpenChange(val: boolean) {
:overlay-blur="overlayBlur"
@opened="emits('opened')"
@closed="onAlertClosed"
@escape-key-down="onEscapeKeyDown"
@escape-key-down="onEscapeKeyDown($event)"
:class="
cn(
containerClass,

View File

@ -54,7 +54,7 @@ const components = globalShareState.getComponents();
const id = useId();
provide('DISMISSABLE_DRAWER_ID', id);
const wrapperRef = ref<HTMLElement>();
// const wrapperRef = ref<HTMLElement>();
const { $t } = useSimpleLocale();
const { isMobile } = useIsMobile();
@ -285,8 +285,8 @@ const getForceMount = computed(() => {
<SheetDescription />
</VisuallyHidden>
</template>
<!-- 注释掉的部分 <div ref="wrapperRef" -->
<div
ref="wrapperRef"
:class="
cn('relative flex-1 overflow-y-auto p-3', contentClass, {
'pointer-events-none': showLoading || submitting,

View File

@ -14,6 +14,7 @@ import {
ref,
} from 'vue';
import { usePreferences } from '@vben-core/preferences';
import { useStore } from '@vben-core/shared/store';
import { DrawerApi } from './drawer-api';
@ -21,6 +22,11 @@ import VbenDrawer from './drawer.vue';
const USER_DRAWER_INJECT_KEY = Symbol('VBEN_DRAWER_INJECT');
const { globalEscapeShortcutKey } = usePreferences();
/**
*
*/
const DEFAULT_DRAWER_PROPS: Partial<DrawerProps> = {};
export function setDefaultDrawerProps(props: Partial<DrawerProps>) {
@ -33,6 +39,10 @@ export function useVbenDrawer<
// Drawer一般会抽离出来所以如果有传入 connectedComponent则表示为外部调用与内部组件进行连接
// 外部的Drawer通过provide/inject传递api
const defaultOptions = {
closeOnPressEscape: globalEscapeShortcutKey.value, // 全局Esc快捷键配置
...options,
};
const { connectedComponent } = options;
if (connectedComponent) {
const extendedApi = reactive({});
@ -45,7 +55,7 @@ export function useVbenDrawer<
// 不能用 Object.assign,会丢失 api 的原型函数
Object.setPrototypeOf(extendedApi, api);
},
options,
defaultOptions,
async reCreateDrawer() {
isDrawerReady.value = false;
await nextTick();
@ -79,7 +89,7 @@ export function useVbenDrawer<
const mergedOptions = {
...DEFAULT_DRAWER_PROPS,
...injectData.options,
...options,
...defaultOptions,
} as DrawerApiOptions;
mergedOptions.onOpenChange = (isOpen: boolean) => {

View File

@ -10,6 +10,7 @@ import {
ref,
} from 'vue';
import { usePreferences } from '@vben-core/preferences';
import { useStore } from '@vben-core/shared/store';
import { ModalApi } from './modal-api';
@ -17,6 +18,10 @@ import VbenModal from './modal.vue';
const USER_MODAL_INJECT_KEY = Symbol('VBEN_MODAL_INJECT');
const { globalEscapeShortcutKey } = usePreferences();
/**
*
*/
const DEFAULT_MODAL_PROPS: Partial<ModalProps> = {};
export function setDefaultModalProps(props: Partial<ModalProps>) {
@ -29,6 +34,10 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
// Modal一般会抽离出来所以如果有传入 connectedComponent则表示为外部调用与内部组件进行连接
// 外部的Modal通过provide/inject传递api
const defaultOptions = {
closeOnPressEscape: globalEscapeShortcutKey.value, // 全局Esc快捷键配置
...options,
};
const { connectedComponent } = options;
if (connectedComponent) {
const extendedApi = reactive({});
@ -42,7 +51,7 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
Object.setPrototypeOf(extendedApi, api);
},
consumed: false,
options,
defaultOptions,
async reCreateModal() {
isModalReady.value = false;
await nextTick();
@ -85,7 +94,7 @@ export function useVbenModal<TParentModalProps extends ModalProps = ModalProps>(
const mergedOptions = {
...DEFAULT_MODAL_PROPS,
...injectData.options,
...options,
...defaultOptions,
} as ModalApiOptions;
mergedOptions.onOpenChange = (isOpen: boolean) => {

View File

@ -45,7 +45,10 @@ function handleClick(value: string) {
class="outline-box p-2"
@click="handleClick(item)"
>
<div :class="`${item}-slow`" class="h-10 w-12 rounded-md bg-accent"></div>
<div
:class="`${item}-slow`"
class="h-10 w-12 rounded-md bg-primary"
></div>
</div>
</div>
</template>

View File

@ -17,6 +17,7 @@ const shortcutKeysGlobalSearch = defineModel<boolean>(
const shortcutKeysLogout = defineModel<boolean>('shortcutKeysLogout');
// const shortcutKeysPreferences = defineModel<boolean>('shortcutKeysPreferences');
const shortcutKeysLockScreen = defineModel<boolean>('shortcutKeysLockScreen');
const shortcutKeysEscape = defineModel<boolean>('shortcutKeysEscape');
const altView = computed(() => (isWindowsOs() ? 'Alt' : '⌥'));
</script>
@ -47,4 +48,8 @@ const altView = computed(() => (isWindowsOs() ? 'Alt' : '⌥'));
{{ $t('ui.widgets.lockScreen.title') }}
<template #shortcut> {{ altView }} L </template>
</SwitchItem>
<SwitchItem v-model="shortcutKeysEscape" :disabled="!shortcutKeysEnable">
{{ $t('preferences.shortcutKeys.escape') }}
<template #shortcut> Esc </template>
</SwitchItem>
</template>

View File

@ -166,6 +166,9 @@ const shortcutKeysGlobalSearch = defineModel<boolean>(
const shortcutKeysGlobalLogout = defineModel<boolean>(
'shortcutKeysGlobalLogout',
);
const shortcutKeysGlobalEscape = defineModel<boolean>(
'shortcutKeysGlobalEscape',
);
const shortcutKeysGlobalLockScreen = defineModel<boolean>(
'shortcutKeysGlobalLockScreen',
@ -520,6 +523,7 @@ function handleCustomPreferencesUpdate(updates: CustomPreferencesRecord) {
v-model:shortcut-keys-global-search="shortcutKeysGlobalSearch"
v-model:shortcut-keys-lock-screen="shortcutKeysGlobalLockScreen"
v-model:shortcut-keys-logout="shortcutKeysGlobalLogout"
v-model:shortcut-keys-escape="shortcutKeysGlobalEscape"
/>
</Block>
</template>

View File

@ -187,6 +187,7 @@
"global": "Global",
"search": "Global Search",
"logout": "Logout",
"escape": "Close Current Window",
"preferences": "Preferences"
},
"widget": {

View File

@ -187,6 +187,7 @@
"global": "全局",
"search": "全局搜索",
"logout": "退出登录",
"escape": "关闭当前窗口",
"preferences": "偏好设置"
},
"widget": {

View File

@ -1006,14 +1006,14 @@ importers:
version: 2.9.7(vue@3.5.34(typescript@6.0.3))
vitepress-plugin-group-icons:
specifier: 'catalog:'
version: 1.7.5(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))
version: 1.7.5(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))
devDependencies:
'@nolebase/vitepress-plugin-git-changelog':
specifier: 'catalog:'
version: 2.18.2(vitepress@2.0.0-alpha.17(@types/node@25.9.1)(async-validator@4.2.5)(axios@1.16.1)(change-case@5.4.4)(jiti@2.7.0)(less@4.6.4)(lightningcss@1.32.0)(nprogress@0.2.0)(postcss@8.5.15)(qrcode@1.5.4)(sass-embedded@1.100.0)(sass@1.100.0)(sortablejs@1.15.7)(terser@5.48.0)(typescript@6.0.3)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3))
'@tailwindcss/vite':
specifier: 'catalog:'
version: 4.3.0(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))
version: 4.3.0(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))
'@vben/tailwind-config':
specifier: workspace:*
version: link:../internal/tailwind-config
@ -1022,7 +1022,7 @@ importers:
version: link:../internal/vite-config
'@vite-pwa/vitepress':
specifier: 'catalog:'
version: 1.1.0(vite-plugin-pwa@1.3.0(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1)(workbox-window@7.4.1))
version: 1.1.0(vite-plugin-pwa@1.3.0(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1)(workbox-window@7.4.1))
vitepress:
specifier: 'catalog:'
version: 2.0.0-alpha.17(@types/node@25.9.1)(async-validator@4.2.5)(axios@1.16.1)(change-case@5.4.4)(jiti@2.7.0)(less@4.6.4)(lightningcss@1.32.0)(nprogress@0.2.0)(postcss@8.5.15)(qrcode@1.5.4)(sass-embedded@1.100.0)(sass@1.100.0)(sortablejs@1.15.7)(terser@5.48.0)(typescript@6.0.3)(yaml@2.9.0)
@ -1522,6 +1522,9 @@ importers:
'@vben-core/icons':
specifier: workspace:*
version: link:../../base/icons
'@vben-core/preferences':
specifier: workspace:*
version: link:../../preferences
'@vben-core/shadcn-ui':
specifier: workspace:*
version: link:../shadcn-ui
@ -14279,6 +14282,13 @@ snapshots:
tailwindcss: 4.3.0
vite: 8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0)
'@tailwindcss/vite@4.3.0(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))':
dependencies:
'@tailwindcss/node': 4.3.0
'@tailwindcss/oxide': 4.3.0
tailwindcss: 4.3.0
vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0)
'@tanstack/match-sorter-utils@8.19.4':
dependencies:
remove-accents: 0.5.0
@ -15015,9 +15025,9 @@ snapshots:
- rollup
- supports-color
'@vite-pwa/vitepress@1.1.0(vite-plugin-pwa@1.3.0(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1)(workbox-window@7.4.1))':
'@vite-pwa/vitepress@1.1.0(vite-plugin-pwa@1.3.0(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1)(workbox-window@7.4.1))':
dependencies:
vite-plugin-pwa: 1.3.0(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1)(workbox-window@7.4.1)
vite-plugin-pwa: 1.3.0(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1)(workbox-window@7.4.1)
'@vitejs/plugin-vue-jsx@5.1.5(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3))':
dependencies:
@ -20397,6 +20407,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
vite-plugin-pwa@1.3.0(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))(workbox-build@7.4.1)(workbox-window@7.4.1):
dependencies:
debug: 4.4.3
pretty-bytes: 6.1.1
tinyglobby: 0.2.16
vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0)
workbox-build: 7.4.1
workbox-window: 7.4.1
transitivePeerDependencies:
- supports-color
vite-plugin-vue-devtools@8.1.2(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0))(vue@3.5.34(typescript@6.0.3)):
dependencies:
'@vue/devtools-core': 8.1.2(vue@3.5.34(typescript@6.0.3))
@ -20481,13 +20502,13 @@ snapshots:
terser: 5.48.0
yaml: 2.9.0
vitepress-plugin-group-icons@1.7.5(vite@8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0)):
vitepress-plugin-group-icons@1.7.5(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0)):
dependencies:
'@iconify-json/logos': 1.2.11
'@iconify-json/vscode-icons': 1.2.50
'@iconify/utils': 3.1.3
optionalDependencies:
vite: 8.0.10(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0)
vite: 8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(less@4.6.4)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(yaml@2.9.0)
vitepress@2.0.0-alpha.17(@types/node@25.9.1)(async-validator@4.2.5)(axios@1.16.1)(change-case@5.4.4)(jiti@2.7.0)(less@4.6.4)(lightningcss@1.32.0)(nprogress@0.2.0)(postcss@8.5.15)(qrcode@1.5.4)(sass-embedded@1.100.0)(sass@1.100.0)(sortablejs@1.15.7)(terser@5.48.0)(typescript@6.0.3)(yaml@2.9.0):
dependencies: