/** * KeyboardShortcutPanel 组件 * * 按 Ctrl+/ 或 ? 显示快捷键面板,模态框列出所有可用快捷键。 * 支持搜索过滤和分组显示(导航/编辑/视图/其他)。 * * 在 useKeyboardShortcuts 中注册 Ctrl+/ 和 ? 快捷键来触发此面板。 */ import React, { useState, useEffect, useRef, useCallback, useMemo } from 'react' import { useTranslation } from '../i18n' import { useFocusTrap } from '../hooks/useKeyboardShortcuts' interface ShortcutEntry { keys: string[] label: string descriptionKey: string group: 'navigation' | 'editing' | 'view' | 'other' } const SHORTCUTS: ShortcutEntry[] = [ { keys: ['Ctrl', 'N'], label: 'Ctrl + N', descriptionKey: 'shortcut_newConsultation', group: 'navigation' }, { keys: ['Ctrl', 'H'], label: 'Ctrl + H', descriptionKey: 'shortcut_history', group: 'navigation' }, { keys: ['Ctrl', 'D'], label: 'Ctrl + D', descriptionKey: 'shortcut_goDashboard', group: 'navigation' }, { keys: ['Ctrl', 'P'], label: 'Ctrl + P', descriptionKey: 'shortcut_patients', group: 'navigation' }, { keys: ['Ctrl', 'S'], label: 'Ctrl + S', descriptionKey: 'shortcut_save', group: 'editing' }, { keys: ['Ctrl', 'K'], label: 'Ctrl + K', descriptionKey: 'shortcut_search', group: 'navigation' }, { keys: ['Escape'], label: 'Esc', descriptionKey: 'shortcut_close', group: 'view' }, { keys: ['Ctrl', '/'], label: 'Ctrl + /', descriptionKey: 'shortcut_shortcutPanel', group: 'view' }, { keys: ['?'], label: '?', descriptionKey: 'shortcut_shortcutPanel', group: 'view' }, ] const GROUP_ORDER: ShortcutEntry['group'][] = ['navigation', 'editing', 'view', 'other'] interface KeyboardShortcutPanelProps { isOpen: boolean onClose: () => void } export default function KeyboardShortcutPanel({ isOpen, onClose }: KeyboardShortcutPanelProps) { const { t } = useTranslation() const [search, setSearch] = useState('') const panelRef = useRef(null) const searchInputRef = useRef(null) // 聚焦陷阱 useFocusTrap(panelRef, isOpen) // 打开时聚焦搜索框 useEffect(() => { if (isOpen) { setSearch('') // 延迟聚焦以确保 DOM 已渲染 setTimeout(() => searchInputRef.current?.focus(), 50) } }, [isOpen]) // Escape 关闭 const handleKeyDown = useCallback( (e: React.KeyboardEvent) => { if (e.key === 'Escape') { onClose() } }, [onClose] ) // 过滤快捷键 const filteredShortcuts = useMemo(() => { const query = search.toLowerCase().trim() if (!query) return SHORTCUTS return SHORTCUTS.filter( (s) => s.label.toLowerCase().includes(query) || t(`ergonomics.${s.descriptionKey}`).toLowerCase().includes(query) || s.group.toLowerCase().includes(query) ) }, [search, t]) // 分组 const grouped = useMemo(() => { const groups: Record = {} for (const g of GROUP_ORDER) { const items = filteredShortcuts.filter((s) => s.group === g) if (items.length > 0) { groups[g] = items } } return groups }, [filteredShortcuts]) if (!isOpen) return null const tGroupLabel = (group: string): string => { const key = `ergonomics.shortcutGroup_${group}` const translated = t(key) return translated !== key ? translated : group } return (
{ if (e.target === e.currentTarget) onClose() }} > {/* 半透明遮罩 */}
{/* 面板 */}
{/* 标题栏 */}

{t('ergonomics.shortcutPanel')}

{/* 搜索框 */}
setSearch(e.target.value)} placeholder={t('ergonomics.searchShortcuts')} className="w-full pl-9 pr-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:border-medical-400 focus:ring-1 focus:ring-medical-400 transition-colors" />
{/* 快捷键列表 */}
{Object.keys(grouped).length === 0 ? (
{t('ergonomics.noShortcutsFound')}
) : ( Object.entries(grouped).map(([group, entries]) => (

{tGroupLabel(group)}

{entries.map((entry, idx) => (
{t(`ergonomics.${entry.descriptionKey}`)}
{entry.keys.map((key, ki) => ( {ki > 0 && ( + )} {key} ))}
))}
)) )}
) }