feat: 添加消息推送能力
This commit is contained in:
208
src/services/browserNotificationService.js
Normal file
208
src/services/browserNotificationService.js
Normal file
@@ -0,0 +1,208 @@
|
||||
// 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 {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');
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.permission !== 'granted') {
|
||||
logger.warn('browserNotificationService', 'Permission not granted');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// 关闭相同 tag 的旧通知
|
||||
if (tag && this.activeNotifications.has(tag)) {
|
||||
const oldNotification = this.activeNotifications.get(tag);
|
||||
oldNotification.close();
|
||||
}
|
||||
|
||||
// 创建通知
|
||||
const notification = new Notification(title, {
|
||||
body,
|
||||
icon,
|
||||
badge: '/badge.png',
|
||||
tag: tag || `notification_${Date.now()}`,
|
||||
requireInteraction,
|
||||
data,
|
||||
silent: false, // 允许声音
|
||||
});
|
||||
|
||||
// 存储通知引用
|
||||
if (tag) {
|
||||
this.activeNotifications.set(tag, notification);
|
||||
}
|
||||
|
||||
// 自动关闭
|
||||
if (autoClose > 0 && !requireInteraction) {
|
||||
setTimeout(() => {
|
||||
notification.close();
|
||||
}, autoClose);
|
||||
}
|
||||
|
||||
// 通知关闭时清理引用
|
||||
notification.onclose = () => {
|
||||
if (tag) {
|
||||
this.activeNotifications.delete(tag);
|
||||
}
|
||||
};
|
||||
|
||||
logger.info('browserNotificationService', 'Notification sent', { title, tag });
|
||||
return notification;
|
||||
} catch (error) {
|
||||
logger.error('browserNotificationService', 'sendNotification', error);
|
||||
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;
|
||||
Reference in New Issue
Block a user