feat: refactor MainLayout component to implement sidebar collapse functionality, enhance navigation item display, and improve layout responsiveness

This commit is contained in:
Supra4E8C
2025-12-09 00:59:40 +08:00
parent b6765b074e
commit e417d3c771
2 changed files with 51 additions and 36 deletions

View File

@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { NavLink, Outlet, useNavigate } from 'react-router-dom'; import { NavLink, Outlet } from 'react-router-dom';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Button } from '@/components/ui/Button'; import { Button } from '@/components/ui/Button';
import { useAuthStore, useConfigStore, useLanguageStore, useNotificationStore, useThemeStore } from '@/stores'; import { useAuthStore, useConfigStore, useLanguageStore, useNotificationStore, useThemeStore } from '@/stores';
@@ -34,15 +34,12 @@ const compareVersions = (latest?: string | null, current?: string | null) => {
export function MainLayout() { export function MainLayout() {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const navigate = useNavigate();
const { showNotification } = useNotificationStore(); const { showNotification } = useNotificationStore();
const apiBase = useAuthStore((state) => state.apiBase); const apiBase = useAuthStore((state) => state.apiBase);
const serverVersion = useAuthStore((state) => state.serverVersion); const serverVersion = useAuthStore((state) => state.serverVersion);
const serverBuildDate = useAuthStore((state) => state.serverBuildDate); const serverBuildDate = useAuthStore((state) => state.serverBuildDate);
const connectionStatus = useAuthStore((state) => state.connectionStatus); const connectionStatus = useAuthStore((state) => state.connectionStatus);
const checkAuth = useAuthStore((state) => state.checkAuth);
const logout = useAuthStore((state) => state.logout);
const config = useConfigStore((state) => state.config); const config = useConfigStore((state) => state.config);
const fetchConfig = useConfigStore((state) => state.fetchConfig); const fetchConfig = useConfigStore((state) => state.fetchConfig);
@@ -54,7 +51,7 @@ export function MainLayout() {
const toggleLanguage = useLanguageStore((state) => state.toggleLanguage); const toggleLanguage = useLanguageStore((state) => state.toggleLanguage);
const [sidebarOpen, setSidebarOpen] = useState(false); const [sidebarOpen, setSidebarOpen] = useState(false);
const [checkingConnection, setCheckingConnection] = useState(false); const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [checkingVersion, setCheckingVersion] = useState(false); const [checkingVersion, setCheckingVersion] = useState(false);
const isLocal = useMemo(() => isLocalhost(window.location.hostname), []); const isLocal = useMemo(() => isLocalhost(window.location.hostname), []);
@@ -96,22 +93,6 @@ export function MainLayout() {
} }
}; };
const handleCheckConnection = async () => {
setCheckingConnection(true);
try {
const ok = await checkAuth();
if (ok) {
showNotification(t('common.connected_status'), 'success');
} else {
showNotification(t('common.disconnected_status'), 'warning');
}
} catch (error: any) {
showNotification(`${t('notification.login_failed')}: ${error?.message || ''}`, 'error');
} finally {
setCheckingConnection(false);
}
};
const handleVersionCheck = async () => { const handleVersionCheck = async () => {
setCheckingVersion(true); setCheckingVersion(true);
try { try {
@@ -141,15 +122,10 @@ export function MainLayout() {
} }
}; };
const handleLogout = () => {
logout();
navigate('/login', { replace: true });
};
return ( return (
<div className="app-shell"> <div className="app-shell">
<aside className={`sidebar ${sidebarOpen ? 'open' : ''}`}> <aside className={`sidebar ${sidebarOpen ? 'open' : ''} ${sidebarCollapsed ? 'collapsed' : ''}`}>
<div className="brand">{t('title.abbr')}</div> <div className="brand">{sidebarCollapsed ? t('title.abbr').charAt(0) : t('title.abbr')}</div>
<div className="nav-section"> <div className="nav-section">
{navItems.map((item) => ( {navItems.map((item) => (
<NavLink <NavLink
@@ -157,11 +133,19 @@ export function MainLayout() {
to={item.path} to={item.path}
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`} className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
onClick={() => setSidebarOpen(false)} onClick={() => setSidebarOpen(false)}
title={sidebarCollapsed ? item.label : undefined}
> >
{item.label} {sidebarCollapsed ? item.label.charAt(0) : item.label}
</NavLink> </NavLink>
))} ))}
</div> </div>
<button
className="sidebar-toggle"
onClick={() => setSidebarCollapsed((prev) => !prev)}
title={sidebarCollapsed ? t('sidebar.expand', { defaultValue: '展开' }) : t('sidebar.collapse', { defaultValue: '收起' })}
>
{sidebarCollapsed ? '»' : '«'}
</button>
</aside> </aside>
<div className="content"> <div className="content">
@@ -185,9 +169,6 @@ export function MainLayout() {
</div> </div>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}> <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Button variant="secondary" size="sm" onClick={handleCheckConnection} loading={checkingConnection}>
{t('header.check_connection')}
</Button>
<Button variant="secondary" size="sm" onClick={handleRefreshAll}> <Button variant="secondary" size="sm" onClick={handleRefreshAll}>
{t('header.refresh_all')} {t('header.refresh_all')}
</Button> </Button>
@@ -200,9 +181,6 @@ export function MainLayout() {
<Button variant="ghost" size="sm" onClick={toggleTheme}> <Button variant="ghost" size="sm" onClick={toggleTheme}>
{t('theme.switch')}: {theme === 'dark' ? t('theme.dark') : t('theme.light')} {t('theme.switch')}: {theme === 'dark' ? t('theme.dark') : t('theme.light')}
</Button> </Button>
<Button variant="danger" size="sm" onClick={handleLogout}>
{t('header.logout')}
</Button>
</div> </div>
</header> </header>

View File

@@ -15,7 +15,25 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: $spacing-lg; gap: $spacing-lg;
transition: transform $transition-normal; transition: width $transition-normal, transform $transition-normal;
position: sticky;
top: 0;
height: 100vh;
overflow-y: auto;
&.collapsed {
width: 60px;
padding: $spacing-lg $spacing-sm;
.brand {
text-align: center;
}
.nav-item {
justify-content: center;
padding: 10px;
}
}
.brand { .brand {
font-weight: 800; font-weight: 800;
@@ -27,6 +45,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: $spacing-sm; gap: $spacing-sm;
flex: 1;
} }
.nav-item { .nav-item {
@@ -51,6 +70,24 @@
} }
} }
.sidebar-toggle {
margin-top: auto;
padding: $spacing-sm;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: $radius-md;
color: var(--text-secondary);
cursor: pointer;
font-size: 16px;
font-weight: bold;
transition: background $transition-fast, color $transition-fast;
&:hover {
background: var(--bg-tertiary, var(--border-color));
color: var(--text-primary);
}
}
@media (max-width: $breakpoint-mobile) { @media (max-width: $breakpoint-mobile) {
position: fixed; position: fixed;
z-index: $z-dropdown; z-index: $z-dropdown;