diff --git a/apps/admin/src/App.tsx b/apps/admin/src/App.tsx index deb9995..bebc481 100644 --- a/apps/admin/src/App.tsx +++ b/apps/admin/src/App.tsx @@ -232,13 +232,7 @@ const App: React.FC = () => { } /> - - - - }> - } /> - + } /> diff --git a/apps/admin/src/components/NotificationBell.tsx b/apps/admin/src/components/NotificationBell.tsx index 87d7e69..8ca53e3 100644 --- a/apps/admin/src/components/NotificationBell.tsx +++ b/apps/admin/src/components/NotificationBell.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { Badge, Popover, Button, List, Typography, Empty } from 'antd'; import { BellOutlined } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; @@ -56,7 +56,10 @@ const NotificationBell: React.FC = () => { setUnreadCount(data.count); } catch { /* ignore */ } }; + const openRef = useRef(open); + openRef.current = open; + // SSE connection — decoupled from popover open state useEffect(() => { fetchUnread(); const token = localStorage.getItem('token'); @@ -66,16 +69,15 @@ const NotificationBell: React.FC = () => { try { JSON.parse(event.data); setUnreadCount((c) => c + 1); - if (open) fetchNotifications(); + if (openRef.current) fetchNotifications(); } catch { /* ignore */ } }; es.onerror = () => { es.close(); - const interval = setInterval(fetchUnread, 60_000); - return () => clearInterval(interval); + setInterval(fetchUnread, 60_000); }; return () => es.close(); - }, [open]); + }, []); const handleOpen = (visible: boolean) => { setOpen(visible); diff --git a/apps/admin/src/hooks/useNotifications.ts b/apps/admin/src/hooks/useNotifications.ts index deb3e85..cc2dd7c 100644 --- a/apps/admin/src/hooks/useNotifications.ts +++ b/apps/admin/src/hooks/useNotifications.ts @@ -1,4 +1,4 @@ -import { useState, useEffect, useCallback } from 'react'; +import { useState, useEffect, useCallback, useRef } from 'react'; import api from '../api'; interface Notification { @@ -41,16 +41,18 @@ export function useNotifications() { } }; + const pollRef = { current: undefined as number | undefined }; + es.onerror = () => { es.close(); - const interval = setInterval(() => { + pollRef.current = setInterval(() => { fetchUnreadCount(); }, 60_000); - return () => clearInterval(interval); }; return () => { es.close(); + if (pollRef.current !== undefined) clearInterval(pollRef.current); }; }, [fetchUnreadCount]); diff --git a/apps/server/src/notifications/notifications.controller.ts b/apps/server/src/notifications/notifications.controller.ts index 82213c4..c866b80 100644 --- a/apps/server/src/notifications/notifications.controller.ts +++ b/apps/server/src/notifications/notifications.controller.ts @@ -48,6 +48,9 @@ export class NotificationsController { @Sse('stream') stream(@Req() req: AuthenticatedRequest): Observable { const userId = req.user.id; + req.on('close', () => { + this.service.unsubscribe(userId); + }); return this.service.subscribe(userId).pipe( map((notification) => ({ data: JSON.stringify({ diff --git a/apps/server/src/schedules/schedules.controller.ts b/apps/server/src/schedules/schedules.controller.ts index 3922305..a96cede 100644 --- a/apps/server/src/schedules/schedules.controller.ts +++ b/apps/server/src/schedules/schedules.controller.ts @@ -108,6 +108,7 @@ export class SchedulesController { @Request() req: { user?: { id: number; username: string }; headers?: Record }, ) { const { ipAddress, userAgent } = extractRequestInfo(req); + const existing = await this.service.findOne(+id); try { const result = await this.service.update(+id, dto); await this.logService.log({ @@ -126,7 +127,7 @@ export class SchedulesController { if (error instanceof ConflictException) { try { const conflicts = await this.service.checkConflict( - dto.classroomId ?? 0, dto.weekDay ?? 0, dto.startTime ?? '', dto.endTime ?? '', dto.startDate ?? '', dto.endDate ?? '', + existing.classroomId, existing.weekDay, existing.startTime, existing.endTime, existing.startDate, existing.endDate, ); const teacherIds = [...new Set(conflicts.map(c => c.teacherId).filter(Boolean))]; if (teacherIds.length > 0) { @@ -134,7 +135,7 @@ export class SchedulesController { recipientIds: teacherIds, type: NotificationType.SCHEDULE_CONFLICT, title: '排课冲突', - content: `教室${dto.classroomId} 周${dto.weekDay} ${dto.startTime}-${dto.endTime} (更新) 与已有排课冲突`, + content: `教室${existing.classroomId} 周${existing.weekDay} ${existing.startTime}-${existing.endTime} (更新) 与已有排课冲突`, }); } } catch {}