// src/services/browserNotificationService.js /** * 浏览器原生通知服务 * 提供系统级通知功能(Web Notifications API) */ import { logger } from '../utils/logger'; class BrowserNotificationService { constructor() { this.permission = this.isSupported() ? Notification.permission : 'denied'; this.activeNotifications = new Map(); // 存储活跃的通知 } /** * 检查浏览器是否支持通知 API */ isSupported() { return 'Notification' in window; } /** * 获取当前权限状态 * @returns {string} 'granted' | 'denied' | 'default' */ getPermissionStatus() { if (!this.isSupported()) { return 'denied'; } return Notification.permission; } /** * 检查是否有通知权限 * @returns {boolean} */ hasPermission() { return this.isSupported() && Notification.permission === 'granted'; } /** * 请求通知权限 * @returns {Promise} 权限状态 */ async requestPermission() { if (!this.isSupported()) { logger.warn('browserNotificationService', 'Notifications not supported'); return 'denied'; } if (this.permission === 'granted') { logger.info('browserNotificationService', 'Permission already granted'); return 'granted'; } try { const permission = await Notification.requestPermission(); this.permission = permission; logger.info('browserNotificationService', `Permission ${permission}`); return permission; } catch (error) { logger.error('browserNotificationService', 'requestPermission', error); return 'denied'; } } /** * 发送浏览器通知 * @param {Object} options 通知选项 * @param {string} options.title 标题 * @param {string} options.body 内容 * @param {string} options.icon 图标路径 * @param {string} options.tag 标签(防止重复) * @param {boolean} options.requireInteraction 是否需要用户交互才关闭 * @param {Object} options.data 自定义数据(如跳转链接) * @param {number} options.autoClose 自动关闭时间(毫秒) * @returns {Notification|null} 通知对象 */ sendNotification({ title, body, icon = '/logo192.png', tag, requireInteraction = false, data = {}, autoClose = 0, }) { // 详细日志:检查支持状态 if (!this.isSupported()) { logger.warn('browserNotificationService', 'Notifications not supported'); console.warn('[browserNotificationService] ❌ 浏览器不支持通知 API'); return null; } // 详细日志:检查权限状态 const currentPermission = Notification.permission; console.log('[browserNotificationService] 当前权限状态:', currentPermission); if (currentPermission !== 'granted') { logger.warn('browserNotificationService', 'Permission not granted', { permission: currentPermission }); console.warn(`[browserNotificationService] ❌ 权限未授予: ${currentPermission}`); return null; } console.log('[browserNotificationService] ✅ 准备发送通知:', { title, body, tag, requireInteraction, autoClose }); try { // 关闭相同 tag 的旧通知 if (tag && this.activeNotifications.has(tag)) { const oldNotification = this.activeNotifications.get(tag); oldNotification.close(); console.log('[browserNotificationService] 关闭旧通知:', tag); } // 创建通知 const finalTag = tag || `notification_${Date.now()}`; console.log('[browserNotificationService] 创建通知对象...'); const notification = new Notification(title, { body, icon, badge: '/badge.png', tag: finalTag, requireInteraction, data, silent: false, // 允许声音 }); console.log('[browserNotificationService] ✅ 通知对象已创建:', notification); // 存储通知引用 if (tag) { this.activeNotifications.set(tag, notification); console.log('[browserNotificationService] 通知已存储到活跃列表'); } // 自动关闭 if (autoClose > 0 && !requireInteraction) { console.log(`[browserNotificationService] 设置自动关闭: ${autoClose}ms`); setTimeout(() => { notification.close(); console.log('[browserNotificationService] 通知已自动关闭'); }, autoClose); } // 通知关闭时清理引用 notification.onclose = () => { console.log('[browserNotificationService] 通知被关闭:', finalTag); if (tag) { this.activeNotifications.delete(tag); } }; // 通知点击事件 notification.onclick = (event) => { console.log('[browserNotificationService] 通知被点击:', finalTag, data); }; // 通知显示事件 notification.onshow = () => { console.log('[browserNotificationService] ✅ 通知已显示:', finalTag); }; // 通知错误事件 notification.onerror = (error) => { console.error('[browserNotificationService] ❌ 通知显示错误:', error); }; logger.info('browserNotificationService', 'Notification sent', { title, tag: finalTag }); console.log('[browserNotificationService] ✅ 通知发送成功!'); return notification; } catch (error) { logger.error('browserNotificationService', 'sendNotification', error); console.error('[browserNotificationService] ❌ 发送通知时发生错误:', error); console.error('[browserNotificationService] 错误详情:', { name: error.name, message: error.message, stack: error.stack }); return null; } } /** * 设置通知点击处理 * @param {Notification} notification 通知对象 * @param {Function} navigate React Router navigate 函数 */ setupClickHandler(notification, navigate) { if (!notification) return; notification.onclick = (event) => { event.preventDefault(); // 聚焦窗口 window.focus(); // 跳转链接 if (notification.data?.link) { navigate(notification.data.link); } // 关闭通知 notification.close(); logger.info('browserNotificationService', 'Notification clicked', notification.data); }; } /** * 关闭所有活跃通知 */ closeAll() { this.activeNotifications.forEach(notification => { notification.close(); }); this.activeNotifications.clear(); logger.info('browserNotificationService', 'All notifications closed'); } /** * 根据通知数据发送浏览器通知 * @param {Object} notificationData 通知数据 * @param {Function} navigate React Router navigate 函数 */ sendFromNotificationData(notificationData, navigate) { const { type, priority, title, content, link, extra } = notificationData; // 生成唯一 tag const tag = `${type}_${Date.now()}`; // 判断是否需要用户交互(紧急通知不自动关闭) const requireInteraction = priority === 'urgent'; // 发送通知 const notification = this.sendNotification({ title: title || '新通知', body: content || '', tag, requireInteraction, data: { link, ...extra }, autoClose: requireInteraction ? 0 : 8000, // 紧急通知不自动关闭 }); // 设置点击处理 if (notification && navigate) { this.setupClickHandler(notification, navigate); } return notification; } } // 导出单例 export const browserNotificationService = new BrowserNotificationService(); export default browserNotificationService;