dvc890's picture
Upload 39 files
ed76988 verified
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>
);
};