diff --git a/package.json b/package.json index 4b6560a..66a86e8 100644 --- a/package.json +++ b/package.json @@ -9,12 +9,10 @@ "preview": "vite preview" }, "dependencies": { - "mousetrap": "^1.6.5", "vue": "^3.5.13", "vue-router": "^4.5.0" }, "devDependencies": { - "@types/mousetrap": "^1.6.15", "@types/node": "^22.10.1", "@vitejs/plugin-vue": "^5.2.1", "sass-embedded": "^1.82.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d88be0e..818e04c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,6 @@ importers: .: dependencies: - mousetrap: - specifier: ^1.6.5 - version: 1.6.5 vue: specifier: ^3.5.13 version: 3.5.13(typescript@5.7.2) @@ -18,9 +15,6 @@ importers: specifier: ^4.5.0 version: 4.5.0(vue@3.5.13(typescript@5.7.2)) devDependencies: - '@types/mousetrap': - specifier: ^1.6.15 - version: 1.6.15 '@types/node': specifier: ^22.10.1 version: 22.10.1 @@ -304,9 +298,6 @@ packages: '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} - '@types/mousetrap@1.6.15': - resolution: {integrity: sha512-qL0hyIMNPow317QWW/63RvL1x5MVMV+Ru3NaY9f/CuEpCqrmb7WeuK2071ZY5hczOnm38qExWM2i2WtkXLSqFw==} - '@types/node@22.10.1': resolution: {integrity: sha512-qKgsUwfHZV2WCWLAnVP1JqnpE6Im6h3Y0+fYgMTasNQ7V++CBX5OT1as0g0f+OyubbFqhf6XVNIsmN4IIhEgGQ==} @@ -385,9 +376,6 @@ packages: magic-string@0.30.15: resolution: {integrity: sha512-zXeaYRgZ6ldS1RJJUrMrYgNJ4fdwnyI6tVqoiIhyCyv5IVTK9BU8Ic2l253GGETQHxI4HNUwhJ3fjDhKqEoaAw==} - mousetrap@1.6.5: - resolution: {integrity: sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==} - nanoid@3.3.8: resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -766,8 +754,6 @@ snapshots: '@types/estree@1.0.6': {} - '@types/mousetrap@1.6.15': {} - '@types/node@22.10.1': dependencies: undici-types: 6.20.0 @@ -881,8 +867,6 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 - mousetrap@1.6.5: {} - nanoid@3.3.8: {} picocolors@1.1.1: {} diff --git a/src/components/ProjectList.vue b/src/components/ProjectList.vue index 7998e51..2d10dba 100644 --- a/src/components/ProjectList.vue +++ b/src/components/ProjectList.vue @@ -2,8 +2,7 @@ import VirtualList from './VirtualList.vue' import { ref, onMounted, onBeforeUnmount, computed } from 'vue' import type { ListItem, MenuItem } from '@/types' -import Mousetrap from 'mousetrap' -import 'mousetrap/plugins/global-bind/mousetrap-global-bind' +import { keyboardManager } from '@/utils/KeyboardManager' /** * 组件属性接口 @@ -54,7 +53,7 @@ const toastMessage = ref('') /** 是否正在使用键盘导航菜单 */ const isKeyboardNavigation = ref(false) /** 快捷键处理函数映射 */ -const shortcutHandlers = new Map void>() +const shortcutHandlers = new Map void>() // ===== 定时器 ===== /** 鼠标悬停禁用定时器 */ @@ -105,12 +104,10 @@ const closeMenu = (): void => { isKeyboardNavigation.value = false // 清除快捷键绑定 - shortcutHandlers.forEach((_, shortcut) => { - Mousetrap.unbind(shortcut) - }) + keyboardManager.disable() shortcutHandlers.clear() - // 临时禁用鼠标悬停,防止菜单关闭时立即触发悬停效果 + // 临时禁用鼠标悬停,防止菜单关闭时���即触发悬停效果 temporaryDisableHover.value = true if (hoverDisableTimer) clearTimeout(hoverDisableTimer) hoverDisableTimer = window.setTimeout(() => { @@ -158,7 +155,7 @@ const handleMenuTriggerClick = (e: MouseEvent): void => { * - 左方向键/ESC:关闭菜单 * - 上下方向键:在菜单打开时导航菜单项 * - 回车键:在菜单打开时选择当前菜单项 - * @param e 键盘事件对象 + * @param e 键盘事件��象 */ const handleKeyDown = (e: KeyboardEvent): void => { if (e.key === 'ArrowRight' && !showMenu.value && selectedItem.value) { @@ -181,7 +178,7 @@ const handleKeyDown = (e: KeyboardEvent): void => { case 'ArrowDown': e.preventDefault() isKeyboardNavigation.value = true - // 向下选��菜单项,到底部时循环到顶部 + // 向下选择菜单项,到底部时循环到顶部 selectedMenuIndex.value = selectedMenuIndex.value >= currentMenuItems.value.length - 1 ? 0 @@ -232,10 +229,7 @@ onBeforeUnmount(() => { document.removeEventListener('keydown', handleKeyDown) if (toastTimer) clearTimeout(toastTimer) if (hoverDisableTimer) clearTimeout(hoverDisableTimer) - // 清除所有快捷键绑定 - shortcutHandlers.forEach((_, shortcut) => { - Mousetrap.unbind(shortcut) - }) + keyboardManager.disable() }) // ===== 过渡钩子 ===== @@ -257,46 +251,31 @@ const onAfterLeave = (el: Element): void => { (el as HTMLElement).style.display = '' } -// 在导入后添加 Mousetrap 配置 -Mousetrap.prototype.stopCallback = (e: KeyboardEvent, element: HTMLElement, combo: string) => { - // 当菜单打开时,总是响应快捷键 - if (showMenu.value) { - e.preventDefault() - e.stopPropagation() - return false - } - // 否则使用默认行为 - return element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA' -} - // 修改 bindShortcuts 函数 const bindShortcuts = (): void => { // 清除之前的绑定 shortcutHandlers.forEach((_, shortcut) => { - Mousetrap.unbind(shortcut) + keyboardManager.unbind(shortcut) }) shortcutHandlers.clear() // 只在菜单打开时绑定快捷键 if (showMenu.value) { + keyboardManager.enable() currentMenuItems.value.forEach((item) => { if (item.shortcut) { - const handler = (e: ExtendedKeyboardEvent) => { - // 阻止浏览器默认行为 - e.preventDefault() - e.stopPropagation() - + const handler = (e: KeyboardEvent) => { if (selectedItem.value || !item.action.toString().includes('selectedItem')) { item.action(selectedItem.value) - // 执行后关闭菜单 closeMenu() } - return false // 阻止事件冒泡 } shortcutHandlers.set(item.shortcut, handler) - Mousetrap.bindGlobal(item.shortcut, handler) + keyboardManager.bind(item.shortcut, handler) } }) + } else { + keyboardManager.disable() } } @@ -438,7 +417,7 @@ const bindShortcuts = (): void => { min-height: 0; } -/* 工具栏:固定高度,顶部边框和阴影效果 */ +/* 工具栏固定高度,顶部边框和阴影效果 */ .toolbar { height: 40px; background-color: #fff; diff --git a/src/pages/Project.vue b/src/pages/Project.vue index 875e906..4e55d82 100644 --- a/src/pages/Project.vue +++ b/src/pages/Project.vue @@ -26,7 +26,7 @@ const initListData = () => { action: (item) => { console.log('打开项目:', item?.name) }, - shortcut: 'ctrl+o' + shortcut: 'alt+o' }, { id: 'edit', diff --git a/src/utils/KeyboardManager.ts b/src/utils/KeyboardManager.ts new file mode 100644 index 0000000..2cb1327 --- /dev/null +++ b/src/utils/KeyboardManager.ts @@ -0,0 +1,116 @@ +type KeyboardCallback = (e: KeyboardEvent) => void + +export class KeyboardManager { + private shortcuts: Map = new Map() + private pressedKeys: Set = new Set() + private enabled: boolean = false + + constructor() { + this.handleKeyDown = this.handleKeyDown.bind(this) + this.handleKeyUp = this.handleKeyUp.bind(this) + } + + /** + * 启用快捷键监听 + */ + enable(): void { + if (!this.enabled) { + window.addEventListener('keydown', this.handleKeyDown) + window.addEventListener('keyup', this.handleKeyUp) + this.enabled = true + } + } + + /** + * 禁用快捷键监听 + */ + disable(): void { + if (this.enabled) { + window.removeEventListener('keydown', this.handleKeyDown) + window.removeEventListener('keyup', this.handleKeyUp) + this.enabled = false + this.pressedKeys.clear() + } + } + + /** + * 绑定快捷键 + */ + bind(shortcut: string, callback: KeyboardCallback): void { + this.shortcuts.set(this.normalizeShortcut(shortcut), callback) + } + + /** + * 解绑快捷键 + */ + unbind(shortcut: string): void { + this.shortcuts.delete(this.normalizeShortcut(shortcut)) + } + + /** + * 清除所有快捷键绑定 + */ + clear(): void { + this.shortcuts.clear() + this.pressedKeys.clear() + } + + private handleKeyDown(e: KeyboardEvent): void { + // 忽略在输入框中的按键 + if (this.shouldIgnoreInput(e)) { + return + } + + const key = this.normalizeKey(e.key.toLowerCase()) + this.pressedKeys.add(key) + + const currentShortcut = Array.from(this.pressedKeys).sort().join('+') + const callback = this.shortcuts.get(currentShortcut) + + if (callback) { + e.preventDefault() + e.stopPropagation() + callback(e) + } + } + + private handleKeyUp(e: KeyboardEvent): void { + const key = this.normalizeKey(e.key.toLowerCase()) + this.pressedKeys.delete(key) + } + + private normalizeKey(key: string): string { + const keyMap: Record = { + 'control': 'ctrl', + 'command': 'cmd', + 'meta': 'cmd', + 'escape': 'esc', + ' ': 'space', + 'arrowup': 'up', + 'arrowdown': 'down', + 'arrowleft': 'left', + 'arrowright': 'right', + } + return keyMap[key] || key + } + + private normalizeShortcut(shortcut: string): string { + return shortcut + .toLowerCase() + .split('+') + .map(key => this.normalizeKey(key.trim())) + .sort() + .join('+') + } + + private shouldIgnoreInput(e: KeyboardEvent): boolean { + const element = e.target as HTMLElement + return element.tagName === 'INPUT' || + element.tagName === 'TEXTAREA' || + element.tagName === 'SELECT' || + element.isContentEditable + } +} + +// 导出单例实例 +export const keyboardManager = new KeyboardManager() \ No newline at end of file