Spaces:
Sleeping
Sleeping
File size: 7,524 Bytes
08cac2e 8f856eb 129fac6 ed76988 8f856eb 33ebc65 f780380 33ebc65 f780380 8f856eb d567faa 129fac6 8f856eb 08cac2e ed76988 08cac2e d567faa ed76988 08cac2e ed76988 08cac2e 8f856eb 08cac2e 129fac6 08cac2e 8f856eb 129fac6 8f856eb d567faa 129fac6 8f856eb ed76988 33ebc65 f780380 33ebc65 129fac6 f780380 33ebc65 f780380 33ebc65 f780380 d567faa 8f856eb ed76988 129fac6 079d21f d567faa 8f856eb 33ebc65 129fac6 f780380 33ebc65 129fac6 33ebc65 f780380 33ebc65 8f856eb ed76988 33ebc65 129fac6 33ebc65 d567faa |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 |
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>
);
}; |