diff --git a/src/contexts/NotificationContext.js b/src/contexts/NotificationContext.js index f9efafcf..b7cb6528 100644 --- a/src/contexts/NotificationContext.js +++ b/src/contexts/NotificationContext.js @@ -62,6 +62,7 @@ export const NotificationProvider = ({ children }) => { const reconnectedTimerRef = useRef(null); // 用于自动消失 RECONNECTED 状态 const processedEventIds = useRef(new Set()); // 用于Socket层去重,记录已处理的事件ID const MAX_PROCESSED_IDS = 1000; // 最多保留1000个ID,避免内存泄漏 + const notificationTimers = useRef(new Map()); // 跟踪所有通知的自动关闭定时器 // ⚡ 使用权限引导管理 Hook const { shouldShowGuide, markGuideAsShown } = usePermissionGuide(); @@ -71,9 +72,20 @@ export const NotificationProvider = ({ children }) => { try { audioRef.current = new Audio(notificationSound); audioRef.current.volume = 0.5; + logger.info('NotificationContext', 'Audio initialized'); } catch (error) { logger.error('NotificationContext', 'Audio initialization failed', error); } + + // 清理函数:释放音频资源 + return () => { + if (audioRef.current) { + audioRef.current.pause(); + audioRef.current.src = ''; + audioRef.current = null; + logger.info('NotificationContext', 'Audio resources cleaned up'); + } + }; }, []); /** @@ -104,6 +116,13 @@ export const NotificationProvider = ({ children }) => { const removeNotification = useCallback((id, wasClicked = false) => { logger.info('NotificationContext', 'Removing notification', { id, wasClicked }); + // 清理对应的定时器 + if (notificationTimers.current.has(id)) { + clearTimeout(notificationTimers.current.get(id)); + notificationTimers.current.delete(id); + logger.info('NotificationContext', 'Cleared auto-close timer', { id }); + } + // 监控埋点:追踪关闭(非点击的情况) setNotifications(prev => { const notification = prev.find(n => n.id === id); @@ -119,6 +138,14 @@ export const NotificationProvider = ({ children }) => { */ const clearAllNotifications = useCallback(() => { logger.info('NotificationContext', 'Clearing all notifications'); + + // 清理所有定时器 + notificationTimers.current.forEach((timerId, id) => { + clearTimeout(timerId); + logger.info('NotificationContext', 'Cleared timer during clear all', { id }); + }); + notificationTimers.current.clear(); + setNotifications([]); }, []); @@ -446,9 +473,16 @@ export const NotificationProvider = ({ children }) => { // 自动关闭 if (newNotification.autoClose && newNotification.autoClose > 0) { - setTimeout(() => { + const timerId = setTimeout(() => { removeNotification(newNotification.id); }, newNotification.autoClose); + + // 将定时器ID保存到Map中 + notificationTimers.current.set(newNotification.id, timerId); + logger.info('NotificationContext', 'Set auto-close timer', { + id: newNotification.id, + delay: newNotification.autoClose + }); } }, [playNotificationSound, removeNotification]); @@ -548,34 +582,11 @@ export const NotificationProvider = ({ children }) => { const isPageHidden = document.hidden; // 页面是否在后台 - // ========== 原分发策略(按优先级区分)- 已废弃 ========== - // 策略 1: 紧急通知 - 双重保障(浏览器 + 网页) - // if (priority === PRIORITY_LEVELS.URGENT) { - // logger.info('NotificationContext', 'Urgent notification: sending browser + web'); - // // 总是发送浏览器通知 - // sendBrowserNotification(newNotification); - // // 如果在前台,也显示网页通知 - // if (!isPageHidden) { - // addWebNotification(newNotification); - // } - // } - // 策略 2: 重要通知 - 智能切换(后台=浏览器,前台=网页) - // else if (priority === PRIORITY_LEVELS.IMPORTANT) { - // if (isPageHidden) { - // logger.info('NotificationContext', 'Important notification (background): sending browser'); - // sendBrowserNotification(newNotification); - // } else { - // logger.info('NotificationContext', 'Important notification (foreground): sending web'); - // addWebNotification(newNotification); - // } - // } - // 策略 3: 普通通知 - 仅网页通知 - // else { - // logger.info('NotificationContext', 'Normal notification: sending web only'); - // addWebNotification(newNotification); - // } - - // ========== 新分发策略(仅区分前后台) ========== + // ========== 通知分发策略(区分前后台) ========== + // 策略: 根据页面可见性智能分发通知 + // - 页面在后台: 发送浏览器通知(系统级提醒) + // - 页面在前台: 发送网页通知(页面内 Toast) + // 注: 不再区分优先级,统一使用前后台策略 if (isPageHidden) { // 页面在后台:发送浏览器通知 logger.info('NotificationContext', 'Page hidden: sending browser notification'); @@ -688,7 +699,18 @@ export const NotificationProvider = ({ children }) => { logger.info('NotificationContext', 'Received new event', data); // ========== Socket层去重检查 ========== - const eventId = data.id || `${data.type}_${data.publishTime}`; + // 生成更健壮的事件ID + const eventId = data.id || + `${data.type || 'unknown'}_${data.publishTime || Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + // 如果缺少原始ID,记录警告 + if (!data.id) { + logger.warn('NotificationContext', 'Event missing ID, generated fallback', { + eventId, + eventType: data.type, + title: data.title + }); + } if (processedEventIds.current.has(eventId)) { logger.debug('NotificationContext', 'Duplicate event ignored at socket level', { eventId }); @@ -744,6 +766,19 @@ export const NotificationProvider = ({ children }) => { return () => { logger.info('NotificationContext', 'Cleaning up socket connection'); + // 清理 reconnected 状态定时器 + if (reconnectedTimerRef.current) { + clearTimeout(reconnectedTimerRef.current); + reconnectedTimerRef.current = null; + } + + // 清理所有通知的自动关闭定时器 + notificationTimers.current.forEach((timerId, id) => { + clearTimeout(timerId); + logger.info('NotificationContext', 'Cleared timer during cleanup', { id }); + }); + notificationTimers.current.clear(); + socket.off('connect'); socket.off('disconnect'); socket.off('connect_error'); @@ -837,6 +872,48 @@ export const NotificationProvider = ({ children }) => { } }, []); + /** + * 同步浏览器通知权限状态 + * 场景: + * 1. 用户在其他标签页授权后返回 + * 2. 用户在浏览器设置中修改权限 + * 3. 页面长时间打开后权限状态变化 + */ + useEffect(() => { + const checkPermission = () => { + const current = browserNotificationService.getPermissionStatus(); + if (current !== browserPermission) { + logger.info('NotificationContext', 'Browser permission changed', { + old: browserPermission, + new: current + }); + setBrowserPermission(current); + + // 如果权限被授予,显示成功提示 + if (current === 'granted' && browserPermission !== 'granted') { + toast({ + title: '桌面通知已开启', + description: '您现在可以在后台接收重要通知', + status: 'success', + duration: 3000, + isClosable: true, + }); + } + } + }; + + // 页面聚焦时检查 + window.addEventListener('focus', checkPermission); + + // 定期检查(可选,用于捕获浏览器设置中的变化) + const intervalId = setInterval(checkPermission, 30000); // 每30秒检查一次 + + return () => { + window.removeEventListener('focus', checkPermission); + clearInterval(intervalId); + }; + }, [browserPermission, toast]); + const value = { notifications, isConnected,