259 lines
8.5 KiB
JavaScript
259 lines
8.5 KiB
JavaScript
// 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;
|