You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

145 lines
5.5 KiB

import { NavLink } from 'react-router-dom'
import {
LayoutDashboard, Users, LogOut, Settings, FolderOpen,
Shield, Menu as MenuIcon, Building2, User, ChevronDown,
Cpu, Key, MessageSquare, BarChart3, Wallet,
} from 'lucide-react'
import type { LucideIcon } from 'lucide-react'
import { useAuth } from '@/contexts/AuthContext'
import { useState, useRef, useEffect } from 'react'
import type { MenuItem } from '@/types'
const iconMap: Record<string, LucideIcon> = {
User, LayoutDashboard, Users, FolderOpen, Shield,
Menu: MenuIcon, Building2, Settings,
Cpu, Key, MessageSquare, BarChart3, Wallet,
}
function getIcon(iconName: string): LucideIcon {
return iconMap[iconName] || LayoutDashboard
}
export function Sidebar() {
const { user, logout, userMenus, currentOrg, userOrgs, switchOrg } = useAuth()
const [orgDropdownOpen, setOrgDropdownOpen] = useState(false)
const dropdownRef = useRef<HTMLDivElement>(null)
useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) {
setOrgDropdownOpen(false)
}
}
document.addEventListener('mousedown', handleClickOutside)
return () => document.removeEventListener('mousedown', handleClickOutside)
}, [])
const handleSwitchOrg = async (orgId: number) => {
await switchOrg(orgId)
setOrgDropdownOpen(false)
}
const menuItems: MenuItem[] = userMenus.length > 0 ? userMenus : []
return (
<aside className="w-64 h-screen bg-sidebar-bg backdrop-blur-xl border-r border-border flex flex-col fixed left-0 top-0 z-40">
{/* Logo */}
<div className="h-16 flex items-center px-6 border-b border-border shrink-0">
<div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-lg bg-gradient-to-br from-sky-500 to-blue-600 flex items-center justify-center glow-primary">
<span className="text-lg font-bold text-white font-display">B</span>
</div>
<div>
<h1 className="text-lg font-bold text-foreground font-display leading-tight">BASE</h1>
<p className="text-xs text-text-muted font-body"></p>
</div>
</div>
</div>
{/* Org Switcher */}
{userOrgs.length > 0 && (
<div className="px-4 pt-4" ref={dropdownRef}>
<button
onClick={() => setOrgDropdownOpen(!orgDropdownOpen)}
className="w-full flex items-center justify-between px-3 py-2 rounded-lg bg-muted border border-border-secondary text-sm text-text-secondary hover:bg-surface-hover transition-colors"
>
<span className="truncate">{currentOrg?.name || '选择机构'}</span>
<ChevronDown className={cn('h-4 w-4 transition-transform', orgDropdownOpen && 'rotate-180')} />
</button>
{orgDropdownOpen && (
<div className="mt-1 py-1 rounded-lg bg-card border border-border-secondary shadow-xl">
{userOrgs.map((org) => (
<button
key={org.orgId}
onClick={() => handleSwitchOrg(org.orgId)}
className={cn(
'w-full text-left px-3 py-2 text-sm transition-colors',
currentOrg?.id === org.orgId
? 'text-sky-400 bg-sky-500/10'
: 'text-text-secondary hover:text-foreground hover:bg-surface-hover'
)}
>
<div className="truncate">{org.orgName}</div>
<div className="text-xs text-text-muted">{org.roleName}</div>
</button>
))}
</div>
)}
</div>
)}
{/* Navigation */}
<nav className="flex-1 p-4 space-y-1 overflow-y-auto">
{menuItems.map((item) => {
const Icon = getIcon(item.icon)
return (
<NavLink
key={item.path}
to={item.path}
className={({ isActive }) =>
cn(
'flex items-center gap-3 px-4 py-3 rounded-lg transition-all duration-200',
'font-body text-sm font-medium',
isActive
? 'bg-gradient-to-r from-sky-500/20 to-blue-600/20 text-sky-400 border border-sky-500/30'
: 'text-text-secondary hover:bg-surface-hover hover:text-foreground'
)
}
>
<Icon className="h-5 w-5" />
{item.name}
</NavLink>
)
})}
</nav>
{/* User Info */}
<div className="h-14 flex items-center gap-3 px-5 border-t border-border shrink-0">
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-amber-500 to-orange-600 flex items-center justify-center">
<span className="text-sm font-bold text-foreground font-display">
{user?.username?.[0]?.toUpperCase() || 'U'}
</span>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground font-body truncate">
{user?.username || 'User'}
</p>
<p className="text-xs text-text-muted font-body truncate">
{user?.role || ''}
</p>
</div>
<button
onClick={logout}
className="p-1.5 text-text-muted hover:text-red-400 transition-colors"
title="退出登录"
>
<LogOut className="h-4 w-4" />
</button>
</div>
</aside>
)
}
function cn(...classes: (string | undefined | false | null)[]) {
return classes.filter(Boolean).join(' ')
}