chore: initial import to Nexus_Edu
This commit is contained in:
161
src/components/Sidebar.tsx
Normal file
161
src/components/Sidebar.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
LayoutDashboard, BookOpen, FileQuestion, Users, Settings, LogOut,
|
||||
Bell, Search, GraduationCap, ScrollText, ClipboardList, Database,
|
||||
Menu, X, CalendarDays, Terminal
|
||||
} from 'lucide-react';
|
||||
import { useAuth } from '@/lib/auth-context';
|
||||
import { getApiMode, setApiMode } from '@/services/api';
|
||||
|
||||
const NavItem = ({ icon: Icon, label, href, isActive }: any) => (
|
||||
<Link href={href} className="block w-full">
|
||||
<div className={`
|
||||
w-full flex items-center gap-3 px-4 py-3 rounded-2xl transition-all duration-300 group relative overflow-hidden
|
||||
${isActive
|
||||
? 'text-white shadow-lg shadow-blue-500/30'
|
||||
: 'text-gray-500 hover:bg-gray-100 hover:text-gray-900'
|
||||
}
|
||||
`}>
|
||||
{isActive && (
|
||||
// Fix: cast props to any to avoid framer-motion type errors
|
||||
<motion.div
|
||||
{...({
|
||||
layoutId: "activeNav",
|
||||
initial: false,
|
||||
transition: { type: "spring", stiffness: 400, damping: 30 }
|
||||
} as any)}
|
||||
className="absolute inset-0 bg-blue-600 z-0"
|
||||
/>
|
||||
)}
|
||||
<div className="relative z-10 flex items-center gap-3">
|
||||
<Icon size={20} strokeWidth={isActive ? 2.5 : 2} />
|
||||
<span className="text-[15px] font-medium tracking-tight">{label}</span>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
|
||||
export const Sidebar = () => {
|
||||
const { user, logout } = useAuth();
|
||||
const pathname = usePathname() || '';
|
||||
const router = useRouter();
|
||||
const [isMock, setIsMock] = useState(getApiMode());
|
||||
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
||||
|
||||
const renderNavItems = () => {
|
||||
if (user?.role === 'Student') {
|
||||
return (
|
||||
<>
|
||||
<NavItem icon={LayoutDashboard} label="我的概览" href="/dashboard" isActive={pathname === '/dashboard'} />
|
||||
<NavItem icon={ClipboardList} label="我的作业" href="/assignments" isActive={pathname.startsWith('/assignments')} />
|
||||
<NavItem icon={Users} label="我的班级" href="/classes" isActive={pathname.startsWith('/classes')} />
|
||||
<NavItem icon={CalendarDays} label="我的课表" href="/schedule" isActive={pathname === '/schedule'} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<NavItem icon={LayoutDashboard} label="教学概览" href="/dashboard" isActive={pathname === '/dashboard'} />
|
||||
<NavItem icon={BookOpen} label="课程标准" href="/curriculum" isActive={pathname === '/curriculum'} />
|
||||
<NavItem icon={ScrollText} label="考试引擎" href="/exams" isActive={pathname.startsWith('/exams')} />
|
||||
<NavItem icon={ClipboardList} label="作业发布" href="/assignments" isActive={pathname.startsWith('/assignments')} />
|
||||
<NavItem icon={FileQuestion} label="题库资源" href="/questions" isActive={pathname === '/questions'} />
|
||||
<NavItem icon={CalendarDays} label="课表管理" href="/schedule" isActive={pathname === '/schedule'} />
|
||||
<NavItem icon={Users} label="班级管理" href="/classes" isActive={pathname.startsWith('/classes')} />
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
const SidebarContent = () => (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex items-center gap-4 px-2 py-6 mb-2">
|
||||
<div className="w-10 h-10 rounded-xl bg-gradient-to-br from-blue-500 to-indigo-600 flex items-center justify-center text-white font-bold shadow-lg shadow-blue-500/30">
|
||||
<GraduationCap size={20} />
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-bold text-xl tracking-tight block leading-none">EduNexus</span>
|
||||
<span className="text-xs text-gray-400 font-medium tracking-wide">专业版</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 space-y-2">
|
||||
{renderNavItems()}
|
||||
</nav>
|
||||
|
||||
<div className="pt-6 border-t border-gray-100 space-y-2">
|
||||
<button
|
||||
onClick={() => { setApiMode(!isMock); setIsMock(!isMock); }}
|
||||
className={`w-full flex items-center gap-3 px-4 py-2.5 rounded-xl transition-colors text-xs font-bold uppercase tracking-wider border ${isMock ? 'bg-amber-50 text-amber-600 border-amber-200' : 'bg-green-50 text-green-600 border-green-200'}`}
|
||||
>
|
||||
<Database size={14} />
|
||||
{isMock ? 'Mock Data' : 'Real API'}
|
||||
</button>
|
||||
|
||||
<NavItem icon={Terminal} label="控制台配置" href="/consoleconfig" isActive={pathname === '/consoleconfig'} />
|
||||
<NavItem icon={Settings} label="系统设置" href="/settings" isActive={pathname === '/settings'} />
|
||||
|
||||
<button
|
||||
onClick={logout}
|
||||
className="w-full flex items-center gap-3 px-4 py-3 rounded-2xl text-red-500 hover:bg-red-50 transition-colors group"
|
||||
>
|
||||
<LogOut size={20} className="group-hover:-translate-x-1 transition-transform" />
|
||||
<span className="text-[15px] font-medium">退出登录</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Desktop Sidebar */}
|
||||
<aside className="fixed w-72 h-screen pl-6 py-6 hidden lg:block z-50">
|
||||
<div className="h-full bg-white/80 backdrop-blur-xl rounded-[32px] border border-white/50 shadow-[0_20px_40px_rgba(0,0,0,0.04)] flex flex-col p-6">
|
||||
<SidebarContent />
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Mobile Toggle */}
|
||||
<div className="lg:hidden fixed top-4 left-4 z-50">
|
||||
<button onClick={() => setIsMobileMenuOpen(true)} className="p-2 bg-white rounded-xl shadow-sm">
|
||||
<Menu size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Mobile Menu Overlay */}
|
||||
<AnimatePresence>
|
||||
{isMobileMenuOpen && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
|
||||
className="fixed inset-0 bg-black/20 backdrop-blur-sm z-50 lg:hidden"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
/>
|
||||
{/* Fix: cast props to any to avoid framer-motion type errors */}
|
||||
<motion.div
|
||||
{...({
|
||||
initial: { x: '-100%' },
|
||||
animate: { x: 0 },
|
||||
exit: { x: '-100%' },
|
||||
transition: { type: 'spring', stiffness: 300, damping: 30 }
|
||||
} as any)}
|
||||
className="fixed inset-y-0 left-0 w-72 bg-white z-50 p-6 flex flex-col shadow-2xl lg:hidden"
|
||||
>
|
||||
<div className="absolute top-4 right-4">
|
||||
<button onClick={() => setIsMobileMenuOpen(false)} className="p-2 hover:bg-gray-100 rounded-full">
|
||||
<X size={20} className="text-gray-500" />
|
||||
</button>
|
||||
</div>
|
||||
<SidebarContent />
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user