feat: 添加消息推送能力
This commit is contained in:
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user