feat(组件): 添加快捷键帮助面板 UI

- 实现快捷键帮助弹窗界面
- 按分类(导航、编辑、通用)分组显示
- 支持 Mac/Windows 快捷键格式切换
- 预置常用快捷键说明
- 按 Esc 关闭面板
This commit is contained in:
gaoziman 2025-12-27 23:56:19 +08:00
parent 827a033d41
commit f859ffe4cd

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