import { useState, useEffect, useRef, useMemo } from 'react'; import { Box, IconButton, Stack, Tooltip, Typography, InputBase, Paper, List, ListItemButton, ListItemIcon, ListItemText, ClickAwayListener, } from '@mui/material'; import SettingsIcon from '@mui/icons-material/Settings'; import OpenInNewIcon from '@mui/icons-material/OpenInNew'; import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew'; import SearchIcon from '@mui/icons-material/Search'; import HistoryIcon from '@mui/icons-material/History'; import CloseIcon from '@mui/icons-material/Close'; import LanguageIcon from '@mui/icons-material/Language'; import LightModeIcon from '@mui/icons-material/LightMode'; import DarkModeIcon from '@mui/icons-material/DarkMode'; import SettingsBrightnessIcon from '@mui/icons-material/SettingsBrightness'; import { useRouter } from '@/providers/RouterProvider'; import { useThemeMode } from '@/providers/ThemeModeProvider'; import { FEATURES, FeatureConfig } from '@/config/features'; import { storageUtil } from '@/utils/chromeStorage'; import { openExtensionPage } from '@/utils/chromeTabs'; import { useTranslation } from 'react-i18next'; import { alpha } from '@mui/material/styles'; import { SUPPORTED_LANGUAGES, normalizeLanguage } from '@/i18n'; import { topBarStyles } from '@/config/pageTheme'; export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void }) { const { currentPage, goBack, navigateTo } = useRouter(); const { mode, setMode } = useThemeMode(); const { t, i18n } = useTranslation(['common', 'features']); const [searchQuery, setSearchQuery] = useState(''); const [showResults, setShowResults] = useState(false); const [searchHistory, setSearchHistory] = useState([]); const [selectedIndex, setSelectedIndex] = useState(-1); const inputRef = useRef(null); // 加载搜索历史 useEffect(() => { storageUtil .get('app/searchHistory', []) .then((history) => { setSearchHistory(history || []); }) .catch((error) => { console.error('加载搜索历史失败:', error); }); }, []); // 模糊搜索逻辑 const searchResults = useMemo(() => { if (!searchQuery.trim()) return []; const query = searchQuery.toLowerCase(); return FEATURES.filter((f) => { if (f.key === 'dashboard') return false; const label = t(f.labelKey).toLowerCase(); const desc = t(f.descriptionKey).toLowerCase(); return label.includes(query) || desc.includes(query); }); }, [searchQuery, t]); const displayedHistory = useMemo(() => { if (searchQuery.trim()) return []; return searchHistory.slice(0, topBarStyles.SEARCH_HISTORY_DISPLAY); }, [searchHistory, searchQuery]); const handleOpenInTab = async () => { await openExtensionPage('popup.html', { mode: 'tab' }); window.close(); }; const handleSearchChange = (e: React.ChangeEvent) => { setSearchQuery(e.target.value); setShowResults(true); setSelectedIndex(-1); }; const saveToHistory = async (query: string) => { if (!query.trim()) return; setSearchHistory((prev) => { const newHistory = [query, ...prev.filter((h) => h !== query)].slice( 0, topBarStyles.SEARCH_HISTORY_LIMIT, ); storageUtil.set('app/searchHistory', newHistory).catch((error) => { console.error('保存搜索历史失败:', error); }); return newHistory; }); }; 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 nextIndex = (currentIndex + 1) % SUPPORTED_LANGUAGES.length; const newLng = SUPPORTED_LANGUAGES[nextIndex]; await i18n.changeLanguage(newLng); await storageUtil.set('app/language', newLng); }; const cycleThemeMode = () => { const next = { light: 'dark', dark: 'system', system: 'light' } as const; setMode(next[mode]); }; const ThemeIcon = mode === 'light' ? LightModeIcon : mode === 'dark' ? DarkModeIcon : SettingsBrightnessIcon; 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') { if (selectedIndex >= 0) { if (searchQuery.trim()) { handleSelectFeature(searchResults[selectedIndex]); } else { const selectedQuery = displayedHistory[selectedIndex]; setSearchQuery(selectedQuery); setSelectedIndex(-1); // 触发搜索:如果匹配到功能则跳转,否则保持搜索词展示结果 const matchedFeature = FEATURES.find( (f) => f.key !== 'dashboard' && t(f.labelKey) === selectedQuery, ); if (matchedFeature) { handleSelectFeature(matchedFeature); } } } 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 && ( theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.05)' : 'grey.50', '&:hover': { bgcolor: (theme) => theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.1)' : 'grey.200', }, }} > )} {t('common:appName')} setShowResults(false)}> setShowResults(true)} onKeyDown={handleKeyDown} inputProps={{ 'aria-label': t('common:buttons.search') }} startAdornment={} endAdornment={ searchQuery && ( { setSearchQuery(''); setSelectedIndex(-1); }} aria-label={t('common:buttons.clearSearch')} > ) } sx={{ width: '100%', bgcolor: (theme) => theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.05)' : 'grey.50', px: 1.5, py: 0.5, borderRadius: 2, fontSize: '0.875rem', border: '1px solid', borderColor: (theme) => theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.1)' : 'transparent', transition: 'all 0.2s', '&:hover': { bgcolor: (theme) => theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.08)' : 'grey.100', }, '&.Mui-focused': { bgcolor: 'background.paper', borderColor: 'primary.main', boxShadow: (theme) => `0 0 0 2px ${alpha(theme.palette.primary.main, 0.15)}`, }, }} /> {showResults && (searchQuery.trim() || displayedHistory.length > 0) && ( {searchQuery.trim() ? ( searchResults.length > 0 ? ( searchResults.map((feature, index) => ( handleSelectFeature(feature)} role="option" aria-selected={selectedIndex === index} sx={{ py: 1 }} > {feature.icon} )) ) : ( {t('common:buttons.noResults')} ) ) : ( <> {t('common:buttons.recentSearch')} {displayedHistory.map((item, index) => ( { setSearchQuery(item); setSelectedIndex(-1); }} role="option" aria-selected={selectedIndex === index} > ))} )} )} ); }