Spaces:
Running
Running
| import React, { useState, useEffect, useLayoutEffect } from 'react'; | |
| import { LayoutDashboard, Users, BookOpen, GraduationCap, Settings, LogOut, FileText, School, UserCog, Palette, X, Building, Gamepad2, CalendarCheck, UserCircle, MessageSquare, Bot, ArrowUp, ArrowDown, Save, UserCheck, Download, Smartphone } from 'lucide-react'; | |
| import { UserRole } from '../types'; | |
| import { api } from '../services/api'; | |
| interface SidebarProps { | |
| currentView: string; | |
| onChangeView: (view: string) => void; | |
| userRole: UserRole; | |
| onLogout: () => void; | |
| isOpen: boolean; | |
| onClose: () => void; | |
| } | |
| interface MenuItem { | |
| id: string; | |
| label: string; | |
| icon: React.ElementType; | |
| roles: UserRole[]; | |
| } | |
| export const Sidebar: React.FC<SidebarProps> = ({ currentView, onChangeView, userRole, onLogout, isOpen, onClose }) => { | |
| const currentUser = api.auth.getCurrentUser(); | |
| const canSeeAI = userRole === UserRole.ADMIN || (userRole === UserRole.TEACHER && currentUser?.aiAccess); | |
| const isHomeroom = userRole === UserRole.TEACHER && !!currentUser?.homeroomClass; | |
| // Default Items | |
| const defaultItems: MenuItem[] = [ | |
| { id: 'dashboard', label: '工作台', icon: LayoutDashboard, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER, UserRole.STUDENT] }, | |
| { id: 'my-class', label: '我的班级', icon: UserCheck, roles: isHomeroom ? [UserRole.TEACHER] : [] }, | |
| { id: 'ai-assistant', label: 'AI 智能助教', icon: Bot, roles: canSeeAI ? [UserRole.ADMIN, UserRole.TEACHER] : [] }, | |
| { id: 'attendance', label: '考勤管理', icon: CalendarCheck, roles: [UserRole.TEACHER, UserRole.PRINCIPAL] }, | |
| { id: 'games', label: '互动教学', icon: Gamepad2, roles: [UserRole.TEACHER, UserRole.STUDENT] }, | |
| { id: 'wishes', label: '心愿与反馈', icon: MessageSquare, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER, UserRole.STUDENT] }, | |
| { id: 'students', label: '学生管理', icon: Users, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER] }, | |
| { id: 'classes', label: '班级管理', icon: School, roles: [UserRole.ADMIN, UserRole.PRINCIPAL] }, | |
| { id: 'schools', label: '学校管理', icon: Building, roles: [UserRole.ADMIN] }, | |
| { id: 'courses', label: '课程管理', icon: BookOpen, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER] }, | |
| { id: 'grades', label: '成绩管理', icon: GraduationCap, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER] }, | |
| { id: 'reports', label: '报表统计', icon: FileText, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER, UserRole.STUDENT] }, | |
| { id: 'subjects', label: '学科设置', icon: Palette, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER] }, | |
| { id: 'users', label: '用户管理', icon: UserCog, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER] }, | |
| { id: 'profile', label: '个人中心', icon: UserCircle, roles: [UserRole.ADMIN, UserRole.PRINCIPAL, UserRole.TEACHER, UserRole.STUDENT] }, | |
| { id: 'settings', label: '系统设置', icon: Settings, roles: [UserRole.ADMIN, UserRole.PRINCIPAL] }, | |
| ]; | |
| const [menuItems, setMenuItems] = useState<MenuItem[]>(defaultItems); | |
| const [isEditing, setIsEditing] = useState(false); | |
| const [installPrompt, setInstallPrompt] = useState<any>(null); | |
| const [isStandalone, setIsStandalone] = useState(false); | |
| // Robust PWA Check | |
| useLayoutEffect(() => { | |
| const checkStandalone = () => { | |
| const isStandaloneMode = | |
| window.matchMedia('(display-mode: standalone)').matches || | |
| (window.navigator as any).standalone === true || | |
| document.referrer.includes('android-app://'); | |
| setIsStandalone(isStandaloneMode); | |
| }; | |
| checkStandalone(); | |
| window.matchMedia('(display-mode: standalone)').addEventListener('change', checkStandalone); | |
| return () => window.matchMedia('(display-mode: standalone)').removeEventListener('change', checkStandalone); | |
| }, []); | |
| // Capture PWA Prompt | |
| useEffect(() => { | |
| if ((window as any).deferredPrompt) { | |
| setInstallPrompt((window as any).deferredPrompt); | |
| } | |
| const handler = (e: any) => { | |
| e.preventDefault(); | |
| (window as any).deferredPrompt = e; | |
| setInstallPrompt(e); | |
| }; | |
| window.addEventListener('beforeinstallprompt', handler); | |
| return () => window.removeEventListener('beforeinstallprompt', handler); | |
| }, []); | |
| const handleInstallClick = async () => { | |
| const promptEvent = installPrompt || (window as any).deferredPrompt; | |
| if (!promptEvent) { | |
| alert('安装功能当前不可用。请尝试点击浏览器菜单中的“添加到主屏幕”或“安装应用”。'); | |
| return; | |
| } | |
| promptEvent.prompt(); | |
| const { outcome } = await promptEvent.userChoice; | |
| if (outcome === 'accepted') { | |
| setInstallPrompt(null); | |
| (window as any).deferredPrompt = null; | |
| } | |
| }; | |
| useEffect(() => { | |
| // Load saved order | |
| if (currentUser?.menuOrder && currentUser.menuOrder.length > 0) { | |
| const ordered: MenuItem[] = []; | |
| const map = new Map(defaultItems.map(i => [i.id, i])); | |
| // Add saved items in order | |
| currentUser.menuOrder.forEach(id => { | |
| if (map.has(id)) { | |
| ordered.push(map.get(id)!); | |
| map.delete(id); | |
| } | |
| }); | |
| // Append any new/remaining items | |
| map.forEach(item => ordered.push(item)); | |
| setMenuItems(ordered); | |
| } | |
| }, []); | |
| const handleMove = (index: number, direction: -1 | 1) => { | |
| const newItems = [...menuItems]; | |
| const targetIndex = index + direction; | |
| if (targetIndex < 0 || targetIndex >= newItems.length) return; | |
| [newItems[index], newItems[targetIndex]] = [newItems[targetIndex], newItems[index]]; | |
| setMenuItems(newItems); | |
| }; | |
| const saveOrder = async () => { | |
| setIsEditing(false); | |
| const orderIds = menuItems.map(i => i.id); | |
| if (currentUser && currentUser._id) { | |
| try { | |
| await api.users.saveMenuOrder(currentUser._id, orderIds); | |
| // Update local user object | |
| const updatedUser = { ...currentUser, menuOrder: orderIds }; | |
| localStorage.setItem('user', JSON.stringify(updatedUser)); | |
| } catch(e) { console.error("Failed to save menu order"); } | |
| } | |
| }; | |
| const sidebarClasses = ` | |
| fixed inset-y-0 left-0 z-50 w-64 bg-slate-900 text-white transition-transform duration-300 ease-in-out transform | |
| flex flex-col h-full | |
| ${isOpen ? 'translate-x-0' : '-translate-x-full'} | |
| lg:relative lg:translate-x-0 shadow-2xl lg:shadow-none | |
| `; | |
| return ( | |
| <> | |
| {isOpen && <div className="fixed inset-0 bg-black/50 z-40 lg:hidden backdrop-blur-sm" onClick={onClose}></div>} | |
| <div className={sidebarClasses}> | |
| <div className="flex items-center justify-between h-20 border-b border-slate-700 px-6 shrink-0"> | |
| <div className="flex items-center space-x-2"> | |
| <GraduationCap className="h-8 w-8 text-blue-400" /> | |
| <span className="text-xl font-bold bg-clip-text text-transparent bg-gradient-to-r from-blue-400 to-teal-300"> | |
| 智慧校园 | |
| </span> | |
| </div> | |
| <button onClick={onClose} className="lg:hidden text-slate-400 hover:text-white"><X size={24} /></button> | |
| </div> | |
| <div className="flex-1 overflow-y-auto py-4 custom-scrollbar"> | |
| <div className="px-4 mb-2 flex justify-between items-center"> | |
| <span className="text-xs text-slate-500 font-bold uppercase">主菜单</span> | |
| <button | |
| onClick={() => isEditing ? saveOrder() : setIsEditing(true)} | |
| className={`text-xs px-2 py-1 rounded hover:bg-slate-800 ${isEditing ? 'text-green-400' : 'text-slate-600'}`} | |
| > | |
| {isEditing ? '完成' : '调整'} | |
| </button> | |
| </div> | |
| <nav className="space-y-1 px-2"> | |
| {menuItems.map((item, idx) => { | |
| if (item.roles.length > 0 && !item.roles.includes(userRole)) return null; | |
| if (item.id === 'ai-assistant' && !canSeeAI) return null; | |
| // Special check for 'my-class': only homeroom teachers | |
| if (item.id === 'my-class' && !isHomeroom) return null; | |
| const Icon = item.icon; | |
| const isActive = currentView === item.id; | |
| return ( | |
| <div key={item.id} className="flex items-center gap-1 group"> | |
| <button | |
| onClick={() => !isEditing && onChangeView(item.id)} | |
| disabled={isEditing} | |
| className={`flex-1 flex items-center space-x-3 px-4 py-3 rounded-lg transition-colors duration-200 ${ | |
| isActive | |
| ? 'bg-blue-600 text-white shadow-lg shadow-blue-900/50' | |
| : 'text-slate-300 hover:bg-slate-800 hover:text-white' | |
| } ${isEditing ? 'opacity-70 cursor-grab' : ''}`} | |
| > | |
| <Icon size={20} /> | |
| <span className="font-medium">{item.label}</span> | |
| </button> | |
| {isEditing && ( | |
| <div className="flex flex-col gap-1 pr-1"> | |
| <button onClick={()=>handleMove(idx, -1)} className="p-1 hover:bg-slate-700 rounded text-slate-400 hover:text-white"><ArrowUp size={12}/></button> | |
| <button onClick={()=>handleMove(idx, 1)} className="p-1 hover:bg-slate-700 rounded text-slate-400 hover:text-white"><ArrowDown size={12}/></button> | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| })} | |
| </nav> | |
| </div> | |
| <div className="p-4 border-t border-slate-700 shrink-0 space-y-2"> | |
| {!isStandalone && ( | |
| <> | |
| {installPrompt ? ( | |
| <button onClick={handleInstallClick} className="w-full flex items-center space-x-3 px-4 py-3 rounded-lg bg-gradient-to-r from-blue-600 to-indigo-600 text-white hover:shadow-lg transition-all duration-200 animate-in fade-in slide-in-from-bottom-2"> | |
| <Download size={20} /> | |
| <span className="font-bold">安装到桌面/手机</span> | |
| </button> | |
| ) : ( | |
| <div className="lg:hidden px-4 py-2 bg-slate-800 rounded-lg text-[10px] text-slate-400 border border-slate-700 flex gap-2 items-start"> | |
| <Smartphone size={14} className="mt-0.5 shrink-0"/> | |
| <div> | |
| 若未显示安装按钮: | |
| <br/>iOS: 点击分享 → 添加到主屏幕 | |
| <br/>Android: 点击菜单 → 安装应用 | |
| </div> | |
| </div> | |
| )} | |
| </> | |
| )} | |
| <div className="px-4 py-2 text-xs text-slate-500 flex justify-between"> | |
| <span>当前角色:</span> | |
| <span className="text-slate-300 font-bold"> | |
| {userRole === 'ADMIN' ? '超级管理员' : | |
| userRole === 'PRINCIPAL' ? '校长' : | |
| userRole === 'TEACHER' ? '教师' : '学生'} | |
| </span> | |
| </div> | |
| <button onClick={onLogout} className="w-full flex items-center space-x-3 px-4 py-3 rounded-lg text-slate-400 hover:bg-red-500/10 hover:text-red-400 transition-all duration-200"> | |
| <LogOut size={20} /> | |
| <span className="font-medium">退出登录</span> | |
| </button> | |
| </div> | |
| </div> | |
| </> | |
| ); | |
| }; |