feat: 炫酷的登录页
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
import { useRef, useEffect } from 'react'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { X, ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import { useTabsStore } from '@/store/tabs'
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
ContextMenu, ContextMenuContent, ContextMenuItem,
|
||||
ContextMenuSeparator, ContextMenuTrigger,
|
||||
} from '@/components/ui/context-menu'
|
||||
import type { SystemMenu } from '@/api/system'
|
||||
|
||||
/** Resolve page title from menu tree by path */
|
||||
function resolveTitle(menus: SystemMenu[], path: string): string {
|
||||
for (const m of menus) {
|
||||
if (m.path === path) return m.title || m.name
|
||||
if (m.children?.length) {
|
||||
const found = resolveTitle(m.children, path)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export default function TabBar() {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const menus = useAuthStore(s => s.menus)
|
||||
const { tabs, activeTab, addTab, removeTab, setActiveTab, closeOthers, closeAll, closeRight, closeLeft } = useTabsStore()
|
||||
const scrollRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Auto-register tab on route change
|
||||
useEffect(() => {
|
||||
const path = location.pathname
|
||||
if (path === '/login') return
|
||||
const title = resolveTitle(menus || [], path)
|
||||
|| path.split('/').filter(Boolean).pop()?.replace(/^\w/, c => c.toUpperCase()) || 'Page'
|
||||
addTab({ path, title, closable: path !== '/dashboard' })
|
||||
}, [location.pathname, menus])
|
||||
|
||||
const handleClick = (path: string) => {
|
||||
setActiveTab(path)
|
||||
navigate(path)
|
||||
}
|
||||
|
||||
const handleClose = (e: React.MouseEvent, path: string) => {
|
||||
e.stopPropagation()
|
||||
const next = removeTab(path)
|
||||
navigate(next)
|
||||
}
|
||||
|
||||
const handleCloseOthers = (path: string) => {
|
||||
closeOthers(path)
|
||||
navigate(path)
|
||||
}
|
||||
|
||||
const handleCloseAll = () => {
|
||||
const home = closeAll()
|
||||
navigate(home)
|
||||
}
|
||||
|
||||
const scroll = (dir: 'left' | 'right') => {
|
||||
scrollRef.current?.scrollBy({ left: dir === 'left' ? -200 : 200, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
const showArrows = tabs.length > 8
|
||||
|
||||
return (
|
||||
<div className="flex items-center h-[38px] bg-white/20 dark:bg-black/10 border-b border-white/20 dark:border-white/5 px-1 gap-0.5 select-none">
|
||||
{showArrows && (
|
||||
<button onClick={() => scroll('left')} className="shrink-0 flex items-center justify-center w-6 h-6 rounded hover:bg-white/40 dark:hover:bg-white/10 text-muted-foreground">
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div ref={scrollRef} className="flex-1 flex items-center gap-0.5 overflow-x-auto scrollbar-none">
|
||||
{tabs.map(tab => (
|
||||
<ContextMenu key={tab.path}>
|
||||
<ContextMenuTrigger>
|
||||
<button
|
||||
onClick={() => handleClick(tab.path)}
|
||||
className={cn(
|
||||
'group relative flex items-center gap-1.5 h-[28px] px-3 rounded-md text-xs font-medium whitespace-nowrap transition-all duration-200',
|
||||
activeTab === tab.path
|
||||
? 'bg-white dark:bg-slate-800 text-foreground shadow-sm border border-white/60 dark:border-white/10'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-white/50 dark:hover:bg-white/5'
|
||||
)}
|
||||
>
|
||||
{activeTab === tab.path && (
|
||||
<span className="absolute bottom-0 left-1/2 -translate-x-1/2 w-4 h-[2px] bg-primary rounded-full" />
|
||||
)}
|
||||
<span className="max-w-[100px] truncate">{tab.title}</span>
|
||||
{tab.closable && (
|
||||
<span
|
||||
onClick={e => handleClose(e, tab.path)}
|
||||
className={cn(
|
||||
'flex items-center justify-center w-4 h-4 rounded-full transition-all',
|
||||
activeTab === tab.path
|
||||
? 'opacity-60 hover:opacity-100 hover:bg-red-100 hover:text-red-500 dark:hover:bg-red-900/30'
|
||||
: 'opacity-0 group-hover:opacity-60 hover:!opacity-100 hover:bg-red-100 hover:text-red-500 dark:hover:bg-red-900/30'
|
||||
)}
|
||||
>
|
||||
<X className="h-2.5 w-2.5" />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
</ContextMenuTrigger>
|
||||
<ContextMenuContent className="w-44">
|
||||
{tab.closable && (
|
||||
<ContextMenuItem onClick={() => { const next = removeTab(tab.path); navigate(next) }}>
|
||||
关闭当前
|
||||
</ContextMenuItem>
|
||||
)}
|
||||
<ContextMenuItem onClick={() => handleCloseOthers(tab.path)}>
|
||||
关闭其他
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={() => { closeRight(tab.path); navigate(tab.path) }}>
|
||||
关闭右侧
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem onClick={() => { closeLeft(tab.path); navigate(tab.path) }}>
|
||||
关闭左侧
|
||||
</ContextMenuItem>
|
||||
<ContextMenuSeparator />
|
||||
<ContextMenuItem onClick={handleCloseAll}>
|
||||
关闭所有
|
||||
</ContextMenuItem>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{showArrows && (
|
||||
<button onClick={() => scroll('right')} className="shrink-0 flex items-center justify-center w-6 h-6 rounded hover:bg-white/40 dark:hover:bg-white/10 text-muted-foreground">
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user