feat: 消息通知能力测试
This commit is contained in:
@@ -3,6 +3,7 @@ import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useToast } from '@chakra-ui/react';
|
||||
import { logger } from '../utils/logger';
|
||||
import { useNotification } from '../contexts/NotificationContext';
|
||||
|
||||
// 创建认证上下文
|
||||
const AuthContext = createContext();
|
||||
@@ -23,6 +24,7 @@ export const AuthProvider = ({ children }) => {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const toast = useToast();
|
||||
const { showWelcomeGuide } = useNotification();
|
||||
|
||||
// 检查Session状态
|
||||
const checkSession = async () => {
|
||||
@@ -156,6 +158,11 @@ export const AuthProvider = ({ children }) => {
|
||||
// isClosable: true,
|
||||
// });
|
||||
|
||||
// ⚡ 登录成功后显示欢迎引导(延迟2秒,避免与登录Toast冲突)
|
||||
setTimeout(() => {
|
||||
showWelcomeGuide();
|
||||
}, 2000);
|
||||
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
@@ -203,6 +210,11 @@ export const AuthProvider = ({ children }) => {
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
// ⚡ 注册成功后显示欢迎引导(延迟2秒)
|
||||
setTimeout(() => {
|
||||
showWelcomeGuide();
|
||||
}, 2000);
|
||||
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
@@ -252,6 +264,11 @@ export const AuthProvider = ({ children }) => {
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
// ⚡ 注册成功后显示欢迎引导(延迟2秒)
|
||||
setTimeout(() => {
|
||||
showWelcomeGuide();
|
||||
}, 2000);
|
||||
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
@@ -299,6 +316,11 @@ export const AuthProvider = ({ children }) => {
|
||||
isClosable: true,
|
||||
});
|
||||
|
||||
// ⚡ 注册成功后显示欢迎引导(延迟2秒)
|
||||
setTimeout(() => {
|
||||
showWelcomeGuide();
|
||||
}, 2000);
|
||||
|
||||
return { success: true };
|
||||
|
||||
} catch (error) {
|
||||
|
||||
@@ -60,6 +60,8 @@ export const NotificationProvider = ({ children }) => {
|
||||
const [maxReconnectAttempts, setMaxReconnectAttempts] = useState(Infinity);
|
||||
const audioRef = useRef(null);
|
||||
const reconnectedTimerRef = useRef(null); // 用于自动消失 RECONNECTED 状态
|
||||
const processedEventIds = useRef(new Set()); // 用于Socket层去重,记录已处理的事件ID
|
||||
const MAX_PROCESSED_IDS = 1000; // 最多保留1000个ID,避免内存泄漏
|
||||
|
||||
// ⚡ 使用权限引导管理 Hook
|
||||
const { shouldShowGuide, markGuideAsShown } = usePermissionGuide();
|
||||
@@ -435,12 +437,23 @@ export const NotificationProvider = ({ children }) => {
|
||||
* @param {object} notification - 通知对象
|
||||
*/
|
||||
const addNotification = useCallback(async (notification) => {
|
||||
// ========== 显示层去重检查 ==========
|
||||
const notificationId = notification.id || `notif_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
// 检查当前显示队列中是否已存在该通知
|
||||
const isDuplicate = notifications.some(n => n.id === notificationId);
|
||||
if (isDuplicate) {
|
||||
logger.debug('NotificationContext', 'Duplicate notification ignored at display level', { id: notificationId });
|
||||
return notificationId; // 返回ID但不显示
|
||||
}
|
||||
// ========== 显示层去重检查结束 ==========
|
||||
|
||||
// 根据优先级获取自动关闭时长
|
||||
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)}`,
|
||||
id: notificationId, // 使用预先生成的ID
|
||||
type: notification.type || 'info',
|
||||
severity: notification.severity || 'info',
|
||||
title: notification.title || '通知',
|
||||
@@ -453,94 +466,108 @@ export const NotificationProvider = ({ children }) => {
|
||||
|
||||
logger.info('NotificationContext', 'Adding notification', newNotification);
|
||||
|
||||
// ========== 智能权限请求策略 ==========
|
||||
// 首次收到重要/紧急通知时,自动请求桌面通知权限
|
||||
if (priority === PRIORITY_LEVELS.URGENT || priority === PRIORITY_LEVELS.IMPORTANT) {
|
||||
if (browserPermission === 'default' && !hasRequestedPermission) {
|
||||
logger.info('NotificationContext', 'First important notification, requesting browser permission');
|
||||
await requestBrowserPermission();
|
||||
}
|
||||
// 如果权限被拒绝,提示用户可以开启
|
||||
else if (browserPermission === 'denied' && hasRequestedPermission) {
|
||||
// 显示带"开启"按钮的 Toast(仅重要/紧急通知)
|
||||
const toastId = 'enable-notification-toast';
|
||||
if (!toast.isActive(toastId)) {
|
||||
toast({
|
||||
id: toastId,
|
||||
title: newNotification.title,
|
||||
description: '💡 开启桌面通知以便后台接收',
|
||||
status: 'warning',
|
||||
duration: 10000,
|
||||
isClosable: true,
|
||||
position: 'top',
|
||||
render: ({ onClose }) => (
|
||||
<Box
|
||||
p={4}
|
||||
bg="orange.500"
|
||||
color="white"
|
||||
borderRadius="md"
|
||||
boxShadow="lg"
|
||||
>
|
||||
<HStack spacing={3} align="start">
|
||||
<Box flex={1}>
|
||||
<Text fontWeight="bold" mb={1}>
|
||||
{newNotification.title}
|
||||
</Text>
|
||||
<Text fontSize="sm" opacity={0.9}>
|
||||
💡 开启桌面通知以便后台接收
|
||||
</Text>
|
||||
</Box>
|
||||
<Button
|
||||
size="sm"
|
||||
colorScheme="whiteAlpha"
|
||||
onClick={() => {
|
||||
requestBrowserPermission();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
开启
|
||||
</Button>
|
||||
<CloseButton onClick={onClose} />
|
||||
// ========== 增强权限请求策略 ==========
|
||||
// 只要收到通知,就检查并提示用户授权
|
||||
|
||||
// 如果权限是default(未授权),自动请求
|
||||
if (browserPermission === 'default' && !hasRequestedPermission) {
|
||||
logger.info('NotificationContext', 'Auto-requesting browser permission on notification');
|
||||
await requestBrowserPermission();
|
||||
}
|
||||
// 如果权限是denied(已拒绝),提供设置指引
|
||||
else if (browserPermission === 'denied') {
|
||||
const toastId = 'browser-permission-denied-guide';
|
||||
if (!toast.isActive(toastId)) {
|
||||
toast({
|
||||
id: toastId,
|
||||
duration: 12000,
|
||||
isClosable: true,
|
||||
position: 'top',
|
||||
render: ({ onClose }) => (
|
||||
<Box
|
||||
p={4}
|
||||
bg="orange.500"
|
||||
color="white"
|
||||
borderRadius="md"
|
||||
boxShadow="lg"
|
||||
maxW="400px"
|
||||
>
|
||||
<VStack spacing={3} align="stretch">
|
||||
<HStack spacing={2}>
|
||||
<Icon as={BellIcon} boxSize={5} />
|
||||
<Text fontWeight="bold" fontSize="md">
|
||||
浏览器通知已被拒绝
|
||||
</Text>
|
||||
</HStack>
|
||||
</Box>
|
||||
),
|
||||
});
|
||||
}
|
||||
<Text fontSize="sm" opacity={0.9}>
|
||||
{newNotification.title}
|
||||
</Text>
|
||||
<Text fontSize="xs" opacity={0.8}>
|
||||
💡 如需接收桌面通知,请在浏览器设置中允许通知权限
|
||||
</Text>
|
||||
<VStack spacing={1} align="start" fontSize="xs" opacity={0.7}>
|
||||
<Text>Chrome: 地址栏左侧 🔒 → 网站设置 → 通知</Text>
|
||||
<Text>Safari: 偏好设置 → 网站 → 通知</Text>
|
||||
<Text>Edge: 地址栏右侧 ⋯ → 网站权限 → 通知</Text>
|
||||
</VStack>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
colorScheme="whiteAlpha"
|
||||
onClick={onClose}
|
||||
alignSelf="flex-end"
|
||||
>
|
||||
知道了
|
||||
</Button>
|
||||
</VStack>
|
||||
</Box>
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const isPageHidden = document.hidden; // 页面是否在后台
|
||||
|
||||
// ========== 智能分发策略 ==========
|
||||
|
||||
// ========== 原分发策略(按优先级区分)- 已废弃 ==========
|
||||
// 策略 1: 紧急通知 - 双重保障(浏览器 + 网页)
|
||||
if (priority === PRIORITY_LEVELS.URGENT) {
|
||||
logger.info('NotificationContext', 'Urgent notification: sending browser + web');
|
||||
// 总是发送浏览器通知
|
||||
sendBrowserNotification(newNotification);
|
||||
// 如果在前台,也显示网页通知
|
||||
if (!isPageHidden) {
|
||||
addWebNotification(newNotification);
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
// 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');
|
||||
// else {
|
||||
// logger.info('NotificationContext', 'Normal notification: sending web only');
|
||||
// addWebNotification(newNotification);
|
||||
// }
|
||||
|
||||
// ========== 新分发策略(仅区分前后台) ==========
|
||||
if (isPageHidden) {
|
||||
// 页面在后台:发送浏览器通知
|
||||
logger.info('NotificationContext', 'Page hidden: sending browser notification');
|
||||
sendBrowserNotification(newNotification);
|
||||
} else {
|
||||
// 页面在前台:发送网页通知
|
||||
logger.info('NotificationContext', 'Page visible: sending web notification');
|
||||
addWebNotification(newNotification);
|
||||
}
|
||||
|
||||
return newNotification.id;
|
||||
}, [sendBrowserNotification, addWebNotification, browserPermission, hasRequestedPermission, requestBrowserPermission]);
|
||||
}, [notifications, toast, sendBrowserNotification, addWebNotification, browserPermission, hasRequestedPermission, requestBrowserPermission]);
|
||||
|
||||
// 连接到 Socket 服务
|
||||
useEffect(() => {
|
||||
@@ -624,6 +651,27 @@ export const NotificationProvider = ({ children }) => {
|
||||
socket.on('new_event', (data) => {
|
||||
logger.info('NotificationContext', 'Received new event', data);
|
||||
|
||||
// ========== Socket层去重检查 ==========
|
||||
const eventId = data.id || `${data.type}_${data.publishTime}`;
|
||||
|
||||
if (processedEventIds.current.has(eventId)) {
|
||||
logger.debug('NotificationContext', 'Duplicate event ignored at socket level', { eventId });
|
||||
return; // 重复事件,直接忽略
|
||||
}
|
||||
|
||||
// 记录已处理的事件ID
|
||||
processedEventIds.current.add(eventId);
|
||||
|
||||
// 限制Set大小,避免内存泄漏
|
||||
if (processedEventIds.current.size > MAX_PROCESSED_IDS) {
|
||||
const idsArray = Array.from(processedEventIds.current);
|
||||
processedEventIds.current = new Set(idsArray.slice(-MAX_PROCESSED_IDS));
|
||||
logger.debug('NotificationContext', 'Cleaned up old processed event IDs', {
|
||||
kept: MAX_PROCESSED_IDS
|
||||
});
|
||||
}
|
||||
// ========== Socket层去重检查结束 ==========
|
||||
|
||||
// 使用适配器转换事件格式
|
||||
const notification = adaptEventToNotification(data);
|
||||
addNotification(notification);
|
||||
|
||||
Reference in New Issue
Block a user