feat: 消息通知能力测试

This commit is contained in:
zdl
2025-10-23 15:25:36 +08:00
parent 45b88309b3
commit 1ba8b8fd2f
4 changed files with 477 additions and 270 deletions

View File

@@ -299,8 +299,8 @@ const NotificationItem = React.memo(({ notification, onClose, isNewest = false }
const isDark = useColorModeValue(false, true);
const priorityBgOpacity = getPriorityBgOpacity(priority, isDark);
// 根据优先级调整背景色深度
const getPriorityBgColor = () => {
// 根据优先级调整背景色深度(使用 useMemo 缓存计算结果)
const priorityBgColor = useMemo(() => {
const colorScheme = typeConfig.colorScheme;
// 亮色模式:根据优先级使用不同深度的颜色
if (!isDark) {
@@ -323,31 +323,41 @@ const NotificationItem = React.memo(({ notification, onClose, isNewest = false }
return 'gray.800';
}
}
};
}, [isDark, priority, typeConfig]);
// 颜色配置 - 支持亮色/暗色模式(使用 useMemo 优化)
// 颜色配置 - 支持亮色/暗色模式
// ⚠️ 必须在组件顶层调用 useColorModeValue不能在 useMemo 内部调用
const borderColor = useColorModeValue(
typeConfig.borderColor,
typeConfig.darkBorderColor || `${typeConfig.colorScheme}.400`
);
const iconColor = useColorModeValue(
typeConfig.iconColor,
typeConfig.darkIconColor || `${typeConfig.colorScheme}.300`
);
const textColor = useColorModeValue('gray.800', 'gray.100');
const subTextColor = useColorModeValue('gray.600', 'gray.300');
const metaTextColor = useColorModeValue('gray.500', 'gray.500');
const hoverBgColor = useColorModeValue(
typeConfig.hoverBg,
typeConfig.darkHoverBg || `${typeConfig.colorScheme}.700`
);
const closeButtonHoverBgColor = useColorModeValue(
`${typeConfig.colorScheme}.200`,
`${typeConfig.colorScheme}.700`
);
// 使用 useMemo 缓存颜色对象(避免不必要的重新创建)
const colors = useMemo(() => ({
bg: getPriorityBgColor(),
border: useColorModeValue(
typeConfig.borderColor,
typeConfig.darkBorderColor || `${typeConfig.colorScheme}.400`
),
icon: useColorModeValue(
typeConfig.iconColor,
typeConfig.darkIconColor || `${typeConfig.colorScheme}.300`
),
text: useColorModeValue('gray.800', 'gray.100'),
subText: useColorModeValue('gray.600', 'gray.300'),
metaText: useColorModeValue('gray.500', 'gray.500'),
hoverBg: useColorModeValue(
typeConfig.hoverBg,
typeConfig.darkHoverBg || `${typeConfig.colorScheme}.700`
),
closeButtonHoverBg: useColorModeValue(
`${typeConfig.colorScheme}.200`,
`${typeConfig.colorScheme}.700`
),
}), [isDark, priority, typeConfig]);
bg: priorityBgColor,
border: borderColor,
icon: iconColor,
text: textColor,
subText: subTextColor,
metaText: metaTextColor,
hoverBg: hoverBgColor,
closeButtonHoverBg: closeButtonHoverBgColor,
}), [priorityBgColor, borderColor, iconColor, textColor, subTextColor, metaTextColor, hoverBgColor, closeButtonHoverBgColor]);
// 点击处理(只有真正可点击时才执行)- 使用 useCallback 优化
const handleClick = useCallback(() => {
@@ -636,6 +646,11 @@ const NotificationContainer = () => {
}
}, [notifications]);
// ⚠️ 颜色配置 - 必须在条件return之前调用所有Hooks
const collapseBg = useColorModeValue('gray.100', 'gray.700');
const collapseHoverBg = useColorModeValue('gray.200', 'gray.600');
const collapseTextColor = useColorModeValue('gray.700', 'gray.200');
// 如果没有通知,不渲染
if (notifications.length === 0) {
return null;
@@ -647,11 +662,6 @@ const NotificationContainer = () => {
const visibleNotifications = isExpanded ? notifications : notifications.slice(0, maxVisible);
const hiddenCount = notifications.length - maxVisible;
// 颜色配置
const collapseBg = useColorModeValue('gray.100', 'gray.700');
const collapseHoverBg = useColorModeValue('gray.200', 'gray.600');
const collapseTextColor = useColorModeValue('gray.700', 'gray.200');
// 构建无障碍描述
const containerAriaLabel = hasMore
? `通知中心,共有 ${notifications.length} 条通知,当前显示 ${visibleNotifications.length} 条,${isExpanded ? '已展开全部' : `还有 ${hiddenCount} 条折叠`}。使用Tab键导航Enter键或空格键查看详情。`

View File

@@ -4,7 +4,7 @@
* 用于手动测试4种通知类型
*/
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import {
Box,
Button,
@@ -16,8 +16,15 @@ import {
useDisclosure,
Badge,
Divider,
Alert,
AlertIcon,
AlertTitle,
AlertDescription,
Code,
UnorderedList,
ListItem,
} from '@chakra-ui/react';
import { MdNotifications, MdClose, MdVolumeOff, MdVolumeUp, MdCampaign, MdTrendingUp, MdArticle, MdAssessment } from 'react-icons/md';
import { MdNotifications, MdClose, MdVolumeOff, MdVolumeUp, MdCampaign, MdTrendingUp, MdArticle, MdAssessment, MdWarning } from 'react-icons/md';
import { useNotification } from '../../contexts/NotificationContext';
import { SOCKET_TYPE } from '../../services/socket';
import { NOTIFICATION_TYPES, PRIORITY_LEVELS } from '../../constants/notificationTypes';
@@ -27,6 +34,62 @@ const NotificationTestTool = () => {
const { addNotification, soundEnabled, toggleSound, isConnected, clearAllNotifications, notifications, browserPermission, requestBrowserPermission } = useNotification();
const [testCount, setTestCount] = useState(0);
// 测试状态
const [isTestingNotification, setIsTestingNotification] = useState(false);
const [testCountdown, setTestCountdown] = useState(0);
const [notificationShown, setNotificationShown] = useState(null); // null | true | false
// 系统环境检测
const [isFullscreen, setIsFullscreen] = useState(false);
const [isMacOS, setIsMacOS] = useState(false);
// 故障排查面板
const { isOpen: isTroubleshootOpen, onToggle: onTroubleshootToggle } = useDisclosure();
// 检测系统环境
useEffect(() => {
// 检测是否为 macOS
const platform = navigator.platform.toLowerCase();
setIsMacOS(platform.includes('mac'));
// 检测全屏状态
const checkFullscreen = () => {
setIsFullscreen(!!document.fullscreenElement);
};
document.addEventListener('fullscreenchange', checkFullscreen);
checkFullscreen();
return () => {
document.removeEventListener('fullscreenchange', checkFullscreen);
};
}, []);
// 倒计时逻辑
useEffect(() => {
if (testCountdown > 0) {
const timer = setTimeout(() => {
setTestCountdown(testCountdown - 1);
}, 1000);
return () => clearTimeout(timer);
} else if (testCountdown === 0 && isTestingNotification) {
// 倒计时结束,询问用户
setIsTestingNotification(false);
// 延迟一下再询问,确保用户有时间看到通知
setTimeout(() => {
const sawNotification = window.confirm('您是否看到了浏览器桌面通知?\n\n点击"确定"表示看到了\n点击"取消"表示没看到');
setNotificationShown(sawNotification);
if (!sawNotification) {
// 没看到通知,展开故障排查面板
if (!isTroubleshootOpen) {
onTroubleshootToggle();
}
}
}, 500);
}
}, [testCountdown, isTestingNotification, isTroubleshootOpen, onTroubleshootToggle]);
// 浏览器权限状态标签
const getPermissionLabel = () => {
switch (browserPermission) {
@@ -86,51 +149,6 @@ const NotificationTestTool = () => {
setTestCount(prev => prev + 1);
};
// 股票动向测试数据(涨)
const testStockAlertUp = () => {
addNotification({
type: NOTIFICATION_TYPES.STOCK_ALERT,
priority: PRIORITY_LEVELS.URGENT,
title: '【测试】您关注的股票触发预警',
content: '宁德时代(300750) 当前价格 ¥245.50,盘中涨幅达 +5.2%,已触达您设置的目标价位',
publishTime: Date.now(),
pushTime: Date.now(),
isAIGenerated: false,
clickable: true,
link: '/stock-overview?code=300750',
extra: {
stockCode: '300750',
stockName: '宁德时代',
priceChange: '+5.2%',
currentPrice: '245.50',
},
autoClose: 10000,
});
setTestCount(prev => prev + 1);
};
// 股票动向测试数据(跌)
const testStockAlertDown = () => {
addNotification({
type: NOTIFICATION_TYPES.STOCK_ALERT,
priority: PRIORITY_LEVELS.IMPORTANT,
title: '【测试】您关注的股票异常波动',
content: '比亚迪(002594) 5分钟内跌幅达 -3.8%,当前价格 ¥198.20,建议关注',
publishTime: Date.now(),
pushTime: Date.now(),
isAIGenerated: false,
clickable: true,
link: '/stock-overview?code=002594',
extra: {
stockCode: '002594',
stockName: '比亚迪',
priceChange: '-3.8%',
currentPrice: '198.20',
},
autoClose: 10000,
});
setTestCount(prev => prev + 1);
};
// 事件动向测试数据
const testEventAlert = () => {
@@ -179,30 +197,6 @@ const NotificationTestTool = () => {
setTestCount(prev => prev + 1);
};
// AI分析报告测试数据
const testAIReport = () => {
addNotification({
type: NOTIFICATION_TYPES.ANALYSIS_REPORT,
priority: PRIORITY_LEVELS.NORMAL,
title: '【测试】AI产业链投资机会分析',
content: '随着大模型应用加速落地,算力、数据、应用三大方向均存在投资机会,重点关注海光信息、寒武纪',
publishTime: Date.now(),
pushTime: Date.now(),
author: {
name: 'AI分析师',
organization: '价值前沿',
},
isAIGenerated: true,
clickable: true,
link: '/forecast-report?id=test005',
extra: {
reportType: '策略报告',
industry: '人工智能',
},
autoClose: 12000,
});
setTestCount(prev => prev + 1);
};
// 预测通知测试数据(不可跳转)
const testPrediction = () => {
@@ -272,42 +266,11 @@ const NotificationTestTool = () => {
}, 5000);
};
// 测试全部类型(层叠效果)
const testAllTypes = () => {
const tests = [testAnnouncement, testStockAlertUp, testEventAlert, testAnalysisReport];
tests.forEach((test, index) => {
setTimeout(() => test(), index * 600);
});
};
// 测试优先级
const testPriority = () => {
[
{ priority: PRIORITY_LEVELS.URGENT, label: '紧急' },
{ priority: PRIORITY_LEVELS.IMPORTANT, label: '重要' },
{ priority: PRIORITY_LEVELS.NORMAL, label: '普通' },
].forEach((item, index) => {
setTimeout(() => {
addNotification({
type: NOTIFICATION_TYPES.ANNOUNCEMENT,
priority: item.priority,
title: `【测试】${item.label}优先级通知`,
content: `这是一条${item.label}优先级的测试通知,用于验证优先级标签显示`,
publishTime: Date.now(),
pushTime: Date.now(),
isAIGenerated: false,
clickable: false,
autoClose: 10000,
});
setTestCount(prev => prev + 1);
}, index * 600);
});
};
return (
<Box
position="fixed"
top="316px"
top="116px"
right={4}
zIndex={9998}
bg="white"
@@ -363,28 +326,6 @@ const NotificationTestTool = () => {
公告通知
</Button>
{/* 股票动向 */}
<HStack spacing={2}>
<Button
size="sm"
colorScheme="red"
leftIcon={<MdTrendingUp />}
onClick={testStockAlertUp}
flex={1}
>
股票上涨
</Button>
<Button
size="sm"
colorScheme="green"
leftIcon={<MdTrendingUp style={{ transform: 'rotate(180deg)' }} />}
onClick={testStockAlertDown}
flex={1}
>
股票下跌
</Button>
</HStack>
{/* 事件动向 */}
<Button
size="sm"
@@ -396,27 +337,14 @@ const NotificationTestTool = () => {
</Button>
{/* 分析报告 */}
<HStack spacing={2}>
<Button
size="sm"
colorScheme="purple"
leftIcon={<MdAssessment />}
onClick={testAnalysisReport}
flex={1}
>
分析报告
</Button>
<Badge colorScheme="purple" alignSelf="center">AI</Badge>
<Button
size="sm"
colorScheme="purple"
variant="outline"
onClick={testAIReport}
flex={1}
>
AI报告
</Button>
</HStack>
<Button
size="sm"
colorScheme="purple"
leftIcon={<MdAssessment />}
onClick={testAnalysisReport}
>
分析报告
</Button>
{/* 预测通知 */}
<Button
@@ -434,24 +362,6 @@ const NotificationTestTool = () => {
组合测试
</Text>
{/* 层叠测试 */}
<Button
size="sm"
colorScheme="teal"
onClick={testAllTypes}
>
层叠测试4种类型
</Button>
{/* 优先级测试 */}
<Button
size="sm"
colorScheme="pink"
onClick={testPriority}
>
优先级测试3个级别
</Button>
{/* 预测→详情流程测试 */}
<Button
size="sm"
@@ -479,6 +389,93 @@ const NotificationTestTool = () => {
</Button>
)}
{/* 测试浏览器通知按钮 */}
{browserPermission === 'granted' && (
<Button
size="sm"
colorScheme="green"
leftIcon={<MdNotifications />}
onClick={() => {
console.log('测试浏览器通知按钮被点击');
console.log('Notification support:', 'Notification' in window);
console.log('Notification permission:', Notification?.permission);
console.log('Platform:', navigator.platform);
console.log('Fullscreen:', !!document.fullscreenElement);
// 直接使用原生 Notification API 测试
if (!('Notification' in window)) {
alert('您的浏览器不支持桌面通知');
return;
}
if (Notification.permission !== 'granted') {
alert('浏览器通知权限未授予\n当前权限状态' + Notification.permission);
return;
}
// 重置状态
setNotificationShown(null);
setIsTestingNotification(true);
setTestCountdown(8); // 8秒倒计时
try {
console.log('正在创建浏览器通知...');
const notification = new Notification('【测试】浏览器通知测试', {
body: '如果您看到这条系统级通知,说明浏览器通知功能正常工作',
icon: '/logo192.png',
badge: '/badge.png',
tag: 'test_notification_' + Date.now(),
requireInteraction: false,
});
console.log('浏览器通知创建成功:', notification);
// 监听通知显示(成功显示)
notification.onshow = () => {
console.log('✅ 浏览器通知已显示onshow 事件触发)');
setNotificationShown(true);
};
// 监听通知错误
notification.onerror = (error) => {
console.error('❌ 浏览器通知错误:', error);
setNotificationShown(false);
};
// 监听通知关闭
notification.onclose = () => {
console.log('浏览器通知已关闭');
};
// 8秒后自动关闭
setTimeout(() => {
notification.close();
console.log('浏览器通知已自动关闭');
}, 8000);
// 点击通知时聚焦窗口
notification.onclick = () => {
console.log('浏览器通知被点击');
window.focus();
notification.close();
setNotificationShown(true);
};
setTestCount(prev => prev + 1);
} catch (error) {
console.error('创建浏览器通知失败:', error);
alert('创建浏览器通知失败:' + error.message);
setIsTestingNotification(false);
setNotificationShown(false);
}
}}
isLoading={isTestingNotification}
loadingText={`等待通知... ${testCountdown}s`}
>
{isTestingNotification ? `等待通知... ${testCountdown}s` : '测试浏览器通知(直接)'}
</Button>
)}
{/* 浏览器通知状态说明 */}
{browserPermission === 'granted' && (
<Text fontSize="xs" color="green.500">
@@ -491,6 +488,136 @@ const NotificationTestTool = () => {
</Text>
)}
{/* 实时权限状态 */}
<HStack spacing={2} justify="center">
<Text fontSize="xs" color="gray.500">
实际权限
</Text>
<Badge
colorScheme={
('Notification' in window && Notification.permission === 'granted') ? 'green' :
('Notification' in window && Notification.permission === 'denied') ? 'red' : 'gray'
}
>
{('Notification' in window) ? Notification.permission : '不支持'}
</Badge>
</HStack>
{/* 环境警告 */}
{isFullscreen && (
<Alert status="warning" size="sm" borderRadius="md">
<AlertIcon />
<Box fontSize="xs">
<Text fontWeight="bold">全屏模式</Text>
<Text>某些浏览器在全屏模式下不显示通知</Text>
</Box>
</Alert>
)}
{isMacOS && notificationShown === false && (
<Alert status="error" size="sm" borderRadius="md">
<AlertIcon />
<Box fontSize="xs">
<Text fontWeight="bold">未检测到通知显示</Text>
<Text>可能是专注模式阻止了通知</Text>
</Box>
</Alert>
)}
<Divider />
{/* 故障排查面板 */}
<VStack spacing={2} align="stretch">
<Button
size="sm"
variant="outline"
colorScheme="orange"
leftIcon={<MdWarning />}
onClick={onTroubleshootToggle}
>
{isTroubleshootOpen ? '收起' : '故障排查指南'}
</Button>
<Collapse in={isTroubleshootOpen} animateOpacity>
<VStack spacing={3} align="stretch" p={3} bg="orange.50" borderRadius="md">
<Text fontSize="xs" fontWeight="bold" color="orange.800">
如果看不到浏览器通知请检查
</Text>
{/* macOS 专注模式 */}
{isMacOS && (
<Alert status="warning" size="sm">
<AlertIcon />
<Box fontSize="xs">
<AlertTitle fontSize="xs">macOS 专注模式</AlertTitle>
<AlertDescription>
<UnorderedList spacing={1} mt={1}>
<ListItem>点击右上角控制中心</ListItem>
<ListItem>关闭专注模式勿扰模式</ListItem>
<ListItem>或者系统设置 专注模式 关闭</ListItem>
</UnorderedList>
</AlertDescription>
</Box>
</Alert>
)}
{/* macOS 系统通知设置 */}
{isMacOS && (
<Alert status="info" size="sm">
<AlertIcon />
<Box fontSize="xs">
<AlertTitle fontSize="xs">macOS 系统通知设置</AlertTitle>
<AlertDescription>
<UnorderedList spacing={1} mt={1}>
<ListItem>系统设置 通知</ListItem>
<ListItem>找到 <Code fontSize="xs">Google Chrome</Code> <Code fontSize="xs">Microsoft Edge</Code></ListItem>
<ListItem>确保允许通知已开启</ListItem>
<ListItem>通知样式设置为横幅提醒</ListItem>
</UnorderedList>
</AlertDescription>
</Box>
</Alert>
)}
{/* Chrome 浏览器设置 */}
<Alert status="info" size="sm">
<AlertIcon />
<Box fontSize="xs">
<AlertTitle fontSize="xs">Chrome 浏览器设置</AlertTitle>
<AlertDescription>
<UnorderedList spacing={1} mt={1}>
<ListItem>地址栏输入: <Code fontSize="xs">chrome://settings/content/notifications</Code></ListItem>
<ListItem>确保网站可以请求发送通知已开启</ListItem>
<ListItem>检查本站点是否在允许列表中</ListItem>
</UnorderedList>
</AlertDescription>
</Box>
</Alert>
{/* 全屏模式提示 */}
{isFullscreen && (
<Alert status="warning" size="sm">
<AlertIcon />
<Box fontSize="xs">
<AlertTitle fontSize="xs">退出全屏模式</AlertTitle>
<AlertDescription>
<Code fontSize="xs">ESC</Code> 退
</AlertDescription>
</Box>
</Alert>
)}
{/* 测试结果反馈 */}
{notificationShown === true && (
<Alert status="success" size="sm">
<AlertIcon />
<Text fontSize="xs"> 通知功能正常</Text>
</Alert>
)}
</VStack>
</Collapse>
</VStack>
<Divider />
{/* 功能按钮 */}