Files
vf_react/src/services/browserNotificationService.js
2025-11-10 12:45:34 +08:00

259 lines
8.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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<string>} 权限状态
*/
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;