Compare commits

...

5 Commits

Author SHA1 Message Date
gaoziman
c48d05885a feat(集成): 集成快捷键系统到应用
- 在根布局添加 HotkeysProvider
- 重构 Sidebar 使用 useHotkeys 注册快捷键
- 移除原有的手动键盘事件监听
- 添加 N 键新建对话快捷键
- 添加 Cmd/Ctrl+K 和 Cmd/Ctrl+/ 搜索快捷键
2025-12-27 23:56:26 +08:00
gaoziman
f859ffe4cd feat(组件): 添加快捷键帮助面板 UI
- 实现快捷键帮助弹窗界面
- 按分类(导航、编辑、通用)分组显示
- 支持 Mac/Windows 快捷键格式切换
- 预置常用快捷键说明
- 按 Esc 关闭面板
2025-12-27 23:56:19 +08:00
gaoziman
827a033d41 feat(快捷键): 实现 useHotkeys Hook
- 提供 useHotkeys 单个快捷键注册
- 提供 useHotkeysGroup 批量注册
- 提供 useHotkeysHelper 帮助面板控制
- 提供 useRegisteredHotkeys 获取已注册快捷键
- 支持依赖数组自动更新
2025-12-27 23:56:12 +08:00
gaoziman
14e3185664 feat(快捷键): 实现全局快捷键 Provider
- 实现全局键盘事件监听器
- 支持快捷键注册和注销机制
- 处理 IME 输入法状态检测
- 支持输入框内快捷键过滤
- 内置 ? 键显示帮助面板
- 内置 Esc 键关闭弹窗
2025-12-27 23:56:05 +08:00
gaoziman
9080c3af21 feat(类型): 添加快捷键系统类型定义
- 定义 ModifierKey、HotkeyScope、HotkeyCategory 类型
- 定义 HotkeyConfig 快捷键配置接口
- 定义 HotkeysContextType Context 类型
- 实现 formatHotkey 格式化显示函数
2025-12-27 23:55:55 +08:00
6 changed files with 851 additions and 18 deletions

View File

@ -3,6 +3,7 @@ import "./globals.css";
import { SettingsProvider } from "@/components/providers/SettingsProvider"; import { SettingsProvider } from "@/components/providers/SettingsProvider";
import { AuthProvider } from "@/providers/AuthProvider"; import { AuthProvider } from "@/providers/AuthProvider";
import { PromptOptimizerProvider } from "@/providers/PromptOptimizerProvider"; import { PromptOptimizerProvider } from "@/providers/PromptOptimizerProvider";
import { HotkeysProvider } from "@/providers/HotkeysProvider";
import { Toaster } from "@/components/ui/Toast"; import { Toaster } from "@/components/ui/Toast";
export const metadata: Metadata = { export const metadata: Metadata = {
@ -23,10 +24,12 @@ export default function RootLayout({
<body className="antialiased"> <body className="antialiased">
<AuthProvider> <AuthProvider>
<SettingsProvider> <SettingsProvider>
<HotkeysProvider>
<PromptOptimizerProvider> <PromptOptimizerProvider>
{children} {children}
<Toaster /> <Toaster />
</PromptOptimizerProvider> </PromptOptimizerProvider>
</HotkeysProvider>
</SettingsProvider> </SettingsProvider>
</AuthProvider> </AuthProvider>
</body> </body>

View File

@ -11,6 +11,7 @@ import { useConversations } from '@/hooks/useConversations';
import { useAuth } from '@/providers/AuthProvider'; import { useAuth } from '@/providers/AuthProvider';
import type { Conversation } from '@/drizzle/schema'; import type { Conversation } from '@/drizzle/schema';
import { useState, useRef, useEffect, useCallback } from 'react'; import { useState, useRef, useEffect, useCallback } from 'react';
import { useHotkeys } from '@/hooks/useHotkeys';
interface SidebarProps { interface SidebarProps {
isOpen?: boolean; isOpen?: boolean;
@ -54,21 +55,45 @@ export function Sidebar({ isOpen = true }: SidebarProps) {
}; };
}, [menuOpen]); }, [menuOpen]);
// 全局键盘快捷键⌘K / Ctrl+K 打开搜索 // 快捷键N - 新建对话(单键,非输入框时生效)
useEffect(() => { useHotkeys(
const handleKeyDown = (e: KeyboardEvent) => { 'new-chat',
// ⌘K (Mac) 或 Ctrl+K (Windows/Linux) {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') { key: 'n',
e.preventDefault(); description: '新建对话',
setShowSearchModal(true); category: '导航',
} action: useCallback(() => setShowNewChatModal(true), []),
}; },
[setShowNewChatModal]
);
document.addEventListener('keydown', handleKeyDown); // 快捷键Cmd/Ctrl + K - 打开搜索
return () => { useHotkeys(
document.removeEventListener('keydown', handleKeyDown); 'open-search-k',
}; {
}, []); key: 'k',
modifiers: ['ctrl', 'meta'],
description: '打开搜索',
category: '导航',
action: useCallback(() => setShowSearchModal(true), []),
allowInInput: true,
},
[setShowSearchModal]
);
// 快捷键Cmd/Ctrl + / - 打开搜索(备用)
useHotkeys(
'open-search-slash',
{
key: '/',
modifiers: ['ctrl', 'meta'],
description: '打开搜索',
category: '导航',
action: useCallback(() => setShowSearchModal(true), []),
allowInInput: true,
},
[setShowSearchModal]
);
// 创建新对话 - 显示选择助手弹框 // 创建新对话 - 显示选择助手弹框
const handleNewChat = () => { const handleNewChat = () => {

View File

@ -0,0 +1,207 @@
'use client';
import { useEffect, useState, useMemo } from 'react';
import { X, Keyboard } from 'lucide-react';
import { cn } from '@/lib/utils';
import type { RegisteredHotkey, HotkeyCategory } from '@/types/hotkeys';
import { formatHotkey } from '@/types/hotkeys';
interface HotkeysHelperProps {
isOpen: boolean;
onClose: () => void;
registeredHotkeys?: RegisteredHotkey[];
}
// 预定义的快捷键列表(包括系统内置的)
const BUILTIN_HOTKEYS: RegisteredHotkey[] = [
{
id: 'new-chat',
config: {
key: 'n',
description: '新建对话',
category: '导航',
action: () => {},
},
},
{
id: 'open-search',
config: {
key: 'k',
modifiers: ['meta'],
description: '打开搜索',
category: '导航',
action: () => {},
},
},
{
id: 'help',
config: {
key: '?',
description: '显示快捷键帮助',
category: '通用',
action: () => {},
},
},
{
id: 'close-modal',
config: {
key: 'Escape',
description: '关闭弹窗',
category: '通用',
action: () => {},
},
},
];
/**
*
*/
export function HotkeysHelper({ isOpen, onClose, registeredHotkeys = [] }: HotkeysHelperProps) {
// 检测是否为 Mac 系统
const [isMac, setIsMac] = useState(true);
useEffect(() => {
setIsMac(navigator.platform.toLowerCase().includes('mac'));
}, []);
// 合并内置和动态快捷键,按分类分组
const groupedHotkeys = useMemo(() => {
const allHotkeys = [...BUILTIN_HOTKEYS, ...registeredHotkeys];
// 去重(以 id 为准,优先保留 BUILTIN
const uniqueHotkeys = allHotkeys.reduce((acc, hotkey) => {
if (!acc.find((h) => h.id === hotkey.id)) {
acc.push(hotkey);
}
return acc;
}, [] as RegisteredHotkey[]);
// 按分类分组
const groups: Record<HotkeyCategory, RegisteredHotkey[]> = {
: [],
: [],
: [],
};
uniqueHotkeys.forEach((hotkey) => {
const category = hotkey.config.category || '通用';
if (groups[category]) {
groups[category].push(hotkey);
} else {
groups['通用'].push(hotkey);
}
});
// 过滤空分类
return Object.entries(groups).filter(([, items]) => items.length > 0);
}, [registeredHotkeys]);
if (!isOpen) return null;
return (
<>
{/* 遮罩层 */}
<div
className="fixed inset-0 bg-black/50 backdrop-blur-sm z-[200] animate-fade-in-fast"
onClick={onClose}
/>
{/* 帮助面板 */}
<div className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-[201] w-full max-w-[480px] px-4">
<div className="bg-[var(--color-bg-primary)] rounded shadow-2xl overflow-hidden animate-scale-in-fast">
{/* 头部 */}
<div className="flex items-center justify-between px-6 py-4 border-b border-[var(--color-border-light)]">
<div className="flex items-center gap-3">
<div className="w-10 h-10 flex items-center justify-center bg-[var(--color-primary-alpha)] rounded-lg">
<Keyboard size={20} className="text-[var(--color-primary)]" />
</div>
<div>
<h2 className="text-lg font-semibold text-[var(--color-text-primary)]">
</h2>
<p className="text-xs text-[var(--color-text-tertiary)]">
使
</p>
</div>
</div>
<button
onClick={onClose}
className="w-8 h-8 flex items-center justify-center rounded-lg text-[var(--color-text-tertiary)] hover:bg-[var(--color-bg-hover)] hover:text-[var(--color-text-secondary)] transition-colors"
>
<X size={18} />
</button>
</div>
{/* 快捷键列表 */}
<div className="px-6 py-4 max-h-[60vh] overflow-y-auto">
{groupedHotkeys.map(([category, hotkeys], groupIndex) => (
<div
key={category}
className={cn(groupIndex > 0 && 'mt-6')}
>
{/* 分类标题 */}
<h3 className="text-xs font-semibold text-[var(--color-text-tertiary)] uppercase tracking-wider mb-3">
{category}
</h3>
{/* 快捷键项 */}
<div className="space-y-2">
{hotkeys.map((hotkey) => (
<div
key={hotkey.id}
className="flex items-center justify-between py-2 px-3 rounded-lg hover:bg-[var(--color-bg-secondary)] transition-colors"
>
<span className="text-sm text-[var(--color-text-primary)]">
{hotkey.config.description}
</span>
<HotkeyBadge config={hotkey.config} isMac={isMac} />
</div>
))}
</div>
</div>
))}
</div>
{/* 底部提示 */}
<div className="px-6 py-3 bg-[var(--color-bg-secondary)] border-t border-[var(--color-border-light)]">
<div className="flex items-center justify-between text-xs text-[var(--color-text-tertiary)]">
<span> <kbd className="inline-flex items-center justify-center min-w-[20px] h-5 px-1.5 bg-[var(--color-bg-primary)] border border-[var(--color-border)] rounded text-[10px] mx-1">Esc</kbd> </span>
<span>{isMac ? 'macOS' : 'Windows/Linux'}</span>
</div>
</div>
</div>
</div>
</>
);
}
/**
*
*/
function HotkeyBadge({
config,
isMac,
}: {
config: RegisteredHotkey['config'];
isMac: boolean;
}) {
const keys = formatHotkey(config, isMac).split(isMac ? ' ' : '+');
return (
<div className="flex items-center gap-1">
{keys.map((key, index) => (
<kbd
key={index}
className={cn(
'inline-flex items-center justify-center min-w-[24px] h-6 px-2',
'bg-[var(--color-bg-tertiary)] border border-[var(--color-border)]',
'rounded text-xs font-medium text-[var(--color-text-secondary)]',
'shadow-sm'
)}
>
{key}
</kbd>
))}
</div>
);
}

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();
}

View File

@ -0,0 +1,270 @@
'use client';
import React, {
createContext,
useContext,
useState,
useCallback,
useEffect,
useRef,
} from 'react';
import type {
HotkeyConfig,
HotkeysContextType,
RegisteredHotkey,
ModifierKey,
} from '@/types/hotkeys';
import { HotkeysHelper } from '@/components/ui/HotkeysHelper';
// 创建 Context
const HotkeysContext = createContext<HotkeysContextType | null>(null);
/**
*
*/
function isInputElement(target: EventTarget | null): boolean {
if (!target || !(target instanceof HTMLElement)) return false;
const tagName = target.tagName.toLowerCase();
// 检查是否是输入类型元素
if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') {
return true;
}
// 检查是否是 contentEditable 元素
if (target.isContentEditable) {
return true;
}
// 检查父元素是否是 contentEditable
const closestEditable = target.closest('[contenteditable="true"]');
if (closestEditable) {
return true;
}
return false;
}
/**
*
*/
function checkModifiers(
event: KeyboardEvent,
modifiers: ModifierKey[] = []
): boolean {
const hasCtrl = modifiers.includes('ctrl');
const hasMeta = modifiers.includes('meta');
const hasAlt = modifiers.includes('alt');
const hasShift = modifiers.includes('shift');
// 对于 ctrl 和 meta我们允许任一匹配跨平台兼容
const ctrlOrMetaRequired = hasCtrl || hasMeta;
const ctrlOrMetaPressed = event.ctrlKey || event.metaKey;
if (ctrlOrMetaRequired && !ctrlOrMetaPressed) return false;
if (!ctrlOrMetaRequired && ctrlOrMetaPressed) return false;
if (hasAlt !== event.altKey) return false;
if (hasShift !== event.shiftKey) return false;
return true;
}
/**
*
*/
function matchHotkey(
event: KeyboardEvent,
config: HotkeyConfig,
isInInput: boolean
): boolean {
// 检查是否启用
if (config.enabled !== undefined) {
const isEnabled =
typeof config.enabled === 'function' ? config.enabled() : config.enabled;
if (!isEnabled) return false;
}
// 检查作用域
const hasModifiers = config.modifiers && config.modifiers.length > 0;
// 如果在输入框中
if (isInInput) {
// 单键快捷键(无修饰键)在输入框中不触发
if (!hasModifiers && !config.allowInInput) {
return false;
}
// 有修饰键的快捷键默认也不在输入框触发,除非明确设置 allowInInput
if (hasModifiers && !config.allowInInput) {
return false;
}
}
// 检查按键是否匹配(不区分大小写)
const eventKey = event.key.toLowerCase();
const configKey = config.key.toLowerCase();
if (eventKey !== configKey) return false;
// 检查修饰键
if (!checkModifiers(event, config.modifiers)) return false;
return true;
}
/**
* Provider
*/
export function HotkeysProvider({ children }: { children: React.ReactNode }) {
// 使用 ref 存储快捷键,避免重新渲染
const hotkeysRef = useRef<Map<string, HotkeyConfig>>(new Map());
// 帮助面板状态
const [isHelperOpen, setIsHelperOpen] = useState(false);
// 全局禁用状态
const [isDisabled, setIsDisabled] = useState(false);
// 强制更新(用于 getAll
const [, forceUpdate] = useState({});
/**
*
*/
const register = useCallback((id: string, config: HotkeyConfig) => {
hotkeysRef.current.set(id, config);
forceUpdate({});
}, []);
/**
*
*/
const unregister = useCallback((id: string) => {
hotkeysRef.current.delete(id);
forceUpdate({});
}, []);
/**
*
*/
const setEnabled = useCallback((id: string, enabled: boolean) => {
const config = hotkeysRef.current.get(id);
if (config) {
hotkeysRef.current.set(id, { ...config, enabled });
}
}, []);
/**
*
*/
const getAll = useCallback((): RegisteredHotkey[] => {
const result: RegisteredHotkey[] = [];
hotkeysRef.current.forEach((config, id) => {
result.push({ id, config });
});
return result;
}, []);
/**
*
*/
const toggleHelper = useCallback(() => {
setIsHelperOpen((prev) => !prev);
}, []);
/**
*
*/
const closeHelper = useCallback(() => {
setIsHelperOpen(false);
}, []);
/**
*
*/
const openHelper = useCallback(() => {
setIsHelperOpen(true);
}, []);
/**
*
*/
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
// 检查 IME 输入状态
if (event.isComposing) return;
// 全局禁用时只允许 Escape 关闭帮助面板
if (isDisabled && event.key !== 'Escape') return;
// 检查是否在输入框中
const isInInput = isInputElement(event.target);
// 特殊处理:? 键显示帮助面板
if (event.key === '?' && !isInInput && !event.ctrlKey && !event.metaKey && !event.altKey) {
event.preventDefault();
toggleHelper();
return;
}
// 特殊处理Escape 关闭帮助面板
if (event.key === 'Escape' && isHelperOpen) {
event.preventDefault();
closeHelper();
return;
}
// 遍历所有注册的快捷键
for (const [, config] of hotkeysRef.current) {
if (matchHotkey(event, config, isInInput)) {
// 阻止默认行为
if (config.preventDefault !== false) {
event.preventDefault();
}
// 执行回调
config.action();
return;
}
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
}, [isDisabled, isHelperOpen, toggleHelper, closeHelper]);
// Context 值
const contextValue: HotkeysContextType = {
register,
unregister,
setEnabled,
getAll,
isHelperOpen,
toggleHelper,
closeHelper,
openHelper,
isDisabled,
setDisabled: setIsDisabled,
};
return (
<HotkeysContext.Provider value={contextValue}>
{children}
{/* 快捷键帮助面板 */}
<HotkeysHelper isOpen={isHelperOpen} onClose={closeHelper} />
</HotkeysContext.Provider>
);
}
/**
* 使 Context Hook
*/
export function useHotkeysContext(): HotkeysContextType {
const context = useContext(HotkeysContext);
if (!context) {
throw new Error('useHotkeysContext must be used within a HotkeysProvider');
}
return context;
}

120
src/types/hotkeys.ts Normal file
View File

@ -0,0 +1,120 @@
/**
*
*/
/** 修饰键类型 */
export type ModifierKey = 'ctrl' | 'meta' | 'alt' | 'shift';
/** 快捷键作用域 */
export type HotkeyScope = 'global' | 'modal' | 'input';
/** 快捷键分类 */
export type HotkeyCategory = '导航' | '编辑' | '通用';
/**
*
*/
export interface HotkeyConfig {
/** 主键(如 'n', '/', 'k', 'Escape' */
key: string;
/** 修饰键组合 */
modifiers?: ModifierKey[];
/** 快捷键描述 */
description: string;
/** 分类 */
category?: HotkeyCategory;
/** 触发回调 */
action: () => void;
/** 是否启用(可以是布尔值或函数) */
enabled?: boolean | (() => boolean);
/** 是否阻止默认行为(默认 true */
preventDefault?: boolean;
/** 作用域global-全局, modal-仅弹窗, input-包括输入框 */
scope?: HotkeyScope;
/** 是否允许在输入框中触发(默认 false仅对有修饰键的快捷键生效 */
allowInInput?: boolean;
}
/**
*
*/
export interface RegisteredHotkey {
id: string;
config: HotkeyConfig;
}
/**
* Context
*/
export interface HotkeysContextType {
/** 注册快捷键 */
register: (id: string, config: HotkeyConfig) => void;
/** 注销快捷键 */
unregister: (id: string) => void;
/** 设置快捷键启用状态 */
setEnabled: (id: string, enabled: boolean) => void;
/** 获取所有已注册的快捷键 */
getAll: () => RegisteredHotkey[];
/** 帮助面板是否打开 */
isHelperOpen: boolean;
/** 切换帮助面板 */
toggleHelper: () => void;
/** 关闭帮助面板 */
closeHelper: () => void;
/** 打开帮助面板 */
openHelper: () => void;
/** 全局禁用状态(用于弹窗打开时暂停快捷键) */
isDisabled: boolean;
/** 设置全局禁用状态 */
setDisabled: (disabled: boolean) => void;
}
/**
*
* @param config
* @param isMac Mac
* @returns
*/
export function formatHotkey(config: HotkeyConfig, isMac: boolean = true): string {
const parts: string[] = [];
if (config.modifiers?.includes('ctrl')) {
parts.push(isMac ? '⌃' : 'Ctrl');
}
if (config.modifiers?.includes('meta')) {
parts.push(isMac ? '⌘' : 'Ctrl');
}
if (config.modifiers?.includes('alt')) {
parts.push(isMac ? '⌥' : 'Alt');
}
if (config.modifiers?.includes('shift')) {
parts.push(isMac ? '⇧' : 'Shift');
}
// 格式化主键显示
const keyDisplay = formatKeyDisplay(config.key);
parts.push(keyDisplay);
return isMac ? parts.join(' ') : parts.join('+');
}
/**
*
*/
function formatKeyDisplay(key: string): string {
const keyMap: Record<string, string> = {
'Escape': 'Esc',
'ArrowUp': '↑',
'ArrowDown': '↓',
'ArrowLeft': '←',
'ArrowRight': '→',
'Enter': '↵',
' ': 'Space',
'/': '/',
',': ',',
'.': '.',
'?': '?',
};
return keyMap[key] || key.toUpperCase();
}