用 Tailwind CSS 重构所有共享组件
This commit is contained in:
+155
-234
@@ -1,35 +1,22 @@
|
||||
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';
|
||||
Settings,
|
||||
ExternalLink,
|
||||
ArrowLeft,
|
||||
Search,
|
||||
History,
|
||||
X,
|
||||
Globe,
|
||||
Sun,
|
||||
Moon,
|
||||
Monitor,
|
||||
} from 'lucide-react';
|
||||
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';
|
||||
|
||||
const topBarStyles = {
|
||||
@@ -130,8 +117,7 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
|
||||
setMode(next[mode]);
|
||||
};
|
||||
|
||||
const ThemeIcon =
|
||||
mode === 'light' ? LightModeIcon : mode === 'dark' ? DarkModeIcon : SettingsBrightnessIcon;
|
||||
const ThemeIcon = mode === 'light' ? Sun : mode === 'dark' ? Moon : Monitor;
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
const totalItems = searchQuery.trim() ? searchResults.length : displayedHistory.length;
|
||||
@@ -170,224 +156,159 @@ export default function TopBar({ onOpenOptions }: { onOpenOptions: () => void })
|
||||
const isDashboard = currentPage === 'dashboard';
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
sx={{
|
||||
px: { xs: 1, sm: 2 },
|
||||
py: 1.5,
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
bgcolor: 'background.paper',
|
||||
zIndex: topBarStyles.Z_INDEX,
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ width: { xs: 32, sm: 40 } }}>
|
||||
<div className="flex justify-between items-center px-3 sm:px-4 py-3 border-b border-gray-200 bg-white relative z-[1100]">
|
||||
<div className="w-8 sm:w-10">
|
||||
{!isDashboard && (
|
||||
<IconButton
|
||||
size="small"
|
||||
<button
|
||||
type="button"
|
||||
onClick={goBack}
|
||||
aria-label={t('common:buttons.back')}
|
||||
sx={{
|
||||
bgcolor: (theme) =>
|
||||
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',
|
||||
},
|
||||
}}
|
||||
className="p-1.5 rounded-md bg-gray-50 hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<ArrowBackIosNewIcon sx={{ fontSize: 14 }} />
|
||||
</IconButton>
|
||||
<ArrowLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</Box>
|
||||
</div>
|
||||
|
||||
<Typography
|
||||
variant="subtitle2"
|
||||
sx={{
|
||||
fontWeight: 800,
|
||||
letterSpacing: '0.5px',
|
||||
textTransform: 'uppercase',
|
||||
fontSize: '0.75rem',
|
||||
color: 'text.secondary',
|
||||
ml: 1,
|
||||
display: { xs: 'none', md: 'block' },
|
||||
}}
|
||||
>
|
||||
<span className="hidden md:block text-xs font-extrabold tracking-wider uppercase text-gray-500 ml-2">
|
||||
{t('common:appName')}
|
||||
</Typography>
|
||||
</span>
|
||||
|
||||
<Box sx={{ flex: 1, mx: { xs: 1, sm: 2 }, position: 'relative', maxWidth: 400 }}>
|
||||
<ClickAwayListener onClickAway={() => setShowResults(false)}>
|
||||
<Box>
|
||||
<InputBase
|
||||
ref={inputRef}
|
||||
placeholder={t('common:buttons.search')}
|
||||
value={searchQuery}
|
||||
onChange={handleSearchChange}
|
||||
onFocus={() => setShowResults(true)}
|
||||
onKeyDown={handleKeyDown}
|
||||
inputProps={{ 'aria-label': t('common:buttons.search') }}
|
||||
startAdornment={<SearchIcon sx={{ color: 'text.disabled', mr: 1, fontSize: 20 }} />}
|
||||
endAdornment={
|
||||
searchQuery && (
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setSearchQuery('');
|
||||
setSelectedIndex(-1);
|
||||
}}
|
||||
aria-label={t('common:buttons.clearSearch')}
|
||||
>
|
||||
<CloseIcon sx={{ fontSize: 16 }} />
|
||||
</IconButton>
|
||||
)
|
||||
}
|
||||
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)}`,
|
||||
},
|
||||
<div className="flex-1 mx-2 sm:mx-4 relative max-w-[400px]">
|
||||
<div className="relative">
|
||||
<div className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400">
|
||||
<Search className="h-4 w-4" />
|
||||
</div>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
placeholder={t('common:buttons.search')}
|
||||
value={searchQuery}
|
||||
onChange={handleSearchChange}
|
||||
onFocus={() => setShowResults(true)}
|
||||
onKeyDown={handleKeyDown}
|
||||
aria-label={t('common:buttons.search')}
|
||||
className="w-full pl-9 pr-8 py-1.5 text-sm rounded-lg border border-transparent bg-gray-50 focus:bg-white focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 transition-all"
|
||||
/>
|
||||
{searchQuery && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setSearchQuery('');
|
||||
setSelectedIndex(-1);
|
||||
}}
|
||||
/>
|
||||
aria-label={t('common:buttons.clearSearch')}
|
||||
className="absolute right-2 top-1/2 -translate-y-1/2 p-0.5 rounded-md text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
<X className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{showResults && (searchQuery.trim() || displayedHistory.length > 0) && (
|
||||
<Paper
|
||||
elevation={8}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '100%',
|
||||
left: 0,
|
||||
right: 0,
|
||||
mt: 1,
|
||||
maxHeight: topBarStyles.DROPDOWN_MAX_HEIGHT,
|
||||
overflow: 'auto',
|
||||
borderRadius: 2,
|
||||
zIndex: topBarStyles.DROPDOWN_Z_INDEX,
|
||||
}}
|
||||
>
|
||||
<List disablePadding role="listbox">
|
||||
{searchQuery.trim() ? (
|
||||
searchResults.length > 0 ? (
|
||||
searchResults.map((feature, index) => (
|
||||
<ListItemButton
|
||||
key={feature.key}
|
||||
selected={selectedIndex === index}
|
||||
onClick={() => handleSelectFeature(feature)}
|
||||
role="option"
|
||||
aria-selected={selectedIndex === index}
|
||||
sx={{ py: 1 }}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 40 }}>
|
||||
{feature.icon && <feature.icon sx={{ fontSize: 20 }} />}
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={t(feature.labelKey)}
|
||||
secondary={t(feature.descriptionKey)}
|
||||
primaryTypographyProps={{ variant: 'body2', fontWeight: 600 }}
|
||||
secondaryTypographyProps={{ variant: 'caption', noWrap: true }}
|
||||
/>
|
||||
</ListItemButton>
|
||||
))
|
||||
) : (
|
||||
<Box sx={{ py: 3, textAlign: 'center' }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{t('common:buttons.noResults')}
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<Box sx={{ px: 2, py: 1 }}>
|
||||
<Typography variant="caption" fontWeight={700} color="text.disabled">
|
||||
{t('common:buttons.recentSearch')}
|
||||
</Typography>
|
||||
</Box>
|
||||
{displayedHistory.map((item, index) => (
|
||||
<ListItemButton
|
||||
key={item}
|
||||
selected={selectedIndex === index}
|
||||
onClick={() => {
|
||||
setSearchQuery(item);
|
||||
setSelectedIndex(-1);
|
||||
}}
|
||||
role="option"
|
||||
aria-selected={selectedIndex === index}
|
||||
>
|
||||
<ListItemIcon sx={{ minWidth: 40 }}>
|
||||
<HistoryIcon sx={{ fontSize: 18, color: 'text.disabled' }} />
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={item}
|
||||
primaryTypographyProps={{ variant: 'body2' }}
|
||||
/>
|
||||
</ListItemButton>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</List>
|
||||
</Paper>
|
||||
)}
|
||||
</Box>
|
||||
</ClickAwayListener>
|
||||
</Box>
|
||||
{showResults && (searchQuery.trim() || displayedHistory.length > 0) && (
|
||||
<div className="absolute top-full left-0 right-0 mt-2 bg-white rounded-lg shadow-lg border border-gray-200 max-h-[300px] overflow-auto z-[1200]">
|
||||
<ul role="listbox" className="py-1">
|
||||
{searchQuery.trim() ? (
|
||||
searchResults.length > 0 ? (
|
||||
searchResults.map((feature, index) => (
|
||||
<li
|
||||
key={feature.key}
|
||||
role="option"
|
||||
aria-selected={selectedIndex === index}
|
||||
onClick={() => handleSelectFeature(feature)}
|
||||
className={`flex items-center gap-3 px-3 py-2 cursor-pointer transition-colors ${
|
||||
selectedIndex === index ? 'bg-blue-50' : 'hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="w-8 h-8 flex items-center justify-center text-gray-500">
|
||||
{feature.icon && <feature.icon className="h-5 w-5" />}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-semibold text-gray-900 truncate">
|
||||
{t(feature.labelKey)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 truncate">
|
||||
{t(feature.descriptionKey)}
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
))
|
||||
) : (
|
||||
<li className="px-4 py-6 text-center">
|
||||
<p className="text-sm text-gray-500">{t('common:buttons.noResults')}</p>
|
||||
</li>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<li className="px-4 py-2">
|
||||
<span className="text-xs font-bold text-gray-400">
|
||||
{t('common:buttons.recentSearch')}
|
||||
</span>
|
||||
</li>
|
||||
{displayedHistory.map((item, index) => (
|
||||
<li
|
||||
key={item}
|
||||
role="option"
|
||||
aria-selected={selectedIndex === index}
|
||||
onClick={() => {
|
||||
setSearchQuery(item);
|
||||
setSelectedIndex(-1);
|
||||
}}
|
||||
className={`flex items-center gap-3 px-3 py-2 cursor-pointer transition-colors ${
|
||||
selectedIndex === index ? 'bg-blue-50' : 'hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<div className="w-8 h-8 flex items-center justify-center text-gray-400">
|
||||
<History className="h-4 w-4" />
|
||||
</div>
|
||||
<span className="text-sm text-gray-700">{item}</span>
|
||||
</li>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Stack direction="row" spacing={0.5} sx={{ justifyContent: 'flex-end', flexShrink: 0 }}>
|
||||
<Tooltip title={t('common:buttons.toggleLanguage')}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={toggleLanguage}
|
||||
aria-label={t('common:buttons.toggleLanguage')}
|
||||
>
|
||||
<LanguageIcon sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t(`common:buttons.themeMode.${mode}`)}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={cycleThemeMode}
|
||||
aria-label={t('common:buttons.toggleTheme')}
|
||||
>
|
||||
<ThemeIcon sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('common:buttons.openInTab')}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={handleOpenInTab}
|
||||
aria-label={t('common:buttons.openInTab')}
|
||||
>
|
||||
<OpenInNewIcon sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={t('common:buttons.settings')}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={onOpenOptions}
|
||||
aria-label={t('common:buttons.settings')}
|
||||
>
|
||||
<SettingsIcon sx={{ fontSize: 18 }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleLanguage}
|
||||
aria-label={t('common:buttons.toggleLanguage')}
|
||||
title={t('common:buttons.toggleLanguage')}
|
||||
className="p-1.5 rounded-md hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<Globe className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={cycleThemeMode}
|
||||
aria-label={t('common:buttons.toggleTheme')}
|
||||
title={t(`common:buttons.themeMode.${mode}`)}
|
||||
className="p-1.5 rounded-md hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<ThemeIcon className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleOpenInTab}
|
||||
aria-label={t('common:buttons.openInTab')}
|
||||
title={t('common:buttons.openInTab')}
|
||||
className="p-1.5 rounded-md hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpenOptions}
|
||||
aria-label={t('common:buttons.settings')}
|
||||
title={t('common:buttons.settings')}
|
||||
className="p-1.5 rounded-md hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user