mirror of
https://github.com/fantastic-admin/basic.git
synced 2026-10-06 12:03:02 +08:00
feat: 新增 hotkeys 子包,统一维护全局快捷键
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
"dependencies": {
|
||||
"@fantastic-admin/components": "workspace:*",
|
||||
"@fantastic-admin/composables": "workspace:*",
|
||||
"@fantastic-admin/hotkeys": "workspace:*",
|
||||
"@fantastic-admin/settings": "workspace:*",
|
||||
"@fantastic-admin/types": "workspace:*",
|
||||
"@vee-validate/zod": "catalog:",
|
||||
@@ -28,7 +29,6 @@
|
||||
"dayjs": "catalog:",
|
||||
"eruda": "catalog:",
|
||||
"es-toolkit": "catalog:",
|
||||
"hotkeys-js": "catalog:",
|
||||
"mitt": "catalog:",
|
||||
"nprogress": "catalog:",
|
||||
"path-browserify": "catalog:",
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
# Hotkeys 设计说明
|
||||
|
||||
这个目录参考 `fantastic-admin/v6-dev/apps/example/src/hotkeys` 的设计,目标是把当前项目里分散在各组件中的快捷键逻辑收拢成一套轻量方案。
|
||||
|
||||
## 设计目标
|
||||
|
||||
当前方案只解决 4 件事:
|
||||
|
||||
1. 快捷键定义不要散落在多个组件里重复写
|
||||
2. 快捷键帮助面板不要再手写一份展示文案
|
||||
3. 业务组件不要反复手写 `hotkeys()` / `unbind()`
|
||||
4. 不引入过重的 command system
|
||||
|
||||
所以采用:
|
||||
|
||||
- `registry.ts` 统一定义
|
||||
- `useHotkeys.ts` 统一注册
|
||||
- 业务 handler 仍留在业务组件
|
||||
|
||||
这是一种“集中定义 + 就地执行”的轻量折中方案。
|
||||
|
||||
## 目录职责
|
||||
|
||||
### `registry.ts`
|
||||
|
||||
统一维护当前项目的快捷键元数据:
|
||||
|
||||
- id
|
||||
- keys
|
||||
- enabled
|
||||
- help
|
||||
|
||||
这里负责:
|
||||
|
||||
- 统一的 id 常量
|
||||
- 真实键位
|
||||
- 是否启用的条件
|
||||
- 帮助面板展示文案
|
||||
- 聚合业务扩展
|
||||
|
||||
> 注意:这里不放业务逻辑 handler。
|
||||
|
||||
### `registry.extend.ts`
|
||||
|
||||
用于放置项目自定义扩展快捷键,避免把业务项全部堆进内建定义中。
|
||||
|
||||
### `types.ts`
|
||||
|
||||
定义快捷键系统的基础类型,包括:
|
||||
|
||||
- `HotkeyBinding`
|
||||
- `HotkeyContext`
|
||||
- `HotkeyHelpMeta`
|
||||
- `HotkeyHandler`
|
||||
|
||||
### `useHotkeys.ts`
|
||||
|
||||
对 `hotkeys-js` 的薄封装,提供:
|
||||
|
||||
- `useHotkey`:注册单个快捷键
|
||||
- `useHotkeyBindings`:批量注册快捷键
|
||||
|
||||
并统一处理:
|
||||
|
||||
- 注册 / 卸载
|
||||
- `active` 启停
|
||||
- `binding.enabled` 可用性判断
|
||||
- 默认 `preventDefault`
|
||||
|
||||
## 当前项目已落地的快捷键
|
||||
|
||||
### 全局
|
||||
|
||||
- `system.info.open`
|
||||
- `menuSearch.open`
|
||||
- `page.reload`
|
||||
|
||||
### 主导航
|
||||
|
||||
- `menu.next`
|
||||
- `menu.prev`
|
||||
|
||||
### 标签栏
|
||||
|
||||
- `tabbar.prev`
|
||||
- `tabbar.next`
|
||||
- `tabbar.closeCurrent`
|
||||
- `tabbar.gotoVisibleIndex`
|
||||
- `tabbar.gotoLast`
|
||||
|
||||
### 菜单搜索弹窗
|
||||
|
||||
- `menuSearch.moveUp`
|
||||
- `menuSearch.moveDown`
|
||||
- `menuSearch.confirm`
|
||||
- `menuSearch.close`
|
||||
|
||||
## 使用约定
|
||||
|
||||
1. 新增快捷键,优先在 `registry.ts` 或 `registry.extend.ts` 补定义
|
||||
2. 展示在帮助面板中的快捷键,必须配置 `help`
|
||||
3. handler 尽量保留在最了解业务的组件中
|
||||
4. 不直接在业务组件里继续散写 `hotkeys('xxx')`
|
||||
|
||||
## 现阶段差异
|
||||
|
||||
参考项目里还有页面最大化相关快捷键;当前项目暂未引入对应页面最大化能力,所以本次迁移只保留当前项目已有功能,并把原有 `F5` 页面刷新也纳入统一 registry 管理。
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { HotkeyBinding } from '@fantastic-admin/hotkeys'
|
||||
|
||||
/**
|
||||
* 业务扩展快捷键 id
|
||||
*
|
||||
* 使用方式:
|
||||
*
|
||||
* export const EXT_HOTKEY_ID = {
|
||||
* demoOpen: 'demo.open',
|
||||
* } as const
|
||||
*/
|
||||
export const EXT_HOTKEY_ID = {} as const
|
||||
|
||||
type ExtendHotkeyId = typeof EXT_HOTKEY_ID[keyof typeof EXT_HOTKEY_ID]
|
||||
|
||||
/**
|
||||
* 业务扩展全局快捷键
|
||||
*
|
||||
* 示例:
|
||||
*
|
||||
* export const extendGlobalHotkeyBindings = [
|
||||
* {
|
||||
* id: EXT_HOTKEY_ID.demoOpen,
|
||||
* keys: ['command+j', 'ctrl+j'],
|
||||
* enabled: ctx => ctx.settings.toolbar.menuSearch.enable,
|
||||
* help: {
|
||||
* group: 'global',
|
||||
* titleKey: 'global.demoOpen',
|
||||
* order: 90,
|
||||
* visible: ctx => ctx.settings.toolbar.menuSearch.enable,
|
||||
* displayKeys: {
|
||||
* default: ['Ctrl', 'J'],
|
||||
* mac: ['⌘', 'J'],
|
||||
* },
|
||||
* },
|
||||
* },
|
||||
* ] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
*/
|
||||
export const extendGlobalHotkeyBindings = [] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
|
||||
/**
|
||||
* 业务扩展局部快捷键
|
||||
*
|
||||
* 示例:
|
||||
*
|
||||
* export const extendScopedHotkeyBindings = [
|
||||
* {
|
||||
* id: EXT_HOTKEY_ID.demoConfirm,
|
||||
* keys: ['enter'],
|
||||
* },
|
||||
* {
|
||||
* id: EXT_HOTKEY_ID.demoClose,
|
||||
* keys: ['esc'],
|
||||
* },
|
||||
* ] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
*/
|
||||
export const extendScopedHotkeyBindings = [] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
@@ -0,0 +1,41 @@
|
||||
import type {
|
||||
HotkeyBinding,
|
||||
HotkeyContextBase,
|
||||
HotkeyDisplayKeys,
|
||||
HotkeyHandler,
|
||||
HotkeyHandlerArgs,
|
||||
HotkeyHandlersMap,
|
||||
HotkeyHelpGroup,
|
||||
HotkeyHelpMeta,
|
||||
} from '@fantastic-admin/hotkeys'
|
||||
import { createHotkeys } from '@fantastic-admin/hotkeys'
|
||||
import { EXT_HOTKEY_ID, extendGlobalHotkeyBindings, extendScopedHotkeyBindings } from './extend'
|
||||
|
||||
const appHotkeys = createHotkeys({
|
||||
extendIds: EXT_HOTKEY_ID,
|
||||
extendGlobalHotkeyBindings,
|
||||
extendScopedHotkeyBindings,
|
||||
getContext: () => ({
|
||||
settings: useAppSettingsStore().settings,
|
||||
}),
|
||||
})
|
||||
|
||||
export const HOTKEY_ID = appHotkeys.HOTKEY_ID
|
||||
export const hotkeyIds = appHotkeys.hotkeyIds
|
||||
export const globalHotkeyBindings = appHotkeys.globalHotkeyBindings
|
||||
export const menuSearchHotkeyBindings = appHotkeys.menuSearchHotkeyBindings
|
||||
export const hotkeyBindings = appHotkeys.hotkeyBindings
|
||||
export const useHotkey = appHotkeys.useHotkey
|
||||
export const useHotkeyBindings = appHotkeys.useHotkeyBindings
|
||||
|
||||
export type HotkeyId = typeof HOTKEY_ID[keyof typeof HOTKEY_ID]
|
||||
export type {
|
||||
HotkeyBinding,
|
||||
HotkeyContextBase,
|
||||
HotkeyDisplayKeys,
|
||||
HotkeyHandler,
|
||||
HotkeyHandlerArgs,
|
||||
HotkeyHandlersMap,
|
||||
HotkeyHelpGroup,
|
||||
HotkeyHelpMeta,
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import type { HotkeyBinding } from './types'
|
||||
|
||||
/**
|
||||
* 业务扩展快捷键 id
|
||||
*
|
||||
* 使用方式:
|
||||
*
|
||||
* export const EXT_HOTKEY_ID = {
|
||||
* demoOpen: 'demo.open',
|
||||
* } as const
|
||||
*/
|
||||
export const EXT_HOTKEY_ID = {} as const
|
||||
|
||||
/**
|
||||
* 业务扩展全局快捷键
|
||||
*/
|
||||
export const extendGlobalHotkeyBindings: HotkeyBinding[] = []
|
||||
|
||||
/**
|
||||
* 业务扩展局部快捷键
|
||||
*/
|
||||
export const extendScopedHotkeyBindings: HotkeyBinding[] = []
|
||||
@@ -1,210 +0,0 @@
|
||||
import type { HotkeyBinding } from './types'
|
||||
import { EXT_HOTKEY_ID, extendGlobalHotkeyBindings, extendScopedHotkeyBindings } from './registry.extend'
|
||||
|
||||
export const HOTKEY_ID = {
|
||||
systemInfoOpen: 'system.info.open',
|
||||
menuSearchOpen: 'menuSearch.open',
|
||||
menuNext: 'menu.next',
|
||||
menuPrev: 'menu.prev',
|
||||
tabbarPrev: 'tabbar.prev',
|
||||
tabbarNext: 'tabbar.next',
|
||||
tabbarCloseCurrent: 'tabbar.closeCurrent',
|
||||
tabbarGotoVisibleIndex: 'tabbar.gotoVisibleIndex',
|
||||
tabbarGotoLast: 'tabbar.gotoLast',
|
||||
pageReload: 'page.reload',
|
||||
menuSearchMoveUp: 'menuSearch.moveUp',
|
||||
menuSearchMoveDown: 'menuSearch.moveDown',
|
||||
menuSearchConfirm: 'menuSearch.confirm',
|
||||
menuSearchClose: 'menuSearch.close',
|
||||
...EXT_HOTKEY_ID,
|
||||
} as const
|
||||
|
||||
export const hotkeyIds = Object.values(HOTKEY_ID)
|
||||
|
||||
/**
|
||||
* Hotkeys registry 维护约定
|
||||
*
|
||||
* 1. 新增快捷键优先在这里补充定义,不要在业务组件里直接写死键位字符串
|
||||
* 2. 这里仅维护“定义”和“展示元数据”,不要把业务 handler 塞进 registry
|
||||
* 3. 配置类开关尽量写在 enabled / help.visible 中,避免帮助面板与真实行为不一致
|
||||
* 4. 需要展示在快捷键帮助面板中的项,必须补充 help
|
||||
* 5. 仅局部场景使用的快捷键,也建议在这里定义,再由对应组件按需注册
|
||||
*/
|
||||
export const globalHotkeyBindings: HotkeyBinding[] = [
|
||||
{
|
||||
id: HOTKEY_ID.systemInfoOpen,
|
||||
keys: ['command+i', 'ctrl+i'],
|
||||
help: {
|
||||
group: 'global',
|
||||
titleKey: 'global.system',
|
||||
order: 10,
|
||||
displayKeys: {
|
||||
default: ['Ctrl', 'I'],
|
||||
mac: ['⌘', 'I'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchOpen,
|
||||
keys: ['command+k', 'ctrl+k'],
|
||||
enabled: ctx => ctx.settings.toolbar.menuSearch.enable && ctx.settings.toolbar.menuSearch.hotkeys,
|
||||
help: {
|
||||
group: 'global',
|
||||
titleKey: 'global.search',
|
||||
order: 20,
|
||||
visible: ctx => ctx.settings.toolbar.menuSearch.enable && ctx.settings.toolbar.menuSearch.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Ctrl', 'K'],
|
||||
mac: ['⌘', 'K'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuNext,
|
||||
keys: ['alt+`'],
|
||||
enabled: ctx => ctx.settings.menu.hotkeys && ['side', 'head'].includes(ctx.settings.menu.mode),
|
||||
help: {
|
||||
group: 'nav',
|
||||
titleKey: 'nav.next',
|
||||
order: 10,
|
||||
visible: ctx => ctx.settings.menu.hotkeys && ['side', 'head'].includes(ctx.settings.menu.mode),
|
||||
displayKeys: {
|
||||
default: ['Alt', '`'],
|
||||
mac: ['⌥', '`'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuPrev,
|
||||
keys: ['alt+shift+`'],
|
||||
enabled: ctx => ctx.settings.menu.hotkeys && ['side', 'head'].includes(ctx.settings.menu.mode),
|
||||
help: {
|
||||
group: 'nav',
|
||||
titleKey: 'nav.prev',
|
||||
order: 20,
|
||||
visible: ctx => ctx.settings.menu.hotkeys && ['side', 'head'].includes(ctx.settings.menu.mode),
|
||||
displayKeys: {
|
||||
default: ['Alt', 'Shift', '`'],
|
||||
mac: ['⌥', '⇧', '`'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarPrev,
|
||||
keys: ['alt+left'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.prev',
|
||||
order: 10,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', '←'],
|
||||
mac: ['⌥', '←'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarNext,
|
||||
keys: ['alt+right'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.next',
|
||||
order: 20,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', '→'],
|
||||
mac: ['⌥', '→'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarCloseCurrent,
|
||||
keys: ['alt+w'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.close',
|
||||
order: 30,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', 'W'],
|
||||
mac: ['⌥', 'W'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarGotoVisibleIndex,
|
||||
keys: ['alt+1', 'alt+2', 'alt+3', 'alt+4', 'alt+5', 'alt+6', 'alt+7', 'alt+8', 'alt+9'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.n',
|
||||
order: 40,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', '1~9'],
|
||||
mac: ['⌥', '1~9'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarGotoLast,
|
||||
keys: ['alt+0'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.last',
|
||||
order: 50,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', '0'],
|
||||
mac: ['⌥', '0'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.pageReload,
|
||||
keys: ['f5'],
|
||||
},
|
||||
...extendGlobalHotkeyBindings,
|
||||
]
|
||||
|
||||
export const menuSearchHotkeyBindings: HotkeyBinding[] = [
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchMoveUp,
|
||||
keys: ['up'],
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchMoveDown,
|
||||
keys: ['down'],
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchConfirm,
|
||||
keys: ['enter'],
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchClose,
|
||||
keys: ['esc'],
|
||||
},
|
||||
]
|
||||
|
||||
export const hotkeyBindings: HotkeyBinding[] = [
|
||||
...globalHotkeyBindings,
|
||||
...menuSearchHotkeyBindings,
|
||||
...extendScopedHotkeyBindings,
|
||||
]
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
const registeredIds = hotkeyBindings.map(item => item.id)
|
||||
const missingIds = hotkeyIds.filter(id => !registeredIds.includes(id))
|
||||
const duplicateIds = registeredIds.filter((id, index) => registeredIds.indexOf(id) !== index)
|
||||
|
||||
if (missingIds.length || duplicateIds.length) {
|
||||
console.warn('[hotkeys] registry consistency check failed', {
|
||||
missingIds,
|
||||
duplicateIds: [...new Set(duplicateIds)],
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { useAppSettingsStore } from '@/store/modules/app/settings'
|
||||
|
||||
export type HotkeyHelpGroup = 'global' | 'nav' | 'tabbar' | 'page'
|
||||
export type HotkeyId = typeof import('./registry').HOTKEY_ID[keyof typeof import('./registry').HOTKEY_ID]
|
||||
|
||||
export interface HotkeyContext {
|
||||
settings: ReturnType<typeof useAppSettingsStore>['settings']
|
||||
}
|
||||
|
||||
export interface HotkeyDisplayKeys {
|
||||
default: string[]
|
||||
mac?: string[]
|
||||
}
|
||||
|
||||
export interface HotkeyHelpMeta {
|
||||
group: HotkeyHelpGroup
|
||||
titleKey: string
|
||||
order?: number
|
||||
visible?: (ctx: HotkeyContext) => boolean
|
||||
displayKeys: HotkeyDisplayKeys
|
||||
}
|
||||
|
||||
export interface HotkeyBinding {
|
||||
id: HotkeyId
|
||||
keys: string[]
|
||||
preventDefault?: boolean
|
||||
enabled?: (ctx: HotkeyContext) => boolean
|
||||
help?: HotkeyHelpMeta
|
||||
}
|
||||
|
||||
export interface HotkeyHandlerArgs {
|
||||
event: KeyboardEvent
|
||||
hotkey: string
|
||||
}
|
||||
|
||||
export type HotkeyHandler = (args: HotkeyHandlerArgs) => void
|
||||
|
||||
export type HotkeyHandlersMap = Partial<Record<HotkeyId, HotkeyHandler>>
|
||||
@@ -1,69 +0,0 @@
|
||||
import type { HotkeyHandler, HotkeyHandlersMap, HotkeyId } from './types'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import { hotkeyBindings, hotkeyIds } from './registry'
|
||||
|
||||
export function useHotkey(id: HotkeyId, handler: HotkeyHandler, active: MaybeRefOrGetter<boolean> = true) {
|
||||
const binding = hotkeyBindings.find(item => item.id === id)
|
||||
const appSettingsStore = useAppSettingsStore()
|
||||
const hotkeyContext = {
|
||||
settings: appSettingsStore.settings,
|
||||
}
|
||||
let currentHandler: ((event: KeyboardEvent, hotkeyHandler: { key: string }) => void) | undefined
|
||||
|
||||
function bind() {
|
||||
if (currentHandler || !binding) {
|
||||
return
|
||||
}
|
||||
const currentBinding = binding
|
||||
|
||||
currentHandler = (event, hotkeyHandler) => {
|
||||
if (currentBinding.enabled && !currentBinding.enabled(hotkeyContext)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (currentBinding.preventDefault !== false) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
handler({
|
||||
event,
|
||||
hotkey: hotkeyHandler.key,
|
||||
})
|
||||
}
|
||||
|
||||
hotkeys(currentBinding.keys.join(','), currentHandler)
|
||||
}
|
||||
|
||||
function unbind() {
|
||||
if (!currentHandler || !binding) {
|
||||
return
|
||||
}
|
||||
|
||||
hotkeys.unbind(binding.keys.join(','), currentHandler)
|
||||
currentHandler = undefined
|
||||
}
|
||||
|
||||
watch(() => toValue(active), (val) => {
|
||||
if (val) {
|
||||
bind()
|
||||
}
|
||||
else {
|
||||
unbind()
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unbind()
|
||||
})
|
||||
}
|
||||
|
||||
export function useHotkeyBindings(handlers: HotkeyHandlersMap, active: MaybeRefOrGetter<boolean> = true) {
|
||||
hotkeyIds.forEach((id) => {
|
||||
const handler = handlers[id]
|
||||
if (handler) {
|
||||
useHotkey(id, handler, active)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { globalHotkeyBindings } from '@/hotkeys/registry'
|
||||
import { hotkeyBindings } from '@/hotkeys'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
|
||||
defineOptions({
|
||||
@@ -11,10 +11,10 @@ const hotkeyContext = {
|
||||
settings: appSettingsStore.settings,
|
||||
}
|
||||
|
||||
const groupTitleMap = {
|
||||
global: '全局',
|
||||
nav: '主导航',
|
||||
tabbar: '标签栏',
|
||||
const helpGroupMeta = {
|
||||
global: { title: '全局', order: 10 },
|
||||
nav: { title: '主导航', order: 20 },
|
||||
tabbar: { title: '标签栏', order: 30 },
|
||||
} as const
|
||||
|
||||
const itemTitleMap: Record<string, string> = {
|
||||
@@ -32,42 +32,52 @@ const itemTitleMap: Record<string, string> = {
|
||||
const isShow = ref(false)
|
||||
|
||||
const helpGroups = computed(() => {
|
||||
const groups = [
|
||||
{ id: 'global', title: groupTitleMap.global },
|
||||
{ id: 'nav', title: groupTitleMap.nav },
|
||||
{ id: 'tabbar', title: groupTitleMap.tabbar },
|
||||
] as const
|
||||
const groups = new Map<string, {
|
||||
id: string
|
||||
title: string
|
||||
order: number
|
||||
items: {
|
||||
id: string
|
||||
title: string
|
||||
order: number
|
||||
displayKeys: string[]
|
||||
}[]
|
||||
}>()
|
||||
|
||||
return groups.map((group) => {
|
||||
const items = globalHotkeyBindings
|
||||
.filter(binding => binding.help?.group === group.id)
|
||||
.filter((binding) => {
|
||||
if (!binding.help) {
|
||||
return false
|
||||
}
|
||||
if (binding.help.visible) {
|
||||
return binding.help.visible(hotkeyContext)
|
||||
}
|
||||
return true
|
||||
})
|
||||
.sort((a, b) => (a.help?.order ?? 0) - (b.help?.order ?? 0))
|
||||
.map((binding) => {
|
||||
const displayKeys = appSettingsStore.os === 'mac' && binding.help?.displayKeys.mac
|
||||
? binding.help.displayKeys.mac
|
||||
: binding.help?.displayKeys.default ?? []
|
||||
|
||||
return {
|
||||
id: binding.id,
|
||||
title: binding.help?.titleKey ? itemTitleMap[binding.help.titleKey] : '',
|
||||
displayKeys,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...group,
|
||||
items,
|
||||
hotkeyBindings.forEach((binding) => {
|
||||
const help = binding.help
|
||||
if (!help) {
|
||||
return
|
||||
}
|
||||
}).filter(group => group.items.length > 0)
|
||||
if (help.visible && !help.visible(hotkeyContext)) {
|
||||
return
|
||||
}
|
||||
|
||||
const meta = helpGroupMeta[help.group as keyof typeof helpGroupMeta]
|
||||
const group = groups.get(help.group) ?? {
|
||||
id: help.group,
|
||||
title: help.groupTitleKey ? itemTitleMap[help.groupTitleKey] ?? help.groupTitleKey : meta?.title ?? help.group,
|
||||
order: meta?.order ?? 100,
|
||||
items: [],
|
||||
}
|
||||
|
||||
group.items.push({
|
||||
id: binding.id,
|
||||
title: itemTitleMap[help.titleKey] ?? help.titleKey,
|
||||
order: help.order ?? 0,
|
||||
displayKeys: appSettingsStore.os === 'mac' && help.displayKeys.mac
|
||||
? help.displayKeys.mac
|
||||
: help.displayKeys.default,
|
||||
})
|
||||
groups.set(help.group, group)
|
||||
})
|
||||
|
||||
return Array.from(groups.values())
|
||||
.map(group => ({
|
||||
...group,
|
||||
items: [...group.items].sort((a, b) => a.order - b.order),
|
||||
}))
|
||||
.sort((a, b) => a.order - b.order)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import { useSlots } from '@/slots'
|
||||
import Logo from '../Logo/index.vue'
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { TabbarRecordRaw } from '@fantastic-admin/types'
|
||||
import { useMagicKeys } from '@vueuse/core'
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import { useSlots } from '@/slots'
|
||||
|
||||
defineOptions({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import Search from './search.vue'
|
||||
|
||||
defineOptions({
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { MenuRecordRaw } from '@fantastic-admin/types'
|
||||
import { match } from 'pinyin-pro'
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import Breadcrumb from '@/layouts/components/Breadcrumb/index.vue'
|
||||
import BreadcrumbItem from '@/layouts/components/Breadcrumb/item.vue'
|
||||
import { resolveRoutePath } from '@/utils'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useElementSize, useScroll } from '@vueuse/core'
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import { useSlots } from '@/slots'
|
||||
import { cn } from '@/utils'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"dependencies": {
|
||||
"@fantastic-admin/components": "workspace:*",
|
||||
"@fantastic-admin/composables": "workspace:*",
|
||||
"@fantastic-admin/hotkeys": "workspace:*",
|
||||
"@fantastic-admin/settings": "workspace:*",
|
||||
"@fantastic-admin/types": "workspace:*",
|
||||
"@vee-validate/zod": "catalog:",
|
||||
@@ -28,7 +29,6 @@
|
||||
"dayjs": "catalog:",
|
||||
"eruda": "catalog:",
|
||||
"es-toolkit": "catalog:",
|
||||
"hotkeys-js": "catalog:",
|
||||
"mitt": "catalog:",
|
||||
"nprogress": "catalog:",
|
||||
"path-browserify": "catalog:",
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
# Hotkeys 设计说明
|
||||
|
||||
这个目录参考 `fantastic-admin/v6-dev/apps/example/src/hotkeys` 的设计,目标是把当前项目里分散在各组件中的快捷键逻辑收拢成一套轻量方案。
|
||||
|
||||
## 设计目标
|
||||
|
||||
当前方案只解决 4 件事:
|
||||
|
||||
1. 快捷键定义不要散落在多个组件里重复写
|
||||
2. 快捷键帮助面板不要再手写一份展示文案
|
||||
3. 业务组件不要反复手写 `hotkeys()` / `unbind()`
|
||||
4. 不引入过重的 command system
|
||||
|
||||
所以采用:
|
||||
|
||||
- `registry.ts` 统一定义
|
||||
- `useHotkeys.ts` 统一注册
|
||||
- 业务 handler 仍留在业务组件
|
||||
|
||||
这是一种“集中定义 + 就地执行”的轻量折中方案。
|
||||
|
||||
## 目录职责
|
||||
|
||||
### `registry.ts`
|
||||
|
||||
统一维护当前项目的快捷键元数据:
|
||||
|
||||
- id
|
||||
- keys
|
||||
- enabled
|
||||
- help
|
||||
|
||||
这里负责:
|
||||
|
||||
- 统一的 id 常量
|
||||
- 真实键位
|
||||
- 是否启用的条件
|
||||
- 帮助面板展示文案
|
||||
- 聚合业务扩展
|
||||
|
||||
> 注意:这里不放业务逻辑 handler。
|
||||
|
||||
### `registry.extend.ts`
|
||||
|
||||
用于放置项目自定义扩展快捷键,避免把业务项全部堆进内建定义中。
|
||||
|
||||
### `types.ts`
|
||||
|
||||
定义快捷键系统的基础类型,包括:
|
||||
|
||||
- `HotkeyBinding`
|
||||
- `HotkeyContext`
|
||||
- `HotkeyHelpMeta`
|
||||
- `HotkeyHandler`
|
||||
|
||||
### `useHotkeys.ts`
|
||||
|
||||
对 `hotkeys-js` 的薄封装,提供:
|
||||
|
||||
- `useHotkey`:注册单个快捷键
|
||||
- `useHotkeyBindings`:批量注册快捷键
|
||||
|
||||
并统一处理:
|
||||
|
||||
- 注册 / 卸载
|
||||
- `active` 启停
|
||||
- `binding.enabled` 可用性判断
|
||||
- 默认 `preventDefault`
|
||||
|
||||
## 当前项目已落地的快捷键
|
||||
|
||||
### 全局
|
||||
|
||||
- `system.info.open`
|
||||
- `menuSearch.open`
|
||||
- `page.reload`
|
||||
|
||||
### 主导航
|
||||
|
||||
- `menu.next`
|
||||
- `menu.prev`
|
||||
|
||||
### 标签栏
|
||||
|
||||
- `tabbar.prev`
|
||||
- `tabbar.next`
|
||||
- `tabbar.closeCurrent`
|
||||
- `tabbar.gotoVisibleIndex`
|
||||
- `tabbar.gotoLast`
|
||||
|
||||
### 菜单搜索弹窗
|
||||
|
||||
- `menuSearch.moveUp`
|
||||
- `menuSearch.moveDown`
|
||||
- `menuSearch.confirm`
|
||||
- `menuSearch.close`
|
||||
|
||||
## 使用约定
|
||||
|
||||
1. 新增快捷键,优先在 `registry.ts` 或 `registry.extend.ts` 补定义
|
||||
2. 展示在帮助面板中的快捷键,必须配置 `help`
|
||||
3. handler 尽量保留在最了解业务的组件中
|
||||
4. 不直接在业务组件里继续散写 `hotkeys('xxx')`
|
||||
|
||||
## 现阶段差异
|
||||
|
||||
参考项目里还有页面最大化相关快捷键;当前项目暂未引入对应页面最大化能力,所以本次迁移只保留当前项目已有功能,并把原有 `F5` 页面刷新也纳入统一 registry 管理。
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { HotkeyBinding } from '@fantastic-admin/hotkeys'
|
||||
|
||||
/**
|
||||
* 业务扩展快捷键 id
|
||||
*
|
||||
* 使用方式:
|
||||
*
|
||||
* export const EXT_HOTKEY_ID = {
|
||||
* demoOpen: 'demo.open',
|
||||
* } as const
|
||||
*/
|
||||
export const EXT_HOTKEY_ID = {} as const
|
||||
|
||||
type ExtendHotkeyId = typeof EXT_HOTKEY_ID[keyof typeof EXT_HOTKEY_ID]
|
||||
|
||||
/**
|
||||
* 业务扩展全局快捷键
|
||||
*
|
||||
* 示例:
|
||||
*
|
||||
* export const extendGlobalHotkeyBindings = [
|
||||
* {
|
||||
* id: EXT_HOTKEY_ID.demoOpen,
|
||||
* keys: ['command+j', 'ctrl+j'],
|
||||
* enabled: ctx => ctx.settings.toolbar.menuSearch.enable,
|
||||
* help: {
|
||||
* group: 'global',
|
||||
* titleKey: 'global.demoOpen',
|
||||
* order: 90,
|
||||
* visible: ctx => ctx.settings.toolbar.menuSearch.enable,
|
||||
* displayKeys: {
|
||||
* default: ['Ctrl', 'J'],
|
||||
* mac: ['⌘', 'J'],
|
||||
* },
|
||||
* },
|
||||
* },
|
||||
* ] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
*/
|
||||
export const extendGlobalHotkeyBindings = [] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
|
||||
/**
|
||||
* 业务扩展局部快捷键
|
||||
*
|
||||
* 示例:
|
||||
*
|
||||
* export const extendScopedHotkeyBindings = [
|
||||
* {
|
||||
* id: EXT_HOTKEY_ID.demoConfirm,
|
||||
* keys: ['enter'],
|
||||
* },
|
||||
* {
|
||||
* id: EXT_HOTKEY_ID.demoClose,
|
||||
* keys: ['esc'],
|
||||
* },
|
||||
* ] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
*/
|
||||
export const extendScopedHotkeyBindings = [] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
@@ -0,0 +1,41 @@
|
||||
import type {
|
||||
HotkeyBinding,
|
||||
HotkeyContextBase,
|
||||
HotkeyDisplayKeys,
|
||||
HotkeyHandler,
|
||||
HotkeyHandlerArgs,
|
||||
HotkeyHandlersMap,
|
||||
HotkeyHelpGroup,
|
||||
HotkeyHelpMeta,
|
||||
} from '@fantastic-admin/hotkeys'
|
||||
import { createHotkeys } from '@fantastic-admin/hotkeys'
|
||||
import { EXT_HOTKEY_ID, extendGlobalHotkeyBindings, extendScopedHotkeyBindings } from './extend'
|
||||
|
||||
const appHotkeys = createHotkeys({
|
||||
extendIds: EXT_HOTKEY_ID,
|
||||
extendGlobalHotkeyBindings,
|
||||
extendScopedHotkeyBindings,
|
||||
getContext: () => ({
|
||||
settings: useAppSettingsStore().settings,
|
||||
}),
|
||||
})
|
||||
|
||||
export const HOTKEY_ID = appHotkeys.HOTKEY_ID
|
||||
export const hotkeyIds = appHotkeys.hotkeyIds
|
||||
export const globalHotkeyBindings = appHotkeys.globalHotkeyBindings
|
||||
export const menuSearchHotkeyBindings = appHotkeys.menuSearchHotkeyBindings
|
||||
export const hotkeyBindings = appHotkeys.hotkeyBindings
|
||||
export const useHotkey = appHotkeys.useHotkey
|
||||
export const useHotkeyBindings = appHotkeys.useHotkeyBindings
|
||||
|
||||
export type HotkeyId = typeof HOTKEY_ID[keyof typeof HOTKEY_ID]
|
||||
export type {
|
||||
HotkeyBinding,
|
||||
HotkeyContextBase,
|
||||
HotkeyDisplayKeys,
|
||||
HotkeyHandler,
|
||||
HotkeyHandlerArgs,
|
||||
HotkeyHandlersMap,
|
||||
HotkeyHelpGroup,
|
||||
HotkeyHelpMeta,
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import type { HotkeyBinding } from './types'
|
||||
|
||||
/**
|
||||
* 业务扩展快捷键 id
|
||||
*
|
||||
* 使用方式:
|
||||
*
|
||||
* export const EXT_HOTKEY_ID = {
|
||||
* demoOpen: 'demo.open',
|
||||
* } as const
|
||||
*/
|
||||
export const EXT_HOTKEY_ID = {} as const
|
||||
|
||||
/**
|
||||
* 业务扩展全局快捷键
|
||||
*/
|
||||
export const extendGlobalHotkeyBindings: HotkeyBinding[] = []
|
||||
|
||||
/**
|
||||
* 业务扩展局部快捷键
|
||||
*/
|
||||
export const extendScopedHotkeyBindings: HotkeyBinding[] = []
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { useAppSettingsStore } from '@/store/modules/app/settings'
|
||||
|
||||
export type HotkeyHelpGroup = 'global' | 'nav' | 'tabbar' | 'page'
|
||||
export type HotkeyId = typeof import('./registry').HOTKEY_ID[keyof typeof import('./registry').HOTKEY_ID]
|
||||
|
||||
export interface HotkeyContext {
|
||||
settings: ReturnType<typeof useAppSettingsStore>['settings']
|
||||
}
|
||||
|
||||
export interface HotkeyDisplayKeys {
|
||||
default: string[]
|
||||
mac?: string[]
|
||||
}
|
||||
|
||||
export interface HotkeyHelpMeta {
|
||||
group: HotkeyHelpGroup
|
||||
titleKey: string
|
||||
order?: number
|
||||
visible?: (ctx: HotkeyContext) => boolean
|
||||
displayKeys: HotkeyDisplayKeys
|
||||
}
|
||||
|
||||
export interface HotkeyBinding {
|
||||
id: HotkeyId
|
||||
keys: string[]
|
||||
preventDefault?: boolean
|
||||
enabled?: (ctx: HotkeyContext) => boolean
|
||||
help?: HotkeyHelpMeta
|
||||
}
|
||||
|
||||
export interface HotkeyHandlerArgs {
|
||||
event: KeyboardEvent
|
||||
hotkey: string
|
||||
}
|
||||
|
||||
export type HotkeyHandler = (args: HotkeyHandlerArgs) => void
|
||||
|
||||
export type HotkeyHandlersMap = Partial<Record<HotkeyId, HotkeyHandler>>
|
||||
@@ -1,69 +0,0 @@
|
||||
import type { HotkeyHandler, HotkeyHandlersMap, HotkeyId } from './types'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import { hotkeyBindings, hotkeyIds } from './registry'
|
||||
|
||||
export function useHotkey(id: HotkeyId, handler: HotkeyHandler, active: MaybeRefOrGetter<boolean> = true) {
|
||||
const binding = hotkeyBindings.find(item => item.id === id)
|
||||
const appSettingsStore = useAppSettingsStore()
|
||||
const hotkeyContext = {
|
||||
settings: appSettingsStore.settings,
|
||||
}
|
||||
let currentHandler: ((event: KeyboardEvent, hotkeyHandler: { key: string }) => void) | undefined
|
||||
|
||||
function bind() {
|
||||
if (currentHandler || !binding) {
|
||||
return
|
||||
}
|
||||
const currentBinding = binding
|
||||
|
||||
currentHandler = (event, hotkeyHandler) => {
|
||||
if (currentBinding.enabled && !currentBinding.enabled(hotkeyContext)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (currentBinding.preventDefault !== false) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
handler({
|
||||
event,
|
||||
hotkey: hotkeyHandler.key,
|
||||
})
|
||||
}
|
||||
|
||||
hotkeys(currentBinding.keys.join(','), currentHandler)
|
||||
}
|
||||
|
||||
function unbind() {
|
||||
if (!currentHandler || !binding) {
|
||||
return
|
||||
}
|
||||
|
||||
hotkeys.unbind(binding.keys.join(','), currentHandler)
|
||||
currentHandler = undefined
|
||||
}
|
||||
|
||||
watch(() => toValue(active), (val) => {
|
||||
if (val) {
|
||||
bind()
|
||||
}
|
||||
else {
|
||||
unbind()
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unbind()
|
||||
})
|
||||
}
|
||||
|
||||
export function useHotkeyBindings(handlers: HotkeyHandlersMap, active: MaybeRefOrGetter<boolean> = true) {
|
||||
hotkeyIds.forEach((id) => {
|
||||
const handler = handlers[id]
|
||||
if (handler) {
|
||||
useHotkey(id, handler, active)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { globalHotkeyBindings } from '@/hotkeys/registry'
|
||||
import { hotkeyBindings } from '@/hotkeys'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
|
||||
defineOptions({
|
||||
@@ -11,10 +11,10 @@ const hotkeyContext = {
|
||||
settings: appSettingsStore.settings,
|
||||
}
|
||||
|
||||
const groupTitleMap = {
|
||||
global: '全局',
|
||||
nav: '主导航',
|
||||
tabbar: '标签栏',
|
||||
const helpGroupMeta = {
|
||||
global: { title: '全局', order: 10 },
|
||||
nav: { title: '主导航', order: 20 },
|
||||
tabbar: { title: '标签栏', order: 30 },
|
||||
} as const
|
||||
|
||||
const itemTitleMap: Record<string, string> = {
|
||||
@@ -32,42 +32,52 @@ const itemTitleMap: Record<string, string> = {
|
||||
const isShow = ref(false)
|
||||
|
||||
const helpGroups = computed(() => {
|
||||
const groups = [
|
||||
{ id: 'global', title: groupTitleMap.global },
|
||||
{ id: 'nav', title: groupTitleMap.nav },
|
||||
{ id: 'tabbar', title: groupTitleMap.tabbar },
|
||||
] as const
|
||||
const groups = new Map<string, {
|
||||
id: string
|
||||
title: string
|
||||
order: number
|
||||
items: {
|
||||
id: string
|
||||
title: string
|
||||
order: number
|
||||
displayKeys: string[]
|
||||
}[]
|
||||
}>()
|
||||
|
||||
return groups.map((group) => {
|
||||
const items = globalHotkeyBindings
|
||||
.filter(binding => binding.help?.group === group.id)
|
||||
.filter((binding) => {
|
||||
if (!binding.help) {
|
||||
return false
|
||||
}
|
||||
if (binding.help.visible) {
|
||||
return binding.help.visible(hotkeyContext)
|
||||
}
|
||||
return true
|
||||
})
|
||||
.sort((a, b) => (a.help?.order ?? 0) - (b.help?.order ?? 0))
|
||||
.map((binding) => {
|
||||
const displayKeys = appSettingsStore.os === 'mac' && binding.help?.displayKeys.mac
|
||||
? binding.help.displayKeys.mac
|
||||
: binding.help?.displayKeys.default ?? []
|
||||
|
||||
return {
|
||||
id: binding.id,
|
||||
title: binding.help?.titleKey ? itemTitleMap[binding.help.titleKey] : '',
|
||||
displayKeys,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...group,
|
||||
items,
|
||||
hotkeyBindings.forEach((binding) => {
|
||||
const help = binding.help
|
||||
if (!help) {
|
||||
return
|
||||
}
|
||||
}).filter(group => group.items.length > 0)
|
||||
if (help.visible && !help.visible(hotkeyContext)) {
|
||||
return
|
||||
}
|
||||
|
||||
const meta = helpGroupMeta[help.group as keyof typeof helpGroupMeta]
|
||||
const group = groups.get(help.group) ?? {
|
||||
id: help.group,
|
||||
title: help.groupTitleKey ? itemTitleMap[help.groupTitleKey] ?? help.groupTitleKey : meta?.title ?? help.group,
|
||||
order: meta?.order ?? 100,
|
||||
items: [],
|
||||
}
|
||||
|
||||
group.items.push({
|
||||
id: binding.id,
|
||||
title: itemTitleMap[help.titleKey] ?? help.titleKey,
|
||||
order: help.order ?? 0,
|
||||
displayKeys: appSettingsStore.os === 'mac' && help.displayKeys.mac
|
||||
? help.displayKeys.mac
|
||||
: help.displayKeys.default,
|
||||
})
|
||||
groups.set(help.group, group)
|
||||
})
|
||||
|
||||
return Array.from(groups.values())
|
||||
.map(group => ({
|
||||
...group,
|
||||
items: [...group.items].sort((a, b) => a.order - b.order),
|
||||
}))
|
||||
.sort((a, b) => a.order - b.order)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import { useSlots } from '@/slots'
|
||||
import Logo from '../Logo/index.vue'
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { TabbarRecordRaw } from '@fantastic-admin/types'
|
||||
import { useMagicKeys } from '@vueuse/core'
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import { useSlots } from '@/slots'
|
||||
|
||||
defineOptions({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import Search from './search.vue'
|
||||
|
||||
defineOptions({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { MenuRecordRaw } from '@fantastic-admin/types'
|
||||
import { match } from 'pinyin-pro'
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import Breadcrumb from '@/layouts/components/Breadcrumb/index.vue'
|
||||
import BreadcrumbItem from '@/layouts/components/Breadcrumb/item.vue'
|
||||
import { resolveRoutePath } from '@/utils'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useElementSize, useScroll } from '@vueuse/core'
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import { useSlots } from '@/slots'
|
||||
import { cn } from '@/utils'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"dependencies": {
|
||||
"@fantastic-admin/components": "workspace:*",
|
||||
"@fantastic-admin/composables": "workspace:*",
|
||||
"@fantastic-admin/hotkeys": "workspace:*",
|
||||
"@fantastic-admin/settings": "workspace:*",
|
||||
"@fantastic-admin/types": "workspace:*",
|
||||
"@vee-validate/zod": "catalog:",
|
||||
@@ -27,7 +28,6 @@
|
||||
"dayjs": "catalog:",
|
||||
"eruda": "catalog:",
|
||||
"es-toolkit": "catalog:",
|
||||
"hotkeys-js": "catalog:",
|
||||
"mitt": "catalog:",
|
||||
"nprogress": "catalog:",
|
||||
"path-browserify": "catalog:",
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
# Hotkeys 设计说明
|
||||
|
||||
这个目录参考 `fantastic-admin/v6-dev/apps/example/src/hotkeys` 的设计,目标是把当前项目里分散在各组件中的快捷键逻辑收拢成一套轻量方案。
|
||||
|
||||
## 设计目标
|
||||
|
||||
当前方案只解决 4 件事:
|
||||
|
||||
1. 快捷键定义不要散落在多个组件里重复写
|
||||
2. 快捷键帮助面板不要再手写一份展示文案
|
||||
3. 业务组件不要反复手写 `hotkeys()` / `unbind()`
|
||||
4. 不引入过重的 command system
|
||||
|
||||
所以采用:
|
||||
|
||||
- `registry.ts` 统一定义
|
||||
- `useHotkeys.ts` 统一注册
|
||||
- 业务 handler 仍留在业务组件
|
||||
|
||||
这是一种“集中定义 + 就地执行”的轻量折中方案。
|
||||
|
||||
## 目录职责
|
||||
|
||||
### `registry.ts`
|
||||
|
||||
统一维护当前项目的快捷键元数据:
|
||||
|
||||
- id
|
||||
- keys
|
||||
- enabled
|
||||
- help
|
||||
|
||||
这里负责:
|
||||
|
||||
- 统一的 id 常量
|
||||
- 真实键位
|
||||
- 是否启用的条件
|
||||
- 帮助面板展示文案
|
||||
- 聚合业务扩展
|
||||
|
||||
> 注意:这里不放业务逻辑 handler。
|
||||
|
||||
### `registry.extend.ts`
|
||||
|
||||
用于放置项目自定义扩展快捷键,避免把业务项全部堆进内建定义中。
|
||||
|
||||
### `types.ts`
|
||||
|
||||
定义快捷键系统的基础类型,包括:
|
||||
|
||||
- `HotkeyBinding`
|
||||
- `HotkeyContext`
|
||||
- `HotkeyHelpMeta`
|
||||
- `HotkeyHandler`
|
||||
|
||||
### `useHotkeys.ts`
|
||||
|
||||
对 `hotkeys-js` 的薄封装,提供:
|
||||
|
||||
- `useHotkey`:注册单个快捷键
|
||||
- `useHotkeyBindings`:批量注册快捷键
|
||||
|
||||
并统一处理:
|
||||
|
||||
- 注册 / 卸载
|
||||
- `active` 启停
|
||||
- `binding.enabled` 可用性判断
|
||||
- 默认 `preventDefault`
|
||||
|
||||
## 当前项目已落地的快捷键
|
||||
|
||||
### 全局
|
||||
|
||||
- `system.info.open`
|
||||
- `menuSearch.open`
|
||||
- `page.reload`
|
||||
|
||||
### 主导航
|
||||
|
||||
- `menu.next`
|
||||
- `menu.prev`
|
||||
|
||||
### 标签栏
|
||||
|
||||
- `tabbar.prev`
|
||||
- `tabbar.next`
|
||||
- `tabbar.closeCurrent`
|
||||
- `tabbar.gotoVisibleIndex`
|
||||
- `tabbar.gotoLast`
|
||||
|
||||
### 菜单搜索弹窗
|
||||
|
||||
- `menuSearch.moveUp`
|
||||
- `menuSearch.moveDown`
|
||||
- `menuSearch.confirm`
|
||||
- `menuSearch.close`
|
||||
|
||||
## 使用约定
|
||||
|
||||
1. 新增快捷键,优先在 `registry.ts` 或 `registry.extend.ts` 补定义
|
||||
2. 展示在帮助面板中的快捷键,必须配置 `help`
|
||||
3. handler 尽量保留在最了解业务的组件中
|
||||
4. 不直接在业务组件里继续散写 `hotkeys('xxx')`
|
||||
|
||||
## 现阶段差异
|
||||
|
||||
参考项目里还有页面最大化相关快捷键;当前项目暂未引入对应页面最大化能力,所以本次迁移只保留当前项目已有功能,并把原有 `F5` 页面刷新也纳入统一 registry 管理。
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { HotkeyBinding } from '@fantastic-admin/hotkeys'
|
||||
|
||||
/**
|
||||
* 业务扩展快捷键 id
|
||||
*
|
||||
* 使用方式:
|
||||
*
|
||||
* export const EXT_HOTKEY_ID = {
|
||||
* demoOpen: 'demo.open',
|
||||
* } as const
|
||||
*/
|
||||
export const EXT_HOTKEY_ID = {} as const
|
||||
|
||||
type ExtendHotkeyId = typeof EXT_HOTKEY_ID[keyof typeof EXT_HOTKEY_ID]
|
||||
|
||||
/**
|
||||
* 业务扩展全局快捷键
|
||||
*
|
||||
* 示例:
|
||||
*
|
||||
* export const extendGlobalHotkeyBindings = [
|
||||
* {
|
||||
* id: EXT_HOTKEY_ID.demoOpen,
|
||||
* keys: ['command+j', 'ctrl+j'],
|
||||
* enabled: ctx => ctx.settings.toolbar.menuSearch.enable,
|
||||
* help: {
|
||||
* group: 'global',
|
||||
* titleKey: 'global.demoOpen',
|
||||
* order: 90,
|
||||
* visible: ctx => ctx.settings.toolbar.menuSearch.enable,
|
||||
* displayKeys: {
|
||||
* default: ['Ctrl', 'J'],
|
||||
* mac: ['⌘', 'J'],
|
||||
* },
|
||||
* },
|
||||
* },
|
||||
* ] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
*/
|
||||
export const extendGlobalHotkeyBindings = [] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
|
||||
/**
|
||||
* 业务扩展局部快捷键
|
||||
*
|
||||
* 示例:
|
||||
*
|
||||
* export const extendScopedHotkeyBindings = [
|
||||
* {
|
||||
* id: EXT_HOTKEY_ID.demoConfirm,
|
||||
* keys: ['enter'],
|
||||
* },
|
||||
* {
|
||||
* id: EXT_HOTKEY_ID.demoClose,
|
||||
* keys: ['esc'],
|
||||
* },
|
||||
* ] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
*/
|
||||
export const extendScopedHotkeyBindings = [] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
@@ -0,0 +1,41 @@
|
||||
import type {
|
||||
HotkeyBinding,
|
||||
HotkeyContextBase,
|
||||
HotkeyDisplayKeys,
|
||||
HotkeyHandler,
|
||||
HotkeyHandlerArgs,
|
||||
HotkeyHandlersMap,
|
||||
HotkeyHelpGroup,
|
||||
HotkeyHelpMeta,
|
||||
} from '@fantastic-admin/hotkeys'
|
||||
import { createHotkeys } from '@fantastic-admin/hotkeys'
|
||||
import { EXT_HOTKEY_ID, extendGlobalHotkeyBindings, extendScopedHotkeyBindings } from './extend'
|
||||
|
||||
const appHotkeys = createHotkeys({
|
||||
extendIds: EXT_HOTKEY_ID,
|
||||
extendGlobalHotkeyBindings,
|
||||
extendScopedHotkeyBindings,
|
||||
getContext: () => ({
|
||||
settings: useAppSettingsStore().settings,
|
||||
}),
|
||||
})
|
||||
|
||||
export const HOTKEY_ID = appHotkeys.HOTKEY_ID
|
||||
export const hotkeyIds = appHotkeys.hotkeyIds
|
||||
export const globalHotkeyBindings = appHotkeys.globalHotkeyBindings
|
||||
export const menuSearchHotkeyBindings = appHotkeys.menuSearchHotkeyBindings
|
||||
export const hotkeyBindings = appHotkeys.hotkeyBindings
|
||||
export const useHotkey = appHotkeys.useHotkey
|
||||
export const useHotkeyBindings = appHotkeys.useHotkeyBindings
|
||||
|
||||
export type HotkeyId = typeof HOTKEY_ID[keyof typeof HOTKEY_ID]
|
||||
export type {
|
||||
HotkeyBinding,
|
||||
HotkeyContextBase,
|
||||
HotkeyDisplayKeys,
|
||||
HotkeyHandler,
|
||||
HotkeyHandlerArgs,
|
||||
HotkeyHandlersMap,
|
||||
HotkeyHelpGroup,
|
||||
HotkeyHelpMeta,
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import type { HotkeyBinding } from './types'
|
||||
|
||||
/**
|
||||
* 业务扩展快捷键 id
|
||||
*
|
||||
* 使用方式:
|
||||
*
|
||||
* export const EXT_HOTKEY_ID = {
|
||||
* demoOpen: 'demo.open',
|
||||
* } as const
|
||||
*/
|
||||
export const EXT_HOTKEY_ID = {} as const
|
||||
|
||||
/**
|
||||
* 业务扩展全局快捷键
|
||||
*/
|
||||
export const extendGlobalHotkeyBindings: HotkeyBinding[] = []
|
||||
|
||||
/**
|
||||
* 业务扩展局部快捷键
|
||||
*/
|
||||
export const extendScopedHotkeyBindings: HotkeyBinding[] = []
|
||||
@@ -1,210 +0,0 @@
|
||||
import type { HotkeyBinding } from './types'
|
||||
import { EXT_HOTKEY_ID, extendGlobalHotkeyBindings, extendScopedHotkeyBindings } from './registry.extend'
|
||||
|
||||
export const HOTKEY_ID = {
|
||||
systemInfoOpen: 'system.info.open',
|
||||
menuSearchOpen: 'menuSearch.open',
|
||||
menuNext: 'menu.next',
|
||||
menuPrev: 'menu.prev',
|
||||
tabbarPrev: 'tabbar.prev',
|
||||
tabbarNext: 'tabbar.next',
|
||||
tabbarCloseCurrent: 'tabbar.closeCurrent',
|
||||
tabbarGotoVisibleIndex: 'tabbar.gotoVisibleIndex',
|
||||
tabbarGotoLast: 'tabbar.gotoLast',
|
||||
pageReload: 'page.reload',
|
||||
menuSearchMoveUp: 'menuSearch.moveUp',
|
||||
menuSearchMoveDown: 'menuSearch.moveDown',
|
||||
menuSearchConfirm: 'menuSearch.confirm',
|
||||
menuSearchClose: 'menuSearch.close',
|
||||
...EXT_HOTKEY_ID,
|
||||
} as const
|
||||
|
||||
export const hotkeyIds = Object.values(HOTKEY_ID)
|
||||
|
||||
/**
|
||||
* Hotkeys registry 维护约定
|
||||
*
|
||||
* 1. 新增快捷键优先在这里补充定义,不要在业务组件里直接写死键位字符串
|
||||
* 2. 这里仅维护“定义”和“展示元数据”,不要把业务 handler 塞进 registry
|
||||
* 3. 配置类开关尽量写在 enabled / help.visible 中,避免帮助面板与真实行为不一致
|
||||
* 4. 需要展示在快捷键帮助面板中的项,必须补充 help
|
||||
* 5. 仅局部场景使用的快捷键,也建议在这里定义,再由对应组件按需注册
|
||||
*/
|
||||
export const globalHotkeyBindings: HotkeyBinding[] = [
|
||||
{
|
||||
id: HOTKEY_ID.systemInfoOpen,
|
||||
keys: ['command+i', 'ctrl+i'],
|
||||
help: {
|
||||
group: 'global',
|
||||
titleKey: 'global.system',
|
||||
order: 10,
|
||||
displayKeys: {
|
||||
default: ['Ctrl', 'I'],
|
||||
mac: ['⌘', 'I'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchOpen,
|
||||
keys: ['command+k', 'ctrl+k'],
|
||||
enabled: ctx => ctx.settings.toolbar.menuSearch.enable && ctx.settings.toolbar.menuSearch.hotkeys,
|
||||
help: {
|
||||
group: 'global',
|
||||
titleKey: 'global.search',
|
||||
order: 20,
|
||||
visible: ctx => ctx.settings.toolbar.menuSearch.enable && ctx.settings.toolbar.menuSearch.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Ctrl', 'K'],
|
||||
mac: ['⌘', 'K'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuNext,
|
||||
keys: ['alt+`'],
|
||||
enabled: ctx => ctx.settings.menu.hotkeys && ['side', 'head'].includes(ctx.settings.menu.mode),
|
||||
help: {
|
||||
group: 'nav',
|
||||
titleKey: 'nav.next',
|
||||
order: 10,
|
||||
visible: ctx => ctx.settings.menu.hotkeys && ['side', 'head'].includes(ctx.settings.menu.mode),
|
||||
displayKeys: {
|
||||
default: ['Alt', '`'],
|
||||
mac: ['⌥', '`'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuPrev,
|
||||
keys: ['alt+shift+`'],
|
||||
enabled: ctx => ctx.settings.menu.hotkeys && ['side', 'head'].includes(ctx.settings.menu.mode),
|
||||
help: {
|
||||
group: 'nav',
|
||||
titleKey: 'nav.prev',
|
||||
order: 20,
|
||||
visible: ctx => ctx.settings.menu.hotkeys && ['side', 'head'].includes(ctx.settings.menu.mode),
|
||||
displayKeys: {
|
||||
default: ['Alt', 'Shift', '`'],
|
||||
mac: ['⌥', '⇧', '`'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarPrev,
|
||||
keys: ['alt+left'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.prev',
|
||||
order: 10,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', '←'],
|
||||
mac: ['⌥', '←'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarNext,
|
||||
keys: ['alt+right'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.next',
|
||||
order: 20,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', '→'],
|
||||
mac: ['⌥', '→'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarCloseCurrent,
|
||||
keys: ['alt+w'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.close',
|
||||
order: 30,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', 'W'],
|
||||
mac: ['⌥', 'W'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarGotoVisibleIndex,
|
||||
keys: ['alt+1', 'alt+2', 'alt+3', 'alt+4', 'alt+5', 'alt+6', 'alt+7', 'alt+8', 'alt+9'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.n',
|
||||
order: 40,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', '1~9'],
|
||||
mac: ['⌥', '1~9'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarGotoLast,
|
||||
keys: ['alt+0'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.last',
|
||||
order: 50,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', '0'],
|
||||
mac: ['⌥', '0'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.pageReload,
|
||||
keys: ['f5'],
|
||||
},
|
||||
...extendGlobalHotkeyBindings,
|
||||
]
|
||||
|
||||
export const menuSearchHotkeyBindings: HotkeyBinding[] = [
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchMoveUp,
|
||||
keys: ['up'],
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchMoveDown,
|
||||
keys: ['down'],
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchConfirm,
|
||||
keys: ['enter'],
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchClose,
|
||||
keys: ['esc'],
|
||||
},
|
||||
]
|
||||
|
||||
export const hotkeyBindings: HotkeyBinding[] = [
|
||||
...globalHotkeyBindings,
|
||||
...menuSearchHotkeyBindings,
|
||||
...extendScopedHotkeyBindings,
|
||||
]
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
const registeredIds = hotkeyBindings.map(item => item.id)
|
||||
const missingIds = hotkeyIds.filter(id => !registeredIds.includes(id))
|
||||
const duplicateIds = registeredIds.filter((id, index) => registeredIds.indexOf(id) !== index)
|
||||
|
||||
if (missingIds.length || duplicateIds.length) {
|
||||
console.warn('[hotkeys] registry consistency check failed', {
|
||||
missingIds,
|
||||
duplicateIds: [...new Set(duplicateIds)],
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { useAppSettingsStore } from '@/store/modules/app/settings'
|
||||
|
||||
export type HotkeyHelpGroup = 'global' | 'nav' | 'tabbar' | 'page'
|
||||
export type HotkeyId = typeof import('./registry').HOTKEY_ID[keyof typeof import('./registry').HOTKEY_ID]
|
||||
|
||||
export interface HotkeyContext {
|
||||
settings: ReturnType<typeof useAppSettingsStore>['settings']
|
||||
}
|
||||
|
||||
export interface HotkeyDisplayKeys {
|
||||
default: string[]
|
||||
mac?: string[]
|
||||
}
|
||||
|
||||
export interface HotkeyHelpMeta {
|
||||
group: HotkeyHelpGroup
|
||||
titleKey: string
|
||||
order?: number
|
||||
visible?: (ctx: HotkeyContext) => boolean
|
||||
displayKeys: HotkeyDisplayKeys
|
||||
}
|
||||
|
||||
export interface HotkeyBinding {
|
||||
id: HotkeyId
|
||||
keys: string[]
|
||||
preventDefault?: boolean
|
||||
enabled?: (ctx: HotkeyContext) => boolean
|
||||
help?: HotkeyHelpMeta
|
||||
}
|
||||
|
||||
export interface HotkeyHandlerArgs {
|
||||
event: KeyboardEvent
|
||||
hotkey: string
|
||||
}
|
||||
|
||||
export type HotkeyHandler = (args: HotkeyHandlerArgs) => void
|
||||
|
||||
export type HotkeyHandlersMap = Partial<Record<HotkeyId, HotkeyHandler>>
|
||||
@@ -1,69 +0,0 @@
|
||||
import type { HotkeyHandler, HotkeyHandlersMap, HotkeyId } from './types'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import { hotkeyBindings, hotkeyIds } from './registry'
|
||||
|
||||
export function useHotkey(id: HotkeyId, handler: HotkeyHandler, active: MaybeRefOrGetter<boolean> = true) {
|
||||
const binding = hotkeyBindings.find(item => item.id === id)
|
||||
const appSettingsStore = useAppSettingsStore()
|
||||
const hotkeyContext = {
|
||||
settings: appSettingsStore.settings,
|
||||
}
|
||||
let currentHandler: ((event: KeyboardEvent, hotkeyHandler: { key: string }) => void) | undefined
|
||||
|
||||
function bind() {
|
||||
if (currentHandler || !binding) {
|
||||
return
|
||||
}
|
||||
const currentBinding = binding
|
||||
|
||||
currentHandler = (event, hotkeyHandler) => {
|
||||
if (currentBinding.enabled && !currentBinding.enabled(hotkeyContext)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (currentBinding.preventDefault !== false) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
handler({
|
||||
event,
|
||||
hotkey: hotkeyHandler.key,
|
||||
})
|
||||
}
|
||||
|
||||
hotkeys(currentBinding.keys.join(','), currentHandler)
|
||||
}
|
||||
|
||||
function unbind() {
|
||||
if (!currentHandler || !binding) {
|
||||
return
|
||||
}
|
||||
|
||||
hotkeys.unbind(binding.keys.join(','), currentHandler)
|
||||
currentHandler = undefined
|
||||
}
|
||||
|
||||
watch(() => toValue(active), (val) => {
|
||||
if (val) {
|
||||
bind()
|
||||
}
|
||||
else {
|
||||
unbind()
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unbind()
|
||||
})
|
||||
}
|
||||
|
||||
export function useHotkeyBindings(handlers: HotkeyHandlersMap, active: MaybeRefOrGetter<boolean> = true) {
|
||||
hotkeyIds.forEach((id) => {
|
||||
const handler = handlers[id]
|
||||
if (handler) {
|
||||
useHotkey(id, handler, active)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { globalHotkeyBindings } from '@/hotkeys/registry'
|
||||
import { hotkeyBindings } from '@/hotkeys'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
|
||||
defineOptions({
|
||||
@@ -11,10 +11,10 @@ const hotkeyContext = {
|
||||
settings: appSettingsStore.settings,
|
||||
}
|
||||
|
||||
const groupTitleMap = {
|
||||
global: '全局',
|
||||
nav: '主导航',
|
||||
tabbar: '标签栏',
|
||||
const helpGroupMeta = {
|
||||
global: { title: '全局', order: 10 },
|
||||
nav: { title: '主导航', order: 20 },
|
||||
tabbar: { title: '标签栏', order: 30 },
|
||||
} as const
|
||||
|
||||
const itemTitleMap: Record<string, string> = {
|
||||
@@ -32,42 +32,52 @@ const itemTitleMap: Record<string, string> = {
|
||||
const isShow = ref(false)
|
||||
|
||||
const helpGroups = computed(() => {
|
||||
const groups = [
|
||||
{ id: 'global', title: groupTitleMap.global },
|
||||
{ id: 'nav', title: groupTitleMap.nav },
|
||||
{ id: 'tabbar', title: groupTitleMap.tabbar },
|
||||
] as const
|
||||
const groups = new Map<string, {
|
||||
id: string
|
||||
title: string
|
||||
order: number
|
||||
items: {
|
||||
id: string
|
||||
title: string
|
||||
order: number
|
||||
displayKeys: string[]
|
||||
}[]
|
||||
}>()
|
||||
|
||||
return groups.map((group) => {
|
||||
const items = globalHotkeyBindings
|
||||
.filter(binding => binding.help?.group === group.id)
|
||||
.filter((binding) => {
|
||||
if (!binding.help) {
|
||||
return false
|
||||
}
|
||||
if (binding.help.visible) {
|
||||
return binding.help.visible(hotkeyContext)
|
||||
}
|
||||
return true
|
||||
})
|
||||
.sort((a, b) => (a.help?.order ?? 0) - (b.help?.order ?? 0))
|
||||
.map((binding) => {
|
||||
const displayKeys = appSettingsStore.os === 'mac' && binding.help?.displayKeys.mac
|
||||
? binding.help.displayKeys.mac
|
||||
: binding.help?.displayKeys.default ?? []
|
||||
|
||||
return {
|
||||
id: binding.id,
|
||||
title: binding.help?.titleKey ? itemTitleMap[binding.help.titleKey] : '',
|
||||
displayKeys,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...group,
|
||||
items,
|
||||
hotkeyBindings.forEach((binding) => {
|
||||
const help = binding.help
|
||||
if (!help) {
|
||||
return
|
||||
}
|
||||
}).filter(group => group.items.length > 0)
|
||||
if (help.visible && !help.visible(hotkeyContext)) {
|
||||
return
|
||||
}
|
||||
|
||||
const meta = helpGroupMeta[help.group as keyof typeof helpGroupMeta]
|
||||
const group = groups.get(help.group) ?? {
|
||||
id: help.group,
|
||||
title: help.groupTitleKey ? itemTitleMap[help.groupTitleKey] ?? help.groupTitleKey : meta?.title ?? help.group,
|
||||
order: meta?.order ?? 100,
|
||||
items: [],
|
||||
}
|
||||
|
||||
group.items.push({
|
||||
id: binding.id,
|
||||
title: itemTitleMap[help.titleKey] ?? help.titleKey,
|
||||
order: help.order ?? 0,
|
||||
displayKeys: appSettingsStore.os === 'mac' && help.displayKeys.mac
|
||||
? help.displayKeys.mac
|
||||
: help.displayKeys.default,
|
||||
})
|
||||
groups.set(help.group, group)
|
||||
})
|
||||
|
||||
return Array.from(groups.values())
|
||||
.map(group => ({
|
||||
...group,
|
||||
items: [...group.items].sort((a, b) => a.order - b.order),
|
||||
}))
|
||||
.sort((a, b) => a.order - b.order)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import { useSlots } from '@/slots'
|
||||
import Logo from '../Logo/index.vue'
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { TabbarRecordRaw } from '@fantastic-admin/types'
|
||||
import { useMagicKeys } from '@vueuse/core'
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import { useSlots } from '@/slots'
|
||||
|
||||
defineOptions({
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import Search from './search.vue'
|
||||
|
||||
defineOptions({
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { MenuRecordRaw } from '@fantastic-admin/types'
|
||||
import { match } from 'pinyin-pro'
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import Breadcrumb from '@/layouts/components/Breadcrumb/index.vue'
|
||||
import BreadcrumbItem from '@/layouts/components/Breadcrumb/item.vue'
|
||||
import { resolveRoutePath } from '@/utils'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useElementSize, useScroll } from '@vueuse/core'
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import { useSlots } from '@/slots'
|
||||
import { cn } from '@/utils'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"dependencies": {
|
||||
"@fantastic-admin/components": "workspace:*",
|
||||
"@fantastic-admin/composables": "workspace:*",
|
||||
"@fantastic-admin/hotkeys": "workspace:*",
|
||||
"@fantastic-admin/settings": "workspace:*",
|
||||
"@fantastic-admin/types": "workspace:*",
|
||||
"@vee-validate/zod": "catalog:",
|
||||
@@ -27,7 +28,6 @@
|
||||
"dayjs": "catalog:",
|
||||
"eruda": "catalog:",
|
||||
"es-toolkit": "catalog:",
|
||||
"hotkeys-js": "catalog:",
|
||||
"mitt": "catalog:",
|
||||
"nprogress": "catalog:",
|
||||
"path-browserify": "catalog:",
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
# Hotkeys 设计说明
|
||||
|
||||
这个目录参考 `fantastic-admin/v6-dev/apps/example/src/hotkeys` 的设计,目标是把当前项目里分散在各组件中的快捷键逻辑收拢成一套轻量方案。
|
||||
|
||||
## 设计目标
|
||||
|
||||
当前方案只解决 4 件事:
|
||||
|
||||
1. 快捷键定义不要散落在多个组件里重复写
|
||||
2. 快捷键帮助面板不要再手写一份展示文案
|
||||
3. 业务组件不要反复手写 `hotkeys()` / `unbind()`
|
||||
4. 不引入过重的 command system
|
||||
|
||||
所以采用:
|
||||
|
||||
- `registry.ts` 统一定义
|
||||
- `useHotkeys.ts` 统一注册
|
||||
- 业务 handler 仍留在业务组件
|
||||
|
||||
这是一种“集中定义 + 就地执行”的轻量折中方案。
|
||||
|
||||
## 目录职责
|
||||
|
||||
### `registry.ts`
|
||||
|
||||
统一维护当前项目的快捷键元数据:
|
||||
|
||||
- id
|
||||
- keys
|
||||
- enabled
|
||||
- help
|
||||
|
||||
这里负责:
|
||||
|
||||
- 统一的 id 常量
|
||||
- 真实键位
|
||||
- 是否启用的条件
|
||||
- 帮助面板展示文案
|
||||
- 聚合业务扩展
|
||||
|
||||
> 注意:这里不放业务逻辑 handler。
|
||||
|
||||
### `registry.extend.ts`
|
||||
|
||||
用于放置项目自定义扩展快捷键,避免把业务项全部堆进内建定义中。
|
||||
|
||||
### `types.ts`
|
||||
|
||||
定义快捷键系统的基础类型,包括:
|
||||
|
||||
- `HotkeyBinding`
|
||||
- `HotkeyContext`
|
||||
- `HotkeyHelpMeta`
|
||||
- `HotkeyHandler`
|
||||
|
||||
### `useHotkeys.ts`
|
||||
|
||||
对 `hotkeys-js` 的薄封装,提供:
|
||||
|
||||
- `useHotkey`:注册单个快捷键
|
||||
- `useHotkeyBindings`:批量注册快捷键
|
||||
|
||||
并统一处理:
|
||||
|
||||
- 注册 / 卸载
|
||||
- `active` 启停
|
||||
- `binding.enabled` 可用性判断
|
||||
- 默认 `preventDefault`
|
||||
|
||||
## 当前项目已落地的快捷键
|
||||
|
||||
### 全局
|
||||
|
||||
- `system.info.open`
|
||||
- `menuSearch.open`
|
||||
- `page.reload`
|
||||
|
||||
### 主导航
|
||||
|
||||
- `menu.next`
|
||||
- `menu.prev`
|
||||
|
||||
### 标签栏
|
||||
|
||||
- `tabbar.prev`
|
||||
- `tabbar.next`
|
||||
- `tabbar.closeCurrent`
|
||||
- `tabbar.gotoVisibleIndex`
|
||||
- `tabbar.gotoLast`
|
||||
|
||||
### 菜单搜索弹窗
|
||||
|
||||
- `menuSearch.moveUp`
|
||||
- `menuSearch.moveDown`
|
||||
- `menuSearch.confirm`
|
||||
- `menuSearch.close`
|
||||
|
||||
## 使用约定
|
||||
|
||||
1. 新增快捷键,优先在 `registry.ts` 或 `registry.extend.ts` 补定义
|
||||
2. 展示在帮助面板中的快捷键,必须配置 `help`
|
||||
3. handler 尽量保留在最了解业务的组件中
|
||||
4. 不直接在业务组件里继续散写 `hotkeys('xxx')`
|
||||
|
||||
## 现阶段差异
|
||||
|
||||
参考项目里还有页面最大化相关快捷键;当前项目暂未引入对应页面最大化能力,所以本次迁移只保留当前项目已有功能,并把原有 `F5` 页面刷新也纳入统一 registry 管理。
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { HotkeyBinding } from '@fantastic-admin/hotkeys'
|
||||
|
||||
/**
|
||||
* 业务扩展快捷键 id
|
||||
*
|
||||
* 使用方式:
|
||||
*
|
||||
* export const EXT_HOTKEY_ID = {
|
||||
* demoOpen: 'demo.open',
|
||||
* } as const
|
||||
*/
|
||||
export const EXT_HOTKEY_ID = {} as const
|
||||
|
||||
type ExtendHotkeyId = typeof EXT_HOTKEY_ID[keyof typeof EXT_HOTKEY_ID]
|
||||
|
||||
/**
|
||||
* 业务扩展全局快捷键
|
||||
*
|
||||
* 示例:
|
||||
*
|
||||
* export const extendGlobalHotkeyBindings = [
|
||||
* {
|
||||
* id: EXT_HOTKEY_ID.demoOpen,
|
||||
* keys: ['command+j', 'ctrl+j'],
|
||||
* enabled: ctx => ctx.settings.toolbar.menuSearch.enable,
|
||||
* help: {
|
||||
* group: 'global',
|
||||
* titleKey: 'global.demoOpen',
|
||||
* order: 90,
|
||||
* visible: ctx => ctx.settings.toolbar.menuSearch.enable,
|
||||
* displayKeys: {
|
||||
* default: ['Ctrl', 'J'],
|
||||
* mac: ['⌘', 'J'],
|
||||
* },
|
||||
* },
|
||||
* },
|
||||
* ] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
*/
|
||||
export const extendGlobalHotkeyBindings = [] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
|
||||
/**
|
||||
* 业务扩展局部快捷键
|
||||
*
|
||||
* 示例:
|
||||
*
|
||||
* export const extendScopedHotkeyBindings = [
|
||||
* {
|
||||
* id: EXT_HOTKEY_ID.demoConfirm,
|
||||
* keys: ['enter'],
|
||||
* },
|
||||
* {
|
||||
* id: EXT_HOTKEY_ID.demoClose,
|
||||
* keys: ['esc'],
|
||||
* },
|
||||
* ] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
*/
|
||||
export const extendScopedHotkeyBindings = [] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
@@ -0,0 +1,41 @@
|
||||
import type {
|
||||
HotkeyBinding,
|
||||
HotkeyContextBase,
|
||||
HotkeyDisplayKeys,
|
||||
HotkeyHandler,
|
||||
HotkeyHandlerArgs,
|
||||
HotkeyHandlersMap,
|
||||
HotkeyHelpGroup,
|
||||
HotkeyHelpMeta,
|
||||
} from '@fantastic-admin/hotkeys'
|
||||
import { createHotkeys } from '@fantastic-admin/hotkeys'
|
||||
import { EXT_HOTKEY_ID, extendGlobalHotkeyBindings, extendScopedHotkeyBindings } from './extend'
|
||||
|
||||
const appHotkeys = createHotkeys({
|
||||
extendIds: EXT_HOTKEY_ID,
|
||||
extendGlobalHotkeyBindings,
|
||||
extendScopedHotkeyBindings,
|
||||
getContext: () => ({
|
||||
settings: useAppSettingsStore().settings,
|
||||
}),
|
||||
})
|
||||
|
||||
export const HOTKEY_ID = appHotkeys.HOTKEY_ID
|
||||
export const hotkeyIds = appHotkeys.hotkeyIds
|
||||
export const globalHotkeyBindings = appHotkeys.globalHotkeyBindings
|
||||
export const menuSearchHotkeyBindings = appHotkeys.menuSearchHotkeyBindings
|
||||
export const hotkeyBindings = appHotkeys.hotkeyBindings
|
||||
export const useHotkey = appHotkeys.useHotkey
|
||||
export const useHotkeyBindings = appHotkeys.useHotkeyBindings
|
||||
|
||||
export type HotkeyId = typeof HOTKEY_ID[keyof typeof HOTKEY_ID]
|
||||
export type {
|
||||
HotkeyBinding,
|
||||
HotkeyContextBase,
|
||||
HotkeyDisplayKeys,
|
||||
HotkeyHandler,
|
||||
HotkeyHandlerArgs,
|
||||
HotkeyHandlersMap,
|
||||
HotkeyHelpGroup,
|
||||
HotkeyHelpMeta,
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import type { HotkeyBinding } from './types'
|
||||
|
||||
/**
|
||||
* 业务扩展快捷键 id
|
||||
*
|
||||
* 使用方式:
|
||||
*
|
||||
* export const EXT_HOTKEY_ID = {
|
||||
* demoOpen: 'demo.open',
|
||||
* } as const
|
||||
*/
|
||||
export const EXT_HOTKEY_ID = {} as const
|
||||
|
||||
/**
|
||||
* 业务扩展全局快捷键
|
||||
*/
|
||||
export const extendGlobalHotkeyBindings: HotkeyBinding[] = []
|
||||
|
||||
/**
|
||||
* 业务扩展局部快捷键
|
||||
*/
|
||||
export const extendScopedHotkeyBindings: HotkeyBinding[] = []
|
||||
@@ -1,210 +0,0 @@
|
||||
import type { HotkeyBinding } from './types'
|
||||
import { EXT_HOTKEY_ID, extendGlobalHotkeyBindings, extendScopedHotkeyBindings } from './registry.extend'
|
||||
|
||||
export const HOTKEY_ID = {
|
||||
systemInfoOpen: 'system.info.open',
|
||||
menuSearchOpen: 'menuSearch.open',
|
||||
menuNext: 'menu.next',
|
||||
menuPrev: 'menu.prev',
|
||||
tabbarPrev: 'tabbar.prev',
|
||||
tabbarNext: 'tabbar.next',
|
||||
tabbarCloseCurrent: 'tabbar.closeCurrent',
|
||||
tabbarGotoVisibleIndex: 'tabbar.gotoVisibleIndex',
|
||||
tabbarGotoLast: 'tabbar.gotoLast',
|
||||
pageReload: 'page.reload',
|
||||
menuSearchMoveUp: 'menuSearch.moveUp',
|
||||
menuSearchMoveDown: 'menuSearch.moveDown',
|
||||
menuSearchConfirm: 'menuSearch.confirm',
|
||||
menuSearchClose: 'menuSearch.close',
|
||||
...EXT_HOTKEY_ID,
|
||||
} as const
|
||||
|
||||
export const hotkeyIds = Object.values(HOTKEY_ID)
|
||||
|
||||
/**
|
||||
* Hotkeys registry 维护约定
|
||||
*
|
||||
* 1. 新增快捷键优先在这里补充定义,不要在业务组件里直接写死键位字符串
|
||||
* 2. 这里仅维护“定义”和“展示元数据”,不要把业务 handler 塞进 registry
|
||||
* 3. 配置类开关尽量写在 enabled / help.visible 中,避免帮助面板与真实行为不一致
|
||||
* 4. 需要展示在快捷键帮助面板中的项,必须补充 help
|
||||
* 5. 仅局部场景使用的快捷键,也建议在这里定义,再由对应组件按需注册
|
||||
*/
|
||||
export const globalHotkeyBindings: HotkeyBinding[] = [
|
||||
{
|
||||
id: HOTKEY_ID.systemInfoOpen,
|
||||
keys: ['command+i', 'ctrl+i'],
|
||||
help: {
|
||||
group: 'global',
|
||||
titleKey: 'global.system',
|
||||
order: 10,
|
||||
displayKeys: {
|
||||
default: ['Ctrl', 'I'],
|
||||
mac: ['⌘', 'I'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchOpen,
|
||||
keys: ['command+k', 'ctrl+k'],
|
||||
enabled: ctx => ctx.settings.toolbar.menuSearch.enable && ctx.settings.toolbar.menuSearch.hotkeys,
|
||||
help: {
|
||||
group: 'global',
|
||||
titleKey: 'global.search',
|
||||
order: 20,
|
||||
visible: ctx => ctx.settings.toolbar.menuSearch.enable && ctx.settings.toolbar.menuSearch.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Ctrl', 'K'],
|
||||
mac: ['⌘', 'K'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuNext,
|
||||
keys: ['alt+`'],
|
||||
enabled: ctx => ctx.settings.menu.hotkeys && ['side', 'head'].includes(ctx.settings.menu.mode),
|
||||
help: {
|
||||
group: 'nav',
|
||||
titleKey: 'nav.next',
|
||||
order: 10,
|
||||
visible: ctx => ctx.settings.menu.hotkeys && ['side', 'head'].includes(ctx.settings.menu.mode),
|
||||
displayKeys: {
|
||||
default: ['Alt', '`'],
|
||||
mac: ['⌥', '`'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuPrev,
|
||||
keys: ['alt+shift+`'],
|
||||
enabled: ctx => ctx.settings.menu.hotkeys && ['side', 'head'].includes(ctx.settings.menu.mode),
|
||||
help: {
|
||||
group: 'nav',
|
||||
titleKey: 'nav.prev',
|
||||
order: 20,
|
||||
visible: ctx => ctx.settings.menu.hotkeys && ['side', 'head'].includes(ctx.settings.menu.mode),
|
||||
displayKeys: {
|
||||
default: ['Alt', 'Shift', '`'],
|
||||
mac: ['⌥', '⇧', '`'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarPrev,
|
||||
keys: ['alt+left'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.prev',
|
||||
order: 10,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', '←'],
|
||||
mac: ['⌥', '←'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarNext,
|
||||
keys: ['alt+right'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.next',
|
||||
order: 20,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', '→'],
|
||||
mac: ['⌥', '→'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarCloseCurrent,
|
||||
keys: ['alt+w'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.close',
|
||||
order: 30,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', 'W'],
|
||||
mac: ['⌥', 'W'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarGotoVisibleIndex,
|
||||
keys: ['alt+1', 'alt+2', 'alt+3', 'alt+4', 'alt+5', 'alt+6', 'alt+7', 'alt+8', 'alt+9'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.n',
|
||||
order: 40,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', '1~9'],
|
||||
mac: ['⌥', '1~9'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarGotoLast,
|
||||
keys: ['alt+0'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.last',
|
||||
order: 50,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', '0'],
|
||||
mac: ['⌥', '0'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.pageReload,
|
||||
keys: ['f5'],
|
||||
},
|
||||
...extendGlobalHotkeyBindings,
|
||||
]
|
||||
|
||||
export const menuSearchHotkeyBindings: HotkeyBinding[] = [
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchMoveUp,
|
||||
keys: ['up'],
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchMoveDown,
|
||||
keys: ['down'],
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchConfirm,
|
||||
keys: ['enter'],
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchClose,
|
||||
keys: ['esc'],
|
||||
},
|
||||
]
|
||||
|
||||
export const hotkeyBindings: HotkeyBinding[] = [
|
||||
...globalHotkeyBindings,
|
||||
...menuSearchHotkeyBindings,
|
||||
...extendScopedHotkeyBindings,
|
||||
]
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
const registeredIds = hotkeyBindings.map(item => item.id)
|
||||
const missingIds = hotkeyIds.filter(id => !registeredIds.includes(id))
|
||||
const duplicateIds = registeredIds.filter((id, index) => registeredIds.indexOf(id) !== index)
|
||||
|
||||
if (missingIds.length || duplicateIds.length) {
|
||||
console.warn('[hotkeys] registry consistency check failed', {
|
||||
missingIds,
|
||||
duplicateIds: [...new Set(duplicateIds)],
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { useAppSettingsStore } from '@/store/modules/app/settings'
|
||||
|
||||
export type HotkeyHelpGroup = 'global' | 'nav' | 'tabbar' | 'page'
|
||||
export type HotkeyId = typeof import('./registry').HOTKEY_ID[keyof typeof import('./registry').HOTKEY_ID]
|
||||
|
||||
export interface HotkeyContext {
|
||||
settings: ReturnType<typeof useAppSettingsStore>['settings']
|
||||
}
|
||||
|
||||
export interface HotkeyDisplayKeys {
|
||||
default: string[]
|
||||
mac?: string[]
|
||||
}
|
||||
|
||||
export interface HotkeyHelpMeta {
|
||||
group: HotkeyHelpGroup
|
||||
titleKey: string
|
||||
order?: number
|
||||
visible?: (ctx: HotkeyContext) => boolean
|
||||
displayKeys: HotkeyDisplayKeys
|
||||
}
|
||||
|
||||
export interface HotkeyBinding {
|
||||
id: HotkeyId
|
||||
keys: string[]
|
||||
preventDefault?: boolean
|
||||
enabled?: (ctx: HotkeyContext) => boolean
|
||||
help?: HotkeyHelpMeta
|
||||
}
|
||||
|
||||
export interface HotkeyHandlerArgs {
|
||||
event: KeyboardEvent
|
||||
hotkey: string
|
||||
}
|
||||
|
||||
export type HotkeyHandler = (args: HotkeyHandlerArgs) => void
|
||||
|
||||
export type HotkeyHandlersMap = Partial<Record<HotkeyId, HotkeyHandler>>
|
||||
@@ -1,69 +0,0 @@
|
||||
import type { HotkeyHandler, HotkeyHandlersMap, HotkeyId } from './types'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import { hotkeyBindings, hotkeyIds } from './registry'
|
||||
|
||||
export function useHotkey(id: HotkeyId, handler: HotkeyHandler, active: MaybeRefOrGetter<boolean> = true) {
|
||||
const binding = hotkeyBindings.find(item => item.id === id)
|
||||
const appSettingsStore = useAppSettingsStore()
|
||||
const hotkeyContext = {
|
||||
settings: appSettingsStore.settings,
|
||||
}
|
||||
let currentHandler: ((event: KeyboardEvent, hotkeyHandler: { key: string }) => void) | undefined
|
||||
|
||||
function bind() {
|
||||
if (currentHandler || !binding) {
|
||||
return
|
||||
}
|
||||
const currentBinding = binding
|
||||
|
||||
currentHandler = (event, hotkeyHandler) => {
|
||||
if (currentBinding.enabled && !currentBinding.enabled(hotkeyContext)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (currentBinding.preventDefault !== false) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
handler({
|
||||
event,
|
||||
hotkey: hotkeyHandler.key,
|
||||
})
|
||||
}
|
||||
|
||||
hotkeys(currentBinding.keys.join(','), currentHandler)
|
||||
}
|
||||
|
||||
function unbind() {
|
||||
if (!currentHandler || !binding) {
|
||||
return
|
||||
}
|
||||
|
||||
hotkeys.unbind(binding.keys.join(','), currentHandler)
|
||||
currentHandler = undefined
|
||||
}
|
||||
|
||||
watch(() => toValue(active), (val) => {
|
||||
if (val) {
|
||||
bind()
|
||||
}
|
||||
else {
|
||||
unbind()
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unbind()
|
||||
})
|
||||
}
|
||||
|
||||
export function useHotkeyBindings(handlers: HotkeyHandlersMap, active: MaybeRefOrGetter<boolean> = true) {
|
||||
hotkeyIds.forEach((id) => {
|
||||
const handler = handlers[id]
|
||||
if (handler) {
|
||||
useHotkey(id, handler, active)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { globalHotkeyBindings } from '@/hotkeys/registry'
|
||||
import { hotkeyBindings } from '@/hotkeys'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
|
||||
defineOptions({
|
||||
@@ -11,10 +11,10 @@ const hotkeyContext = {
|
||||
settings: appSettingsStore.settings,
|
||||
}
|
||||
|
||||
const groupTitleMap = {
|
||||
global: '全局',
|
||||
nav: '主导航',
|
||||
tabbar: '标签栏',
|
||||
const helpGroupMeta = {
|
||||
global: { title: '全局', order: 10 },
|
||||
nav: { title: '主导航', order: 20 },
|
||||
tabbar: { title: '标签栏', order: 30 },
|
||||
} as const
|
||||
|
||||
const itemTitleMap: Record<string, string> = {
|
||||
@@ -32,42 +32,52 @@ const itemTitleMap: Record<string, string> = {
|
||||
const isShow = ref(false)
|
||||
|
||||
const helpGroups = computed(() => {
|
||||
const groups = [
|
||||
{ id: 'global', title: groupTitleMap.global },
|
||||
{ id: 'nav', title: groupTitleMap.nav },
|
||||
{ id: 'tabbar', title: groupTitleMap.tabbar },
|
||||
] as const
|
||||
const groups = new Map<string, {
|
||||
id: string
|
||||
title: string
|
||||
order: number
|
||||
items: {
|
||||
id: string
|
||||
title: string
|
||||
order: number
|
||||
displayKeys: string[]
|
||||
}[]
|
||||
}>()
|
||||
|
||||
return groups.map((group) => {
|
||||
const items = globalHotkeyBindings
|
||||
.filter(binding => binding.help?.group === group.id)
|
||||
.filter((binding) => {
|
||||
if (!binding.help) {
|
||||
return false
|
||||
}
|
||||
if (binding.help.visible) {
|
||||
return binding.help.visible(hotkeyContext)
|
||||
}
|
||||
return true
|
||||
})
|
||||
.sort((a, b) => (a.help?.order ?? 0) - (b.help?.order ?? 0))
|
||||
.map((binding) => {
|
||||
const displayKeys = appSettingsStore.os === 'mac' && binding.help?.displayKeys.mac
|
||||
? binding.help.displayKeys.mac
|
||||
: binding.help?.displayKeys.default ?? []
|
||||
|
||||
return {
|
||||
id: binding.id,
|
||||
title: binding.help?.titleKey ? itemTitleMap[binding.help.titleKey] : '',
|
||||
displayKeys,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...group,
|
||||
items,
|
||||
hotkeyBindings.forEach((binding) => {
|
||||
const help = binding.help
|
||||
if (!help) {
|
||||
return
|
||||
}
|
||||
}).filter(group => group.items.length > 0)
|
||||
if (help.visible && !help.visible(hotkeyContext)) {
|
||||
return
|
||||
}
|
||||
|
||||
const meta = helpGroupMeta[help.group as keyof typeof helpGroupMeta]
|
||||
const group = groups.get(help.group) ?? {
|
||||
id: help.group,
|
||||
title: help.groupTitleKey ? itemTitleMap[help.groupTitleKey] ?? help.groupTitleKey : meta?.title ?? help.group,
|
||||
order: meta?.order ?? 100,
|
||||
items: [],
|
||||
}
|
||||
|
||||
group.items.push({
|
||||
id: binding.id,
|
||||
title: itemTitleMap[help.titleKey] ?? help.titleKey,
|
||||
order: help.order ?? 0,
|
||||
displayKeys: appSettingsStore.os === 'mac' && help.displayKeys.mac
|
||||
? help.displayKeys.mac
|
||||
: help.displayKeys.default,
|
||||
})
|
||||
groups.set(help.group, group)
|
||||
})
|
||||
|
||||
return Array.from(groups.values())
|
||||
.map(group => ({
|
||||
...group,
|
||||
items: [...group.items].sort((a, b) => a.order - b.order),
|
||||
}))
|
||||
.sort((a, b) => a.order - b.order)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import { useSlots } from '@/slots'
|
||||
import Logo from '../Logo/index.vue'
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { TabbarRecordRaw } from '@fantastic-admin/types'
|
||||
import { useMagicKeys } from '@vueuse/core'
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import { useSlots } from '@/slots'
|
||||
|
||||
defineOptions({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import Search from './search.vue'
|
||||
|
||||
defineOptions({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { MenuRecordRaw } from '@fantastic-admin/types'
|
||||
import { match } from 'pinyin-pro'
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import Breadcrumb from '@/layouts/components/Breadcrumb/index.vue'
|
||||
import BreadcrumbItem from '@/layouts/components/Breadcrumb/item.vue'
|
||||
import { resolveRoutePath } from '@/utils'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useElementSize, useScroll } from '@vueuse/core'
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import { useSlots } from '@/slots'
|
||||
import { cn } from '@/utils'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"dependencies": {
|
||||
"@fantastic-admin/components": "workspace:*",
|
||||
"@fantastic-admin/composables": "workspace:*",
|
||||
"@fantastic-admin/hotkeys": "workspace:*",
|
||||
"@fantastic-admin/settings": "workspace:*",
|
||||
"@fantastic-admin/types": "workspace:*",
|
||||
"@vee-validate/zod": "catalog:",
|
||||
@@ -27,7 +28,6 @@
|
||||
"dayjs": "catalog:",
|
||||
"eruda": "catalog:",
|
||||
"es-toolkit": "catalog:",
|
||||
"hotkeys-js": "catalog:",
|
||||
"mitt": "catalog:",
|
||||
"nprogress": "catalog:",
|
||||
"path-browserify": "catalog:",
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
# Hotkeys 设计说明
|
||||
|
||||
这个目录参考 `fantastic-admin/v6-dev/apps/example/src/hotkeys` 的设计,目标是把当前项目里分散在各组件中的快捷键逻辑收拢成一套轻量方案。
|
||||
|
||||
## 设计目标
|
||||
|
||||
当前方案只解决 4 件事:
|
||||
|
||||
1. 快捷键定义不要散落在多个组件里重复写
|
||||
2. 快捷键帮助面板不要再手写一份展示文案
|
||||
3. 业务组件不要反复手写 `hotkeys()` / `unbind()`
|
||||
4. 不引入过重的 command system
|
||||
|
||||
所以采用:
|
||||
|
||||
- `registry.ts` 统一定义
|
||||
- `useHotkeys.ts` 统一注册
|
||||
- 业务 handler 仍留在业务组件
|
||||
|
||||
这是一种“集中定义 + 就地执行”的轻量折中方案。
|
||||
|
||||
## 目录职责
|
||||
|
||||
### `registry.ts`
|
||||
|
||||
统一维护当前项目的快捷键元数据:
|
||||
|
||||
- id
|
||||
- keys
|
||||
- enabled
|
||||
- help
|
||||
|
||||
这里负责:
|
||||
|
||||
- 统一的 id 常量
|
||||
- 真实键位
|
||||
- 是否启用的条件
|
||||
- 帮助面板展示文案
|
||||
- 聚合业务扩展
|
||||
|
||||
> 注意:这里不放业务逻辑 handler。
|
||||
|
||||
### `registry.extend.ts`
|
||||
|
||||
用于放置项目自定义扩展快捷键,避免把业务项全部堆进内建定义中。
|
||||
|
||||
### `types.ts`
|
||||
|
||||
定义快捷键系统的基础类型,包括:
|
||||
|
||||
- `HotkeyBinding`
|
||||
- `HotkeyContext`
|
||||
- `HotkeyHelpMeta`
|
||||
- `HotkeyHandler`
|
||||
|
||||
### `useHotkeys.ts`
|
||||
|
||||
对 `hotkeys-js` 的薄封装,提供:
|
||||
|
||||
- `useHotkey`:注册单个快捷键
|
||||
- `useHotkeyBindings`:批量注册快捷键
|
||||
|
||||
并统一处理:
|
||||
|
||||
- 注册 / 卸载
|
||||
- `active` 启停
|
||||
- `binding.enabled` 可用性判断
|
||||
- 默认 `preventDefault`
|
||||
|
||||
## 当前项目已落地的快捷键
|
||||
|
||||
### 全局
|
||||
|
||||
- `system.info.open`
|
||||
- `menuSearch.open`
|
||||
- `page.reload`
|
||||
|
||||
### 主导航
|
||||
|
||||
- `menu.next`
|
||||
- `menu.prev`
|
||||
|
||||
### 标签栏
|
||||
|
||||
- `tabbar.prev`
|
||||
- `tabbar.next`
|
||||
- `tabbar.closeCurrent`
|
||||
- `tabbar.gotoVisibleIndex`
|
||||
- `tabbar.gotoLast`
|
||||
|
||||
### 菜单搜索弹窗
|
||||
|
||||
- `menuSearch.moveUp`
|
||||
- `menuSearch.moveDown`
|
||||
- `menuSearch.confirm`
|
||||
- `menuSearch.close`
|
||||
|
||||
## 使用约定
|
||||
|
||||
1. 新增快捷键,优先在 `registry.ts` 或 `registry.extend.ts` 补定义
|
||||
2. 展示在帮助面板中的快捷键,必须配置 `help`
|
||||
3. handler 尽量保留在最了解业务的组件中
|
||||
4. 不直接在业务组件里继续散写 `hotkeys('xxx')`
|
||||
|
||||
## 现阶段差异
|
||||
|
||||
参考项目里还有页面最大化相关快捷键;当前项目暂未引入对应页面最大化能力,所以本次迁移只保留当前项目已有功能,并把原有 `F5` 页面刷新也纳入统一 registry 管理。
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { HotkeyBinding } from '@fantastic-admin/hotkeys'
|
||||
|
||||
/**
|
||||
* 业务扩展快捷键 id
|
||||
*
|
||||
* 使用方式:
|
||||
*
|
||||
* export const EXT_HOTKEY_ID = {
|
||||
* demoOpen: 'demo.open',
|
||||
* } as const
|
||||
*/
|
||||
export const EXT_HOTKEY_ID = {} as const
|
||||
|
||||
type ExtendHotkeyId = typeof EXT_HOTKEY_ID[keyof typeof EXT_HOTKEY_ID]
|
||||
|
||||
/**
|
||||
* 业务扩展全局快捷键
|
||||
*
|
||||
* 示例:
|
||||
*
|
||||
* export const extendGlobalHotkeyBindings = [
|
||||
* {
|
||||
* id: EXT_HOTKEY_ID.demoOpen,
|
||||
* keys: ['command+j', 'ctrl+j'],
|
||||
* enabled: ctx => ctx.settings.toolbar.menuSearch.enable,
|
||||
* help: {
|
||||
* group: 'global',
|
||||
* titleKey: 'global.demoOpen',
|
||||
* order: 90,
|
||||
* visible: ctx => ctx.settings.toolbar.menuSearch.enable,
|
||||
* displayKeys: {
|
||||
* default: ['Ctrl', 'J'],
|
||||
* mac: ['⌘', 'J'],
|
||||
* },
|
||||
* },
|
||||
* },
|
||||
* ] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
*/
|
||||
export const extendGlobalHotkeyBindings = [] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
|
||||
/**
|
||||
* 业务扩展局部快捷键
|
||||
*
|
||||
* 示例:
|
||||
*
|
||||
* export const extendScopedHotkeyBindings = [
|
||||
* {
|
||||
* id: EXT_HOTKEY_ID.demoConfirm,
|
||||
* keys: ['enter'],
|
||||
* },
|
||||
* {
|
||||
* id: EXT_HOTKEY_ID.demoClose,
|
||||
* keys: ['esc'],
|
||||
* },
|
||||
* ] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
*/
|
||||
export const extendScopedHotkeyBindings = [] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
@@ -0,0 +1,41 @@
|
||||
import type {
|
||||
HotkeyBinding,
|
||||
HotkeyContextBase,
|
||||
HotkeyDisplayKeys,
|
||||
HotkeyHandler,
|
||||
HotkeyHandlerArgs,
|
||||
HotkeyHandlersMap,
|
||||
HotkeyHelpGroup,
|
||||
HotkeyHelpMeta,
|
||||
} from '@fantastic-admin/hotkeys'
|
||||
import { createHotkeys } from '@fantastic-admin/hotkeys'
|
||||
import { EXT_HOTKEY_ID, extendGlobalHotkeyBindings, extendScopedHotkeyBindings } from './extend'
|
||||
|
||||
const appHotkeys = createHotkeys({
|
||||
extendIds: EXT_HOTKEY_ID,
|
||||
extendGlobalHotkeyBindings,
|
||||
extendScopedHotkeyBindings,
|
||||
getContext: () => ({
|
||||
settings: useAppSettingsStore().settings,
|
||||
}),
|
||||
})
|
||||
|
||||
export const HOTKEY_ID = appHotkeys.HOTKEY_ID
|
||||
export const hotkeyIds = appHotkeys.hotkeyIds
|
||||
export const globalHotkeyBindings = appHotkeys.globalHotkeyBindings
|
||||
export const menuSearchHotkeyBindings = appHotkeys.menuSearchHotkeyBindings
|
||||
export const hotkeyBindings = appHotkeys.hotkeyBindings
|
||||
export const useHotkey = appHotkeys.useHotkey
|
||||
export const useHotkeyBindings = appHotkeys.useHotkeyBindings
|
||||
|
||||
export type HotkeyId = typeof HOTKEY_ID[keyof typeof HOTKEY_ID]
|
||||
export type {
|
||||
HotkeyBinding,
|
||||
HotkeyContextBase,
|
||||
HotkeyDisplayKeys,
|
||||
HotkeyHandler,
|
||||
HotkeyHandlerArgs,
|
||||
HotkeyHandlersMap,
|
||||
HotkeyHelpGroup,
|
||||
HotkeyHelpMeta,
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import type { HotkeyBinding } from './types'
|
||||
|
||||
/**
|
||||
* 业务扩展快捷键 id
|
||||
*
|
||||
* 使用方式:
|
||||
*
|
||||
* export const EXT_HOTKEY_ID = {
|
||||
* demoOpen: 'demo.open',
|
||||
* } as const
|
||||
*/
|
||||
export const EXT_HOTKEY_ID = {} as const
|
||||
|
||||
/**
|
||||
* 业务扩展全局快捷键
|
||||
*/
|
||||
export const extendGlobalHotkeyBindings: HotkeyBinding[] = []
|
||||
|
||||
/**
|
||||
* 业务扩展局部快捷键
|
||||
*/
|
||||
export const extendScopedHotkeyBindings: HotkeyBinding[] = []
|
||||
@@ -1,210 +0,0 @@
|
||||
import type { HotkeyBinding } from './types'
|
||||
import { EXT_HOTKEY_ID, extendGlobalHotkeyBindings, extendScopedHotkeyBindings } from './registry.extend'
|
||||
|
||||
export const HOTKEY_ID = {
|
||||
systemInfoOpen: 'system.info.open',
|
||||
menuSearchOpen: 'menuSearch.open',
|
||||
menuNext: 'menu.next',
|
||||
menuPrev: 'menu.prev',
|
||||
tabbarPrev: 'tabbar.prev',
|
||||
tabbarNext: 'tabbar.next',
|
||||
tabbarCloseCurrent: 'tabbar.closeCurrent',
|
||||
tabbarGotoVisibleIndex: 'tabbar.gotoVisibleIndex',
|
||||
tabbarGotoLast: 'tabbar.gotoLast',
|
||||
pageReload: 'page.reload',
|
||||
menuSearchMoveUp: 'menuSearch.moveUp',
|
||||
menuSearchMoveDown: 'menuSearch.moveDown',
|
||||
menuSearchConfirm: 'menuSearch.confirm',
|
||||
menuSearchClose: 'menuSearch.close',
|
||||
...EXT_HOTKEY_ID,
|
||||
} as const
|
||||
|
||||
export const hotkeyIds = Object.values(HOTKEY_ID)
|
||||
|
||||
/**
|
||||
* Hotkeys registry 维护约定
|
||||
*
|
||||
* 1. 新增快捷键优先在这里补充定义,不要在业务组件里直接写死键位字符串
|
||||
* 2. 这里仅维护“定义”和“展示元数据”,不要把业务 handler 塞进 registry
|
||||
* 3. 配置类开关尽量写在 enabled / help.visible 中,避免帮助面板与真实行为不一致
|
||||
* 4. 需要展示在快捷键帮助面板中的项,必须补充 help
|
||||
* 5. 仅局部场景使用的快捷键,也建议在这里定义,再由对应组件按需注册
|
||||
*/
|
||||
export const globalHotkeyBindings: HotkeyBinding[] = [
|
||||
{
|
||||
id: HOTKEY_ID.systemInfoOpen,
|
||||
keys: ['command+i', 'ctrl+i'],
|
||||
help: {
|
||||
group: 'global',
|
||||
titleKey: 'global.system',
|
||||
order: 10,
|
||||
displayKeys: {
|
||||
default: ['Ctrl', 'I'],
|
||||
mac: ['⌘', 'I'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchOpen,
|
||||
keys: ['command+k', 'ctrl+k'],
|
||||
enabled: ctx => ctx.settings.toolbar.menuSearch.enable && ctx.settings.toolbar.menuSearch.hotkeys,
|
||||
help: {
|
||||
group: 'global',
|
||||
titleKey: 'global.search',
|
||||
order: 20,
|
||||
visible: ctx => ctx.settings.toolbar.menuSearch.enable && ctx.settings.toolbar.menuSearch.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Ctrl', 'K'],
|
||||
mac: ['⌘', 'K'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuNext,
|
||||
keys: ['alt+`'],
|
||||
enabled: ctx => ctx.settings.menu.hotkeys && ['side', 'head'].includes(ctx.settings.menu.mode),
|
||||
help: {
|
||||
group: 'nav',
|
||||
titleKey: 'nav.next',
|
||||
order: 10,
|
||||
visible: ctx => ctx.settings.menu.hotkeys && ['side', 'head'].includes(ctx.settings.menu.mode),
|
||||
displayKeys: {
|
||||
default: ['Alt', '`'],
|
||||
mac: ['⌥', '`'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuPrev,
|
||||
keys: ['alt+shift+`'],
|
||||
enabled: ctx => ctx.settings.menu.hotkeys && ['side', 'head'].includes(ctx.settings.menu.mode),
|
||||
help: {
|
||||
group: 'nav',
|
||||
titleKey: 'nav.prev',
|
||||
order: 20,
|
||||
visible: ctx => ctx.settings.menu.hotkeys && ['side', 'head'].includes(ctx.settings.menu.mode),
|
||||
displayKeys: {
|
||||
default: ['Alt', 'Shift', '`'],
|
||||
mac: ['⌥', '⇧', '`'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarPrev,
|
||||
keys: ['alt+left'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.prev',
|
||||
order: 10,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', '←'],
|
||||
mac: ['⌥', '←'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarNext,
|
||||
keys: ['alt+right'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.next',
|
||||
order: 20,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', '→'],
|
||||
mac: ['⌥', '→'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarCloseCurrent,
|
||||
keys: ['alt+w'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.close',
|
||||
order: 30,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', 'W'],
|
||||
mac: ['⌥', 'W'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarGotoVisibleIndex,
|
||||
keys: ['alt+1', 'alt+2', 'alt+3', 'alt+4', 'alt+5', 'alt+6', 'alt+7', 'alt+8', 'alt+9'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.n',
|
||||
order: 40,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', '1~9'],
|
||||
mac: ['⌥', '1~9'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarGotoLast,
|
||||
keys: ['alt+0'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.last',
|
||||
order: 50,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', '0'],
|
||||
mac: ['⌥', '0'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.pageReload,
|
||||
keys: ['f5'],
|
||||
},
|
||||
...extendGlobalHotkeyBindings,
|
||||
]
|
||||
|
||||
export const menuSearchHotkeyBindings: HotkeyBinding[] = [
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchMoveUp,
|
||||
keys: ['up'],
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchMoveDown,
|
||||
keys: ['down'],
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchConfirm,
|
||||
keys: ['enter'],
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchClose,
|
||||
keys: ['esc'],
|
||||
},
|
||||
]
|
||||
|
||||
export const hotkeyBindings: HotkeyBinding[] = [
|
||||
...globalHotkeyBindings,
|
||||
...menuSearchHotkeyBindings,
|
||||
...extendScopedHotkeyBindings,
|
||||
]
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
const registeredIds = hotkeyBindings.map(item => item.id)
|
||||
const missingIds = hotkeyIds.filter(id => !registeredIds.includes(id))
|
||||
const duplicateIds = registeredIds.filter((id, index) => registeredIds.indexOf(id) !== index)
|
||||
|
||||
if (missingIds.length || duplicateIds.length) {
|
||||
console.warn('[hotkeys] registry consistency check failed', {
|
||||
missingIds,
|
||||
duplicateIds: [...new Set(duplicateIds)],
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { useAppSettingsStore } from '@/store/modules/app/settings'
|
||||
|
||||
export type HotkeyHelpGroup = 'global' | 'nav' | 'tabbar' | 'page'
|
||||
export type HotkeyId = typeof import('./registry').HOTKEY_ID[keyof typeof import('./registry').HOTKEY_ID]
|
||||
|
||||
export interface HotkeyContext {
|
||||
settings: ReturnType<typeof useAppSettingsStore>['settings']
|
||||
}
|
||||
|
||||
export interface HotkeyDisplayKeys {
|
||||
default: string[]
|
||||
mac?: string[]
|
||||
}
|
||||
|
||||
export interface HotkeyHelpMeta {
|
||||
group: HotkeyHelpGroup
|
||||
titleKey: string
|
||||
order?: number
|
||||
visible?: (ctx: HotkeyContext) => boolean
|
||||
displayKeys: HotkeyDisplayKeys
|
||||
}
|
||||
|
||||
export interface HotkeyBinding {
|
||||
id: HotkeyId
|
||||
keys: string[]
|
||||
preventDefault?: boolean
|
||||
enabled?: (ctx: HotkeyContext) => boolean
|
||||
help?: HotkeyHelpMeta
|
||||
}
|
||||
|
||||
export interface HotkeyHandlerArgs {
|
||||
event: KeyboardEvent
|
||||
hotkey: string
|
||||
}
|
||||
|
||||
export type HotkeyHandler = (args: HotkeyHandlerArgs) => void
|
||||
|
||||
export type HotkeyHandlersMap = Partial<Record<HotkeyId, HotkeyHandler>>
|
||||
@@ -1,69 +0,0 @@
|
||||
import type { HotkeyHandler, HotkeyHandlersMap, HotkeyId } from './types'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import { hotkeyBindings, hotkeyIds } from './registry'
|
||||
|
||||
export function useHotkey(id: HotkeyId, handler: HotkeyHandler, active: MaybeRefOrGetter<boolean> = true) {
|
||||
const binding = hotkeyBindings.find(item => item.id === id)
|
||||
const appSettingsStore = useAppSettingsStore()
|
||||
const hotkeyContext = {
|
||||
settings: appSettingsStore.settings,
|
||||
}
|
||||
let currentHandler: ((event: KeyboardEvent, hotkeyHandler: { key: string }) => void) | undefined
|
||||
|
||||
function bind() {
|
||||
if (currentHandler || !binding) {
|
||||
return
|
||||
}
|
||||
const currentBinding = binding
|
||||
|
||||
currentHandler = (event, hotkeyHandler) => {
|
||||
if (currentBinding.enabled && !currentBinding.enabled(hotkeyContext)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (currentBinding.preventDefault !== false) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
handler({
|
||||
event,
|
||||
hotkey: hotkeyHandler.key,
|
||||
})
|
||||
}
|
||||
|
||||
hotkeys(currentBinding.keys.join(','), currentHandler)
|
||||
}
|
||||
|
||||
function unbind() {
|
||||
if (!currentHandler || !binding) {
|
||||
return
|
||||
}
|
||||
|
||||
hotkeys.unbind(binding.keys.join(','), currentHandler)
|
||||
currentHandler = undefined
|
||||
}
|
||||
|
||||
watch(() => toValue(active), (val) => {
|
||||
if (val) {
|
||||
bind()
|
||||
}
|
||||
else {
|
||||
unbind()
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unbind()
|
||||
})
|
||||
}
|
||||
|
||||
export function useHotkeyBindings(handlers: HotkeyHandlersMap, active: MaybeRefOrGetter<boolean> = true) {
|
||||
hotkeyIds.forEach((id) => {
|
||||
const handler = handlers[id]
|
||||
if (handler) {
|
||||
useHotkey(id, handler, active)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { globalHotkeyBindings } from '@/hotkeys/registry'
|
||||
import { hotkeyBindings } from '@/hotkeys'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
|
||||
defineOptions({
|
||||
@@ -11,10 +11,10 @@ const hotkeyContext = {
|
||||
settings: appSettingsStore.settings,
|
||||
}
|
||||
|
||||
const groupTitleMap = {
|
||||
global: '全局',
|
||||
nav: '主导航',
|
||||
tabbar: '标签栏',
|
||||
const helpGroupMeta = {
|
||||
global: { title: '全局', order: 10 },
|
||||
nav: { title: '主导航', order: 20 },
|
||||
tabbar: { title: '标签栏', order: 30 },
|
||||
} as const
|
||||
|
||||
const itemTitleMap: Record<string, string> = {
|
||||
@@ -32,42 +32,52 @@ const itemTitleMap: Record<string, string> = {
|
||||
const isShow = ref(false)
|
||||
|
||||
const helpGroups = computed(() => {
|
||||
const groups = [
|
||||
{ id: 'global', title: groupTitleMap.global },
|
||||
{ id: 'nav', title: groupTitleMap.nav },
|
||||
{ id: 'tabbar', title: groupTitleMap.tabbar },
|
||||
] as const
|
||||
const groups = new Map<string, {
|
||||
id: string
|
||||
title: string
|
||||
order: number
|
||||
items: {
|
||||
id: string
|
||||
title: string
|
||||
order: number
|
||||
displayKeys: string[]
|
||||
}[]
|
||||
}>()
|
||||
|
||||
return groups.map((group) => {
|
||||
const items = globalHotkeyBindings
|
||||
.filter(binding => binding.help?.group === group.id)
|
||||
.filter((binding) => {
|
||||
if (!binding.help) {
|
||||
return false
|
||||
}
|
||||
if (binding.help.visible) {
|
||||
return binding.help.visible(hotkeyContext)
|
||||
}
|
||||
return true
|
||||
})
|
||||
.sort((a, b) => (a.help?.order ?? 0) - (b.help?.order ?? 0))
|
||||
.map((binding) => {
|
||||
const displayKeys = appSettingsStore.os === 'mac' && binding.help?.displayKeys.mac
|
||||
? binding.help.displayKeys.mac
|
||||
: binding.help?.displayKeys.default ?? []
|
||||
|
||||
return {
|
||||
id: binding.id,
|
||||
title: binding.help?.titleKey ? itemTitleMap[binding.help.titleKey] : '',
|
||||
displayKeys,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...group,
|
||||
items,
|
||||
hotkeyBindings.forEach((binding) => {
|
||||
const help = binding.help
|
||||
if (!help) {
|
||||
return
|
||||
}
|
||||
}).filter(group => group.items.length > 0)
|
||||
if (help.visible && !help.visible(hotkeyContext)) {
|
||||
return
|
||||
}
|
||||
|
||||
const meta = helpGroupMeta[help.group as keyof typeof helpGroupMeta]
|
||||
const group = groups.get(help.group) ?? {
|
||||
id: help.group,
|
||||
title: help.groupTitleKey ? itemTitleMap[help.groupTitleKey] ?? help.groupTitleKey : meta?.title ?? help.group,
|
||||
order: meta?.order ?? 100,
|
||||
items: [],
|
||||
}
|
||||
|
||||
group.items.push({
|
||||
id: binding.id,
|
||||
title: itemTitleMap[help.titleKey] ?? help.titleKey,
|
||||
order: help.order ?? 0,
|
||||
displayKeys: appSettingsStore.os === 'mac' && help.displayKeys.mac
|
||||
? help.displayKeys.mac
|
||||
: help.displayKeys.default,
|
||||
})
|
||||
groups.set(help.group, group)
|
||||
})
|
||||
|
||||
return Array.from(groups.values())
|
||||
.map(group => ({
|
||||
...group,
|
||||
items: [...group.items].sort((a, b) => a.order - b.order),
|
||||
}))
|
||||
.sort((a, b) => a.order - b.order)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import { useSlots } from '@/slots'
|
||||
import Logo from '../Logo/index.vue'
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { TabbarRecordRaw } from '@fantastic-admin/types'
|
||||
import { useMagicKeys } from '@vueuse/core'
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import { useSlots } from '@/slots'
|
||||
|
||||
defineOptions({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import Search from './search.vue'
|
||||
|
||||
defineOptions({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { MenuRecordRaw } from '@fantastic-admin/types'
|
||||
import { match } from 'pinyin-pro'
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import Breadcrumb from '@/layouts/components/Breadcrumb/index.vue'
|
||||
import BreadcrumbItem from '@/layouts/components/Breadcrumb/item.vue'
|
||||
import { resolveRoutePath } from '@/utils'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useElementSize, useScroll } from '@vueuse/core'
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import { useSlots } from '@/slots'
|
||||
import { cn } from '@/utils'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"dependencies": {
|
||||
"@fantastic-admin/components": "workspace:*",
|
||||
"@fantastic-admin/composables": "workspace:*",
|
||||
"@fantastic-admin/hotkeys": "workspace:*",
|
||||
"@fantastic-admin/settings": "workspace:*",
|
||||
"@fantastic-admin/types": "workspace:*",
|
||||
"@vee-validate/zod": "catalog:",
|
||||
@@ -27,7 +28,6 @@
|
||||
"dayjs": "catalog:",
|
||||
"eruda": "catalog:",
|
||||
"es-toolkit": "catalog:",
|
||||
"hotkeys-js": "catalog:",
|
||||
"mitt": "catalog:",
|
||||
"nprogress": "catalog:",
|
||||
"path-browserify": "catalog:",
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
# Hotkeys 设计说明
|
||||
|
||||
这个目录参考 `fantastic-admin/v6-dev/apps/example/src/hotkeys` 的设计,目标是把当前项目里分散在各组件中的快捷键逻辑收拢成一套轻量方案。
|
||||
|
||||
## 设计目标
|
||||
|
||||
当前方案只解决 4 件事:
|
||||
|
||||
1. 快捷键定义不要散落在多个组件里重复写
|
||||
2. 快捷键帮助面板不要再手写一份展示文案
|
||||
3. 业务组件不要反复手写 `hotkeys()` / `unbind()`
|
||||
4. 不引入过重的 command system
|
||||
|
||||
所以采用:
|
||||
|
||||
- `registry.ts` 统一定义
|
||||
- `useHotkeys.ts` 统一注册
|
||||
- 业务 handler 仍留在业务组件
|
||||
|
||||
这是一种“集中定义 + 就地执行”的轻量折中方案。
|
||||
|
||||
## 目录职责
|
||||
|
||||
### `registry.ts`
|
||||
|
||||
统一维护当前项目的快捷键元数据:
|
||||
|
||||
- id
|
||||
- keys
|
||||
- enabled
|
||||
- help
|
||||
|
||||
这里负责:
|
||||
|
||||
- 统一的 id 常量
|
||||
- 真实键位
|
||||
- 是否启用的条件
|
||||
- 帮助面板展示文案
|
||||
- 聚合业务扩展
|
||||
|
||||
> 注意:这里不放业务逻辑 handler。
|
||||
|
||||
### `registry.extend.ts`
|
||||
|
||||
用于放置项目自定义扩展快捷键,避免把业务项全部堆进内建定义中。
|
||||
|
||||
### `types.ts`
|
||||
|
||||
定义快捷键系统的基础类型,包括:
|
||||
|
||||
- `HotkeyBinding`
|
||||
- `HotkeyContext`
|
||||
- `HotkeyHelpMeta`
|
||||
- `HotkeyHandler`
|
||||
|
||||
### `useHotkeys.ts`
|
||||
|
||||
对 `hotkeys-js` 的薄封装,提供:
|
||||
|
||||
- `useHotkey`:注册单个快捷键
|
||||
- `useHotkeyBindings`:批量注册快捷键
|
||||
|
||||
并统一处理:
|
||||
|
||||
- 注册 / 卸载
|
||||
- `active` 启停
|
||||
- `binding.enabled` 可用性判断
|
||||
- 默认 `preventDefault`
|
||||
|
||||
## 当前项目已落地的快捷键
|
||||
|
||||
### 全局
|
||||
|
||||
- `system.info.open`
|
||||
- `menuSearch.open`
|
||||
- `page.reload`
|
||||
|
||||
### 主导航
|
||||
|
||||
- `menu.next`
|
||||
- `menu.prev`
|
||||
|
||||
### 标签栏
|
||||
|
||||
- `tabbar.prev`
|
||||
- `tabbar.next`
|
||||
- `tabbar.closeCurrent`
|
||||
- `tabbar.gotoVisibleIndex`
|
||||
- `tabbar.gotoLast`
|
||||
|
||||
### 菜单搜索弹窗
|
||||
|
||||
- `menuSearch.moveUp`
|
||||
- `menuSearch.moveDown`
|
||||
- `menuSearch.confirm`
|
||||
- `menuSearch.close`
|
||||
|
||||
## 使用约定
|
||||
|
||||
1. 新增快捷键,优先在 `registry.ts` 或 `registry.extend.ts` 补定义
|
||||
2. 展示在帮助面板中的快捷键,必须配置 `help`
|
||||
3. handler 尽量保留在最了解业务的组件中
|
||||
4. 不直接在业务组件里继续散写 `hotkeys('xxx')`
|
||||
|
||||
## 现阶段差异
|
||||
|
||||
参考项目里还有页面最大化相关快捷键;当前项目暂未引入对应页面最大化能力,所以本次迁移只保留当前项目已有功能,并把原有 `F5` 页面刷新也纳入统一 registry 管理。
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { HotkeyBinding } from '@fantastic-admin/hotkeys'
|
||||
|
||||
/**
|
||||
* 业务扩展快捷键 id
|
||||
*
|
||||
* 使用方式:
|
||||
*
|
||||
* export const EXT_HOTKEY_ID = {
|
||||
* demoOpen: 'demo.open',
|
||||
* } as const
|
||||
*/
|
||||
export const EXT_HOTKEY_ID = {} as const
|
||||
|
||||
type ExtendHotkeyId = typeof EXT_HOTKEY_ID[keyof typeof EXT_HOTKEY_ID]
|
||||
|
||||
/**
|
||||
* 业务扩展全局快捷键
|
||||
*
|
||||
* 示例:
|
||||
*
|
||||
* export const extendGlobalHotkeyBindings = [
|
||||
* {
|
||||
* id: EXT_HOTKEY_ID.demoOpen,
|
||||
* keys: ['command+j', 'ctrl+j'],
|
||||
* enabled: ctx => ctx.settings.toolbar.menuSearch.enable,
|
||||
* help: {
|
||||
* group: 'global',
|
||||
* titleKey: 'global.demoOpen',
|
||||
* order: 90,
|
||||
* visible: ctx => ctx.settings.toolbar.menuSearch.enable,
|
||||
* displayKeys: {
|
||||
* default: ['Ctrl', 'J'],
|
||||
* mac: ['⌘', 'J'],
|
||||
* },
|
||||
* },
|
||||
* },
|
||||
* ] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
*/
|
||||
export const extendGlobalHotkeyBindings = [] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
|
||||
/**
|
||||
* 业务扩展局部快捷键
|
||||
*
|
||||
* 示例:
|
||||
*
|
||||
* export const extendScopedHotkeyBindings = [
|
||||
* {
|
||||
* id: EXT_HOTKEY_ID.demoConfirm,
|
||||
* keys: ['enter'],
|
||||
* },
|
||||
* {
|
||||
* id: EXT_HOTKEY_ID.demoClose,
|
||||
* keys: ['esc'],
|
||||
* },
|
||||
* ] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
*/
|
||||
export const extendScopedHotkeyBindings = [] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
@@ -0,0 +1,41 @@
|
||||
import type {
|
||||
HotkeyBinding,
|
||||
HotkeyContextBase,
|
||||
HotkeyDisplayKeys,
|
||||
HotkeyHandler,
|
||||
HotkeyHandlerArgs,
|
||||
HotkeyHandlersMap,
|
||||
HotkeyHelpGroup,
|
||||
HotkeyHelpMeta,
|
||||
} from '@fantastic-admin/hotkeys'
|
||||
import { createHotkeys } from '@fantastic-admin/hotkeys'
|
||||
import { EXT_HOTKEY_ID, extendGlobalHotkeyBindings, extendScopedHotkeyBindings } from './extend'
|
||||
|
||||
const appHotkeys = createHotkeys({
|
||||
extendIds: EXT_HOTKEY_ID,
|
||||
extendGlobalHotkeyBindings,
|
||||
extendScopedHotkeyBindings,
|
||||
getContext: () => ({
|
||||
settings: useAppSettingsStore().settings,
|
||||
}),
|
||||
})
|
||||
|
||||
export const HOTKEY_ID = appHotkeys.HOTKEY_ID
|
||||
export const hotkeyIds = appHotkeys.hotkeyIds
|
||||
export const globalHotkeyBindings = appHotkeys.globalHotkeyBindings
|
||||
export const menuSearchHotkeyBindings = appHotkeys.menuSearchHotkeyBindings
|
||||
export const hotkeyBindings = appHotkeys.hotkeyBindings
|
||||
export const useHotkey = appHotkeys.useHotkey
|
||||
export const useHotkeyBindings = appHotkeys.useHotkeyBindings
|
||||
|
||||
export type HotkeyId = typeof HOTKEY_ID[keyof typeof HOTKEY_ID]
|
||||
export type {
|
||||
HotkeyBinding,
|
||||
HotkeyContextBase,
|
||||
HotkeyDisplayKeys,
|
||||
HotkeyHandler,
|
||||
HotkeyHandlerArgs,
|
||||
HotkeyHandlersMap,
|
||||
HotkeyHelpGroup,
|
||||
HotkeyHelpMeta,
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import type { HotkeyBinding } from './types'
|
||||
|
||||
/**
|
||||
* 业务扩展快捷键 id
|
||||
*
|
||||
* 使用方式:
|
||||
*
|
||||
* export const EXT_HOTKEY_ID = {
|
||||
* demoOpen: 'demo.open',
|
||||
* } as const
|
||||
*/
|
||||
export const EXT_HOTKEY_ID = {} as const
|
||||
|
||||
/**
|
||||
* 业务扩展全局快捷键
|
||||
*/
|
||||
export const extendGlobalHotkeyBindings: HotkeyBinding[] = []
|
||||
|
||||
/**
|
||||
* 业务扩展局部快捷键
|
||||
*/
|
||||
export const extendScopedHotkeyBindings: HotkeyBinding[] = []
|
||||
@@ -1,210 +0,0 @@
|
||||
import type { HotkeyBinding } from './types'
|
||||
import { EXT_HOTKEY_ID, extendGlobalHotkeyBindings, extendScopedHotkeyBindings } from './registry.extend'
|
||||
|
||||
export const HOTKEY_ID = {
|
||||
systemInfoOpen: 'system.info.open',
|
||||
menuSearchOpen: 'menuSearch.open',
|
||||
menuNext: 'menu.next',
|
||||
menuPrev: 'menu.prev',
|
||||
tabbarPrev: 'tabbar.prev',
|
||||
tabbarNext: 'tabbar.next',
|
||||
tabbarCloseCurrent: 'tabbar.closeCurrent',
|
||||
tabbarGotoVisibleIndex: 'tabbar.gotoVisibleIndex',
|
||||
tabbarGotoLast: 'tabbar.gotoLast',
|
||||
pageReload: 'page.reload',
|
||||
menuSearchMoveUp: 'menuSearch.moveUp',
|
||||
menuSearchMoveDown: 'menuSearch.moveDown',
|
||||
menuSearchConfirm: 'menuSearch.confirm',
|
||||
menuSearchClose: 'menuSearch.close',
|
||||
...EXT_HOTKEY_ID,
|
||||
} as const
|
||||
|
||||
export const hotkeyIds = Object.values(HOTKEY_ID)
|
||||
|
||||
/**
|
||||
* Hotkeys registry 维护约定
|
||||
*
|
||||
* 1. 新增快捷键优先在这里补充定义,不要在业务组件里直接写死键位字符串
|
||||
* 2. 这里仅维护“定义”和“展示元数据”,不要把业务 handler 塞进 registry
|
||||
* 3. 配置类开关尽量写在 enabled / help.visible 中,避免帮助面板与真实行为不一致
|
||||
* 4. 需要展示在快捷键帮助面板中的项,必须补充 help
|
||||
* 5. 仅局部场景使用的快捷键,也建议在这里定义,再由对应组件按需注册
|
||||
*/
|
||||
export const globalHotkeyBindings: HotkeyBinding[] = [
|
||||
{
|
||||
id: HOTKEY_ID.systemInfoOpen,
|
||||
keys: ['command+i', 'ctrl+i'],
|
||||
help: {
|
||||
group: 'global',
|
||||
titleKey: 'global.system',
|
||||
order: 10,
|
||||
displayKeys: {
|
||||
default: ['Ctrl', 'I'],
|
||||
mac: ['⌘', 'I'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchOpen,
|
||||
keys: ['command+k', 'ctrl+k'],
|
||||
enabled: ctx => ctx.settings.toolbar.menuSearch.enable && ctx.settings.toolbar.menuSearch.hotkeys,
|
||||
help: {
|
||||
group: 'global',
|
||||
titleKey: 'global.search',
|
||||
order: 20,
|
||||
visible: ctx => ctx.settings.toolbar.menuSearch.enable && ctx.settings.toolbar.menuSearch.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Ctrl', 'K'],
|
||||
mac: ['⌘', 'K'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuNext,
|
||||
keys: ['alt+`'],
|
||||
enabled: ctx => ctx.settings.menu.hotkeys && ['side', 'head'].includes(ctx.settings.menu.mode),
|
||||
help: {
|
||||
group: 'nav',
|
||||
titleKey: 'nav.next',
|
||||
order: 10,
|
||||
visible: ctx => ctx.settings.menu.hotkeys && ['side', 'head'].includes(ctx.settings.menu.mode),
|
||||
displayKeys: {
|
||||
default: ['Alt', '`'],
|
||||
mac: ['⌥', '`'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuPrev,
|
||||
keys: ['alt+shift+`'],
|
||||
enabled: ctx => ctx.settings.menu.hotkeys && ['side', 'head'].includes(ctx.settings.menu.mode),
|
||||
help: {
|
||||
group: 'nav',
|
||||
titleKey: 'nav.prev',
|
||||
order: 20,
|
||||
visible: ctx => ctx.settings.menu.hotkeys && ['side', 'head'].includes(ctx.settings.menu.mode),
|
||||
displayKeys: {
|
||||
default: ['Alt', 'Shift', '`'],
|
||||
mac: ['⌥', '⇧', '`'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarPrev,
|
||||
keys: ['alt+left'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.prev',
|
||||
order: 10,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', '←'],
|
||||
mac: ['⌥', '←'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarNext,
|
||||
keys: ['alt+right'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.next',
|
||||
order: 20,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', '→'],
|
||||
mac: ['⌥', '→'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarCloseCurrent,
|
||||
keys: ['alt+w'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.close',
|
||||
order: 30,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', 'W'],
|
||||
mac: ['⌥', 'W'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarGotoVisibleIndex,
|
||||
keys: ['alt+1', 'alt+2', 'alt+3', 'alt+4', 'alt+5', 'alt+6', 'alt+7', 'alt+8', 'alt+9'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.n',
|
||||
order: 40,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', '1~9'],
|
||||
mac: ['⌥', '1~9'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarGotoLast,
|
||||
keys: ['alt+0'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.last',
|
||||
order: 50,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', '0'],
|
||||
mac: ['⌥', '0'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.pageReload,
|
||||
keys: ['f5'],
|
||||
},
|
||||
...extendGlobalHotkeyBindings,
|
||||
]
|
||||
|
||||
export const menuSearchHotkeyBindings: HotkeyBinding[] = [
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchMoveUp,
|
||||
keys: ['up'],
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchMoveDown,
|
||||
keys: ['down'],
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchConfirm,
|
||||
keys: ['enter'],
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchClose,
|
||||
keys: ['esc'],
|
||||
},
|
||||
]
|
||||
|
||||
export const hotkeyBindings: HotkeyBinding[] = [
|
||||
...globalHotkeyBindings,
|
||||
...menuSearchHotkeyBindings,
|
||||
...extendScopedHotkeyBindings,
|
||||
]
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
const registeredIds = hotkeyBindings.map(item => item.id)
|
||||
const missingIds = hotkeyIds.filter(id => !registeredIds.includes(id))
|
||||
const duplicateIds = registeredIds.filter((id, index) => registeredIds.indexOf(id) !== index)
|
||||
|
||||
if (missingIds.length || duplicateIds.length) {
|
||||
console.warn('[hotkeys] registry consistency check failed', {
|
||||
missingIds,
|
||||
duplicateIds: [...new Set(duplicateIds)],
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { useAppSettingsStore } from '@/store/modules/app/settings'
|
||||
|
||||
export type HotkeyHelpGroup = 'global' | 'nav' | 'tabbar' | 'page'
|
||||
export type HotkeyId = typeof import('./registry').HOTKEY_ID[keyof typeof import('./registry').HOTKEY_ID]
|
||||
|
||||
export interface HotkeyContext {
|
||||
settings: ReturnType<typeof useAppSettingsStore>['settings']
|
||||
}
|
||||
|
||||
export interface HotkeyDisplayKeys {
|
||||
default: string[]
|
||||
mac?: string[]
|
||||
}
|
||||
|
||||
export interface HotkeyHelpMeta {
|
||||
group: HotkeyHelpGroup
|
||||
titleKey: string
|
||||
order?: number
|
||||
visible?: (ctx: HotkeyContext) => boolean
|
||||
displayKeys: HotkeyDisplayKeys
|
||||
}
|
||||
|
||||
export interface HotkeyBinding {
|
||||
id: HotkeyId
|
||||
keys: string[]
|
||||
preventDefault?: boolean
|
||||
enabled?: (ctx: HotkeyContext) => boolean
|
||||
help?: HotkeyHelpMeta
|
||||
}
|
||||
|
||||
export interface HotkeyHandlerArgs {
|
||||
event: KeyboardEvent
|
||||
hotkey: string
|
||||
}
|
||||
|
||||
export type HotkeyHandler = (args: HotkeyHandlerArgs) => void
|
||||
|
||||
export type HotkeyHandlersMap = Partial<Record<HotkeyId, HotkeyHandler>>
|
||||
@@ -1,69 +0,0 @@
|
||||
import type { HotkeyHandler, HotkeyHandlersMap, HotkeyId } from './types'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import { hotkeyBindings, hotkeyIds } from './registry'
|
||||
|
||||
export function useHotkey(id: HotkeyId, handler: HotkeyHandler, active: MaybeRefOrGetter<boolean> = true) {
|
||||
const binding = hotkeyBindings.find(item => item.id === id)
|
||||
const appSettingsStore = useAppSettingsStore()
|
||||
const hotkeyContext = {
|
||||
settings: appSettingsStore.settings,
|
||||
}
|
||||
let currentHandler: ((event: KeyboardEvent, hotkeyHandler: { key: string }) => void) | undefined
|
||||
|
||||
function bind() {
|
||||
if (currentHandler || !binding) {
|
||||
return
|
||||
}
|
||||
const currentBinding = binding
|
||||
|
||||
currentHandler = (event, hotkeyHandler) => {
|
||||
if (currentBinding.enabled && !currentBinding.enabled(hotkeyContext)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (currentBinding.preventDefault !== false) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
handler({
|
||||
event,
|
||||
hotkey: hotkeyHandler.key,
|
||||
})
|
||||
}
|
||||
|
||||
hotkeys(currentBinding.keys.join(','), currentHandler)
|
||||
}
|
||||
|
||||
function unbind() {
|
||||
if (!currentHandler || !binding) {
|
||||
return
|
||||
}
|
||||
|
||||
hotkeys.unbind(binding.keys.join(','), currentHandler)
|
||||
currentHandler = undefined
|
||||
}
|
||||
|
||||
watch(() => toValue(active), (val) => {
|
||||
if (val) {
|
||||
bind()
|
||||
}
|
||||
else {
|
||||
unbind()
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unbind()
|
||||
})
|
||||
}
|
||||
|
||||
export function useHotkeyBindings(handlers: HotkeyHandlersMap, active: MaybeRefOrGetter<boolean> = true) {
|
||||
hotkeyIds.forEach((id) => {
|
||||
const handler = handlers[id]
|
||||
if (handler) {
|
||||
useHotkey(id, handler, active)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { globalHotkeyBindings } from '@/hotkeys/registry'
|
||||
import { hotkeyBindings } from '@/hotkeys'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
|
||||
defineOptions({
|
||||
@@ -11,10 +11,10 @@ const hotkeyContext = {
|
||||
settings: appSettingsStore.settings,
|
||||
}
|
||||
|
||||
const groupTitleMap = {
|
||||
global: '全局',
|
||||
nav: '主导航',
|
||||
tabbar: '标签栏',
|
||||
const helpGroupMeta = {
|
||||
global: { title: '全局', order: 10 },
|
||||
nav: { title: '主导航', order: 20 },
|
||||
tabbar: { title: '标签栏', order: 30 },
|
||||
} as const
|
||||
|
||||
const itemTitleMap: Record<string, string> = {
|
||||
@@ -32,42 +32,52 @@ const itemTitleMap: Record<string, string> = {
|
||||
const isShow = ref(false)
|
||||
|
||||
const helpGroups = computed(() => {
|
||||
const groups = [
|
||||
{ id: 'global', title: groupTitleMap.global },
|
||||
{ id: 'nav', title: groupTitleMap.nav },
|
||||
{ id: 'tabbar', title: groupTitleMap.tabbar },
|
||||
] as const
|
||||
const groups = new Map<string, {
|
||||
id: string
|
||||
title: string
|
||||
order: number
|
||||
items: {
|
||||
id: string
|
||||
title: string
|
||||
order: number
|
||||
displayKeys: string[]
|
||||
}[]
|
||||
}>()
|
||||
|
||||
return groups.map((group) => {
|
||||
const items = globalHotkeyBindings
|
||||
.filter(binding => binding.help?.group === group.id)
|
||||
.filter((binding) => {
|
||||
if (!binding.help) {
|
||||
return false
|
||||
}
|
||||
if (binding.help.visible) {
|
||||
return binding.help.visible(hotkeyContext)
|
||||
}
|
||||
return true
|
||||
})
|
||||
.sort((a, b) => (a.help?.order ?? 0) - (b.help?.order ?? 0))
|
||||
.map((binding) => {
|
||||
const displayKeys = appSettingsStore.os === 'mac' && binding.help?.displayKeys.mac
|
||||
? binding.help.displayKeys.mac
|
||||
: binding.help?.displayKeys.default ?? []
|
||||
|
||||
return {
|
||||
id: binding.id,
|
||||
title: binding.help?.titleKey ? itemTitleMap[binding.help.titleKey] : '',
|
||||
displayKeys,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...group,
|
||||
items,
|
||||
hotkeyBindings.forEach((binding) => {
|
||||
const help = binding.help
|
||||
if (!help) {
|
||||
return
|
||||
}
|
||||
}).filter(group => group.items.length > 0)
|
||||
if (help.visible && !help.visible(hotkeyContext)) {
|
||||
return
|
||||
}
|
||||
|
||||
const meta = helpGroupMeta[help.group as keyof typeof helpGroupMeta]
|
||||
const group = groups.get(help.group) ?? {
|
||||
id: help.group,
|
||||
title: help.groupTitleKey ? itemTitleMap[help.groupTitleKey] ?? help.groupTitleKey : meta?.title ?? help.group,
|
||||
order: meta?.order ?? 100,
|
||||
items: [],
|
||||
}
|
||||
|
||||
group.items.push({
|
||||
id: binding.id,
|
||||
title: itemTitleMap[help.titleKey] ?? help.titleKey,
|
||||
order: help.order ?? 0,
|
||||
displayKeys: appSettingsStore.os === 'mac' && help.displayKeys.mac
|
||||
? help.displayKeys.mac
|
||||
: help.displayKeys.default,
|
||||
})
|
||||
groups.set(help.group, group)
|
||||
})
|
||||
|
||||
return Array.from(groups.values())
|
||||
.map(group => ({
|
||||
...group,
|
||||
items: [...group.items].sort((a, b) => a.order - b.order),
|
||||
}))
|
||||
.sort((a, b) => a.order - b.order)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import { useSlots } from '@/slots'
|
||||
import Logo from '../Logo/index.vue'
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { TabbarRecordRaw } from '@fantastic-admin/types'
|
||||
import { useMagicKeys } from '@vueuse/core'
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import { useSlots } from '@/slots'
|
||||
|
||||
defineOptions({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import Search from './search.vue'
|
||||
|
||||
defineOptions({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { MenuRecordRaw } from '@fantastic-admin/types'
|
||||
import { match } from 'pinyin-pro'
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import Breadcrumb from '@/layouts/components/Breadcrumb/index.vue'
|
||||
import BreadcrumbItem from '@/layouts/components/Breadcrumb/item.vue'
|
||||
import { resolveRoutePath } from '@/utils'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useElementSize, useScroll } from '@vueuse/core'
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import { useSlots } from '@/slots'
|
||||
import { cn } from '@/utils'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"dependencies": {
|
||||
"@fantastic-admin/components": "workspace:*",
|
||||
"@fantastic-admin/composables": "workspace:*",
|
||||
"@fantastic-admin/hotkeys": "workspace:*",
|
||||
"@fantastic-admin/settings": "workspace:*",
|
||||
"@fantastic-admin/types": "workspace:*",
|
||||
"@vee-validate/zod": "catalog:",
|
||||
@@ -28,7 +29,6 @@
|
||||
"element-plus": "catalog:",
|
||||
"eruda": "catalog:",
|
||||
"es-toolkit": "catalog:",
|
||||
"hotkeys-js": "catalog:",
|
||||
"mitt": "catalog:",
|
||||
"nprogress": "catalog:",
|
||||
"path-browserify": "catalog:",
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
# Hotkeys 设计说明
|
||||
|
||||
这个目录参考 `fantastic-admin/v6-dev/apps/example/src/hotkeys` 的设计,目标是把当前项目里分散在各组件中的快捷键逻辑收拢成一套轻量方案。
|
||||
|
||||
## 设计目标
|
||||
|
||||
当前方案只解决 4 件事:
|
||||
|
||||
1. 快捷键定义不要散落在多个组件里重复写
|
||||
2. 快捷键帮助面板不要再手写一份展示文案
|
||||
3. 业务组件不要反复手写 `hotkeys()` / `unbind()`
|
||||
4. 不引入过重的 command system
|
||||
|
||||
所以采用:
|
||||
|
||||
- `registry.ts` 统一定义
|
||||
- `useHotkeys.ts` 统一注册
|
||||
- 业务 handler 仍留在业务组件
|
||||
|
||||
这是一种“集中定义 + 就地执行”的轻量折中方案。
|
||||
|
||||
## 目录职责
|
||||
|
||||
### `registry.ts`
|
||||
|
||||
统一维护当前项目的快捷键元数据:
|
||||
|
||||
- id
|
||||
- keys
|
||||
- enabled
|
||||
- help
|
||||
|
||||
这里负责:
|
||||
|
||||
- 统一的 id 常量
|
||||
- 真实键位
|
||||
- 是否启用的条件
|
||||
- 帮助面板展示文案
|
||||
- 聚合业务扩展
|
||||
|
||||
> 注意:这里不放业务逻辑 handler。
|
||||
|
||||
### `registry.extend.ts`
|
||||
|
||||
用于放置项目自定义扩展快捷键,避免把业务项全部堆进内建定义中。
|
||||
|
||||
### `types.ts`
|
||||
|
||||
定义快捷键系统的基础类型,包括:
|
||||
|
||||
- `HotkeyBinding`
|
||||
- `HotkeyContext`
|
||||
- `HotkeyHelpMeta`
|
||||
- `HotkeyHandler`
|
||||
|
||||
### `useHotkeys.ts`
|
||||
|
||||
对 `hotkeys-js` 的薄封装,提供:
|
||||
|
||||
- `useHotkey`:注册单个快捷键
|
||||
- `useHotkeyBindings`:批量注册快捷键
|
||||
|
||||
并统一处理:
|
||||
|
||||
- 注册 / 卸载
|
||||
- `active` 启停
|
||||
- `binding.enabled` 可用性判断
|
||||
- 默认 `preventDefault`
|
||||
|
||||
## 当前项目已落地的快捷键
|
||||
|
||||
### 全局
|
||||
|
||||
- `system.info.open`
|
||||
- `menuSearch.open`
|
||||
- `page.reload`
|
||||
|
||||
### 主导航
|
||||
|
||||
- `menu.next`
|
||||
- `menu.prev`
|
||||
|
||||
### 标签栏
|
||||
|
||||
- `tabbar.prev`
|
||||
- `tabbar.next`
|
||||
- `tabbar.closeCurrent`
|
||||
- `tabbar.gotoVisibleIndex`
|
||||
- `tabbar.gotoLast`
|
||||
|
||||
### 菜单搜索弹窗
|
||||
|
||||
- `menuSearch.moveUp`
|
||||
- `menuSearch.moveDown`
|
||||
- `menuSearch.confirm`
|
||||
- `menuSearch.close`
|
||||
|
||||
## 使用约定
|
||||
|
||||
1. 新增快捷键,优先在 `registry.ts` 或 `registry.extend.ts` 补定义
|
||||
2. 展示在帮助面板中的快捷键,必须配置 `help`
|
||||
3. handler 尽量保留在最了解业务的组件中
|
||||
4. 不直接在业务组件里继续散写 `hotkeys('xxx')`
|
||||
|
||||
## 现阶段差异
|
||||
|
||||
参考项目里还有页面最大化相关快捷键;当前项目暂未引入对应页面最大化能力,所以本次迁移只保留当前项目已有功能,并把原有 `F5` 页面刷新也纳入统一 registry 管理。
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { HotkeyBinding } from '@fantastic-admin/hotkeys'
|
||||
|
||||
/**
|
||||
* 业务扩展快捷键 id
|
||||
*
|
||||
* 使用方式:
|
||||
*
|
||||
* export const EXT_HOTKEY_ID = {
|
||||
* demoOpen: 'demo.open',
|
||||
* } as const
|
||||
*/
|
||||
export const EXT_HOTKEY_ID = {} as const
|
||||
|
||||
type ExtendHotkeyId = typeof EXT_HOTKEY_ID[keyof typeof EXT_HOTKEY_ID]
|
||||
|
||||
/**
|
||||
* 业务扩展全局快捷键
|
||||
*
|
||||
* 示例:
|
||||
*
|
||||
* export const extendGlobalHotkeyBindings = [
|
||||
* {
|
||||
* id: EXT_HOTKEY_ID.demoOpen,
|
||||
* keys: ['command+j', 'ctrl+j'],
|
||||
* enabled: ctx => ctx.settings.toolbar.menuSearch.enable,
|
||||
* help: {
|
||||
* group: 'global',
|
||||
* titleKey: 'global.demoOpen',
|
||||
* order: 90,
|
||||
* visible: ctx => ctx.settings.toolbar.menuSearch.enable,
|
||||
* displayKeys: {
|
||||
* default: ['Ctrl', 'J'],
|
||||
* mac: ['⌘', 'J'],
|
||||
* },
|
||||
* },
|
||||
* },
|
||||
* ] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
*/
|
||||
export const extendGlobalHotkeyBindings = [] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
|
||||
/**
|
||||
* 业务扩展局部快捷键
|
||||
*
|
||||
* 示例:
|
||||
*
|
||||
* export const extendScopedHotkeyBindings = [
|
||||
* {
|
||||
* id: EXT_HOTKEY_ID.demoConfirm,
|
||||
* keys: ['enter'],
|
||||
* },
|
||||
* {
|
||||
* id: EXT_HOTKEY_ID.demoClose,
|
||||
* keys: ['esc'],
|
||||
* },
|
||||
* ] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
*/
|
||||
export const extendScopedHotkeyBindings = [] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
@@ -0,0 +1,41 @@
|
||||
import type {
|
||||
HotkeyBinding,
|
||||
HotkeyContextBase,
|
||||
HotkeyDisplayKeys,
|
||||
HotkeyHandler,
|
||||
HotkeyHandlerArgs,
|
||||
HotkeyHandlersMap,
|
||||
HotkeyHelpGroup,
|
||||
HotkeyHelpMeta,
|
||||
} from '@fantastic-admin/hotkeys'
|
||||
import { createHotkeys } from '@fantastic-admin/hotkeys'
|
||||
import { EXT_HOTKEY_ID, extendGlobalHotkeyBindings, extendScopedHotkeyBindings } from './extend'
|
||||
|
||||
const appHotkeys = createHotkeys({
|
||||
extendIds: EXT_HOTKEY_ID,
|
||||
extendGlobalHotkeyBindings,
|
||||
extendScopedHotkeyBindings,
|
||||
getContext: () => ({
|
||||
settings: useAppSettingsStore().settings,
|
||||
}),
|
||||
})
|
||||
|
||||
export const HOTKEY_ID = appHotkeys.HOTKEY_ID
|
||||
export const hotkeyIds = appHotkeys.hotkeyIds
|
||||
export const globalHotkeyBindings = appHotkeys.globalHotkeyBindings
|
||||
export const menuSearchHotkeyBindings = appHotkeys.menuSearchHotkeyBindings
|
||||
export const hotkeyBindings = appHotkeys.hotkeyBindings
|
||||
export const useHotkey = appHotkeys.useHotkey
|
||||
export const useHotkeyBindings = appHotkeys.useHotkeyBindings
|
||||
|
||||
export type HotkeyId = typeof HOTKEY_ID[keyof typeof HOTKEY_ID]
|
||||
export type {
|
||||
HotkeyBinding,
|
||||
HotkeyContextBase,
|
||||
HotkeyDisplayKeys,
|
||||
HotkeyHandler,
|
||||
HotkeyHandlerArgs,
|
||||
HotkeyHandlersMap,
|
||||
HotkeyHelpGroup,
|
||||
HotkeyHelpMeta,
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import type { HotkeyBinding } from './types'
|
||||
|
||||
/**
|
||||
* 业务扩展快捷键 id
|
||||
*
|
||||
* 使用方式:
|
||||
*
|
||||
* export const EXT_HOTKEY_ID = {
|
||||
* demoOpen: 'demo.open',
|
||||
* } as const
|
||||
*/
|
||||
export const EXT_HOTKEY_ID = {} as const
|
||||
|
||||
/**
|
||||
* 业务扩展全局快捷键
|
||||
*/
|
||||
export const extendGlobalHotkeyBindings: HotkeyBinding[] = []
|
||||
|
||||
/**
|
||||
* 业务扩展局部快捷键
|
||||
*/
|
||||
export const extendScopedHotkeyBindings: HotkeyBinding[] = []
|
||||
@@ -1,210 +0,0 @@
|
||||
import type { HotkeyBinding } from './types'
|
||||
import { EXT_HOTKEY_ID, extendGlobalHotkeyBindings, extendScopedHotkeyBindings } from './registry.extend'
|
||||
|
||||
export const HOTKEY_ID = {
|
||||
systemInfoOpen: 'system.info.open',
|
||||
menuSearchOpen: 'menuSearch.open',
|
||||
menuNext: 'menu.next',
|
||||
menuPrev: 'menu.prev',
|
||||
tabbarPrev: 'tabbar.prev',
|
||||
tabbarNext: 'tabbar.next',
|
||||
tabbarCloseCurrent: 'tabbar.closeCurrent',
|
||||
tabbarGotoVisibleIndex: 'tabbar.gotoVisibleIndex',
|
||||
tabbarGotoLast: 'tabbar.gotoLast',
|
||||
pageReload: 'page.reload',
|
||||
menuSearchMoveUp: 'menuSearch.moveUp',
|
||||
menuSearchMoveDown: 'menuSearch.moveDown',
|
||||
menuSearchConfirm: 'menuSearch.confirm',
|
||||
menuSearchClose: 'menuSearch.close',
|
||||
...EXT_HOTKEY_ID,
|
||||
} as const
|
||||
|
||||
export const hotkeyIds = Object.values(HOTKEY_ID)
|
||||
|
||||
/**
|
||||
* Hotkeys registry 维护约定
|
||||
*
|
||||
* 1. 新增快捷键优先在这里补充定义,不要在业务组件里直接写死键位字符串
|
||||
* 2. 这里仅维护“定义”和“展示元数据”,不要把业务 handler 塞进 registry
|
||||
* 3. 配置类开关尽量写在 enabled / help.visible 中,避免帮助面板与真实行为不一致
|
||||
* 4. 需要展示在快捷键帮助面板中的项,必须补充 help
|
||||
* 5. 仅局部场景使用的快捷键,也建议在这里定义,再由对应组件按需注册
|
||||
*/
|
||||
export const globalHotkeyBindings: HotkeyBinding[] = [
|
||||
{
|
||||
id: HOTKEY_ID.systemInfoOpen,
|
||||
keys: ['command+i', 'ctrl+i'],
|
||||
help: {
|
||||
group: 'global',
|
||||
titleKey: 'global.system',
|
||||
order: 10,
|
||||
displayKeys: {
|
||||
default: ['Ctrl', 'I'],
|
||||
mac: ['⌘', 'I'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchOpen,
|
||||
keys: ['command+k', 'ctrl+k'],
|
||||
enabled: ctx => ctx.settings.toolbar.menuSearch.enable && ctx.settings.toolbar.menuSearch.hotkeys,
|
||||
help: {
|
||||
group: 'global',
|
||||
titleKey: 'global.search',
|
||||
order: 20,
|
||||
visible: ctx => ctx.settings.toolbar.menuSearch.enable && ctx.settings.toolbar.menuSearch.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Ctrl', 'K'],
|
||||
mac: ['⌘', 'K'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuNext,
|
||||
keys: ['alt+`'],
|
||||
enabled: ctx => ctx.settings.menu.hotkeys && ['side', 'head'].includes(ctx.settings.menu.mode),
|
||||
help: {
|
||||
group: 'nav',
|
||||
titleKey: 'nav.next',
|
||||
order: 10,
|
||||
visible: ctx => ctx.settings.menu.hotkeys && ['side', 'head'].includes(ctx.settings.menu.mode),
|
||||
displayKeys: {
|
||||
default: ['Alt', '`'],
|
||||
mac: ['⌥', '`'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuPrev,
|
||||
keys: ['alt+shift+`'],
|
||||
enabled: ctx => ctx.settings.menu.hotkeys && ['side', 'head'].includes(ctx.settings.menu.mode),
|
||||
help: {
|
||||
group: 'nav',
|
||||
titleKey: 'nav.prev',
|
||||
order: 20,
|
||||
visible: ctx => ctx.settings.menu.hotkeys && ['side', 'head'].includes(ctx.settings.menu.mode),
|
||||
displayKeys: {
|
||||
default: ['Alt', 'Shift', '`'],
|
||||
mac: ['⌥', '⇧', '`'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarPrev,
|
||||
keys: ['alt+left'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.prev',
|
||||
order: 10,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', '←'],
|
||||
mac: ['⌥', '←'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarNext,
|
||||
keys: ['alt+right'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.next',
|
||||
order: 20,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', '→'],
|
||||
mac: ['⌥', '→'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarCloseCurrent,
|
||||
keys: ['alt+w'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.close',
|
||||
order: 30,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', 'W'],
|
||||
mac: ['⌥', 'W'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarGotoVisibleIndex,
|
||||
keys: ['alt+1', 'alt+2', 'alt+3', 'alt+4', 'alt+5', 'alt+6', 'alt+7', 'alt+8', 'alt+9'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.n',
|
||||
order: 40,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', '1~9'],
|
||||
mac: ['⌥', '1~9'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.tabbarGotoLast,
|
||||
keys: ['alt+0'],
|
||||
enabled: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
help: {
|
||||
group: 'tabbar',
|
||||
titleKey: 'tabbar.last',
|
||||
order: 50,
|
||||
visible: ctx => ctx.settings.topbar.tabbar && ctx.settings.tabbar.hotkeys,
|
||||
displayKeys: {
|
||||
default: ['Alt', '0'],
|
||||
mac: ['⌥', '0'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.pageReload,
|
||||
keys: ['f5'],
|
||||
},
|
||||
...extendGlobalHotkeyBindings,
|
||||
]
|
||||
|
||||
export const menuSearchHotkeyBindings: HotkeyBinding[] = [
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchMoveUp,
|
||||
keys: ['up'],
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchMoveDown,
|
||||
keys: ['down'],
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchConfirm,
|
||||
keys: ['enter'],
|
||||
},
|
||||
{
|
||||
id: HOTKEY_ID.menuSearchClose,
|
||||
keys: ['esc'],
|
||||
},
|
||||
]
|
||||
|
||||
export const hotkeyBindings: HotkeyBinding[] = [
|
||||
...globalHotkeyBindings,
|
||||
...menuSearchHotkeyBindings,
|
||||
...extendScopedHotkeyBindings,
|
||||
]
|
||||
|
||||
if (import.meta.env.DEV) {
|
||||
const registeredIds = hotkeyBindings.map(item => item.id)
|
||||
const missingIds = hotkeyIds.filter(id => !registeredIds.includes(id))
|
||||
const duplicateIds = registeredIds.filter((id, index) => registeredIds.indexOf(id) !== index)
|
||||
|
||||
if (missingIds.length || duplicateIds.length) {
|
||||
console.warn('[hotkeys] registry consistency check failed', {
|
||||
missingIds,
|
||||
duplicateIds: [...new Set(duplicateIds)],
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { useAppSettingsStore } from '@/store/modules/app/settings'
|
||||
|
||||
export type HotkeyHelpGroup = 'global' | 'nav' | 'tabbar' | 'page'
|
||||
export type HotkeyId = typeof import('./registry').HOTKEY_ID[keyof typeof import('./registry').HOTKEY_ID]
|
||||
|
||||
export interface HotkeyContext {
|
||||
settings: ReturnType<typeof useAppSettingsStore>['settings']
|
||||
}
|
||||
|
||||
export interface HotkeyDisplayKeys {
|
||||
default: string[]
|
||||
mac?: string[]
|
||||
}
|
||||
|
||||
export interface HotkeyHelpMeta {
|
||||
group: HotkeyHelpGroup
|
||||
titleKey: string
|
||||
order?: number
|
||||
visible?: (ctx: HotkeyContext) => boolean
|
||||
displayKeys: HotkeyDisplayKeys
|
||||
}
|
||||
|
||||
export interface HotkeyBinding {
|
||||
id: HotkeyId
|
||||
keys: string[]
|
||||
preventDefault?: boolean
|
||||
enabled?: (ctx: HotkeyContext) => boolean
|
||||
help?: HotkeyHelpMeta
|
||||
}
|
||||
|
||||
export interface HotkeyHandlerArgs {
|
||||
event: KeyboardEvent
|
||||
hotkey: string
|
||||
}
|
||||
|
||||
export type HotkeyHandler = (args: HotkeyHandlerArgs) => void
|
||||
|
||||
export type HotkeyHandlersMap = Partial<Record<HotkeyId, HotkeyHandler>>
|
||||
@@ -1,69 +0,0 @@
|
||||
import type { HotkeyHandler, HotkeyHandlersMap, HotkeyId } from './types'
|
||||
import hotkeys from 'hotkeys-js'
|
||||
import { hotkeyBindings, hotkeyIds } from './registry'
|
||||
|
||||
export function useHotkey(id: HotkeyId, handler: HotkeyHandler, active: MaybeRefOrGetter<boolean> = true) {
|
||||
const binding = hotkeyBindings.find(item => item.id === id)
|
||||
const appSettingsStore = useAppSettingsStore()
|
||||
const hotkeyContext = {
|
||||
settings: appSettingsStore.settings,
|
||||
}
|
||||
let currentHandler: ((event: KeyboardEvent, hotkeyHandler: { key: string }) => void) | undefined
|
||||
|
||||
function bind() {
|
||||
if (currentHandler || !binding) {
|
||||
return
|
||||
}
|
||||
const currentBinding = binding
|
||||
|
||||
currentHandler = (event, hotkeyHandler) => {
|
||||
if (currentBinding.enabled && !currentBinding.enabled(hotkeyContext)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (currentBinding.preventDefault !== false) {
|
||||
event.preventDefault()
|
||||
}
|
||||
|
||||
handler({
|
||||
event,
|
||||
hotkey: hotkeyHandler.key,
|
||||
})
|
||||
}
|
||||
|
||||
hotkeys(currentBinding.keys.join(','), currentHandler)
|
||||
}
|
||||
|
||||
function unbind() {
|
||||
if (!currentHandler || !binding) {
|
||||
return
|
||||
}
|
||||
|
||||
hotkeys.unbind(binding.keys.join(','), currentHandler)
|
||||
currentHandler = undefined
|
||||
}
|
||||
|
||||
watch(() => toValue(active), (val) => {
|
||||
if (val) {
|
||||
bind()
|
||||
}
|
||||
else {
|
||||
unbind()
|
||||
}
|
||||
}, {
|
||||
immediate: true,
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
unbind()
|
||||
})
|
||||
}
|
||||
|
||||
export function useHotkeyBindings(handlers: HotkeyHandlersMap, active: MaybeRefOrGetter<boolean> = true) {
|
||||
hotkeyIds.forEach((id) => {
|
||||
const handler = handlers[id]
|
||||
if (handler) {
|
||||
useHotkey(id, handler, active)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { globalHotkeyBindings } from '@/hotkeys/registry'
|
||||
import { hotkeyBindings } from '@/hotkeys'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
|
||||
defineOptions({
|
||||
@@ -11,10 +11,10 @@ const hotkeyContext = {
|
||||
settings: appSettingsStore.settings,
|
||||
}
|
||||
|
||||
const groupTitleMap = {
|
||||
global: '全局',
|
||||
nav: '主导航',
|
||||
tabbar: '标签栏',
|
||||
const helpGroupMeta = {
|
||||
global: { title: '全局', order: 10 },
|
||||
nav: { title: '主导航', order: 20 },
|
||||
tabbar: { title: '标签栏', order: 30 },
|
||||
} as const
|
||||
|
||||
const itemTitleMap: Record<string, string> = {
|
||||
@@ -32,42 +32,52 @@ const itemTitleMap: Record<string, string> = {
|
||||
const isShow = ref(false)
|
||||
|
||||
const helpGroups = computed(() => {
|
||||
const groups = [
|
||||
{ id: 'global', title: groupTitleMap.global },
|
||||
{ id: 'nav', title: groupTitleMap.nav },
|
||||
{ id: 'tabbar', title: groupTitleMap.tabbar },
|
||||
] as const
|
||||
const groups = new Map<string, {
|
||||
id: string
|
||||
title: string
|
||||
order: number
|
||||
items: {
|
||||
id: string
|
||||
title: string
|
||||
order: number
|
||||
displayKeys: string[]
|
||||
}[]
|
||||
}>()
|
||||
|
||||
return groups.map((group) => {
|
||||
const items = globalHotkeyBindings
|
||||
.filter(binding => binding.help?.group === group.id)
|
||||
.filter((binding) => {
|
||||
if (!binding.help) {
|
||||
return false
|
||||
}
|
||||
if (binding.help.visible) {
|
||||
return binding.help.visible(hotkeyContext)
|
||||
}
|
||||
return true
|
||||
})
|
||||
.sort((a, b) => (a.help?.order ?? 0) - (b.help?.order ?? 0))
|
||||
.map((binding) => {
|
||||
const displayKeys = appSettingsStore.os === 'mac' && binding.help?.displayKeys.mac
|
||||
? binding.help.displayKeys.mac
|
||||
: binding.help?.displayKeys.default ?? []
|
||||
|
||||
return {
|
||||
id: binding.id,
|
||||
title: binding.help?.titleKey ? itemTitleMap[binding.help.titleKey] : '',
|
||||
displayKeys,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
...group,
|
||||
items,
|
||||
hotkeyBindings.forEach((binding) => {
|
||||
const help = binding.help
|
||||
if (!help) {
|
||||
return
|
||||
}
|
||||
}).filter(group => group.items.length > 0)
|
||||
if (help.visible && !help.visible(hotkeyContext)) {
|
||||
return
|
||||
}
|
||||
|
||||
const meta = helpGroupMeta[help.group as keyof typeof helpGroupMeta]
|
||||
const group = groups.get(help.group) ?? {
|
||||
id: help.group,
|
||||
title: help.groupTitleKey ? itemTitleMap[help.groupTitleKey] ?? help.groupTitleKey : meta?.title ?? help.group,
|
||||
order: meta?.order ?? 100,
|
||||
items: [],
|
||||
}
|
||||
|
||||
group.items.push({
|
||||
id: binding.id,
|
||||
title: itemTitleMap[help.titleKey] ?? help.titleKey,
|
||||
order: help.order ?? 0,
|
||||
displayKeys: appSettingsStore.os === 'mac' && help.displayKeys.mac
|
||||
? help.displayKeys.mac
|
||||
: help.displayKeys.default,
|
||||
})
|
||||
groups.set(help.group, group)
|
||||
})
|
||||
|
||||
return Array.from(groups.values())
|
||||
.map(group => ({
|
||||
...group,
|
||||
items: [...group.items].sort((a, b) => a.order - b.order),
|
||||
}))
|
||||
.sort((a, b) => a.order - b.order)
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import { useSlots } from '@/slots'
|
||||
import Logo from '../Logo/index.vue'
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { TabbarRecordRaw } from '@fantastic-admin/types'
|
||||
import { useMagicKeys } from '@vueuse/core'
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import { useSlots } from '@/slots'
|
||||
|
||||
defineOptions({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import Search from './search.vue'
|
||||
|
||||
defineOptions({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import type { MenuRecordRaw } from '@fantastic-admin/types'
|
||||
import { match } from 'pinyin-pro'
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import Breadcrumb from '@/layouts/components/Breadcrumb/index.vue'
|
||||
import BreadcrumbItem from '@/layouts/components/Breadcrumb/item.vue'
|
||||
import { resolveRoutePath } from '@/utils'
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { useElementSize, useScroll } from '@vueuse/core'
|
||||
import { useHotkeyBindings } from '@/hotkeys/useHotkeys'
|
||||
import { useHotkeyBindings } from '@/hotkeys'
|
||||
import { useSlots } from '@/slots'
|
||||
import { cn } from '@/utils'
|
||||
import eventBus from '@/utils/eventBus'
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"dependencies": {
|
||||
"@fantastic-admin/components": "workspace:*",
|
||||
"@fantastic-admin/composables": "workspace:*",
|
||||
"@fantastic-admin/hotkeys": "workspace:*",
|
||||
"@fantastic-admin/settings": "workspace:*",
|
||||
"@fantastic-admin/types": "workspace:*",
|
||||
"@tinymce/tinymce-vue": "catalog:",
|
||||
@@ -33,7 +34,6 @@
|
||||
"element-plus": "catalog:",
|
||||
"eruda": "catalog:",
|
||||
"es-toolkit": "catalog:",
|
||||
"hotkeys-js": "catalog:",
|
||||
"mitt": "catalog:",
|
||||
"nprogress": "catalog:",
|
||||
"path-browserify": "catalog:",
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
# Hotkeys 设计说明
|
||||
|
||||
这个目录参考 `fantastic-admin/v6-dev/apps/example/src/hotkeys` 的设计,目标是把当前项目里分散在各组件中的快捷键逻辑收拢成一套轻量方案。
|
||||
|
||||
## 设计目标
|
||||
|
||||
当前方案只解决 4 件事:
|
||||
|
||||
1. 快捷键定义不要散落在多个组件里重复写
|
||||
2. 快捷键帮助面板不要再手写一份展示文案
|
||||
3. 业务组件不要反复手写 `hotkeys()` / `unbind()`
|
||||
4. 不引入过重的 command system
|
||||
|
||||
所以采用:
|
||||
|
||||
- `registry.ts` 统一定义
|
||||
- `useHotkeys.ts` 统一注册
|
||||
- 业务 handler 仍留在业务组件
|
||||
|
||||
这是一种“集中定义 + 就地执行”的轻量折中方案。
|
||||
|
||||
## 目录职责
|
||||
|
||||
### `registry.ts`
|
||||
|
||||
统一维护当前项目的快捷键元数据:
|
||||
|
||||
- id
|
||||
- keys
|
||||
- enabled
|
||||
- help
|
||||
|
||||
这里负责:
|
||||
|
||||
- 统一的 id 常量
|
||||
- 真实键位
|
||||
- 是否启用的条件
|
||||
- 帮助面板展示文案
|
||||
- 聚合业务扩展
|
||||
|
||||
> 注意:这里不放业务逻辑 handler。
|
||||
|
||||
### `registry.extend.ts`
|
||||
|
||||
用于放置项目自定义扩展快捷键,避免把业务项全部堆进内建定义中。
|
||||
|
||||
### `types.ts`
|
||||
|
||||
定义快捷键系统的基础类型,包括:
|
||||
|
||||
- `HotkeyBinding`
|
||||
- `HotkeyContext`
|
||||
- `HotkeyHelpMeta`
|
||||
- `HotkeyHandler`
|
||||
|
||||
### `useHotkeys.ts`
|
||||
|
||||
对 `hotkeys-js` 的薄封装,提供:
|
||||
|
||||
- `useHotkey`:注册单个快捷键
|
||||
- `useHotkeyBindings`:批量注册快捷键
|
||||
|
||||
并统一处理:
|
||||
|
||||
- 注册 / 卸载
|
||||
- `active` 启停
|
||||
- `binding.enabled` 可用性判断
|
||||
- 默认 `preventDefault`
|
||||
|
||||
## 当前项目已落地的快捷键
|
||||
|
||||
### 全局
|
||||
|
||||
- `system.info.open`
|
||||
- `menuSearch.open`
|
||||
- `page.reload`
|
||||
|
||||
### 主导航
|
||||
|
||||
- `menu.next`
|
||||
- `menu.prev`
|
||||
|
||||
### 标签栏
|
||||
|
||||
- `tabbar.prev`
|
||||
- `tabbar.next`
|
||||
- `tabbar.closeCurrent`
|
||||
- `tabbar.gotoVisibleIndex`
|
||||
- `tabbar.gotoLast`
|
||||
|
||||
### 菜单搜索弹窗
|
||||
|
||||
- `menuSearch.moveUp`
|
||||
- `menuSearch.moveDown`
|
||||
- `menuSearch.confirm`
|
||||
- `menuSearch.close`
|
||||
|
||||
## 使用约定
|
||||
|
||||
1. 新增快捷键,优先在 `registry.ts` 或 `registry.extend.ts` 补定义
|
||||
2. 展示在帮助面板中的快捷键,必须配置 `help`
|
||||
3. handler 尽量保留在最了解业务的组件中
|
||||
4. 不直接在业务组件里继续散写 `hotkeys('xxx')`
|
||||
|
||||
## 现阶段差异
|
||||
|
||||
参考项目里还有页面最大化相关快捷键;当前项目暂未引入对应页面最大化能力,所以本次迁移只保留当前项目已有功能,并把原有 `F5` 页面刷新也纳入统一 registry 管理。
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { HotkeyBinding } from '@fantastic-admin/hotkeys'
|
||||
|
||||
/**
|
||||
* 业务扩展快捷键 id
|
||||
*
|
||||
* 使用方式:
|
||||
*
|
||||
* export const EXT_HOTKEY_ID = {
|
||||
* demoOpen: 'demo.open',
|
||||
* } as const
|
||||
*/
|
||||
export const EXT_HOTKEY_ID = {} as const
|
||||
|
||||
type ExtendHotkeyId = typeof EXT_HOTKEY_ID[keyof typeof EXT_HOTKEY_ID]
|
||||
|
||||
/**
|
||||
* 业务扩展全局快捷键
|
||||
*
|
||||
* 示例:
|
||||
*
|
||||
* export const extendGlobalHotkeyBindings = [
|
||||
* {
|
||||
* id: EXT_HOTKEY_ID.demoOpen,
|
||||
* keys: ['command+j', 'ctrl+j'],
|
||||
* enabled: ctx => ctx.settings.toolbar.menuSearch.enable,
|
||||
* help: {
|
||||
* group: 'global',
|
||||
* titleKey: 'global.demoOpen',
|
||||
* order: 90,
|
||||
* visible: ctx => ctx.settings.toolbar.menuSearch.enable,
|
||||
* displayKeys: {
|
||||
* default: ['Ctrl', 'J'],
|
||||
* mac: ['⌘', 'J'],
|
||||
* },
|
||||
* },
|
||||
* },
|
||||
* ] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
*/
|
||||
export const extendGlobalHotkeyBindings = [] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
|
||||
/**
|
||||
* 业务扩展局部快捷键
|
||||
*
|
||||
* 示例:
|
||||
*
|
||||
* export const extendScopedHotkeyBindings = [
|
||||
* {
|
||||
* id: EXT_HOTKEY_ID.demoConfirm,
|
||||
* keys: ['enter'],
|
||||
* },
|
||||
* {
|
||||
* id: EXT_HOTKEY_ID.demoClose,
|
||||
* keys: ['esc'],
|
||||
* },
|
||||
* ] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
*/
|
||||
export const extendScopedHotkeyBindings = [] satisfies HotkeyBinding<ExtendHotkeyId>[]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user