feat: 添加消息推送能力,添加新闻催化分析页的合规提示

This commit is contained in:
zdl
2025-10-21 10:59:52 +08:00
parent 6c96299b8f
commit 5a3a3ad42b
15 changed files with 1800 additions and 125 deletions

View File

@@ -0,0 +1,207 @@
// src/contexts/NotificationContext.js
/**
* 通知上下文 - 管理实时消息推送和通知显示
*/
import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react';
import { logger } from '../utils/logger';
import socket, { SOCKET_TYPE } from '../services/socket';
import notificationSound from '../assets/sounds/notification.wav';
// 创建通知上下文
const NotificationContext = createContext();
// 自定义Hook
export const useNotification = () => {
const context = useContext(NotificationContext);
if (!context) {
throw new Error('useNotification must be used within a NotificationProvider');
}
return context;
};
// 通知提供者组件
export const NotificationProvider = ({ children }) => {
const [notifications, setNotifications] = useState([]);
const [isConnected, setIsConnected] = useState(false);
const [soundEnabled, setSoundEnabled] = useState(true);
const audioRef = useRef(null);
// 初始化音频
useEffect(() => {
try {
audioRef.current = new Audio(notificationSound);
audioRef.current.volume = 0.5;
} catch (error) {
logger.error('NotificationContext', 'Audio initialization failed', error);
}
}, []);
/**
* 播放通知音效
*/
const playNotificationSound = useCallback(() => {
if (!soundEnabled || !audioRef.current) {
return;
}
try {
// 重置音频到开始位置
audioRef.current.currentTime = 0;
// 播放音频
audioRef.current.play().catch(error => {
logger.warn('NotificationContext', 'Failed to play notification sound', error);
});
} catch (error) {
logger.error('NotificationContext', 'playNotificationSound', error);
}
}, [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
*/
const removeNotification = useCallback((id) => {
logger.info('NotificationContext', 'Removing notification', { id });
setNotifications(prev => prev.filter(notif => notif.id !== id));
}, []);
/**
* 清空所有通知
*/
const clearAllNotifications = useCallback(() => {
logger.info('NotificationContext', 'Clearing all notifications');
setNotifications([]);
}, []);
/**
* 切换音效开关
*/
const toggleSound = useCallback(() => {
setSoundEnabled(prev => {
const newValue = !prev;
logger.info('NotificationContext', 'Sound toggled', { enabled: newValue });
return newValue;
});
}, []);
// 连接到 Socket 服务
useEffect(() => {
logger.info('NotificationContext', 'Initializing socket connection...');
// 连接 socket
socket.connect();
// 监听连接状态
socket.on('connect', () => {
setIsConnected(true);
logger.info('NotificationContext', 'Socket connected');
// 如果使用 mock可以启动定期推送
if (SOCKET_TYPE === 'MOCK') {
// 启动模拟推送每20秒推送1-2条消息
socket.startMockPush(20000, 2);
logger.info('NotificationContext', 'Mock push started');
}
});
socket.on('disconnect', () => {
setIsConnected(false);
logger.warn('NotificationContext', 'Socket disconnected');
});
// 监听交易通知
socket.on('trade_notification', (data) => {
logger.info('NotificationContext', 'Received trade notification', data);
addNotification(data);
});
// 监听系统通知
socket.on('system_notification', (data) => {
logger.info('NotificationContext', 'Received system notification', data);
addNotification(data);
});
// 清理函数
return () => {
logger.info('NotificationContext', 'Cleaning up socket connection');
// 如果是 mock service停止推送
if (SOCKET_TYPE === 'MOCK') {
socket.stopMockPush();
}
socket.off('connect');
socket.off('disconnect');
socket.off('trade_notification');
socket.off('system_notification');
socket.disconnect();
};
}, []); // eslint-disable-line react-hooks/exhaustive-deps
const value = {
notifications,
isConnected,
soundEnabled,
addNotification,
removeNotification,
clearAllNotifications,
toggleSound,
};
return (
<NotificationContext.Provider value={value}>
{children}
</NotificationContext.Provider>
);
};
export default NotificationContext;