init: initial commit

This commit is contained in:
Blizzard
2026-02-28 17:35:31 +08:00
commit da7ac70eeb
44 changed files with 13146 additions and 0 deletions
+151
View File
@@ -0,0 +1,151 @@
import { Outlet, Navigate, useNavigate, Link, useLocation } from 'react-router-dom';
import { useAuthStore } from '../store/authStore';
import { logoutApi } from '../api/auth';
import {
Radio,
LayoutDashboard,
ListMusic,
Mic2,
Disc3,
FolderOpen,
LogOut,
User as UserIcon,
ChevronDown
} from 'lucide-react';
import { Button } from '../components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "../components/ui/dropdown-menu"
import { Avatar, AvatarFallback, AvatarImage } from "../components/ui/avatar"
export default function AdminLayout() {
const token = useAuthStore((state) => state.token);
const userInfo = useAuthStore((state) => state.userInfo);
const logout = useAuthStore((state) => state.logout);
const navigate = useNavigate();
const location = useLocation();
if (!token) {
return <Navigate to="/login" replace />;
}
const handleLogout = async () => {
try {
await logoutApi();
} catch (e) {
console.warn('登出失败');
}
logout();
navigate('/login');
};
const navItems = [
{ name: '工作台', path: '/', icon: LayoutDashboard },
{ name: '频道分类', path: '/radio/category', icon: ListMusic },
{ name: '频道管理', path: '/radio/channel', icon: Mic2 },
{ name: '节目管理', path: '/radio/program', icon: Disc3 },
{ name: '文件管理', path: '/system/oss', icon: FolderOpen },
];
return (
<div className="flex h-screen bg-slate-50 dark:bg-slate-950 overflow-hidden">
{/* Sidebar */}
<aside className="fixed inset-y-0 left-0 z-50 w-64 bg-slate-900 text-slate-100 hidden md:flex flex-col shadow-xl">
<div className="flex items-center h-16 px-6 border-b border-slate-800 shrink-0">
<div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center mr-3 shadow-lg shadow-primary/20">
<Radio className="w-5 h-5 text-white" />
</div>
<span className="font-bold text-lg tracking-wide uppercase"></span>
</div>
<div className="flex-1 px-4 py-6 space-y-2 overflow-y-auto custom-scrollbar">
{navItems.map((item) => {
const isActive = location.pathname === item.path || (item.path !== '/' && location.pathname.startsWith(item.path));
return (
<Link key={item.path} to={item.path}>
<Button
variant="ghost"
className={`w-full justify-start h-11 px-4 rounded-xl transition-all duration-200 group ${isActive
? 'bg-primary text-primary-foreground shadow-lg shadow-primary/10'
: 'text-slate-400 hover:text-slate-100 hover:bg-slate-800'
}`}
>
<item.icon className={`w-5 h-5 mr-3 shrink-0 ${isActive ? 'text-white' : 'group-hover:text-primary'}`} />
<span className="font-medium">{item.name}</span>
</Button>
</Link>
)
})}
</div>
<div className="p-4 border-t border-slate-800 shrink-0">
<div className="bg-slate-800/50 rounded-2xl p-3 flex items-center space-x-3">
<Avatar className="h-10 w-10 ring-2 ring-slate-700">
<AvatarImage src={userInfo?.avatar || ''} />
<AvatarFallback className="bg-slate-700 text-slate-100">
<UserIcon className="w-5 h-5" />
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0">
<p className="text-sm font-semibold truncate">{userInfo?.nickName || '管理员'}</p>
<p className="text-xs text-slate-500 truncate">{userInfo?.account || 'admin'}</p>
</div>
</div>
</div>
</aside>
{/* Main Area */}
<div className="flex-1 md:ml-64 flex flex-col h-full overflow-hidden relative">
{/* Navbar */}
<header className="h-16 flex items-center justify-between px-8 bg-white dark:bg-slate-900 border-b shrink-0 z-40">
<div className="flex items-center space-x-4">
<div className="md:hidden">
<Radio className="w-6 h-6 text-primary" />
</div>
<h2 className="text-sm font-medium text-slate-500 uppercase tracking-widest hidden sm:block"></h2>
</div>
<div className="flex items-center space-x-4">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-10 px-3 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800 flex items-center space-x-2">
<div className="text-right hidden sm:block">
<p className="text-sm font-medium leading-none">{userInfo?.nickName || '管理员'}</p>
</div>
<ChevronDown className="w-4 h-4 text-slate-400" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56 mt-2 rounded-xl p-1" align="end" forceMount>
<DropdownMenuLabel className="font-normal px-2 py-3">
<div className="flex flex-col space-y-1">
<p className="text-sm font-bold text-slate-900 dark:text-slate-100">{userInfo?.nickName || '管理员'}</p>
<p className="text-xs text-slate-500">{userInfo?.account || 'admin'}</p>
</div>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={handleLogout}
className="cursor-pointer text-rose-500 focus:bg-rose-50 focus:text-rose-600 rounded-lg p-2 font-medium"
>
<LogOut className="mr-3 h-4 w-4" />
<span>退</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>
{/* Page Content */}
<main className="flex-1 overflow-y-auto p-4 md:p-10 bg-slate-50/50 dark:bg-slate-950/50 scroll-smooth">
<div className="max-w-7xl mx-auto h-full">
<Outlet />
</div>
</main>
</div>
</div>
);
}