chore: add frontend/src/components/layout/Sidebar.tsx
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
import React from 'react';
|
||||
import { NavLink, useLocation } from 'react-router-dom';
|
||||
import { useUiStore } from '../../stores';
|
||||
|
||||
const Sidebar: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const { sidebarCollapsed, toggleSidebar } = useUiStore();
|
||||
|
||||
const navItems = [
|
||||
{ path: '/', label: '首页', icon: HomeIcon },
|
||||
{ path: '/record', label: '记账', icon: RecordIcon },
|
||||
{ path: '/budget', label: '预算', icon: BudgetIcon },
|
||||
{ path: '/statistics', label: '统计', icon: StatisticsIcon },
|
||||
];
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={`sidebar ${sidebarCollapsed ? 'collapsed' : ''}`}
|
||||
role="navigation"
|
||||
aria-label="侧边导航"
|
||||
>
|
||||
<div className="sidebar-logo">
|
||||
<div className="sidebar-logo-content">
|
||||
<LogoIcon />
|
||||
{!sidebarCollapsed && <span>个人记账</span>}
|
||||
</div>
|
||||
<div
|
||||
className="sidebar-toggle-wrapper"
|
||||
data-tooltip={sidebarCollapsed ? '展开侧边栏' : '收起侧边栏'}
|
||||
>
|
||||
<button
|
||||
className="sidebar-toggle"
|
||||
type="button"
|
||||
aria-label="折叠/展开侧边栏"
|
||||
onClick={toggleSidebar}
|
||||
>
|
||||
<ChevronIcon />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="sidebar-nav">
|
||||
{navItems.map((item) => (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={({ isActive }) =>
|
||||
`sidebar-nav-item ${isActive ? 'active' : ''}`
|
||||
}
|
||||
aria-current={location.pathname === item.path ? 'page' : undefined}
|
||||
>
|
||||
<item.icon />
|
||||
{!sidebarCollapsed && <span>{item.label}</span>}
|
||||
</NavLink>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<style>{`
|
||||
:root {
|
||||
--primary: #0052ff;
|
||||
--primary-hover: #3761ff;
|
||||
--primary-light: rgba(0, 82, 255, 0.1);
|
||||
--surface: #ffffff;
|
||||
--border: #e5e5e5;
|
||||
--text-primary: #1a1a1a;
|
||||
--text-secondary: #737373;
|
||||
--radius-md: 8px;
|
||||
--space-1: 4px;
|
||||
--space-2: 8px;
|
||||
--space-3: 12px;
|
||||
--space-4: 16px;
|
||||
--space-6: 24px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 240px;
|
||||
background: var(--surface);
|
||||
border-right: 1px solid var(--border);
|
||||
padding: var(--space-6);
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
overflow-y: auto;
|
||||
z-index: 100;
|
||||
transition: width 0.3s ease, padding 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar.collapsed {
|
||||
width: 64px;
|
||||
padding: var(--space-6) var(--space-2);
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: var(--space-6);
|
||||
padding-bottom: var(--space-4);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.sidebar-logo-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.sidebar-nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
color: var(--text-secondary);
|
||||
text-decoration: none;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.sidebar-nav-item:hover {
|
||||
background: var(--primary-light);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.sidebar-nav-item.active {
|
||||
background: var(--primary-light);
|
||||
color: var(--primary);
|
||||
}
|
||||
|
||||
.sidebar.collapsed .sidebar-nav-item {
|
||||
justify-content: center;
|
||||
padding: var(--space-3);
|
||||
}
|
||||
|
||||
.sidebar.collapsed .sidebar-nav-item span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sidebar-toggle {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s ease;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.sidebar-toggle:hover {
|
||||
background: var(--primary-light);
|
||||
color: var(--primary);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
|
||||
.sidebar.collapsed .sidebar-toggle svg {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.sidebar-toggle svg {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.sidebar-toggle-wrapper {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.sidebar-toggle-wrapper::after {
|
||||
content: attr(data-tooltip);
|
||||
position: absolute;
|
||||
left: calc(100% + 12px);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.15s ease;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sidebar-toggle-wrapper::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: calc(100% + 4px);
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
border: 6px solid transparent;
|
||||
border-right-color: #1a1a1a;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: all 0.15s ease;
|
||||
z-index: 1000;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sidebar-toggle-wrapper:hover::after,
|
||||
.sidebar-toggle-wrapper:hover::before {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.sidebar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</aside>
|
||||
);
|
||||
};
|
||||
|
||||
// SVG Icons as React components
|
||||
function LogoIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="28" height="28" style={{ color: '#0052ff' }}>
|
||||
<path d="M21 12V7H5a2 2 0 0 1 0-4h14v4"/>
|
||||
<path d="M3 5v14a2 2 0 0 0 2 2h16v-5"/>
|
||||
<path d="M18 12a2 2 0 0 0 0 4h4v-4h-4z"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function ChevronIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" width="18" height="18">
|
||||
<polyline points="15 18 9 12 15 6"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function HomeIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="20" height="20">
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
||||
<polyline points="9 22 9 12 15 12 15 22"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function RecordIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="20" height="20">
|
||||
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function BudgetIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="20" height="20">
|
||||
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
|
||||
<line x1="8" y1="21" x2="16" y2="21"/>
|
||||
<line x1="12" y1="17" x2="12" y2="21"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function StatisticsIcon() {
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" width="20" height="20">
|
||||
<line x1="18" y1="20" x2="18" y2="10"/>
|
||||
<line x1="12" y1="20" x2="12" y2="4"/>
|
||||
<line x1="6" y1="20" x2="6" y2="14"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default Sidebar;
|
||||
Reference in New Issue
Block a user