Files
sundynix-site/admin/src/components/admin-sidebar.tsx
T
Blizzard 6334dccf4a feat(admin): 侧边栏布局 + 仪表盘 + 全局隐藏滚动条
- 全局隐藏滚动条(web + admin),保留正常滚动
- admin 顶栏改侧边栏导航(仪表盘/文章),响应式移动端抽屉
- 路由重构:首页→仪表盘,文章列表移到 /posts
- 仪表盘页:文章总数/已发布/草稿统计卡片 + 最近更新列表
- 后端 GET /api/admin/stats

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 17:35:05 +08:00

110 lines
3.3 KiB
TypeScript

import { Link, NavLink, useNavigate } from 'react-router-dom'
import { FileText, LayoutDashboard, LogOut, Moon, Sun, X } from 'lucide-react'
import { logout } from '@/api/auth'
import { useTheme } from '@/components/theme-provider'
import { LogoMark } from '@/components/logo'
import { cn } from '@/lib/utils'
const NAV = [
{ to: '/', label: '仪表盘', icon: LayoutDashboard, end: true },
{ to: '/posts', label: '文章', icon: FileText, end: false },
]
interface Props {
open: boolean
onClose: () => void
}
export function AdminSidebar({ open, onClose }: Props) {
const navigate = useNavigate()
const { resolved, setTheme } = useTheme()
const itemCls = ({ isActive }: { isActive: boolean }) =>
cn(
'flex items-center gap-3 rounded-lg px-3 py-2.5 text-[14px] transition-colors',
isActive
? 'bg-accent-soft font-medium text-accent-ink'
: 'text-ink-2 hover:bg-ground hover:text-ink',
)
const bottomBtnCls =
'flex w-full items-center gap-3 rounded-lg px-3 py-2.5 text-[14px] text-ink-2 transition-colors hover:bg-ground hover:text-ink'
return (
<>
{open && (
<div
className="fixed inset-0 z-40 bg-black/40 md:hidden"
onClick={onClose}
aria-hidden="true"
/>
)}
<aside
className={cn(
'fixed inset-y-0 left-0 z-50 flex w-[220px] flex-col border-r border-hairline bg-surface transition-transform md:translate-x-0',
open ? 'translate-x-0' : '-translate-x-full',
)}
>
<div className="flex h-16 items-center justify-between border-b border-hairline px-5">
<Link
to="/"
onClick={onClose}
className="flex items-center gap-2.5 font-mono text-[14px] font-semibold"
>
<LogoMark size={24} />
sundynix <em className="not-italic text-accent">admin</em>
</Link>
<button
type="button"
onClick={onClose}
aria-label="关闭菜单"
className="text-ink-2 md:hidden"
>
<X className="size-5" />
</button>
</div>
<nav className="flex-1 space-y-1 px-3 py-4">
{NAV.map((item) => (
<NavLink
key={item.to}
to={item.to}
end={item.end}
onClick={onClose}
className={itemCls}
>
<item.icon className="size-[18px]" />
{item.label}
</NavLink>
))}
</nav>
<div className="space-y-1 border-t border-hairline p-3">
<button
type="button"
onClick={() => setTheme(resolved === 'dark' ? 'light' : 'dark')}
className={bottomBtnCls}
>
{resolved === 'dark' ? (
<Sun className="size-[18px]" />
) : (
<Moon className="size-[18px]" />
)}
{resolved === 'dark' ? '浅色模式' : '深色模式'}
</button>
<button
type="button"
onClick={() => {
logout()
navigate('/login')
}}
className={bottomBtnCls}
>
<LogOut className="size-[18px]" /> 退
</button>
</div>
</aside>
</>
)
}