import { useState, useEffect, useCallback } from 'react'
import { NavLink, useLocation } from 'react-router-dom'
import { createPortal } from 'react-dom'
import { cn } from '@/lib/utils'
import { Icon } from '@/components/common/Icon'
import { Avatar } from '@/components/common/Avatar'
import { navItems, type NavItem } from './Sidebar'
interface MobileNavProps {
open: boolean
onClose: () => void
userName?: string
userAvatar?: string
isAdmin?: boolean
onSettingsClick?: () => void
onAdminClick?: () => void
onLogout?: () => void
}
export function MobileNav({
open,
onClose,
userName = '用户',
userAvatar,
isAdmin = false,
onSettingsClick,
onAdminClick,
onLogout,
}: MobileNavProps) {
const location = useLocation()
const [expandedItems, setExpandedItems] = useState<Set<String>>(() => {
const initial = new Set<String>()
for (const item of navItems) {
if (item.children?.some((c) => location.pathname.startsWith(c.to))) {
initial.add(item.to)
}
}
return initial
})
const toggleExpand = (key: string) => {
setExpandedItems((prev) => {
const next = new Set(prev)
if (next.has(key)) next.delete(key)
else next.add(key)
return next
})
}
// 阻止背景滚动
useEffect(() => {
if (open) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = ''
}
return () => { document.body.style.overflow = '' }
}, [open])
// ESC 关闭
useEffect(() => {
if (!open) return
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose()
}
document.addEventListener('keydown', handler)
return () => document.removeEventListener('keydown', handler)
}, [open, onClose])
const handleItemClick = useCallback(() => {
onClose()
}, [onClose])
if (!open) return null
return createPortal(
<div className="fixed inset-0 z-50 flex">
{/* 遮罩 */}
<div
className="absolute inset-0 bg-black/40 animate-fade-in"
onClick={onClose}
/>
{/* 面板:从左侧滑入 */}
<aside
className={cn(
'relative w-[280px] max-w-[80vw] h-full',
'bg-[var(--color-surface-0)] dark:bg-[var(--color-surface-1)]',
'shadow-2xl flex flex-col',
'animate-slide-in-right',
)}
>
{/* Header */}
<div className="flex items-center justify-between px-4 py-4 border-b border-[var(--color-border-subtle)]">
<div className="flex items-center gap-2.5">
<Avatar type="user" src={userAvatar} letter={userName.charAt(0).toUpperCase()} size="sm" />
<span className="text-sm font-medium text-[var(--color-text-primary)]">{userName}</span>
</div>
<button
onClick={onClose}
className="w-8 h-8 flex items-center justify-center rounded-lg text-[var(--color-text-tertiary)] hover:text-[var(--color-text-primary)] hover:bg-[var(--color-surface-2)] transition-colors"
>
<Icon name="close" variant="outlined" size="base" />
</button>
</div>
{/* Navigation */}
<nav className="flex-1 overflow-y-auto custom-scrollbar px-3 py-3">
{navItems.map((item) => (
<MobileNavItem
key={item.to}
item={item}
isExpanded={expandedItems.has(item.to)}
onToggleExpand={toggleExpand}
onClick={handleItemClick}
/>
))}
</nav>
{/* Footer actions */}
<div className="border-t border-[var(--color-border-subtle)] px-3 py-2 space-y-0.5">
<button
onClick={() => { onClose(); onSettingsClick?.() }}
className="w-full flex items-center gap-2.5 px-3 py-2.5 rounded-lg text-sm text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-2)] hover:text-[var(--color-text-primary)] transition-colors"
>
<Icon name="settings" size="base" className="text-[var(--color-text-tertiary)]" />
系统设置
</button>
{isAdmin && (
<button
onClick={() => { onClose(); onAdminClick?.() }}
className="w-full flex items-center gap-2.5 px-3 py-2.5 rounded-lg text-sm text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-2)] hover:text-[var(--color-text-primary)] transition-colors"
>
<Icon name="admin_panel_settings" size="base" className="text-[var(--color-text-tertiary)]" />
后台管理
</button>
)}
<button
onClick={() => { onClose(); onLogout?.() }}
className="w-full flex items-center gap-2.5 px-3 py-2.5 rounded-lg text-sm text-red-500 hover:bg-red-50 dark:hover:bg-red-900/20 transition-colors"
>
<Icon name="logout" size="base" className="text-red-400" />
退出登录
</button>
</div>
</aside>
</div>,
document.body,
)
}
// ── 手机端导航项组件 ──
function MobileNavItem({
item,
isExpanded,
onToggleExpand,
onClick,
}: {
item: NavItem
isExpanded: boolean
onToggleExpand: (key: string) => void
onClick: () => void
}) {
const location = useLocation()
const hasChildren = item.children && item.children.length > 0
const isParentActive = hasChildren
? item.children!.some((c) => location.pathname === c.to || location.pathname.startsWith(c.to + '/'))
: false
if (hasChildren) {
return (
<div className="mb-0.5">
<button
onClick={() => onToggleExpand(item.to)}
className={cn(
'relative w-full flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors',
isParentActive
? 'bg-[image:var(--gradient-brand-soft)] text-[color:var(--color-brand-700)] dark:text-[color:var(--color-brand-200)]'
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-2)] hover:text-[var(--color-text-primary)]',
)}
>
<Icon name={item.icon} size="base" />
<span className="flex-1 text-left">{item.label}</span>
<Icon
name={isExpanded ? 'expand_less' : 'expand_more'}
size="sm"
className="text-[var(--color-text-tertiary)] flex-shrink-0"
/>
</button>
<div
className={cn(
'grid transition-all duration-200 ease-out',
isExpanded ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0 pointer-events-none',
)}
>
<div className={isExpanded ? 'overflow-visible' : 'overflow-hidden'}>
<div className="ml-9 mt-0.5 space-y-0.5 pb-0.5">
{item.children!.map((child) => (
<NavLink
key={child.to}
to={child.to}
end
onClick={onClick}
className={({ isActive }) =>
cn(
'block px-3 py-2 rounded-lg text-sm transition-colors',
isActive
? 'text-[color:var(--color-brand-600)] dark:text-[color:var(--color-brand-400)] font-medium bg-[color:var(--color-brand-50)]/50 dark:bg-[color:var(--color-brand-900)]/20'
: 'text-[var(--color-text-tertiary)] hover:text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-2)]',
)
}
>
{child.label}
</NavLink>
))}
</div>
</div>
</div>
</div>
)
}
return (
<NavLink
to={item.to}
end={item.to === '/'}
onClick={onClick}
className={({ isActive }) =>
cn(
'relative flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors mb-0.5',
isActive
? 'bg-[image:var(--gradient-brand-soft)] text-[color:var(--color-brand-700)] dark:text-[color:var(--color-brand-200)]'
: 'text-[var(--color-text-secondary)] hover:bg-[var(--color-surface-2)] hover:text-[var(--color-text-primary)]',
)
}
>
<Icon name={item.icon} size="base" />
<span>{item.label}</span>
</NavLink>
)
}
|