import React, { useEffect, useMemo, useRef, useState } from 'react'; import { ArrowLeft, ExternalLink, Globe, History, Monitor, Moon, Search, Settings, Sun, X, } from 'lucide-react'; import { useRouter } from '@/providers/RouterProvider'; import { useThemeMode } from '@/providers/ThemeModeProvider'; import { FeatureConfig, FEATURES } from '@/config/features'; import { storageUtil } from '@/utils/chromeStorage'; import { openExtensionPage } from '@/utils/chromeTabs'; import { useI18n } from '@/utils/chromeI18n'; import { normalizeLanguage, SUPPORTED_LANGUAGES } from '@/utils/chromeI18n'; import { cn } from '@/lib/utils'; // 1. 引入 shadcn 核心工具函数 // 常量配置抽取(无需写在全局变量或 styles 对象里) const SEARCH_HISTORY_LIMIT = 10; const SEARCH_HISTORY_DISPLAY = 5; export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void }) { const { currentPage, goBack, navigateTo } = useRouter(); const { mode, setMode } = useThemeMode(); const { t, i18n } = useI18n(['common', 'features']); const [searchQuery, setSearchQuery] = useState(''); const [showResults, setShowResults] = useState(false); const [searchHistory, setSearchHistory] = useState([]); const [selectedIndex, setSelectedIndex] = useState(-1); const containerRef = useRef(null); const inputRef = useRef(null); const handleOpenInTab = async () => { await openExtensionPage('popup.html', { mode: 'tab' }); window.close(); }; // 2. 健壮的 Click Outside 逻辑:点击空白处收起搜索框 useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (containerRef.current && !containerRef.current.contains(event.target as Node)) { setShowResults(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); // 从 Chrome Storage 异步初始化历史记录 useEffect(() => { storageUtil .get('app/searchHistory', []) .then((history) => { if (history) setSearchHistory(history); }) .catch((err) => console.error('加载搜索历史失败:', err)); }, []); // 3. 模糊搜索匹配(移除了无意义的 dashboard 干扰项) const searchResults = useMemo(() => { const query = searchQuery.trim().toLowerCase(); if (!query) return []; return FEATURES.filter((f) => { if (f.key === 'dashboard') return false; return ( t(f.labelKey).toLowerCase().includes(query) || t(f.descriptionKey).toLowerCase().includes(query) ); }); }, [searchQuery, t]); const displayedHistory = useMemo(() => { if (searchQuery.trim()) return []; return searchHistory.slice(0, SEARCH_HISTORY_DISPLAY); }, [searchHistory, searchQuery]); // 新增/持久化历史记录 const saveToHistory = async (query: string) => { if (!query.trim()) return; const nextHistory = [query, ...searchHistory.filter((h) => h !== query)].slice( 0, SEARCH_HISTORY_LIMIT, ); setSearchHistory(nextHistory); await storageUtil.set('app/searchHistory', nextHistory).catch((err) => console.error(err)); }; const handleSelectFeature = (feature: FeatureConfig) => { navigateTo(feature.key); saveToHistory(t(feature.labelKey)); setSearchQuery(''); setShowResults(false); }; const toggleLanguage = async () => { const currentLng = normalizeLanguage(i18n.language); const currentIndex = SUPPORTED_LANGUAGES.indexOf(currentLng); const nextLng = SUPPORTED_LANGUAGES[(currentIndex + 1) % SUPPORTED_LANGUAGES.length]; await i18n.changeLanguage(nextLng); await storageUtil.set('app/language', nextLng); }; const cycleThemeMode = () => { const nextMap = { light: 'dark', dark: 'system', system: 'light' } as const; setMode(nextMap[mode]); }; const ThemeIcon = mode === 'light' ? Sun : mode === 'dark' ? Moon : Monitor; // 4. 健壮的键盘导航交互 const handleKeyDown = (e: React.KeyboardEvent) => { const totalItems = searchQuery.trim() ? searchResults.length : displayedHistory.length; if (e.key === 'ArrowDown') { e.preventDefault(); setSelectedIndex((prev) => (prev < totalItems - 1 ? prev + 1 : prev)); } else if (e.key === 'ArrowUp') { e.preventDefault(); setSelectedIndex((prev) => (prev > 0 ? prev - 1 : prev)); } else if (e.key === 'Enter') { e.preventDefault(); if (selectedIndex >= 0 && selectedIndex < totalItems) { if (searchQuery.trim()) { handleSelectFeature(searchResults[selectedIndex]); } else { const selectedQuery = displayedHistory[selectedIndex]; if (selectedQuery) { setSearchQuery(selectedQuery); setSelectedIndex(-1); const matched = FEATURES.find( (f) => f.key !== 'dashboard' && t(f.labelKey) === selectedQuery, ); if (matched) handleSelectFeature(matched); } } } else if (searchQuery.trim() && searchResults.length > 0) { handleSelectFeature(searchResults[0]); } } else if (e.key === 'Escape') { setShowResults(false); inputRef.current?.blur(); } }; const isDashboard = currentPage === 'dashboard'; return (
{/* 左侧:返回按钮区 */}
{!isDashboard && ( )}
{/* 中间:搜索容器 */}
{ setSearchQuery(e.target.value); setShowResults(true); setSelectedIndex(-1); }} onFocus={() => setShowResults(true)} onKeyDown={handleKeyDown} aria-label={t('common_buttons_search')} className="w-full h-9 pl-9 pr-8 text-sm rounded-md border border-input bg-muted/50 transition-all placeholder:text-muted-foreground focus:bg-background focus:outline-none focus:ring-1 focus:ring-ring focus:border-input" /> {searchQuery && ( )}
{/* 动态联想结果卡片 */} {showResults && (searchQuery.trim() || displayedHistory.length > 0) && (
    {searchQuery.trim() ? ( searchResults.length > 0 ? ( searchResults.map((feature, index) => (
  • handleSelectFeature(feature)} className={cn( 'flex items-center gap-3 px-2.5 py-2 rounded-sm cursor-pointer text-sm transition-colors', selectedIndex === index ? 'bg-accent text-accent-foreground' : 'hover:bg-muted/60', )} >
    {feature.icon && }

    {t(feature.labelKey)}

    {t(feature.descriptionKey)}

  • )) ) : (
  • {t('common:buttons.noResults')}
  • ) ) : ( <>
    {t('common:buttons.recentSearch')}
    {displayedHistory.map((item, index) => (
  • { setSearchQuery(item); setSelectedIndex(-1); }} className={cn( 'flex items-center gap-3 px-2.5 py-2 rounded-sm cursor-pointer text-sm transition-colors', selectedIndex === index ? 'bg-accent text-accent-foreground' : 'hover:bg-muted/60', )} > {item}
  • ))} )}
)}
{/* 右侧:操作区 */}
); } // 5. 提炼出高度复用的原子按钮,大幅精简 Tailwind 冗余,符合 shadcn 的灵巧风格 function IconButton({ children, onClick, title, }: { children: React.ReactNode; onClick: () => void; title: string; }) { return ( ); }