/** * CommandPalette 全局搜索 — Cmd/Ctrl+K 唤起,支持全局搜索和快捷导航 */ import { useState, useEffect, useRef, useMemo } from 'react' import { useNavigate } from 'react-router-dom' import { Search, ArrowRight, Clock } from 'lucide-react' import { searchApi } from '../../lib/api-services' /** 搜索结果类型 */ interface SearchResult { type: 'employee' | 'page' | 'action' id: string title: string subtitle?: string link: string icon?: string } /** 快捷页面导航 */ const QUICK_PAGES: SearchResult[] = [ { type: 'page', id: 'dashboard', title: '工作台', link: '/', icon: 'home' }, { type: 'page', id: 'roster', title: '花名册', link: '/roster', icon: 'users' }, { type: 'page', id: 'money', title: '薪税管理', link: '/money', icon: 'wallet' }, { type: 'page', id: 'social', title: '社保公积金', link: '/social', icon: 'shield' }, { type: 'page', id: 'termination', title: '离职管理', link: '/termination', icon: 'userX' }, { type: 'page', id: 'attendance', title: '考勤排班', link: '/attendance', icon: 'calendar' }, { type: 'page', id: 'leave-approval', title: '休假审批', link: '/leave-approval', icon: 'calendar' }, { type: 'page', id: 'risk-center', title: '风险中心', link: '/risk-center', icon: 'alert' }, { type: 'page', id: 'salary-dashboard', title: '薪酬分析', link: '/salary-dashboard', icon: 'chart' }, { type: 'page', id: 'policies', title: '规章制度', link: '/policies', icon: 'file' }, { type: 'page', id: 'company-files', title: '公司文件', link: '/company-files', icon: 'building' }, { type: 'page', id: 'settings', title: '设置', link: '/settings', icon: 'gear' }, ] interface CommandPaletteProps { open: boolean onClose: () => void } export function CommandPalette({ open, onClose }: CommandPaletteProps) { const [query, setQuery] = useState('') const [selectedIndex, setSelectedIndex] = useState(0) const [searchResults, setSearchResults] = useState([]) const [searching, setSearching] = useState(false) const navigate = useNavigate() const inputRef = useRef(null) const listRef = useRef(null) /** 搜索逻辑 */ useEffect(() => { if (!open) { setQuery('') setSelectedIndex(0) setSearchResults([]) return } // 聚焦输入框 setTimeout(() => inputRef.current?.focus(), 50) }, [open]) /** 执行搜索 */ useEffect(() => { if (!query.trim()) { setSearchResults([]) return } const q = query.trim().toLowerCase() setSearching(true) // 本地页面匹配 const localResults = QUICK_PAGES.filter(p => p.title.toLowerCase().includes(q) ) // 远程搜索员工 const timer = setTimeout(async () => { try { const res = await searchApi.search(q) as any const remoteResults: SearchResult[] = (res?.employees || []).map((e: any) => ({ type: 'employee' as const, id: e.id, title: e.name, subtitle: `${e.department || ''} · ${e.position || ''}`, link: `/roster?search=${encodeURIComponent(e.name)}`, })) setSearchResults([...localResults, ...remoteResults]) } catch { setSearchResults(localResults) } finally { setSearching(false) } }, 300) return () => clearTimeout(timer) }, [query]) /** 合并结果(无搜索词时显示快捷页面) */ const displayResults = useMemo(() => { if (!query.trim()) return QUICK_PAGES return searchResults }, [query, searchResults]) /** 键盘导航 */ useEffect(() => { if (!open) return const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'ArrowDown') { e.preventDefault() setSelectedIndex(i => Math.min(i + 1, displayResults.length - 1)) } else if (e.key === 'ArrowUp') { e.preventDefault() setSelectedIndex(i => Math.max(i - 1, 0)) } else if (e.key === 'Enter') { e.preventDefault() const result = displayResults[selectedIndex] if (result) { navigate(result.link) onClose() } } else if (e.key === 'Escape') { e.preventDefault() onClose() } } window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) }, [open, selectedIndex, displayResults, navigate, onClose]) /** 滚动到选中项 */ useEffect(() => { const el = listRef.current?.children[selectedIndex] as HTMLElement el?.scrollIntoView({ block: 'nearest' }) }, [selectedIndex]) if (!open) return null return (
e.stopPropagation()} > {/* 搜索输入 */}
{ setQuery(e.target.value); setSelectedIndex(0) }} placeholder="搜索员工、页面、功能..." className="flex-1 text-sm outline-none bg-transparent" /> ESC
{/* 搜索结果 */}
{displayResults.length === 0 && !searching ? (
{query ? '未找到匹配结果' : '输入关键词搜索'}
) : ( displayResults.map((result, i) => ( )) )} {searching && (
搜索中...
)}
{/* 底部提示 */}
↑↓ 导航 选择
{displayResults.length} 个结果
) }