feat(组件): 添加快捷键帮助面板 UI
- 实现快捷键帮助弹窗界面 - 按分类(导航、编辑、通用)分组显示 - 支持 Mac/Windows 快捷键格式切换 - 预置常用快捷键说明 - 按 Esc 关闭面板
This commit is contained in:
parent
827a033d41
commit
f859ffe4cd
207
src/components/ui/HotkeysHelper.tsx
Normal file
207
src/components/ui/HotkeysHelper.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user