feat: 炫酷的登录页

This commit is contained in:
Blizzard
2026-04-28 16:43:34 +08:00
parent 3cade8e7ef
commit ccb36fa59c
34 changed files with 2390 additions and 253 deletions
+139
View File
@@ -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>
)
}