feat: 添加消息推送能力

This commit is contained in:
zdl
2025-10-21 15:48:38 +08:00
parent 955e0db740
commit 38499ce650
8 changed files with 2485 additions and 417 deletions

View 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;