Enhance Dashboard and Layout Styles

- Updated opacity and blur effects for background elements in DashboardPage.
- Adjusted radial gradient color mixes for visual consistency.
- Increased font sizes and modified letter spacing for improved readability in hero sections.
- Implemented a new glassmorphism effect with backdrop filters in various components.
- Refined sidebar styles, including width adjustments and hover effects.
- Enhanced header styles for better responsiveness and visual hierarchy.
- Introduced new gradient backgrounds and box shadows for a modern look.
- Improved mobile responsiveness with adjusted dimensions and layout properties.
This commit is contained in:
Supra4E8C
2026-04-25 01:05:08 +08:00
Unverified
parent 0bd243e8db
commit 0095933238
3 changed files with 700 additions and 646 deletions
+135 -187
View File
@@ -207,8 +207,6 @@ export function MainLayout() {
const { showNotification } = useNotificationStore();
const location = useLocation();
const apiBase = useAuthStore((state) => state.apiBase);
const connectionStatus = useAuthStore((state) => state.connectionStatus);
const logout = useAuthStore((state) => state.logout);
const config = useConfigStore((state) => state.config);
@@ -224,18 +222,17 @@ export function MainLayout() {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [languageMenuOpen, setLanguageMenuOpen] = useState(false);
const [themeMenuOpen, setThemeMenuOpen] = useState(false);
const [brandExpanded, setBrandExpanded] = useState(true);
const contentRef = useRef<HTMLDivElement | null>(null);
const languageMenuRef = useRef<HTMLDivElement | null>(null);
const themeMenuRef = useRef<HTMLDivElement | null>(null);
const brandCollapseTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const headerRef = useRef<HTMLElement | null>(null);
const fullBrandName = 'CLI Proxy API Management Center';
const abbrBrandName = t('title.abbr');
const isLogsPage = location.pathname.startsWith('/logs');
const showSidebarLabels = !sidebarCollapsed || sidebarOpen;
// 将顶高度写入 CSS 变量,确保侧栏/内容区计算一致,防止滚动时抖动
// 将顶部悬浮控制区高度写入 CSS 变量,供移动端粘性元素和浮层避让。
useLayoutEffect(() => {
const updateHeaderHeight = () => {
const height = headerRef.current?.offsetHeight;
@@ -296,19 +293,6 @@ export function MainLayout() {
};
}, []);
// 5秒后自动收起品牌名称
useEffect(() => {
brandCollapseTimer.current = setTimeout(() => {
setBrandExpanded(false);
}, 5000);
return () => {
if (brandCollapseTimer.current) {
clearTimeout(brandCollapseTimer.current);
}
};
}, []);
useEffect(() => {
if (!languageMenuOpen) {
return;
@@ -361,19 +345,6 @@ export function MainLayout() {
};
}, [themeMenuOpen]);
const handleBrandClick = useCallback(() => {
if (!brandExpanded) {
setBrandExpanded(true);
// 点击展开后,5秒后再次收起
if (brandCollapseTimer.current) {
clearTimeout(brandCollapseTimer.current);
}
brandCollapseTimer.current = setTimeout(() => {
setBrandExpanded(false);
}, 5000);
}
}, [brandExpanded]);
const toggleLanguageMenu = useCallback(() => {
setLanguageMenuOpen((prev) => !prev);
setThemeMenuOpen(false);
@@ -409,15 +380,6 @@ export function MainLayout() {
});
}, [fetchConfig]);
const statusClass =
connectionStatus === 'connected'
? 'success'
: connectionStatus === 'connecting'
? 'warning'
: connectionStatus === 'error'
? 'error'
: 'muted';
const navItems = [
{ path: '/', label: t('nav.dashboard'), icon: sidebarIcons.dashboard },
{ path: '/config', label: t('nav.config_management'), icon: sidebarIcons.config },
@@ -508,176 +470,157 @@ export function MainLayout() {
};
return (
<div className="app-shell">
<div className={`app-shell ${sidebarCollapsed ? 'sidebar-is-collapsed' : ''}`}>
<div className="top-gradient-blur" aria-hidden="true" />
<header className="main-header" ref={headerRef}>
<div className="left">
<button
className="sidebar-toggle-header"
onClick={() => setSidebarCollapsed((prev) => !prev)}
title={
sidebarCollapsed
? t('sidebar.expand', { defaultValue: '展开' })
: t('sidebar.collapse', { defaultValue: '收起' })
}
>
{sidebarCollapsed ? headerIcons.chevronRight : headerIcons.chevronLeft}
</button>
<img src={INLINE_LOGO_JPEG} alt="CPAMC logo" className="brand-logo" />
<div
className={`brand-header ${brandExpanded ? 'expanded' : 'collapsed'}`}
onClick={handleBrandClick}
title={brandExpanded ? undefined : fullBrandName}
>
<span className="brand-full">{fullBrandName}</span>
<span className="brand-abbr">{abbrBrandName}</span>
</div>
</div>
<button
type="button"
className="sidebar-toggle-floating"
onClick={() => setSidebarCollapsed((prev) => !prev)}
title={
sidebarCollapsed
? t('sidebar.expand', { defaultValue: '展开' })
: t('sidebar.collapse', { defaultValue: '收起' })
}
aria-label={
sidebarCollapsed
? t('sidebar.expand', { defaultValue: '展开' })
: t('sidebar.collapse', { defaultValue: '收起' })
}
>
{sidebarCollapsed ? headerIcons.chevronRight : headerIcons.chevronLeft}
</button>
<div className="right">
<div className="connection">
<span className={`status-badge ${statusClass}`}>
{t(
connectionStatus === 'connected'
? 'common.connected_status'
: connectionStatus === 'connecting'
? 'common.connecting_status'
: 'common.disconnected_status'
)}
</span>
<span className="base">{apiBase || '-'}</span>
</div>
<div className="header-actions">
<Button
className="mobile-menu-btn"
variant="ghost"
size="sm"
onClick={() => setSidebarOpen((prev) => !prev)}
>
{headerIcons.menu}
</Button>
<div className="header-actions floating-actions">
<Button
className="mobile-menu-btn"
variant="ghost"
size="sm"
onClick={() => setSidebarOpen((prev) => !prev)}
title={t('sidebar.toggle_expand', { defaultValue: 'Open navigation' })}
aria-label={t('sidebar.toggle_expand', { defaultValue: 'Open navigation' })}
>
{headerIcons.menu}
</Button>
<Button
variant="ghost"
size="sm"
onClick={handleRefreshAll}
title={t('header.refresh_all')}
>
{headerIcons.refresh}
</Button>
<div className={`language-menu ${languageMenuOpen ? 'open' : ''}`} ref={languageMenuRef}>
<Button
variant="ghost"
size="sm"
onClick={handleRefreshAll}
title={t('header.refresh_all')}
onClick={toggleLanguageMenu}
title={t('language.switch')}
aria-label={t('language.switch')}
aria-haspopup="menu"
aria-expanded={languageMenuOpen}
>
{headerIcons.refresh}
{headerIcons.language}
</Button>
<div
className={`language-menu ${languageMenuOpen ? 'open' : ''}`}
ref={languageMenuRef}
>
<Button
variant="ghost"
size="sm"
onClick={toggleLanguageMenu}
title={t('language.switch')}
{languageMenuOpen && (
<div
className="notification entering language-menu-popover"
role="menu"
aria-label={t('language.switch')}
aria-haspopup="menu"
aria-expanded={languageMenuOpen}
>
{headerIcons.language}
</Button>
{languageMenuOpen && (
<div
className="notification entering language-menu-popover"
role="menu"
aria-label={t('language.switch')}
>
{LANGUAGE_ORDER.map((lang) => (
<button
key={lang}
type="button"
className={`language-menu-option ${language === lang ? 'active' : ''}`}
onClick={() => handleLanguageSelect(lang)}
role="menuitemradio"
aria-checked={language === lang}
>
<span>{t(LANGUAGE_LABEL_KEYS[lang])}</span>
{language === lang ? <span className="language-menu-check"></span> : null}
</button>
))}
</div>
)}
</div>
<div className={`theme-menu ${themeMenuOpen ? 'open' : ''}`} ref={themeMenuRef}>
<Button
variant="ghost"
size="sm"
onClick={toggleThemeMenu}
title={t('theme.switch')}
{LANGUAGE_ORDER.map((lang) => (
<button
key={lang}
type="button"
className={`language-menu-option ${language === lang ? 'active' : ''}`}
onClick={() => handleLanguageSelect(lang)}
role="menuitemradio"
aria-checked={language === lang}
>
<span>{t(LANGUAGE_LABEL_KEYS[lang])}</span>
{language === lang ? <span className="language-menu-check"></span> : null}
</button>
))}
</div>
)}
</div>
<div className={`theme-menu ${themeMenuOpen ? 'open' : ''}`} ref={themeMenuRef}>
<Button
variant="ghost"
size="sm"
onClick={toggleThemeMenu}
title={t('theme.switch')}
aria-label={t('theme.switch')}
aria-haspopup="menu"
aria-expanded={themeMenuOpen}
>
{theme === 'auto'
? headerIcons.autoTheme
: theme === 'dark'
? headerIcons.moon
: theme === 'white'
? headerIcons.whiteTheme
: headerIcons.sun}
</Button>
{themeMenuOpen && (
<div
className="notification entering theme-menu-popover"
role="menu"
aria-label={t('theme.switch')}
aria-haspopup="menu"
aria-expanded={themeMenuOpen}
>
{theme === 'auto'
? headerIcons.autoTheme
: theme === 'dark'
? headerIcons.moon
: theme === 'white'
? headerIcons.whiteTheme
: headerIcons.sun}
</Button>
{themeMenuOpen && (
<div
className="notification entering theme-menu-popover"
role="menu"
aria-label={t('theme.switch')}
>
{THEME_CARDS.map((tc) => (
<button
key={tc.key}
type="button"
className={`theme-card ${theme === tc.key ? 'active' : ''}`}
onClick={() => handleThemeSelect(tc.key)}
role="menuitemradio"
aria-checked={theme === tc.key}
{THEME_CARDS.map((tc) => (
<button
key={tc.key}
type="button"
className={`theme-card ${theme === tc.key ? 'active' : ''}`}
onClick={() => handleThemeSelect(tc.key)}
role="menuitemradio"
aria-checked={theme === tc.key}
>
<div
className="theme-card-preview"
style={{
background: tc.colors.bg,
border: `1px solid ${tc.colors.border}`,
}}
>
<div
className="theme-card-preview"
className="theme-card-header"
style={{
background: tc.colors.bg,
border: `1px solid ${tc.colors.border}`,
background: tc.colors.card,
borderBottom: `1px solid ${tc.colors.border}`,
}}
>
/>
<div className="theme-card-body">
<div
className="theme-card-header"
className="theme-card-sidebar"
style={{
background: tc.colors.card,
borderBottom: `1px solid ${tc.colors.border}`,
borderRight: `1px solid ${tc.colors.border}`,
}}
/>
<div className="theme-card-body">
<div className="theme-card-content" style={{ background: tc.colors.bg }}>
<div
className="theme-card-sidebar"
style={{
background: tc.colors.card,
borderRight: `1px solid ${tc.colors.border}`,
}}
className="theme-card-line"
style={{ background: tc.colors.textMuted }}
/>
<div
className="theme-card-line short"
style={{ background: tc.colors.textMuted }}
/>
<div className="theme-card-content" style={{ background: tc.colors.bg }}>
<div
className="theme-card-line"
style={{ background: tc.colors.textMuted }}
/>
<div
className="theme-card-line short"
style={{ background: tc.colors.textMuted }}
/>
</div>
</div>
</div>
<span className="theme-card-label">{t(tc.labelKey)}</span>
</button>
))}
</div>
)}
</div>
<Button variant="ghost" size="sm" onClick={logout} title={t('header.logout')}>
{headerIcons.logout}
</Button>
</div>
<span className="theme-card-label">{t(tc.labelKey)}</span>
</button>
))}
</div>
)}
</div>
<Button variant="ghost" size="sm" onClick={logout} title={t('header.logout')}>
{headerIcons.logout}
</Button>
</div>
</header>
@@ -694,6 +637,11 @@ export function MainLayout() {
<aside
className={`sidebar ${sidebarOpen ? 'open' : ''} ${sidebarCollapsed ? 'collapsed' : ''}`}
>
<div className="sidebar-brand" title={fullBrandName}>
<img src={INLINE_LOGO_JPEG} alt="CPAMC logo" className="sidebar-brand-logo" />
{showSidebarLabels && <span className="sidebar-brand-title">{abbrBrandName}</span>}
</div>
<div className="nav-section">
{navItems.map((item) => (
<NavLink
@@ -701,10 +649,10 @@ export function MainLayout() {
to={item.path}
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
onClick={() => setSidebarOpen(false)}
title={sidebarCollapsed ? item.label : undefined}
title={showSidebarLabels ? undefined : item.label}
>
<span className="nav-icon">{item.icon}</span>
{!sidebarCollapsed && <span className="nav-label">{item.label}</span>}
{showSidebarLabels && <span className="nav-label">{item.label}</span>}
</NavLink>
))}
</div>
+40 -19
View File
@@ -20,6 +20,8 @@
pointer-events: none;
z-index: 0;
overflow: hidden;
opacity: 0.42;
filter: blur(16px);
}
.orb1 {
@@ -29,7 +31,7 @@
border-radius: 50%;
background: radial-gradient(
circle,
color-mix(in srgb, var(--primary-color) 8%, transparent),
color-mix(in srgb, var(--primary-color) 6%, transparent),
transparent 70%
);
top: -140px;
@@ -44,7 +46,7 @@
border-radius: 50%;
background: radial-gradient(
circle,
color-mix(in srgb, var(--success-color) 6%, transparent),
color-mix(in srgb, var(--success-color) 4%, transparent),
transparent 70%
);
bottom: 18%;
@@ -97,10 +99,10 @@
top: 50%;
left: $spacing-xl;
transform: translateY(-50%);
font-size: clamp(64px, 12vw, 120px);
font-size: 104px;
font-weight: 900;
line-height: 1;
letter-spacing: -0.04em;
letter-spacing: 0;
text-transform: uppercase;
color: var(--text-primary);
opacity: 0.04;
@@ -110,7 +112,7 @@
animation: watermarkEnter 0.8s ease-out 0.1s both;
@media (max-width: $breakpoint-mobile) {
font-size: clamp(48px, 14vw, 80px);
font-size: 58px;
left: $spacing-lg;
}
}
@@ -137,7 +139,7 @@
.heroGreeting {
font-size: 13px;
font-weight: 600;
letter-spacing: 0.08em;
letter-spacing: 0;
text-transform: uppercase;
color: var(--primary-color);
animation: fadeSlideUp 0.5s ease-out 0.1s both;
@@ -145,12 +147,16 @@
.heroTitle {
margin: 0;
font-size: clamp(32px, 5vw, 48px);
font-size: 44px;
font-weight: 800;
line-height: 1.1;
letter-spacing: -0.03em;
letter-spacing: 0;
color: var(--text-primary);
animation: fadeSlideUp 0.5s ease-out 0.2s both;
@media (max-width: $breakpoint-mobile) {
font-size: 34px;
}
}
.heroCaring {
@@ -269,7 +275,7 @@
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
letter-spacing: 0;
color: var(--text-tertiary);
margin: 0 0 $spacing-md;
}
@@ -294,9 +300,19 @@
flex-direction: column;
gap: $spacing-md;
padding: $spacing-lg;
background: var(--bg-primary);
border: 1px solid var(--border-color);
background: linear-gradient(
145deg,
color-mix(in srgb, var(--bg-primary) 86%, transparent),
color-mix(in srgb, var(--bg-secondary) 72%, transparent)
);
border: 1px solid color-mix(in srgb, var(--border-color) 66%, transparent);
border-radius: $radius-lg;
--glass-blur: 12px;
backdrop-filter: var(--glass-backdrop-filter);
-webkit-backdrop-filter: var(--glass-backdrop-filter);
box-shadow:
0 18px 42px rgb(0 0 0 / 0.12),
inset 0 1px 0 rgb(255 255 255 / 0.03);
text-decoration: none;
transition:
border-color $transition-fast,
@@ -305,9 +321,11 @@
animation: cardEnter 0.4s ease-out both;
&:hover {
border-color: var(--primary-color);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
transform: translateY(-3px);
border-color: color-mix(in srgb, var(--border-hover) 82%, transparent);
box-shadow:
0 22px 48px rgb(0 0 0 / 0.16),
inset 0 1px 0 rgb(255 255 255 / 0.04);
transform: translateY(-2px);
}
}
@@ -317,8 +335,8 @@
justify-content: center;
background: linear-gradient(
160deg,
color-mix(in srgb, var(--primary-color) 6%, var(--bg-primary)),
var(--bg-primary)
color-mix(in srgb, var(--primary-color) 8%, var(--bg-primary)),
color-mix(in srgb, var(--bg-primary) 84%, transparent)
);
.bentoValue {
@@ -408,14 +426,17 @@
align-items: center;
gap: $spacing-sm;
padding: 6px 14px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
background: color-mix(in srgb, var(--bg-primary) 68%, transparent);
border: 1px solid color-mix(in srgb, var(--border-color) 68%, transparent);
border-radius: $radius-full;
--glass-blur: 10px;
backdrop-filter: var(--glass-backdrop-filter);
-webkit-backdrop-filter: var(--glass-backdrop-filter);
font-size: 13px;
transition: border-color $transition-fast;
&:hover {
border-color: var(--border-hover);
border-color: color-mix(in srgb, var(--border-hover) 82%, transparent);
}
}
+525 -440
View File
File diff suppressed because it is too large Load Diff