'use client' /** * useKeyboardShortcuts - 全局鍵盤快捷鍵 Hook * =========================================== * Phase 11: #56 鍵盤快捷鍵支援 * * Features: * - Y: 長按核准 (需按住指定時間) * - N: 拒絕 * - 方向鍵: 導航 * - ?: 顯示快捷鍵說明 * - Esc: 關閉面板 * * 安全機制: * - Y 鍵需要長按,防止誤觸 * - 輸入框內不觸發 */ import { useEffect, useCallback, useRef, useState } from 'react' // ============================================================================= // Types // ============================================================================= export interface KeyboardShortcutsConfig { /** 長按確認時間 (毫秒),預設 2000ms */ holdDuration?: number /** Y 鍵長按核准回調 */ onApprove?: () => void /** N 鍵拒絕回調 */ onReject?: () => void /** 上/左 導航回調 */ onPrev?: () => void /** 下/右 導航回調 */ onNext?: () => void /** ? 顯示快捷鍵說明 */ onShowShortcuts?: () => void /** Esc 關閉回調 */ onClose?: () => void /** 是否啟用 */ enabled?: boolean } export interface UseKeyboardShortcutsReturn { /** Y 鍵是否正在按住 */ isYKeyHolding: boolean /** Y 鍵長按進度 (0-100) */ yKeyProgress: number } // ============================================================================= // Hook // ============================================================================= export function useKeyboardShortcuts( config: KeyboardShortcutsConfig ): UseKeyboardShortcutsReturn { const { holdDuration = 2000, onApprove, onReject, onPrev, onNext, onShowShortcuts, onClose, enabled = true, } = config const [isYKeyHolding, setIsYKeyHolding] = useState(false) const [yKeyProgress, setYKeyProgress] = useState(0) const yKeyStartTime = useRef(null) const yKeyAnimationFrame = useRef(null) const yKeyTriggered = useRef(false) // 更新 Y 鍵進度 const updateYKeyProgress = useCallback(() => { if (!yKeyStartTime.current) return const elapsed = Date.now() - yKeyStartTime.current const progress = Math.min((elapsed / holdDuration) * 100, 100) setYKeyProgress(progress) if (progress >= 100 && !yKeyTriggered.current) { yKeyTriggered.current = true onApprove?.() // 重置狀態 setIsYKeyHolding(false) setYKeyProgress(0) yKeyStartTime.current = null return } if (progress < 100) { yKeyAnimationFrame.current = requestAnimationFrame(updateYKeyProgress) } }, [holdDuration, onApprove]) // Y 鍵按下 const handleYKeyDown = useCallback(() => { if (yKeyStartTime.current) return // 已經在按住 yKeyStartTime.current = Date.now() yKeyTriggered.current = false setIsYKeyHolding(true) setYKeyProgress(0) yKeyAnimationFrame.current = requestAnimationFrame(updateYKeyProgress) }, [updateYKeyProgress]) // Y 鍵釋放 const handleYKeyUp = useCallback(() => { if (yKeyAnimationFrame.current) { cancelAnimationFrame(yKeyAnimationFrame.current) } yKeyStartTime.current = null setIsYKeyHolding(false) setYKeyProgress(0) }, []) // 主要鍵盤事件處理 useEffect(() => { if (!enabled) return const handleKeyDown = (e: KeyboardEvent) => { // 忽略輸入框內的按鍵 if ( e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement || (e.target as HTMLElement).isContentEditable ) { return } // 忽略組合鍵 if (e.ctrlKey || e.metaKey || e.altKey) { return } switch (e.key.toLowerCase()) { case 'y': e.preventDefault() handleYKeyDown() break case 'n': e.preventDefault() onReject?.() break case 'arrowup': case 'arrowleft': e.preventDefault() onPrev?.() break case 'arrowdown': case 'arrowright': e.preventDefault() onNext?.() break case '?': e.preventDefault() onShowShortcuts?.() break case 'escape': e.preventDefault() onClose?.() break } } const handleKeyUp = (e: KeyboardEvent) => { if (e.key.toLowerCase() === 'y') { handleYKeyUp() } } window.addEventListener('keydown', handleKeyDown) window.addEventListener('keyup', handleKeyUp) return () => { window.removeEventListener('keydown', handleKeyDown) window.removeEventListener('keyup', handleKeyUp) // 清理動畫 if (yKeyAnimationFrame.current) { cancelAnimationFrame(yKeyAnimationFrame.current) } } }, [ enabled, handleYKeyDown, handleYKeyUp, onReject, onPrev, onNext, onShowShortcuts, onClose, ]) return { isYKeyHolding, yKeyProgress, } }