Files
one-api-workers/frontend/src/components/layout/Sidebar.tsx
T
dreamhunter2333andClaude Opus 4.5 4572e5fc9e feat: 使用 React + Vite + shadcn/ui 重构前端
- 使用 React 19 + TypeScript + Vite 7 重构整个前端
- 集成 shadcn/ui 组件库和 Tailwind CSS v4
- 使用 TanStack Query 管理服务端状态
- 使用 Zustand 管理客户端状态(认证)
- 使用 React Router v7 实现路由
- 实现浅粉色主题,支持深色模式
- 添加 API 拦截器和全局错误处理
- 优化 Toast 通知样式
- 管理页面按钮右对齐
- Token 显示脱敏处理

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-20 13:24:45 +08:00

246 lines
7.0 KiB
TypeScript

import { Link, useLocation } from 'react-router-dom'
import { cn } from '@/lib/utils'
import {
LayoutDashboard,
TestTube2,
Link as LinkIcon,
Key,
DollarSign,
Database,
Github,
Moon,
Sun,
LogIn,
LogOut,
} from 'lucide-react'
import { useAuthStore } from '@/store/auth'
import { useState } from 'react'
import { Button } from '../ui/button'
interface NavItem {
title: string
href: string
icon: React.ComponentType<{ className?: string }>
adminOnly?: boolean
}
const navItems: NavItem[] = [
{
title: '仪表盘',
href: '/',
icon: LayoutDashboard,
},
{
title: 'API 测试',
href: '/api-test',
icon: TestTube2,
},
{
title: '频道管理',
href: '/channels',
icon: LinkIcon,
adminOnly: true,
},
{
title: '令牌管理',
href: '/tokens',
icon: Key,
adminOnly: true,
},
{
title: '定价管理',
href: '/pricing',
icon: DollarSign,
adminOnly: true,
},
{
title: '数据库',
href: '/database',
icon: Database,
adminOnly: true,
},
]
interface SidebarProps {
onAuthClick: () => void
}
export function Sidebar({ onAuthClick }: SidebarProps) {
const location = useLocation()
const { isAuthenticated, logout } = useAuthStore()
// Initialize theme from localStorage on mount
const [theme, setTheme] = useState<'light' | 'dark'>(() => {
if (typeof window !== 'undefined') {
const savedTheme = localStorage.getItem('theme')
const isDark = savedTheme === 'dark' || (!savedTheme && document.documentElement.classList.contains('dark'))
if (isDark) {
document.documentElement.classList.add('dark')
}
return isDark ? 'dark' : 'light'
}
return 'light'
})
const toggleTheme = () => {
const newTheme = theme === 'light' ? 'dark' : 'light'
setTheme(newTheme)
localStorage.setItem('theme', newTheme)
if (newTheme === 'dark') {
document.documentElement.classList.add('dark')
} else {
document.documentElement.classList.remove('dark')
}
}
const handleAuthClick = () => {
if (isAuthenticated) {
logout()
} else {
onAuthClick()
}
}
return (
<aside className="w-64 border-r bg-card flex flex-col h-full">
{/* Header */}
<div className="p-6 border-b">
<div className="flex items-center gap-2">
<div className="text-2xl">🤖</div>
<span className="font-semibold text-lg">Awsl One API</span>
</div>
</div>
{/* Navigation */}
<nav className="flex-1 p-4 space-y-6 overflow-y-auto">
<div>
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2 px-3">
主菜单
</div>
<div className="space-y-1">
{navItems
.filter((item) => !item.adminOnly)
.map((item) => (
<Link
key={item.href}
to={item.href}
className={cn(
'flex items-center gap-3 px-3 py-2 rounded-md text-sm transition-colors',
location.pathname === item.href
? 'bg-primary text-primary-foreground'
: 'hover:bg-accent hover:text-accent-foreground'
)}
>
<item.icon className="h-4 w-4" />
<span>{item.title}</span>
</Link>
))}
</div>
</div>
{isAuthenticated && (
<>
<div>
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2 px-3">
管理
</div>
<div className="space-y-1">
{navItems
.filter((item) => item.adminOnly && item.href !== '/database')
.map((item) => (
<Link
key={item.href}
to={item.href}
className={cn(
'flex items-center gap-3 px-3 py-2 rounded-md text-sm transition-colors',
location.pathname === item.href
? 'bg-primary text-primary-foreground'
: 'hover:bg-accent hover:text-accent-foreground'
)}
>
<item.icon className="h-4 w-4" />
<span>{item.title}</span>
</Link>
))}
</div>
</div>
<div>
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-2 px-3">
系统
</div>
<div className="space-y-1">
{navItems
.filter((item) => item.href === '/database')
.map((item) => (
<Link
key={item.href}
to={item.href}
className={cn(
'flex items-center gap-3 px-3 py-2 rounded-md text-sm transition-colors',
location.pathname === item.href
? 'bg-primary text-primary-foreground'
: 'hover:bg-accent hover:text-accent-foreground'
)}
>
<item.icon className="h-4 w-4" />
<span>{item.title}</span>
</Link>
))}
</div>
</div>
</>
)}
</nav>
{/* Footer */}
<div className="p-4 border-t space-y-2">
<div className="flex items-center justify-between text-sm text-muted-foreground px-3 mb-3">
<a
href="https://github.com/dreamhunter2333/awsl-one-api"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-primary hover:underline"
>
<Github className="h-4 w-4" />
GitHub
</a>
<span>v1.0.0</span>
</div>
<Button
variant="outline"
size="sm"
className="w-full justify-start"
onClick={toggleTheme}
>
{theme === 'dark' ? (
<Sun className="h-4 w-4" />
) : (
<Moon className="h-4 w-4" />
)}
<span className="ml-2">切换主题</span>
</Button>
<Button
variant="outline"
size="sm"
className={cn(
'w-full justify-start',
isAuthenticated && 'text-destructive border-destructive hover:bg-destructive hover:text-destructive-foreground'
)}
onClick={handleAuthClick}
>
{isAuthenticated ? (
<LogOut className="h-4 w-4" />
) : (
<LogIn className="h-4 w-4" />
)}
<span className="ml-2">
{isAuthenticated ? '退出登录' : '管理员登录'}
</span>
</Button>
</div>
</aside>
)
}