feat: 炫酷的登录页
This commit is contained in:
+72
-13
@@ -1,9 +1,11 @@
|
||||
import { NavLink, Outlet, useNavigate, useLocation } from 'react-router-dom'
|
||||
import { AnimatePresence, motion } from 'framer-motion'
|
||||
import CommandPalette from '@/components/CommandPalette'
|
||||
import {
|
||||
LayoutDashboard, Users, Shield, MessageSquare, FolderTree, Leaf,
|
||||
LogOut, ChevronDown, Menu, FileText, Settings, Book, Home, Monitor,
|
||||
Hash, Folder, ChevronRight, Search, Bell, ChevronLeft, Bot, Radio,
|
||||
Image, Music, ScrollText, Sun, Moon,
|
||||
Image, Music, ScrollText, Sun, Moon, Trophy, Award, Star, Gift, List,
|
||||
} from 'lucide-react'
|
||||
import { useState, useMemo, useEffect } from 'react'
|
||||
import { useAuthStore } from '@/store/auth'
|
||||
@@ -12,11 +14,11 @@ import type { SystemMenu } from '@/api/system'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { ScrollArea } from '@/components/ui/scroll-area'
|
||||
import TabBar from '@/components/TabBar'
|
||||
import {
|
||||
DropdownMenu, DropdownMenuContent, DropdownMenuItem,
|
||||
DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
// Icon mapping
|
||||
@@ -51,6 +53,11 @@ const iconMap: Record<string, React.ReactNode> = {
|
||||
music: <Music className="h-4 w-4" />,
|
||||
scroll: <ScrollText className="h-4 w-4" />,
|
||||
log: <ScrollText className="h-4 w-4" />,
|
||||
trophy: <Trophy className="h-4 w-4" />,
|
||||
award: <Award className="h-4 w-4" />,
|
||||
star: <Star className="h-4 w-4" />,
|
||||
gift: <Gift className="h-4 w-4" />,
|
||||
list: <List className="h-4 w-4" />,
|
||||
}
|
||||
|
||||
function getIcon(iconName?: string): React.ReactNode {
|
||||
@@ -195,25 +202,62 @@ export default function AdminLayout() {
|
||||
|
||||
// ==================== Shell Render ====================
|
||||
|
||||
import { useRef } from 'react'
|
||||
|
||||
function LayoutShell({ sidebarOpen, mobileMenuOpen, toggleSidebar, setMobileMenu, navItems, user, onLogout }: {
|
||||
sidebarOpen: boolean; mobileMenuOpen: boolean; toggleSidebar: () => void; setMobileMenu: (v: boolean) => void
|
||||
navItems: NavItem[]; user: import('@/api/system').SystemUser | null; onLogout: () => void
|
||||
}) {
|
||||
const { setCmdKOpen } = useAppStore()
|
||||
const location = useLocation()
|
||||
const spotlightRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (spotlightRef.current) {
|
||||
spotlightRef.current.style.background = `radial-gradient(600px circle at ${e.clientX}px ${e.clientY}px, rgba(6,182,212,0.25), transparent 40%)`
|
||||
}
|
||||
}
|
||||
window.addEventListener('mousemove', handleMouseMove)
|
||||
return () => window.removeEventListener('mousemove', handleMouseMove)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-background p-2 lg:p-3 gap-2 lg:gap-3">
|
||||
<div className="flex min-h-screen bg-gradient-to-br from-slate-100 via-white to-cyan-50 dark:from-[#080d17] dark:via-[#080d17] dark:to-[#080d17] p-2 lg:p-3 gap-2 lg:gap-3 relative overflow-hidden">
|
||||
|
||||
{/* Interactive Cursor Spotlight (Illuminates the glass as you move the mouse) */}
|
||||
<div
|
||||
ref={spotlightRef}
|
||||
className="pointer-events-none fixed inset-0 z-0 transition-opacity duration-300 opacity-100 dark:opacity-60"
|
||||
style={{
|
||||
background: `radial-gradient(600px circle at -1000px -1000px, rgba(6,182,212,0.25), transparent 40%)`
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Global Ambient Background */}
|
||||
<div className="fixed inset-0 pointer-events-none overflow-hidden z-0">
|
||||
{/* Cyan Glow Top Left */}
|
||||
<div className="absolute top-[-20%] left-[-10%] w-[50vw] h-[50vw] max-w-[800px] max-h-[800px] bg-cyan-200/40 dark:bg-cyan-600/20 rounded-full blur-[100px] dark:blur-[120px] mix-blend-multiply dark:mix-blend-screen" />
|
||||
{/* Purple Glow Bottom Right */}
|
||||
<div className="absolute bottom-[-20%] right-[-10%] w-[50vw] h-[50vw] max-w-[800px] max-h-[800px] bg-fuchsia-200/40 dark:bg-fuchsia-600/20 rounded-full blur-[100px] dark:blur-[120px] mix-blend-multiply dark:mix-blend-screen" />
|
||||
{/* Subtle Tech Grid */}
|
||||
<div className="absolute inset-0 opacity-[0.04] dark:opacity-[0.05]" style={{ backgroundImage: 'linear-gradient(to right, currentColor 1px, transparent 1px), linear-gradient(to bottom, currentColor 1px, transparent 1px)', backgroundSize: '60px 60px' }} />
|
||||
</div>
|
||||
|
||||
<CommandPalette />
|
||||
{/* Sidebar */}
|
||||
<aside className={cn(
|
||||
'fixed inset-y-2 lg:inset-y-3 left-2 lg:left-3 z-40 flex flex-col rounded-2xl border border-white/40 dark:border-white/5 glass-panel transition-all duration-300 overflow-hidden',
|
||||
'fixed inset-y-2 lg:inset-y-3 left-2 lg:left-3 z-40 flex flex-col rounded-2xl border border-white/60 dark:border-white/10 bg-white/60 dark:bg-black/40 backdrop-blur-2xl shadow-lg transition-all duration-300 overflow-hidden',
|
||||
sidebarOpen ? 'w-[260px]' : 'w-20',
|
||||
mobileMenuOpen ? 'translate-x-0' : '-translate-x-[120%] lg:translate-x-0'
|
||||
)}>
|
||||
{/* Logo */}
|
||||
<div className="flex h-16 items-center justify-between px-5 shrink-0">
|
||||
<div className={cn("flex items-center gap-3 overflow-hidden transition-all", !sidebarOpen && "justify-center w-full")}>
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-emerald-500 to-teal-500 text-white shadow-lg shadow-emerald-500/20">
|
||||
<Leaf className="h-4 w-4" />
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-white shadow-sm overflow-hidden border border-slate-200/60 dark:border-white/10">
|
||||
<img src="/logo.png" alt="Logo" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
{sidebarOpen && <span className="font-bold text-lg tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-emerald-600 to-teal-600 truncate">Sundynix Console</span>}
|
||||
{sidebarOpen && <span className="font-bold text-lg tracking-tight bg-clip-text text-transparent bg-gradient-to-r from-emerald-600 to-teal-600 truncate">Sundynix Cloud</span>}
|
||||
</div>
|
||||
</div>
|
||||
{/* Nav */}
|
||||
@@ -259,7 +303,7 @@ function LayoutShell({ sidebarOpen, mobileMenuOpen, toggleSidebar, setMobileMenu
|
||||
|
||||
{/* Main */}
|
||||
<div className={cn(
|
||||
'flex-1 flex flex-col min-h-[calc(100vh-1rem)] lg:min-h-[calc(100vh-1.5rem)] transition-all duration-300 bg-white/40 dark:bg-slate-900/40 rounded-2xl border border-white/50 dark:border-white/5 shadow-soft overflow-hidden relative backdrop-blur-xl',
|
||||
'relative z-10 flex-1 flex flex-col min-h-[calc(100vh-1rem)] lg:min-h-[calc(100vh-1.5rem)] transition-all duration-300 bg-white/60 dark:bg-black/30 rounded-2xl border border-white/60 dark:border-white/10 shadow-xl overflow-hidden backdrop-blur-2xl',
|
||||
sidebarOpen ? 'lg:ml-[268px]' : 'lg:ml-[88px]'
|
||||
)}>
|
||||
{/* Header */}
|
||||
@@ -277,9 +321,12 @@ function LayoutShell({ sidebarOpen, mobileMenuOpen, toggleSidebar, setMobileMenu
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="hidden md:flex relative group">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground group-focus-within:text-emerald-500 transition-colors" />
|
||||
<Input placeholder="全局搜索..." className="w-56 focus:w-72 pl-9 bg-white/50 dark:bg-black/20 border-white/40 dark:border-white/10 focus:bg-white dark:focus:bg-slate-800 focus:border-emerald-500/30 transition-all h-9 text-sm rounded-full shadow-sm" />
|
||||
<div className="hidden md:flex relative group cursor-pointer" onClick={() => setCmdKOpen(true)}>
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground group-focus-within:text-primary transition-colors" />
|
||||
<div className="flex items-center w-56 pl-9 pr-3 bg-white/50 dark:bg-black/20 border border-white/40 dark:border-white/10 hover:bg-white dark:hover:bg-slate-800 hover:border-primary/30 transition-all h-9 text-sm rounded-full shadow-sm text-muted-foreground">
|
||||
<span className="flex-1 text-left">搜索菜单...</span>
|
||||
<span className="text-[10px] border border-border px-1.5 rounded bg-background/50 leading-tight">⌘K</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" className="relative rounded-full h-9 w-9 hover:bg-white/50 dark:hover:bg-white/10"
|
||||
onClick={() => document.documentElement.classList.toggle('dark')}>
|
||||
@@ -292,11 +339,23 @@ function LayoutShell({ sidebarOpen, mobileMenuOpen, toggleSidebar, setMobileMenu
|
||||
</Button>
|
||||
</div>
|
||||
</header>
|
||||
{/* TabBar */}
|
||||
<TabBar />
|
||||
{/* Content */}
|
||||
<ScrollArea className="flex-1">
|
||||
<main className="p-4 lg:p-6 relative z-0 min-h-full">
|
||||
<div className="mx-auto w-full max-w-[1600px] animate-fade-in-up space-y-6">
|
||||
<Outlet />
|
||||
<div className="mx-auto w-full max-w-[1600px] space-y-6">
|
||||
<AnimatePresence mode="wait">
|
||||
<motion.div
|
||||
key={location.pathname}
|
||||
initial={{ opacity: 0, y: 15 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -15 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<Outlet />
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
</main>
|
||||
</ScrollArea>
|
||||
|
||||
Reference in New Issue
Block a user