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

@@ -7,6 +7,8 @@ import React, { createContext, useContext, useState, useEffect, useCallback, use
import { logger } from '../utils/logger';
import socket, { SOCKET_TYPE } from '../services/socket';
import notificationSound from '../assets/sounds/notification.wav';
import { browserNotificationService } from '../services/browserNotificationService';
import { PRIORITY_LEVELS, NOTIFICATION_CONFIG } from '../constants/notificationTypes';
// 创建通知上下文
const NotificationContext = createContext();
@@ -25,6 +27,7 @@ export const NotificationProvider = ({ children }) => {
const [notifications, setNotifications] = useState([]);
const [isConnected, setIsConnected] = useState(false);
const [soundEnabled, setSoundEnabled] = useState(true);
const [browserPermission, setBrowserPermission] = useState(browserNotificationService.getPermissionStatus());
const audioRef = useRef(null);
// 初始化音频
@@ -57,54 +60,6 @@ export const NotificationProvider = ({ children }) => {
}
}, [soundEnabled]);
/**
* 添加通知到队列
* @param {object} notification - 通知对象
*/
const addNotification = useCallback((notification) => {
const newNotification = {
id: notification.id || `notif_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
type: notification.type || 'info',
severity: notification.severity || 'info',
title: notification.title || '通知',
message: notification.message || '',
timestamp: notification.timestamp || Date.now(),
autoClose: notification.autoClose !== undefined ? notification.autoClose : 8000,
...notification,
};
logger.info('NotificationContext', 'Adding notification', newNotification);
// 新消息插入到数组开头最多保留5条
setNotifications(prev => {
const updated = [newNotification, ...prev];
const maxNotifications = 5;
// 如果超过最大数量,移除最旧的(数组末尾)
if (updated.length > maxNotifications) {
const removed = updated.slice(maxNotifications);
removed.forEach(old => {
logger.info('NotificationContext', 'Auto-removing old notification', { id: old.id });
});
return updated.slice(0, maxNotifications);
}
return updated;
});
// 播放音效
playNotificationSound();
// 自动关闭
if (newNotification.autoClose && newNotification.autoClose > 0) {
setTimeout(() => {
removeNotification(newNotification.id);
}, newNotification.autoClose);
}
return newNotification.id;
}, [playNotificationSound]);
/**
* 移除通知
* @param {string} id - 通知ID
@@ -133,6 +88,144 @@ export const NotificationProvider = ({ children }) => {
});
}, []);
/**
* 请求浏览器通知权限
*/
const requestBrowserPermission = useCallback(async () => {
logger.info('NotificationContext', 'Requesting browser notification permission');
const permission = await browserNotificationService.requestPermission();
setBrowserPermission(permission);
return permission;
}, []);
/**
* 发送浏览器通知
*/
const sendBrowserNotification = useCallback((notificationData) => {
if (browserPermission !== 'granted') {
logger.warn('NotificationContext', 'Browser permission not granted');
return;
}
const { priority, title, content, link, type } = notificationData;
// 生成唯一 tag
const tag = `${type}_${Date.now()}`;
// 判断是否需要用户交互(紧急通知不自动关闭)
const requireInteraction = priority === PRIORITY_LEVELS.URGENT;
// 发送浏览器通知
const notification = browserNotificationService.sendNotification({
title: title || '新通知',
body: content || '',
tag,
requireInteraction,
data: { link },
autoClose: requireInteraction ? 0 : 8000,
});
// 设置点击处理(聚焦窗口并跳转)
if (notification && link) {
notification.onclick = () => {
window.focus();
// 使用 window.location 跳转(不需要 React Router
window.location.hash = link;
notification.close();
};
}
logger.info('NotificationContext', 'Browser notification sent', { title, tag });
}, [browserPermission]);
/**
* 添加网页通知(内部方法)
*/
const addWebNotification = useCallback((newNotification) => {
// 新消息插入到数组开头,最多保留 maxHistory 条
setNotifications(prev => {
const updated = [newNotification, ...prev];
const maxNotifications = NOTIFICATION_CONFIG.maxHistory;
// 如果超过最大数量,移除最旧的(数组末尾)
if (updated.length > maxNotifications) {
const removed = updated.slice(maxNotifications);
removed.forEach(old => {
logger.info('NotificationContext', 'Auto-removing old notification', { id: old.id });
});
return updated.slice(0, maxNotifications);
}
return updated;
});
// 播放音效
playNotificationSound();
// 自动关闭
if (newNotification.autoClose && newNotification.autoClose > 0) {
setTimeout(() => {
removeNotification(newNotification.id);
}, newNotification.autoClose);
}
}, [playNotificationSound, removeNotification]);
/**
* 添加通知到队列
* @param {object} notification - 通知对象
*/
const addNotification = useCallback((notification) => {
// 根据优先级获取自动关闭时长
const priority = notification.priority || PRIORITY_LEVELS.NORMAL;
const defaultAutoClose = NOTIFICATION_CONFIG.autoCloseDuration[priority];
const newNotification = {
id: notification.id || `notif_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
type: notification.type || 'info',
severity: notification.severity || 'info',
title: notification.title || '通知',
message: notification.message || '',
timestamp: notification.timestamp || Date.now(),
priority: priority,
autoClose: notification.autoClose !== undefined ? notification.autoClose : defaultAutoClose,
...notification,
};
logger.info('NotificationContext', 'Adding notification', newNotification);
const isPageHidden = document.hidden; // 页面是否在后台
// ========== 智能分发策略 ==========
// 策略 1: 紧急通知 - 双重保障(浏览器 + 网页)
if (priority === PRIORITY_LEVELS.URGENT) {
logger.info('NotificationContext', 'Urgent notification: sending browser + web');
// 总是发送浏览器通知
sendBrowserNotification(newNotification);
// 如果在前台,也显示网页通知
if (!isPageHidden) {
addWebNotification(newNotification);
}
}
// 策略 2: 重要通知 - 智能切换(后台=浏览器,前台=网页)
else if (priority === PRIORITY_LEVELS.IMPORTANT) {
if (isPageHidden) {
logger.info('NotificationContext', 'Important notification (background): sending browser');
sendBrowserNotification(newNotification);
} else {
logger.info('NotificationContext', 'Important notification (foreground): sending web');
addWebNotification(newNotification);
}
}
// 策略 3: 普通通知 - 仅网页通知
else {
logger.info('NotificationContext', 'Normal notification: sending web only');
addWebNotification(newNotification);
}
return newNotification.id;
}, [sendBrowserNotification, addWebNotification]);
// 连接到 Socket 服务
useEffect(() => {
logger.info('NotificationContext', 'Initializing socket connection...');
@@ -147,9 +240,10 @@ export const NotificationProvider = ({ children }) => {
// 如果使用 mock可以启动定期推送
if (SOCKET_TYPE === 'MOCK') {
// 启动模拟推送:每20秒推送1-2条消息
socket.startMockPush(20000, 2);
logger.info('NotificationContext', 'Mock push started');
// 启动模拟推送:使用配置的间隔和数量
const { interval, maxBatch } = NOTIFICATION_CONFIG.mockPush;
socket.startMockPush(interval, maxBatch);
logger.info('NotificationContext', 'Mock push started', { interval, maxBatch });
}
});
@@ -191,10 +285,12 @@ export const NotificationProvider = ({ children }) => {
notifications,
isConnected,
soundEnabled,
browserPermission,
addNotification,
removeNotification,
clearAllNotifications,
toggleSound,
requestBrowserPermission,
};
return (