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

@@ -0,0 +1,208 @@
// src/services/browserNotificationService.js
/**
* 浏览器原生通知服务
* 提供系统级通知功能Web Notifications API
*/
import { logger } from '../utils/logger';
class BrowserNotificationService {
constructor() {
this.permission = this.isSupported() ? Notification.permission : 'denied';
this.activeNotifications = new Map(); // 存储活跃的通知
}
/**
* 检查浏览器是否支持通知 API
*/
isSupported() {
return 'Notification' in window;
}
/**
* 获取当前权限状态
* @returns {string} 'granted' | 'denied' | 'default'
*/
getPermissionStatus() {
if (!this.isSupported()) {
return 'denied';
}
return Notification.permission;
}
/**
* 请求通知权限
* @returns {Promise<string>} 权限状态
*/
async requestPermission() {
if (!this.isSupported()) {
logger.warn('browserNotificationService', 'Notifications not supported');
return 'denied';
}
if (this.permission === 'granted') {
logger.info('browserNotificationService', 'Permission already granted');
return 'granted';
}
try {
const permission = await Notification.requestPermission();
this.permission = permission;
logger.info('browserNotificationService', `Permission ${permission}`);
return permission;
} catch (error) {
logger.error('browserNotificationService', 'requestPermission', error);
return 'denied';
}
}
/**
* 发送浏览器通知
* @param {Object} options 通知选项
* @param {string} options.title 标题
* @param {string} options.body 内容
* @param {string} options.icon 图标路径
* @param {string} options.tag 标签(防止重复)
* @param {boolean} options.requireInteraction 是否需要用户交互才关闭
* @param {Object} options.data 自定义数据(如跳转链接)
* @param {number} options.autoClose 自动关闭时间(毫秒)
* @returns {Notification|null} 通知对象
*/
sendNotification({
title,
body,
icon = '/logo192.png',
tag,
requireInteraction = false,
data = {},
autoClose = 0,
}) {
if (!this.isSupported()) {
logger.warn('browserNotificationService', 'Notifications not supported');
return null;
}
if (this.permission !== 'granted') {
logger.warn('browserNotificationService', 'Permission not granted');
return null;
}
try {
// 关闭相同 tag 的旧通知
if (tag && this.activeNotifications.has(tag)) {
const oldNotification = this.activeNotifications.get(tag);
oldNotification.close();
}
// 创建通知
const notification = new Notification(title, {
body,
icon,
badge: '/badge.png',
tag: tag || `notification_${Date.now()}`,
requireInteraction,
data,
silent: false, // 允许声音
});
// 存储通知引用
if (tag) {
this.activeNotifications.set(tag, notification);
}
// 自动关闭
if (autoClose > 0 && !requireInteraction) {
setTimeout(() => {
notification.close();
}, autoClose);
}
// 通知关闭时清理引用
notification.onclose = () => {
if (tag) {
this.activeNotifications.delete(tag);
}
};
logger.info('browserNotificationService', 'Notification sent', { title, tag });
return notification;
} catch (error) {
logger.error('browserNotificationService', 'sendNotification', error);
return null;
}
}
/**
* 设置通知点击处理
* @param {Notification} notification 通知对象
* @param {Function} navigate React Router navigate 函数
*/
setupClickHandler(notification, navigate) {
if (!notification) return;
notification.onclick = (event) => {
event.preventDefault();
// 聚焦窗口
window.focus();
// 跳转链接
if (notification.data?.link) {
navigate(notification.data.link);
}
// 关闭通知
notification.close();
logger.info('browserNotificationService', 'Notification clicked', notification.data);
};
}
/**
* 关闭所有活跃通知
*/
closeAll() {
this.activeNotifications.forEach(notification => {
notification.close();
});
this.activeNotifications.clear();
logger.info('browserNotificationService', 'All notifications closed');
}
/**
* 根据通知数据发送浏览器通知
* @param {Object} notificationData 通知数据
* @param {Function} navigate React Router navigate 函数
*/
sendFromNotificationData(notificationData, navigate) {
const { type, priority, title, content, link, extra } = notificationData;
// 生成唯一 tag
const tag = `${type}_${Date.now()}`;
// 判断是否需要用户交互(紧急通知不自动关闭)
const requireInteraction = priority === 'urgent';
// 发送通知
const notification = this.sendNotification({
title: title || '新通知',
body: content || '',
tag,
requireInteraction,
data: { link, ...extra },
autoClose: requireInteraction ? 0 : 8000, // 紧急通知不自动关闭
});
// 设置点击处理
if (notification && navigate) {
this.setupClickHandler(notification, navigate);
}
return notification;
}
}
// 导出单例
export const browserNotificationService = new BrowserNotificationService();
export default browserNotificationService;

View File

@@ -1,61 +1,303 @@
// src/services/mockSocketService.js
/**
* Mock Socket 服务 - 用于开发环境模拟实时推送
* 模拟交易提醒、系统通知等实时消息推送
* 模拟金融资讯、事件动向、分析报告等实时消息推送
*/
import { logger } from '../utils/logger';
import { NOTIFICATION_TYPES, PRIORITY_LEVELS } from '../constants/notificationTypes';
// 模拟交易提醒数据
const mockTradeAlerts = [
// 模拟金融资讯数据
const mockFinancialNews = [
// ========== 公告通知 ==========
{
type: 'trade_alert',
severity: 'success',
title: '买入成功',
message: '您的订单已成功执行:买入 贵州茅台(600519) 100股成交价 ¥1,850.00',
timestamp: Date.now(),
autoClose: 8000,
},
{
type: 'trade_alert',
severity: 'warning',
title: '价格预警',
message: '您关注的股票 比亚迪(002594) 当前价格 ¥245.50,已触达预设价格',
timestamp: Date.now(),
type: NOTIFICATION_TYPES.ANNOUNCEMENT,
priority: PRIORITY_LEVELS.IMPORTANT,
title: '贵州茅台发布2024年度财报公告',
content: '2024年度营收同比增长15.2%净利润创历史新高董事会建议每10股派息180元',
publishTime: new Date('2024-03-28T15:30:00').getTime(),
pushTime: Date.now(),
isAIGenerated: false,
clickable: true,
link: '/event-detail/ann001',
extra: {
announcementType: '财报',
companyCode: '600519',
companyName: '贵州茅台',
},
autoClose: 10000,
},
{
type: 'trade_alert',
severity: 'info',
title: '持仓提醒',
message: '您持有的 宁德时代(300750) 今日涨幅达 5.2%,当前盈利 +¥12,350',
timestamp: Date.now(),
autoClose: 8000,
},
{
type: 'trade_alert',
severity: 'error',
title: '委托失败',
message: '卖出订单失败:五粮液(000858) 当前处于停牌状态,无法交易',
timestamp: Date.now(),
type: NOTIFICATION_TYPES.ANNOUNCEMENT,
priority: PRIORITY_LEVELS.URGENT,
title: '宁德时代发布重大资产重组公告',
content: '公司拟收购某新能源材料公司100%股权交易金额约120亿元预计增厚业绩20%',
publishTime: new Date('2024-03-28T09:00:00').getTime(),
pushTime: Date.now(),
isAIGenerated: false,
clickable: true,
link: '/event-detail/ann002',
extra: {
announcementType: '重组',
companyCode: '300750',
companyName: '宁德时代',
},
autoClose: 12000,
},
{
type: 'system_notification',
severity: 'info',
title: '系统公告',
message: '市场将于15:00收盘请注意及时调整持仓',
timestamp: Date.now(),
type: NOTIFICATION_TYPES.ANNOUNCEMENT,
priority: PRIORITY_LEVELS.NORMAL,
title: '中国平安发布分红派息公告',
content: '2023年度利润分配方案每10股派发现金红利23.0元含税分红率达30.5%',
publishTime: new Date('2024-03-27T16:00:00').getTime(),
pushTime: Date.now(),
isAIGenerated: false,
clickable: true,
link: '/event-detail/ann003',
extra: {
announcementType: '分红',
companyCode: '601318',
companyName: '中国平安',
},
autoClose: 10000,
},
// ========== 股票动向 ==========
{
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',
triggerType: '目标价',
},
autoClose: 10000,
},
{
type: 'trade_alert',
severity: 'success',
title: '分红到账',
message: '您持有的 中国平安(601318) 分红已到账,金额 ¥560.00',
timestamp: Date.now(),
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',
triggerType: '异常波动',
},
autoClose: 10000,
},
{
type: NOTIFICATION_TYPES.STOCK_ALERT,
priority: PRIORITY_LEVELS.NORMAL,
title: '持仓股票表现',
content: '隆基绿能(601012) 今日表现优异,涨幅 +4.5%,您当前持仓浮盈 +¥8,200',
publishTime: Date.now(),
pushTime: Date.now(),
isAIGenerated: false,
clickable: true,
link: '/trading-simulation',
extra: {
stockCode: '601012',
stockName: '隆基绿能',
priceChange: '+4.5%',
profit: '+8200',
},
autoClose: 8000,
},
// ========== 事件动向 ==========
{
type: NOTIFICATION_TYPES.EVENT_ALERT,
priority: PRIORITY_LEVELS.IMPORTANT,
title: '央行宣布降准0.5个百分点',
content: '中国人民银行宣布下调金融机构存款准备金率0.5个百分点释放长期资金约1万亿元利好股市',
publishTime: new Date('2024-03-28T09:00:00').getTime(),
pushTime: Date.now(),
isAIGenerated: false,
clickable: true,
link: '/event-detail/evt001',
extra: {
eventId: 'evt001',
relatedStocks: 12,
impactLevel: '重大利好',
sectors: ['银行', '地产', '基建'],
},
autoClose: 12000,
},
{
type: NOTIFICATION_TYPES.EVENT_ALERT,
priority: PRIORITY_LEVELS.IMPORTANT,
title: '新能源汽车补贴政策延期',
content: '财政部宣布新能源汽车购置补贴政策延长至2024年底涉及比亚迪、理想汽车等5家龙头企业',
publishTime: new Date('2024-03-28T10:30:00').getTime(),
pushTime: Date.now(),
isAIGenerated: false,
clickable: true,
link: '/event-detail/evt002',
extra: {
eventId: 'evt002',
relatedStocks: 5,
impactLevel: '重大利好',
sectors: ['新能源汽车'],
},
autoClose: 12000,
},
{
type: NOTIFICATION_TYPES.EVENT_ALERT,
priority: PRIORITY_LEVELS.NORMAL,
title: '芯片产业扶持政策出台',
content: '工信部发布《半导体产业发展指导意见》未来三年投入500亿专项资金支持芯片研发',
publishTime: new Date('2024-03-27T14:00:00').getTime(),
pushTime: Date.now(),
isAIGenerated: false,
clickable: true,
link: '/event-detail/evt003',
extra: {
eventId: 'evt003',
relatedStocks: 8,
impactLevel: '中长期利好',
sectors: ['半导体', '芯片设计'],
},
autoClose: 10000,
},
// ========== 预测通知 ==========
{
type: NOTIFICATION_TYPES.EVENT_ALERT,
priority: PRIORITY_LEVELS.NORMAL,
title: '【预测】央行可能宣布降准政策',
content: '基于最新宏观数据分析预计央行将在本周宣布降准0.5个百分点,释放长期资金',
publishTime: Date.now(),
pushTime: Date.now(),
isAIGenerated: true,
clickable: false, // ❌ 不可点击
link: null,
extra: {
isPrediction: true,
statusHint: '详细报告生成中...',
relatedPredictionId: 'pred_001',
},
autoClose: 15000,
},
{
type: NOTIFICATION_TYPES.EVENT_ALERT,
priority: PRIORITY_LEVELS.NORMAL,
title: '【预测】新能源补贴政策或将延期',
content: '根据政策趋势分析财政部可能宣布新能源汽车购置补贴政策延长至2025年底',
publishTime: Date.now(),
pushTime: Date.now(),
isAIGenerated: true,
clickable: false, // ❌ 不可点击
link: null,
extra: {
isPrediction: true,
statusHint: '详细报告生成中...',
relatedPredictionId: 'pred_002',
},
autoClose: 15000,
},
// ========== 分析报告 ==========
{
type: NOTIFICATION_TYPES.ANALYSIS_REPORT,
priority: PRIORITY_LEVELS.IMPORTANT,
title: '医药行业深度报告:创新药迎来政策拐点',
content: 'CXO板块持续受益于全球创新药研发外包需求建议关注药明康德、凯莱英等龙头企业',
publishTime: new Date('2024-03-28T08:00:00').getTime(),
pushTime: Date.now(),
author: {
name: '李明',
organization: '中信证券',
},
isAIGenerated: false,
clickable: true,
link: '/forecast-report?id=rpt001',
extra: {
reportType: '行业研报',
industry: '医药',
rating: '强烈推荐',
},
autoClose: 12000,
},
{
type: NOTIFICATION_TYPES.ANALYSIS_REPORT,
priority: PRIORITY_LEVELS.IMPORTANT,
title: 'AI产业链投资机会分析',
content: '随着大模型应用加速落地,算力、数据、应用三大方向均存在投资机会,重点关注海光信息、寒武纪',
publishTime: new Date('2024-03-28T07:30:00').getTime(),
pushTime: Date.now(),
author: {
name: '王芳',
organization: '招商证券',
},
isAIGenerated: true,
clickable: true,
link: '/forecast-report?id=rpt002',
extra: {
reportType: '策略报告',
industry: '人工智能',
rating: '推荐',
},
autoClose: 12000,
},
{
type: NOTIFICATION_TYPES.ANALYSIS_REPORT,
priority: PRIORITY_LEVELS.NORMAL,
title: '比亚迪:新能源汽车龙头业绩持续超预期',
content: '2024年销量目标400万辆海外市场拓展顺利维持"买入"评级目标价280元',
publishTime: new Date('2024-03-27T09:00:00').getTime(),
pushTime: Date.now(),
author: {
name: '张伟',
organization: '国泰君安',
},
isAIGenerated: false,
clickable: true,
link: '/forecast-report?id=rpt003',
extra: {
reportType: '公司研报',
industry: '新能源汽车',
rating: '买入',
targetPrice: '280',
},
autoClose: 10000,
},
{
type: NOTIFICATION_TYPES.ANALYSIS_REPORT,
priority: PRIORITY_LEVELS.NORMAL,
title: '2024年A股市场展望结构性行情延续',
content: 'AI应用、高端制造、自主可控三大主线贯穿全年建议关注科技成长板块配置机会',
publishTime: new Date('2024-03-26T16:00:00').getTime(),
pushTime: Date.now(),
author: {
name: 'AI分析师',
organization: '价值前沿',
},
isAIGenerated: true,
clickable: true,
link: '/forecast-report?id=rpt004',
extra: {
reportType: '策略报告',
industry: '市场策略',
rating: '谨慎乐观',
},
autoClose: 10000,
},
];
class MockSocketService {
@@ -194,9 +436,9 @@ class MockSocketService {
for (let i = 0; i < count; i++) {
// 从模拟数据中随机选择一条
const randomIndex = Math.floor(Math.random() * mockTradeAlerts.length);
const randomIndex = Math.floor(Math.random() * mockFinancialNews.length);
const alert = {
...mockTradeAlerts[randomIndex],
...mockFinancialNews[randomIndex],
timestamp: Date.now(),
id: `mock_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
};