Spaces:
Running
Running
| import React, { useState, useEffect } from 'react'; | |
| import { Bell, Search, Menu, Building, Info, Check, AlertTriangle } from 'lucide-react'; | |
| import { User, School, Notification, UserRole } from '../types'; | |
| import { api } from '../services/api'; | |
| interface HeaderProps { | |
| user: User; | |
| title: string; | |
| onMenuClick?: () => void; | |
| } | |
| export const Header: React.FC<HeaderProps> = ({ user, title, onMenuClick }) => { | |
| const [schools, setSchools] = useState<School[]>([]); | |
| const [selectedSchool, setSelectedSchool] = useState(localStorage.getItem('admin_view_school_id') || ''); | |
| const [currentSchoolName, setCurrentSchoolName] = useState(''); | |
| // Notification State | |
| const [notifications, setNotifications] = useState<Notification[]>([]); | |
| const [showNotif, setShowNotif] = useState(false); | |
| const [hasUnread, setHasUnread] = useState(false); | |
| const fetchSchools = async () => { | |
| // Only ADMIN (Super Admin) can switch schools | |
| if (user.role === UserRole.ADMIN) { | |
| try { | |
| const data = await api.schools.getAll(); | |
| setSchools(data); | |
| if (data.length > 0) { | |
| if (!selectedSchool || !data.find((s: School) => s._id === selectedSchool)) { | |
| const defaultId = data[0]._id!; | |
| setSelectedSchool(defaultId); | |
| localStorage.setItem('admin_view_school_id', defaultId); | |
| if (!localStorage.getItem('admin_view_school_id_init')) { | |
| localStorage.setItem('admin_view_school_id_init', 'true'); | |
| window.location.reload(); | |
| } | |
| } | |
| } | |
| } catch (e) { console.error(e); } | |
| } else { | |
| // Principal, Teacher, Student see their own school | |
| try { | |
| // For display purposes, we can fetch public info or just user's school info | |
| const data = await api.schools.getPublic(); | |
| const mySchool = data.find((s: School) => s._id === user.schoolId); | |
| if (mySchool) setCurrentSchoolName(mySchool.name); | |
| } catch(e) { console.error(e); } | |
| } | |
| }; | |
| useEffect(() => { | |
| fetchSchools(); | |
| // Listen for custom event to refresh schools | |
| const handleSchoolUpdate = () => { | |
| fetchSchools(); | |
| }; | |
| window.addEventListener('school-updated', handleSchoolUpdate); | |
| // Fetch notifications | |
| const fetchNotifs = async () => { | |
| try { | |
| const list = await api.notifications.getAll(user._id || '', user.role); | |
| setNotifications(list); | |
| const lastReadTime = localStorage.getItem('last_read_notif_time'); | |
| if (list.length > 0) { | |
| if (!lastReadTime || new Date(list[0].createTime) > new Date(lastReadTime)) { | |
| setHasUnread(true); | |
| } | |
| } | |
| } catch (e) { console.error(e); } | |
| }; | |
| fetchNotifs(); | |
| return () => { | |
| window.removeEventListener('school-updated', handleSchoolUpdate); | |
| }; | |
| }, [user]); | |
| const handleSchoolChange = (e: React.ChangeEvent<HTMLSelectElement>) => { | |
| const newVal = e.target.value; | |
| if (!newVal) return; | |
| setSelectedSchool(newVal); | |
| localStorage.setItem('admin_view_school_id', newVal); | |
| window.location.reload(); | |
| }; | |
| const handleOpenNotif = () => { | |
| setShowNotif(!showNotif); | |
| if (!showNotif) { | |
| setHasUnread(false); | |
| localStorage.setItem('last_read_notif_time', new Date().toISOString()); | |
| } | |
| }; | |
| const roleLabels: Record<string, string> = { | |
| [UserRole.ADMIN]: '超级管理员', | |
| [UserRole.PRINCIPAL]: '校长', | |
| [UserRole.TEACHER]: '教师', | |
| [UserRole.STUDENT]: '学生' | |
| }; | |
| return ( | |
| <header className="h-16 md:h-20 bg-white border-b border-gray-200 flex items-center justify-between px-4 md:px-8 sticky top-0 z-10 shadow-sm w-full"> | |
| <div className="flex items-center space-x-4"> | |
| <button onClick={onMenuClick} className="lg:hidden p-2 hover:bg-gray-100 rounded-lg text-gray-600 focus:outline-none"> | |
| <Menu size={24} /> | |
| </button> | |
| <h1 className="text-xl md:text-2xl font-bold text-gray-800 truncate max-w-[150px] md:max-w-none">{title}</h1> | |
| </div> | |
| <div className="flex items-center space-x-2 md:space-x-6"> | |
| <div className="hidden md:flex items-center bg-gray-100 rounded-lg px-3 py-1.5"> | |
| <Building size={16} className="text-gray-500 mr-2"/> | |
| {user.role === UserRole.ADMIN ? ( | |
| <select className="bg-transparent text-sm border-none focus:ring-0 text-gray-700 font-medium cursor-pointer min-w-[120px]" value={selectedSchool} onChange={handleSchoolChange}> | |
| {schools.map((s: School) => <option key={s._id} value={s._id}>{s.name}</option>)} | |
| </select> | |
| ) : ( | |
| <span className="text-sm font-medium text-gray-700">{currentSchoolName || '我的学校'}</span> | |
| )} | |
| </div> | |
| <div className="relative hidden md:block"> | |
| <input type="text" placeholder="全局搜索..." className="w-64 pl-10 pr-4 py-2 bg-gray-50 border border-gray-200 rounded-full text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 text-gray-800"/> | |
| <Search className="absolute left-3 top-2.5 text-gray-500" size={16} /> | |
| </div> | |
| <div className="relative"> | |
| <button onClick={handleOpenNotif} className="relative p-2 text-gray-500 hover:text-blue-600 transition-colors"> | |
| <Bell size={20} /> | |
| {hasUnread && <span className="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full border border-white"></span>} | |
| </button> | |
| {showNotif && ( | |
| <div className="absolute right-0 top-12 w-80 bg-white rounded-xl shadow-2xl border border-gray-100 p-4 z-50 animate-in fade-in zoom-in-95"> | |
| <h3 className="font-bold text-gray-800 mb-3 text-sm">系统消息</h3> | |
| <div className="max-h-64 overflow-y-auto space-y-3"> | |
| {notifications.length > 0 ? notifications.map(n => ( | |
| <div key={n._id} className="flex gap-3 items-start border-b border-gray-50 pb-2 last:border-0"> | |
| <div className={`mt-1 w-2 h-2 rounded-full shrink-0 ${n.type==='success'?'bg-green-500':'bg-blue-500'}`}></div> | |
| <div> | |
| <p className="text-sm font-bold text-gray-800">{n.title}</p> | |
| <p className="text-xs text-gray-500 mt-0.5">{n.content}</p> | |
| <p className="text-[10px] text-gray-300 mt-1">{new Date(n.createTime).toLocaleString()}</p> | |
| </div> | |
| </div> | |
| )) : <p className="text-center text-xs text-gray-400 py-4">暂无新消息</p>} | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| <div className="flex items-center space-x-3 border-l pl-4 md:pl-6 border-gray-200"> | |
| <div className="text-right hidden sm:block"> | |
| <p className="text-sm font-semibold text-gray-800">{user.trueName || user.username}</p> | |
| <p className="text-xs text-gray-500">{roleLabels[user.role] || '用户'}</p> | |
| </div> | |
| <img src={user.avatar || `https://api.dicebear.com/7.x/avataaars/svg?seed=${user.username}`} alt="Profile" className="h-8 w-8 md:h-10 md:w-10 rounded-full object-cover border-2 border-white shadow-sm ring-2 ring-gray-100" /> | |
| </div> | |
| </div> | |
| </header> | |
| ); | |
| }; |