feat(快捷键): 实现 useHotkeys Hook

- 提供 useHotkeys 单个快捷键注册
- 提供 useHotkeysGroup 批量注册
- 提供 useHotkeysHelper 帮助面板控制
- 提供 useRegisteredHotkeys 获取已注册快捷键
- 支持依赖数组自动更新
This commit is contained in:
gaoziman 2025-12-27 23:56:12 +08:00
parent 14e3185664
commit 827a033d41

208
src/hooks/useHotkeys.ts Normal file
View File

@ -0,0 +1,208 @@
'use client';
import { useEffect, DependencyList, useMemo } from 'react';
import { useHotkeysContext } from '@/providers/HotkeysProvider';
import type { HotkeyConfig, ModifierKey, HotkeyCategory } from '@/types/hotkeys';
/**
* useHotkeys Hook
*/
export interface UseHotkeysOptions {
/** 主键 */
key: string;
/** 修饰键 */
modifiers?: ModifierKey[];
/** 快捷键描述 */
description: string;
/** 分类 */
category?: HotkeyCategory;
/** 触发回调 */
action: () => void;
/** 是否启用 */
enabled?: boolean | (() => boolean);
/** 是否阻止默认行为 */
preventDefault?: boolean;
/** 是否允许在输入框中触发 */
allowInInput?: boolean;
}
/**
* Hook
*
* @param id
* @param options
* @param deps
*
* @example
* ```tsx
* // 单键快捷键(非输入框时触发)
* useHotkeys('new-chat', {
* key: 'n',
* description: '新建对话',
* category: '导航',
* action: () => setShowNewChatModal(true),
* });
*
* // 组合键快捷键
* useHotkeys('open-search', {
* key: 'k',
* modifiers: ['ctrl', 'meta'], // Ctrl+K 或 Cmd+K
* description: '打开搜索',
* category: '导航',
* action: () => setShowSearchModal(true),
* allowInInput: true, // 在输入框中也生效
* });
* ```
*/
export function useHotkeys(
id: string,
options: UseHotkeysOptions,
deps: DependencyList = []
): void {
const { register, unregister } = useHotkeysContext();
// 将 options 转换为 HotkeyConfig
const config: HotkeyConfig = useMemo(
() => ({
key: options.key,
modifiers: options.modifiers,
description: options.description,
category: options.category,
action: options.action,
enabled: options.enabled,
preventDefault: options.preventDefault,
allowInInput: options.allowInInput,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[
options.key,
options.description,
options.category,
options.enabled,
options.preventDefault,
options.allowInInput,
// modifiers 需要特殊处理(数组比较)
// eslint-disable-next-line react-hooks/exhaustive-deps
JSON.stringify(options.modifiers),
// action 放在 deps 中由用户控制
...deps,
]
);
useEffect(() => {
// 注册快捷键
register(id, {
...config,
action: options.action, // 使用最新的 action
});
// 清理:注销快捷键
return () => {
unregister(id);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id, config, register, unregister]);
}
/**
* Hook
*
* @param configs
* @param deps
*
* @example
* ```tsx
* useHotkeysGroup([
* {
* id: 'new-chat',
* key: 'n',
* description: '新建对话',
* action: () => setShowNewChatModal(true),
* },
* {
* id: 'open-search',
* key: 'k',
* modifiers: ['ctrl', 'meta'],
* description: '打开搜索',
* action: () => setShowSearchModal(true),
* },
* ]);
* ```
*/
export function useHotkeysGroup(
configs: (UseHotkeysOptions & { id: string })[],
deps: DependencyList = []
): void {
const { register, unregister } = useHotkeysContext();
useEffect(() => {
// 批量注册
configs.forEach(({ id, ...options }) => {
register(id, {
key: options.key,
modifiers: options.modifiers,
description: options.description,
category: options.category,
action: options.action,
enabled: options.enabled,
preventDefault: options.preventDefault,
allowInInput: options.allowInInput,
});
});
// 清理:批量注销
return () => {
configs.forEach(({ id }) => {
unregister(id);
});
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [register, unregister, JSON.stringify(configs.map((c) => c.id)), ...deps]);
}
/**
* Hook
*
* @example
* ```tsx
* const { isOpen, toggle, open, close } = useHotkeysHelper();
*
* return (
* <button onClick={toggle}>
* {isOpen ? '关闭帮助' : '快捷键帮助'}
* </button>
* );
* ```
*/
export function useHotkeysHelper() {
const { isHelperOpen, toggleHelper, openHelper, closeHelper } =
useHotkeysContext();
return {
isOpen: isHelperOpen,
toggle: toggleHelper,
open: openHelper,
close: closeHelper,
};
}
/**
* Hook
*
* @example
* ```tsx
* const hotkeys = useRegisteredHotkeys();
*
* return (
* <ul>
* {hotkeys.map(({ id, config }) => (
* <li key={id}>{config.description}</li>
* ))}
* </ul>
* );
* ```
*/
export function useRegisteredHotkeys() {
const { getAll } = useHotkeysContext();
return getAll();
}