Compare commits
14 Commits
986ec05eb1
...
feature_bu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e3c408f13 | ||
|
|
9b4fafc0b0 | ||
|
|
9de4e10637 | ||
|
|
db7efeb1fe | ||
|
|
838e7d7272 | ||
|
|
21564ebf4d | ||
|
|
c593582006 | ||
|
|
e555d22499 | ||
|
|
8ced77c604 | ||
|
|
880c91e3de | ||
|
|
71f2e89072 | ||
|
|
9069a2be55 | ||
|
|
d5686fed9d | ||
|
|
891ea6d88e |
@@ -16,6 +16,15 @@ NODE_ENV=production
|
|||||||
# Mock 配置(生产环境禁用 Mock)
|
# Mock 配置(生产环境禁用 Mock)
|
||||||
REACT_APP_ENABLE_MOCK=false
|
REACT_APP_ENABLE_MOCK=false
|
||||||
|
|
||||||
|
# 🔧 调试模式(生产环境临时调试用)
|
||||||
|
# 开启后会在全局暴露 window.__DEBUG__ 调试 API
|
||||||
|
# ⚠️ 警告: 调试模式会记录所有 API 请求/响应,调试完成后请立即关闭!
|
||||||
|
# 使用方法:
|
||||||
|
# 1. 设置为 true 并重新构建
|
||||||
|
# 2. 在浏览器控制台使用 window.__DEBUG__.help() 查看命令
|
||||||
|
# 3. 调试完成后设置为 false 并重新构建
|
||||||
|
REACT_APP_ENABLE_DEBUG=false
|
||||||
|
|
||||||
# 后端 API 地址(生产环境)
|
# 后端 API 地址(生产环境)
|
||||||
REACT_APP_API_URL=http://49.232.185.254:5001
|
REACT_APP_API_URL=http://49.232.185.254:5001
|
||||||
|
|
||||||
|
|||||||
@@ -110,6 +110,9 @@ module.exports = {
|
|||||||
...webpackConfig.resolve,
|
...webpackConfig.resolve,
|
||||||
alias: {
|
alias: {
|
||||||
...webpackConfig.resolve.alias,
|
...webpackConfig.resolve.alias,
|
||||||
|
// 强制 'debug' 模块解析到 node_modules(避免与 src/devtools/ 冲突)
|
||||||
|
'debug': path.resolve(__dirname, 'node_modules/debug'),
|
||||||
|
|
||||||
// 根目录别名
|
// 根目录别名
|
||||||
'@': path.resolve(__dirname, 'src'),
|
'@': path.resolve(__dirname, 'src'),
|
||||||
|
|
||||||
@@ -119,6 +122,7 @@ module.exports = {
|
|||||||
'@constants': path.resolve(__dirname, 'src/constants'),
|
'@constants': path.resolve(__dirname, 'src/constants'),
|
||||||
'@contexts': path.resolve(__dirname, 'src/contexts'),
|
'@contexts': path.resolve(__dirname, 'src/contexts'),
|
||||||
'@data': path.resolve(__dirname, 'src/data'),
|
'@data': path.resolve(__dirname, 'src/data'),
|
||||||
|
'@devtools': path.resolve(__dirname, 'src/devtools'),
|
||||||
'@hooks': path.resolve(__dirname, 'src/hooks'),
|
'@hooks': path.resolve(__dirname, 'src/hooks'),
|
||||||
'@layouts': path.resolve(__dirname, 'src/layouts'),
|
'@layouts': path.resolve(__dirname, 'src/layouts'),
|
||||||
'@lib': path.resolve(__dirname, 'src/lib'),
|
'@lib': path.resolve(__dirname, 'src/lib'),
|
||||||
@@ -263,6 +267,34 @@ module.exports = {
|
|||||||
logLevel: 'debug',
|
logLevel: 'debug',
|
||||||
pathRewrite: { '^/concept-api': '' },
|
pathRewrite: { '^/concept-api': '' },
|
||||||
},
|
},
|
||||||
|
'/bytedesk-api': {
|
||||||
|
target: 'http://43.143.189.195',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
logLevel: 'debug',
|
||||||
|
pathRewrite: { '^/bytedesk-api': '' },
|
||||||
|
},
|
||||||
|
'/chat': {
|
||||||
|
target: 'http://43.143.189.195',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
logLevel: 'debug',
|
||||||
|
// 不需要pathRewrite,保留/chat路径
|
||||||
|
},
|
||||||
|
'/config': {
|
||||||
|
target: 'http://43.143.189.195',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
logLevel: 'debug',
|
||||||
|
// 不需要pathRewrite,保留/config路径
|
||||||
|
},
|
||||||
|
'/visitor': {
|
||||||
|
target: 'http://43.143.189.195',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: false,
|
||||||
|
logLevel: 'debug',
|
||||||
|
// 不需要pathRewrite,保留/visitor路径
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -48,16 +48,18 @@ npm start
|
|||||||
|
|
||||||
### 3. 触发通知
|
### 3. 触发通知
|
||||||
|
|
||||||
**Mock 模式**(默认):
|
**测试通知**:
|
||||||
- 等待 60 秒,会自动推送 1-2 条通知
|
- 使用调试 API 发送测试通知:
|
||||||
- 或在控制台执行:
|
|
||||||
```javascript
|
```javascript
|
||||||
import { mockSocketService } from './services/mockSocketService.js';
|
// 方式1: 使用调试工具(推荐)
|
||||||
mockSocketService.sendTestNotification();
|
window.__DEBUG__.notification.forceNotification({
|
||||||
```
|
title: '测试通知',
|
||||||
|
body: '验证暗色模式下的通知样式'
|
||||||
|
});
|
||||||
|
|
||||||
**Real 模式**:
|
// 方式2: 等待后端真实推送
|
||||||
- 创建测试事件(运行后端测试脚本)
|
// 确保已连接后端,等待真实事件推送
|
||||||
|
```
|
||||||
|
|
||||||
### 4. 验证效果
|
### 4. 验证效果
|
||||||
|
|
||||||
@@ -139,61 +141,46 @@ npm start
|
|||||||
|
|
||||||
### 手动触发各类型通知
|
### 手动触发各类型通知
|
||||||
|
|
||||||
```javascript
|
> **注意**: Mock Socket 已移除,请使用调试工具或真实后端测试。
|
||||||
// 引入服务
|
|
||||||
import { mockSocketService } from './services/mockSocketService.js';
|
|
||||||
import { NOTIFICATION_TYPES, PRIORITY_LEVELS } from './constants/notificationTypes.js';
|
|
||||||
|
|
||||||
// 测试公告通知(蓝色)
|
```javascript
|
||||||
mockSocketService.sendTestNotification({
|
// 使用调试工具测试不同类型的通知
|
||||||
type: NOTIFICATION_TYPES.ANNOUNCEMENT,
|
// 确保已开启调试模式:REACT_APP_ENABLE_DEBUG=true
|
||||||
priority: PRIORITY_LEVELS.IMPORTANT,
|
|
||||||
|
// 测试公告通知
|
||||||
|
window.__DEBUG__.notification.forceNotification({
|
||||||
title: '测试公告通知',
|
title: '测试公告通知',
|
||||||
content: '这是暗色模式下的蓝色通知',
|
body: '这是暗色模式下的蓝色通知',
|
||||||
timestamp: Date.now(),
|
tag: 'test_announcement',
|
||||||
autoClose: 0,
|
autoClose: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 测试股票上涨(红色)
|
// 测试股票上涨(红色)
|
||||||
mockSocketService.sendTestNotification({
|
window.__DEBUG__.notification.forceNotification({
|
||||||
type: NOTIFICATION_TYPES.STOCK_ALERT,
|
title: '🔴 测试股票上涨',
|
||||||
priority: PRIORITY_LEVELS.URGENT,
|
body: '宁德时代 +5.2%',
|
||||||
title: '测试股票上涨',
|
tag: 'test_stock_up',
|
||||||
content: '宁德时代 +5.2%',
|
|
||||||
extra: { priceChange: '+5.2%' },
|
|
||||||
timestamp: Date.now(),
|
|
||||||
autoClose: 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 测试股票下跌(绿色)
|
// 测试股票下跌(绿色)
|
||||||
mockSocketService.sendTestNotification({
|
window.__DEBUG__.notification.forceNotification({
|
||||||
type: NOTIFICATION_TYPES.STOCK_ALERT,
|
title: '🟢 测试股票下跌',
|
||||||
priority: PRIORITY_LEVELS.IMPORTANT,
|
body: '比亚迪 -3.8%',
|
||||||
title: '测试股票下跌',
|
tag: 'test_stock_down',
|
||||||
content: '比亚迪 -3.8%',
|
|
||||||
extra: { priceChange: '-3.8%' },
|
|
||||||
timestamp: Date.now(),
|
|
||||||
autoClose: 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 测试事件动向(橙色)
|
// 测试事件动向(橙色)
|
||||||
mockSocketService.sendTestNotification({
|
window.__DEBUG__.notification.forceNotification({
|
||||||
type: NOTIFICATION_TYPES.EVENT_ALERT,
|
title: '🟠 测试事件动向',
|
||||||
priority: PRIORITY_LEVELS.IMPORTANT,
|
body: '央行宣布降准',
|
||||||
title: '测试事件动向',
|
tag: 'test_event',
|
||||||
content: '央行宣布降准',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
autoClose: 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 测试分析报告(紫色)
|
// 测试分析报告(紫色)
|
||||||
mockSocketService.sendTestNotification({
|
window.__DEBUG__.notification.forceNotification({
|
||||||
type: NOTIFICATION_TYPES.ANALYSIS_REPORT,
|
title: '🟣 测试分析报告',
|
||||||
priority: PRIORITY_LEVELS.NORMAL,
|
body: '医药行业深度报告',
|
||||||
title: '测试分析报告',
|
tag: 'test_report',
|
||||||
content: '医药行业深度报告',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
autoClose: 0,
|
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -330,13 +330,14 @@ if (Notification.permission === 'granted') {
|
|||||||
|
|
||||||
### 关键文件
|
### 关键文件
|
||||||
|
|
||||||
- `src/services/mockSocketService.js` - Mock Socket 服务
|
- `src/services/socketService.js` - Socket.IO 服务
|
||||||
- `src/services/socketService.js` - 真实 Socket.IO 服务
|
- `src/services/socket/index.js` - Socket 服务导出
|
||||||
- `src/services/socket/index.js` - 统一导出
|
- `src/contexts/NotificationContext.js` - 通知上下文
|
||||||
- `src/contexts/NotificationContext.js` - 通知上下文(含适配器)
|
|
||||||
- `src/hooks/useEventNotifications.js` - React Hook
|
- `src/hooks/useEventNotifications.js` - React Hook
|
||||||
- `src/views/Community/components/EventList.js` - 事件列表集成
|
- `src/views/Community/components/EventList.js` - 事件列表集成
|
||||||
|
|
||||||
|
> **注意**: `mockSocketService.js` 已移除(2025-01-10),现仅使用真实 Socket 连接。
|
||||||
|
|
||||||
### 数据流
|
### 数据流
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
# 实时消息推送系统 - 完整技术文档
|
# 实时消息推送系统 - 完整技术文档
|
||||||
|
|
||||||
> **版本**: v2.11.0
|
> **版本**: v2.11.0
|
||||||
> **更新日期**: 2025-01-07
|
> **更新日期**: 2025-01-10
|
||||||
> **文档类型**: 快速入门 + 完整技术规格
|
> **文档类型**: 快速入门 + 完整技术规格
|
||||||
|
>
|
||||||
|
> ⚠️ **重要更新**: Mock Socket 已移除(2025-01-10),文档中关于 `mockSocketService` 的内容仅供历史参考。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,90 @@
|
|||||||
// name: 'YOU CAN DEFINE USER NAME HERE',
|
// name: 'YOU CAN DEFINE USER NAME HERE',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 根据路径控制Dify机器人显示(只在首页/和home页/home显示)
|
||||||
|
function controlDifyChatbot() {
|
||||||
|
const path = window.location.pathname;
|
||||||
|
const chatbotButton = document.getElementById('dify-chatbot-bubble-button');
|
||||||
|
const chatbotWindow = document.getElementById('dify-chatbot-bubble-window');
|
||||||
|
|
||||||
|
// 只在首页(/)和home页(/home)显示Dify机器人
|
||||||
|
// const shouldShowDify = (path === '/' || path === '/home');
|
||||||
|
// 完全不显示Dify机器人(只使用Bytedesk客服)
|
||||||
|
const shouldShowDify = false
|
||||||
|
|
||||||
|
if (chatbotButton) {
|
||||||
|
chatbotButton.style.display = shouldShowDify ? 'block' : 'none';
|
||||||
|
// 同时设置visibility确保完全隐藏
|
||||||
|
chatbotButton.style.visibility = shouldShowDify ? 'visible' : 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chatbotWindow) {
|
||||||
|
chatbotWindow.style.display = shouldShowDify ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Dify] Path:', path, 'Should show:', shouldShowDify, 'Button found:', !!chatbotButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 轮询检查Dify按钮(因为Dify脚本加载是异步的)
|
||||||
|
let difyCheckCount = 0;
|
||||||
|
const difyCheckInterval = setInterval(function() {
|
||||||
|
const button = document.getElementById('dify-chatbot-bubble-button');
|
||||||
|
if (button || difyCheckCount > 50) { // 最多检查5秒
|
||||||
|
if (button) {
|
||||||
|
console.log('[Dify] Button found, applying control');
|
||||||
|
controlDifyChatbot();
|
||||||
|
}
|
||||||
|
clearInterval(difyCheckInterval);
|
||||||
|
}
|
||||||
|
difyCheckCount++;
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// 页面加载时执行
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
setTimeout(controlDifyChatbot, 1000);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听路由变化(React Router使用pushState)
|
||||||
|
window.addEventListener('popstate', controlDifyChatbot);
|
||||||
|
|
||||||
|
// 监听pushState和replaceState(捕获React Router导航)
|
||||||
|
const originalPushState = history.pushState;
|
||||||
|
const originalReplaceState = history.replaceState;
|
||||||
|
|
||||||
|
history.pushState = function() {
|
||||||
|
originalPushState.apply(history, arguments);
|
||||||
|
setTimeout(controlDifyChatbot, 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
history.replaceState = function() {
|
||||||
|
originalReplaceState.apply(history, arguments);
|
||||||
|
setTimeout(controlDifyChatbot, 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 使用MutationObserver监听DOM变化(捕获Dify按钮插入)
|
||||||
|
const observer = new MutationObserver(function(mutations) {
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
if (mutation.addedNodes.length > 0) {
|
||||||
|
for (const node of mutation.addedNodes) {
|
||||||
|
if (node.id && (node.id.includes('dify') || node.id.includes('chatbot'))) {
|
||||||
|
console.log('[Dify] Detected chatbot element insertion:', node.id);
|
||||||
|
setTimeout(controlDifyChatbot, 100);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 观察body的变化
|
||||||
|
window.addEventListener('DOMContentLoaded', function() {
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: false
|
||||||
|
});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
<script
|
<script
|
||||||
src="https://app.valuefrontier.cn/embed.min.js"
|
src="https://app.valuefrontier.cn/embed.min.js"
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
/**
|
/**
|
||||||
* Service Worker for Browser Notifications
|
* Service Worker for Browser Notifications
|
||||||
* 主要功能:支持浏览器通知的稳定运行
|
* 主要功能:支持浏览器通知的稳定运行
|
||||||
|
*
|
||||||
|
* 注意:此 Service Worker 仅用于通知功能,不拦截任何 HTTP 请求
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CACHE_NAME = 'valuefrontier-v1';
|
|
||||||
|
|
||||||
// Service Worker 安装事件
|
// Service Worker 安装事件
|
||||||
self.addEventListener('install', (event) => {
|
self.addEventListener('install', (event) => {
|
||||||
console.log('[Service Worker] Installing...');
|
console.log('[Service Worker] Installing...');
|
||||||
@@ -56,18 +56,6 @@ self.addEventListener('notificationclose', (event) => {
|
|||||||
console.log('[Service Worker] Notification closed:', event.notification.tag);
|
console.log('[Service Worker] Notification closed:', event.notification.tag);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch 事件 - 基础的网络优先策略
|
|
||||||
self.addEventListener('fetch', (event) => {
|
|
||||||
// 对于通知相关的资源,使用网络优先策略
|
|
||||||
event.respondWith(
|
|
||||||
fetch(event.request)
|
|
||||||
.catch(() => {
|
|
||||||
// 网络失败时,尝试从缓存获取
|
|
||||||
return caches.match(event.request);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 推送消息事件(预留,用于未来的 Push API 集成)
|
// 推送消息事件(预留,用于未来的 Push API 集成)
|
||||||
self.addEventListener('push', (event) => {
|
self.addEventListener('push', (event) => {
|
||||||
console.log('[Service Worker] Push message received:', event);
|
console.log('[Service Worker] Push message received:', event);
|
||||||
|
|||||||
156
src/bytedesk-integration/.env.bytedesk.example
Normal file
156
src/bytedesk-integration/.env.bytedesk.example
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
################################################################################
|
||||||
|
# Bytedesk客服系统环境变量配置示例
|
||||||
|
#
|
||||||
|
# 使用方法:
|
||||||
|
# 1. 复制本文件到vf_react项目根目录(与package.json同级)
|
||||||
|
# cp bytedesk-integration/.env.bytedesk.example .env.local
|
||||||
|
#
|
||||||
|
# 2. 根据实际部署环境修改配置值
|
||||||
|
#
|
||||||
|
# 3. 重启开发服务器使配置生效
|
||||||
|
# npm start
|
||||||
|
#
|
||||||
|
# 注意事项:
|
||||||
|
# - .env.local文件不应提交到Git(已在.gitignore中)
|
||||||
|
# - 开发环境和生产环境应使用不同的配置文件
|
||||||
|
# - 所有以REACT_APP_开头的变量会被打包到前端代码中
|
||||||
|
################################################################################
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Bytedesk服务器配置(必需)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Bytedesk后端服务地址(生产环境)
|
||||||
|
# 格式: http://IP地址 或 https://域名
|
||||||
|
# 示例: http://43.143.189.195 或 https://kefu.yourdomain.com
|
||||||
|
REACT_APP_BYTEDESK_API_URL=http://43.143.189.195
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Bytedesk组织和工作组配置(必需)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 组织ID(Organization UID)
|
||||||
|
# 获取方式: 登录管理后台 -> 设置 -> 组织信息 -> 复制UID
|
||||||
|
# 示例: df_org_uid
|
||||||
|
REACT_APP_BYTEDESK_ORG=df_org_uid
|
||||||
|
|
||||||
|
# 工作组ID(Workgroup SID)
|
||||||
|
# 获取方式: 登录管理后台 -> 客服管理 -> 工作组 -> 复制工作组ID
|
||||||
|
# 示例: df_wg_aftersales (售后服务组)
|
||||||
|
REACT_APP_BYTEDESK_SID=df_wg_aftersales
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 可选配置
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 客服类型
|
||||||
|
# 2 = 人工客服(默认)
|
||||||
|
# 1 = 机器人客服
|
||||||
|
# REACT_APP_BYTEDESK_TYPE=2
|
||||||
|
|
||||||
|
# 语言设置
|
||||||
|
# zh-cn = 简体中文(默认)
|
||||||
|
# en = 英语
|
||||||
|
# ja = 日语
|
||||||
|
# ko = 韩语
|
||||||
|
# REACT_APP_BYTEDESK_LOCALE=zh-cn
|
||||||
|
|
||||||
|
# 客服图标位置
|
||||||
|
# bottom-right = 右下角(默认)
|
||||||
|
# bottom-left = 左下角
|
||||||
|
# top-right = 右上角
|
||||||
|
# top-left = 左上角
|
||||||
|
# REACT_APP_BYTEDESK_PLACEMENT=bottom-right
|
||||||
|
|
||||||
|
# 客服图标边距(像素)
|
||||||
|
# REACT_APP_BYTEDESK_MARGIN_BOTTOM=20
|
||||||
|
# REACT_APP_BYTEDESK_MARGIN_SIDE=20
|
||||||
|
|
||||||
|
# 主题模式
|
||||||
|
# system = 跟随系统(默认)
|
||||||
|
# light = 亮色模式
|
||||||
|
# dark = 暗色模式
|
||||||
|
# REACT_APP_BYTEDESK_THEME_MODE=system
|
||||||
|
|
||||||
|
# 主题色(十六进制颜色)
|
||||||
|
# REACT_APP_BYTEDESK_THEME_COLOR=#0066FF
|
||||||
|
|
||||||
|
# 是否自动弹出客服窗口(不推荐)
|
||||||
|
# true = 页面加载后自动弹出
|
||||||
|
# false = 需用户点击图标弹出(默认)
|
||||||
|
# REACT_APP_BYTEDESK_AUTO_POPUP=false
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 开发环境专用配置
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 开发环境可以使用不同的服务器地址
|
||||||
|
# 取消注释以下行使用本地或测试服务器
|
||||||
|
# REACT_APP_BYTEDESK_API_URL_DEV=http://localhost:9003
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 配置示例 - 不同部署场景
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# ---------- 示例1: 生产环境(域名访问) ----------
|
||||||
|
# REACT_APP_BYTEDESK_API_URL=https://kefu.yourdomain.com
|
||||||
|
# REACT_APP_BYTEDESK_ORG=prod_org_12345
|
||||||
|
# REACT_APP_BYTEDESK_SID=prod_wg_sales
|
||||||
|
|
||||||
|
# ---------- 示例2: 测试环境(IP访问) ----------
|
||||||
|
# REACT_APP_BYTEDESK_API_URL=http://192.168.1.100
|
||||||
|
# REACT_APP_BYTEDESK_ORG=test_org_abc
|
||||||
|
# REACT_APP_BYTEDESK_SID=test_wg_support
|
||||||
|
|
||||||
|
# ---------- 示例3: 本地开发环境 ----------
|
||||||
|
# REACT_APP_BYTEDESK_API_URL=http://localhost:9003
|
||||||
|
# REACT_APP_BYTEDESK_ORG=dev_org_local
|
||||||
|
# REACT_APP_BYTEDESK_SID=dev_wg_test
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 故障排查
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 问题1: 客服图标不显示
|
||||||
|
# 解决方案:
|
||||||
|
# - 检查REACT_APP_BYTEDESK_API_URL是否可访问
|
||||||
|
# - 确认.env文件在项目根目录
|
||||||
|
# - 重启开发服务器(npm start)
|
||||||
|
# - 查看浏览器控制台是否有错误
|
||||||
|
|
||||||
|
# 问题2: 连接不上后端服务
|
||||||
|
# 解决方案:
|
||||||
|
# - 确认后端服务已启动(docker ps查看bytedesk-prod容器)
|
||||||
|
# - 检查CORS配置(后端.env.production中的BYTEDESK_CORS_ALLOWED_ORIGINS)
|
||||||
|
# - 确认防火墙未阻止80/443端口
|
||||||
|
|
||||||
|
# 问题3: ORG或SID配置错误
|
||||||
|
# 解决方案:
|
||||||
|
# - 登录管理后台http://43.143.189.195/admin/
|
||||||
|
# - 导航到"设置" -> "组织信息"获取ORG
|
||||||
|
# - 导航到"客服管理" -> "工作组"获取SID
|
||||||
|
# - 确保复制的ID没有多余空格
|
||||||
|
|
||||||
|
# 问题4: 多工作组场景
|
||||||
|
# 解决方案:
|
||||||
|
# - 可以为不同页面配置不同的SID
|
||||||
|
# - 在bytedesk.config.js中使用条件判断
|
||||||
|
# - 示例: 售后页面用售后组SID,销售页面用销售组SID
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 安全提示
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 1. 不要在代码中硬编码API地址和ID
|
||||||
|
# 2. .env.local文件不应提交到Git仓库
|
||||||
|
# 3. 生产环境建议使用HTTPS
|
||||||
|
# 4. 定期更新后端服务器的安全补丁
|
||||||
|
# 5. 不要在公开的代码库中暴露组织ID和工作组ID
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# 更多信息
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# Bytedesk官方文档: https://docs.bytedesk.com
|
||||||
|
# 技术支持: 访问http://43.143.189.195/chat/联系在线客服
|
||||||
|
# GitHub: https://github.com/Bytedesk/bytedesk
|
||||||
237
src/bytedesk-integration/App.jsx.example
Normal file
237
src/bytedesk-integration/App.jsx.example
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
/**
|
||||||
|
* vf_react App.jsx集成示例
|
||||||
|
*
|
||||||
|
* 本文件展示如何在vf_react项目中集成Bytedesk客服系统
|
||||||
|
*
|
||||||
|
* 集成步骤:
|
||||||
|
* 1. 将bytedesk-integration文件夹复制到src/目录
|
||||||
|
* 2. 在App.jsx中导入BytedeskWidget和配置
|
||||||
|
* 3. 添加BytedeskWidget组件(代码如下)
|
||||||
|
* 4. 配置.env文件(参考.env.bytedesk.example)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom'; // 如果使用react-router
|
||||||
|
import BytedeskWidget from './bytedesk-integration/components/BytedeskWidget';
|
||||||
|
import { getBytedeskConfig, shouldShowCustomerService } from './bytedesk-integration/config/bytedesk.config';
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 方案一: 全局集成(推荐)
|
||||||
|
// 适用场景: 客服系统需要在所有页面显示
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
// ========== vf_react原有代码保持不变 ==========
|
||||||
|
// 这里是您原有的App.jsx代码
|
||||||
|
// 例如: const [user, setUser] = useState(null);
|
||||||
|
// 例如: const [theme, setTheme] = useState('light');
|
||||||
|
// ... 保持原有逻辑不变 ...
|
||||||
|
|
||||||
|
// ========== Bytedesk集成代码开始 ==========
|
||||||
|
|
||||||
|
const location = useLocation(); // 获取当前路径
|
||||||
|
const [showBytedesk, setShowBytedesk] = useState(false);
|
||||||
|
|
||||||
|
// 根据页面路径决定是否显示客服
|
||||||
|
useEffect(() => {
|
||||||
|
const shouldShow = shouldShowCustomerService(location.pathname);
|
||||||
|
setShowBytedesk(shouldShow);
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
|
// 获取Bytedesk配置
|
||||||
|
const bytedeskConfig = getBytedeskConfig();
|
||||||
|
|
||||||
|
// 客服加载成功回调
|
||||||
|
const handleBytedeskLoad = (bytedesk) => {
|
||||||
|
console.log('[App] Bytedesk客服系统加载成功', bytedesk);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 客服加载失败回调
|
||||||
|
const handleBytedeskError = (error) => {
|
||||||
|
console.error('[App] Bytedesk客服系统加载失败', error);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ========== Bytedesk集成代码结束 ==========
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
{/* ========== vf_react原有内容保持不变 ========== */}
|
||||||
|
{/* 这里是您原有的App.jsx JSX代码 */}
|
||||||
|
{/* 例如: <Header /> */}
|
||||||
|
{/* 例如: <Router> <Routes> ... </Routes> </Router> */}
|
||||||
|
{/* ... 保持原有结构不变 ... */}
|
||||||
|
|
||||||
|
{/* ========== Bytedesk客服Widget ========== */}
|
||||||
|
{showBytedesk && (
|
||||||
|
<BytedeskWidget
|
||||||
|
config={bytedeskConfig}
|
||||||
|
autoLoad={true}
|
||||||
|
onLoad={handleBytedeskLoad}
|
||||||
|
onError={handleBytedeskError}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 方案二: 带用户信息集成
|
||||||
|
// 适用场景: 需要将登录用户信息传递给客服端
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/*
|
||||||
|
import React, { useState, useEffect, useContext } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import BytedeskWidget from './bytedesk-integration/components/BytedeskWidget';
|
||||||
|
import { getBytedeskConfigWithUser, shouldShowCustomerService } from './bytedesk-integration/config/bytedesk.config';
|
||||||
|
import { AuthContext } from './contexts/AuthContext'; // 假设您有用户认证Context
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
// 获取登录用户信息
|
||||||
|
const { user } = useContext(AuthContext);
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
|
const [showBytedesk, setShowBytedesk] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const shouldShow = shouldShowCustomerService(location.pathname);
|
||||||
|
setShowBytedesk(shouldShow);
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
|
// 根据用户信息生成配置
|
||||||
|
const bytedeskConfig = user
|
||||||
|
? getBytedeskConfigWithUser(user)
|
||||||
|
: getBytedeskConfig();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
// ... 您的原有代码 ...
|
||||||
|
|
||||||
|
{showBytedesk && (
|
||||||
|
<BytedeskWidget
|
||||||
|
config={bytedeskConfig}
|
||||||
|
autoLoad={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 方案三: 条件性加载
|
||||||
|
// 适用场景: 只在特定条件下显示客服(如用户已登录、特定用户角色等)
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/*
|
||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import BytedeskWidget from './bytedesk-integration/components/BytedeskWidget';
|
||||||
|
import { getBytedeskConfig } from './bytedesk-integration/config/bytedesk.config';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [user, setUser] = useState(null);
|
||||||
|
const [showBytedesk, setShowBytedesk] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 只有在用户登录且为普通用户时显示客服
|
||||||
|
if (user && user.role === 'customer') {
|
||||||
|
setShowBytedesk(true);
|
||||||
|
} else {
|
||||||
|
setShowBytedesk(false);
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
|
||||||
|
const bytedeskConfig = getBytedeskConfig();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
// ... 您的原有代码 ...
|
||||||
|
|
||||||
|
{showBytedesk && (
|
||||||
|
<BytedeskWidget
|
||||||
|
config={bytedeskConfig}
|
||||||
|
autoLoad={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 方案四: 动态控制显示/隐藏
|
||||||
|
// 适用场景: 需要通过按钮或其他交互控制客服显示
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/*
|
||||||
|
import React, { useState } from 'react';
|
||||||
|
import BytedeskWidget from './bytedesk-integration/components/BytedeskWidget';
|
||||||
|
import { getBytedeskConfig } from './bytedesk-integration/config/bytedesk.config';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [showBytedesk, setShowBytedesk] = useState(false);
|
||||||
|
const bytedeskConfig = getBytedeskConfig();
|
||||||
|
|
||||||
|
const toggleBytedesk = () => {
|
||||||
|
setShowBytedesk(prev => !prev);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="App">
|
||||||
|
// ... 您的原有代码 ...
|
||||||
|
|
||||||
|
{/* 自定义客服按钮 *\/}
|
||||||
|
<button onClick={toggleBytedesk} className="custom-service-button">
|
||||||
|
{showBytedesk ? '关闭客服' : '联系客服'}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{/* 客服Widget *\/}
|
||||||
|
{showBytedesk && (
|
||||||
|
<BytedeskWidget
|
||||||
|
config={bytedeskConfig}
|
||||||
|
autoLoad={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// 重要提示
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 1. CSS样式兼容性
|
||||||
|
* - Bytedesk Widget使用Shadow DOM,不会影响您的全局样式
|
||||||
|
* - Widget的样式可通过config中的theme配置调整
|
||||||
|
*
|
||||||
|
* 2. 性能优化
|
||||||
|
* - Widget脚本采用异步加载,不会阻塞页面渲染
|
||||||
|
* - 建议在非关键页面(如登录、支付页)隐藏客服
|
||||||
|
*
|
||||||
|
* 3. 错误处理
|
||||||
|
* - 如果客服脚本加载失败,不会影响主应用
|
||||||
|
* - 建议添加onError回调进行错误监控
|
||||||
|
*
|
||||||
|
* 4. 调试模式
|
||||||
|
* - 查看浏览器控制台的[Bytedesk]前缀日志
|
||||||
|
* - 检查Network面板确认脚本加载成功
|
||||||
|
*
|
||||||
|
* 5. 生产部署
|
||||||
|
* - 确保.env文件配置正确(特别是REACT_APP_BYTEDESK_API_URL)
|
||||||
|
* - 确保CORS已在后端配置(允许您的前端域名)
|
||||||
|
* - 在管理后台配置正确的工作组ID(sid)
|
||||||
|
*/
|
||||||
177
src/bytedesk-integration/components/BytedeskWidget.jsx
Normal file
177
src/bytedesk-integration/components/BytedeskWidget.jsx
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
/**
|
||||||
|
* Bytedesk客服Widget组件
|
||||||
|
* 用于vf_react项目集成
|
||||||
|
*
|
||||||
|
* 使用方法:
|
||||||
|
* import BytedeskWidget from './components/BytedeskWidget';
|
||||||
|
* import { getBytedeskConfig } from './config/bytedesk.config';
|
||||||
|
*
|
||||||
|
* <BytedeskWidget
|
||||||
|
* config={getBytedeskConfig()}
|
||||||
|
* autoLoad={true}
|
||||||
|
* />
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
const BytedeskWidget = ({
|
||||||
|
config,
|
||||||
|
autoLoad = true,
|
||||||
|
onLoad,
|
||||||
|
onError
|
||||||
|
}) => {
|
||||||
|
const scriptRef = useRef(null);
|
||||||
|
const widgetRef = useRef(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// 如果不自动加载或配置未设置,跳过
|
||||||
|
if (!autoLoad || !config) {
|
||||||
|
if (!config) {
|
||||||
|
console.warn('[Bytedesk] 配置未设置,客服组件未加载');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Bytedesk] 开始加载客服Widget...', config);
|
||||||
|
|
||||||
|
// 加载Bytedesk Widget脚本
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.src = 'https://www.weiyuai.cn/embed/bytedesk-web.js';
|
||||||
|
script.async = true;
|
||||||
|
script.id = 'bytedesk-web-script';
|
||||||
|
|
||||||
|
script.onload = () => {
|
||||||
|
console.log('[Bytedesk] Widget脚本加载成功');
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (window.BytedeskWeb) {
|
||||||
|
console.log('[Bytedesk] 初始化Widget');
|
||||||
|
const bytedesk = new window.BytedeskWeb(config);
|
||||||
|
bytedesk.init();
|
||||||
|
|
||||||
|
widgetRef.current = bytedesk;
|
||||||
|
console.log('[Bytedesk] Widget初始化成功');
|
||||||
|
|
||||||
|
if (onLoad) {
|
||||||
|
onLoad(bytedesk);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('BytedeskWeb对象未定义');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Bytedesk] Widget初始化失败:', error);
|
||||||
|
if (onError) {
|
||||||
|
onError(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
script.onerror = (error) => {
|
||||||
|
console.error('[Bytedesk] Widget脚本加载失败:', error);
|
||||||
|
if (onError) {
|
||||||
|
onError(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 添加脚本到页面
|
||||||
|
document.body.appendChild(script);
|
||||||
|
scriptRef.current = script;
|
||||||
|
|
||||||
|
// 清理函数
|
||||||
|
return () => {
|
||||||
|
console.log('[Bytedesk] 清理Widget');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 调用Widget的destroy方法(如果存在)
|
||||||
|
if (widgetRef.current && typeof widgetRef.current.destroy === 'function') {
|
||||||
|
console.log('[Bytedesk] 调用Widget.destroy()');
|
||||||
|
widgetRef.current.destroy();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[Bytedesk] Widget.destroy()失败:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 移除脚本
|
||||||
|
if (scriptRef.current) {
|
||||||
|
if (document.body.contains(scriptRef.current)) {
|
||||||
|
document.body.removeChild(scriptRef.current);
|
||||||
|
}
|
||||||
|
scriptRef.current = null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[Bytedesk] 移除脚本失败:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 移除Widget DOM元素(使用更安全的remove()方法)
|
||||||
|
const widgetElements = document.querySelectorAll('[class*="bytedesk"], [id*="bytedesk"]');
|
||||||
|
widgetElements.forEach(el => {
|
||||||
|
try {
|
||||||
|
if (el && el.parentNode) {
|
||||||
|
// 优先使用remove()方法(更现代、更安全)
|
||||||
|
if (typeof el.remove === 'function') {
|
||||||
|
el.remove();
|
||||||
|
} else {
|
||||||
|
el.parentNode.removeChild(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (removeError) {
|
||||||
|
console.warn('[Bytedesk] 移除DOM元素失败:', el, removeError.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[Bytedesk] 清理DOM元素失败:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 清理全局对象
|
||||||
|
if (window.BytedeskWeb) {
|
||||||
|
delete window.BytedeskWeb;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[Bytedesk] 清理全局对象失败:', error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[Bytedesk] Widget清理完成');
|
||||||
|
};
|
||||||
|
}, [config, autoLoad, onLoad, onError]);
|
||||||
|
|
||||||
|
// 不渲染任何元素(Widget会自动插入DOM到body)
|
||||||
|
// 返回null避免React DOM管理冲突
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
BytedeskWidget.propTypes = {
|
||||||
|
config: PropTypes.shape({
|
||||||
|
apiUrl: PropTypes.string.isRequired,
|
||||||
|
htmlUrl: PropTypes.string.isRequired,
|
||||||
|
placement: PropTypes.oneOf(['bottom-right', 'bottom-left', 'top-right', 'top-left']),
|
||||||
|
marginBottom: PropTypes.number,
|
||||||
|
marginSide: PropTypes.number,
|
||||||
|
autoPopup: PropTypes.bool,
|
||||||
|
locale: PropTypes.string,
|
||||||
|
bubbleConfig: PropTypes.shape({
|
||||||
|
show: PropTypes.bool,
|
||||||
|
icon: PropTypes.string,
|
||||||
|
title: PropTypes.string,
|
||||||
|
subtitle: PropTypes.string,
|
||||||
|
}),
|
||||||
|
theme: PropTypes.shape({
|
||||||
|
mode: PropTypes.oneOf(['light', 'dark', 'system']),
|
||||||
|
backgroundColor: PropTypes.string,
|
||||||
|
textColor: PropTypes.string,
|
||||||
|
}),
|
||||||
|
chatConfig: PropTypes.shape({
|
||||||
|
org: PropTypes.string.isRequired,
|
||||||
|
t: PropTypes.string.isRequired,
|
||||||
|
sid: PropTypes.string.isRequired,
|
||||||
|
}).isRequired,
|
||||||
|
}),
|
||||||
|
autoLoad: PropTypes.bool,
|
||||||
|
onLoad: PropTypes.func,
|
||||||
|
onError: PropTypes.func,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BytedeskWidget;
|
||||||
130
src/bytedesk-integration/config/bytedesk.config.js
Normal file
130
src/bytedesk-integration/config/bytedesk.config.js
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
/**
|
||||||
|
* Bytedesk客服配置文件
|
||||||
|
* 指向43.143.189.195服务器
|
||||||
|
*
|
||||||
|
* 环境变量配置(.env文件):
|
||||||
|
* REACT_APP_BYTEDESK_API_URL=http://43.143.189.195
|
||||||
|
* REACT_APP_BYTEDESK_ORG=df_org_uid
|
||||||
|
* REACT_APP_BYTEDESK_SID=df_wg_aftersales
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 从环境变量读取配置
|
||||||
|
const BYTEDESK_API_URL = process.env.REACT_APP_BYTEDESK_API_URL || 'http://43.143.189.195';
|
||||||
|
const BYTEDESK_ORG = process.env.REACT_APP_BYTEDESK_ORG || 'df_org_uid';
|
||||||
|
const BYTEDESK_SID = process.env.REACT_APP_BYTEDESK_SID || 'df_wg_aftersales';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bytedesk客服基础配置
|
||||||
|
*/
|
||||||
|
export const bytedeskConfig = {
|
||||||
|
// API服务地址
|
||||||
|
apiUrl: BYTEDESK_API_URL,
|
||||||
|
// 聊天页面地址
|
||||||
|
htmlUrl: `${BYTEDESK_API_URL}/chat/`,
|
||||||
|
|
||||||
|
// 客服图标位置
|
||||||
|
placement: 'bottom-right', // bottom-right | bottom-left | top-right | top-left
|
||||||
|
|
||||||
|
// 边距设置(像素)
|
||||||
|
marginBottom: 20,
|
||||||
|
marginSide: 20,
|
||||||
|
|
||||||
|
// 自动弹出(不推荐)
|
||||||
|
autoPopup: false,
|
||||||
|
|
||||||
|
// 语言设置
|
||||||
|
locale: 'zh-cn', // zh-cn | en | ja | ko
|
||||||
|
|
||||||
|
// 客服图标配置
|
||||||
|
bubbleConfig: {
|
||||||
|
show: true, // 是否显示客服图标
|
||||||
|
icon: '💬', // 图标(emoji或图片URL)
|
||||||
|
title: '在线客服', // 鼠标悬停标题
|
||||||
|
subtitle: '点击咨询', // 副标题
|
||||||
|
},
|
||||||
|
|
||||||
|
// 主题配置
|
||||||
|
theme: {
|
||||||
|
mode: 'system', // light | dark | system
|
||||||
|
backgroundColor: '#0066FF', // 主题色
|
||||||
|
textColor: '#ffffff', // 文字颜色
|
||||||
|
},
|
||||||
|
|
||||||
|
// 聊天配置(必需)
|
||||||
|
chatConfig: {
|
||||||
|
org: BYTEDESK_ORG, // 组织ID
|
||||||
|
t: '2', // 类型: 2=客服, 1=机器人
|
||||||
|
sid: BYTEDESK_SID, // 工作组ID
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取Bytedesk配置(根据环境自动切换)
|
||||||
|
*
|
||||||
|
* @returns {Object} Bytedesk配置对象
|
||||||
|
*/
|
||||||
|
export const getBytedeskConfig = () => {
|
||||||
|
// 开发环境使用代理(绕过X-Frame-Options限制)
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
return {
|
||||||
|
...bytedeskConfig,
|
||||||
|
apiUrl: '/bytedesk-api',
|
||||||
|
htmlUrl: '/bytedesk-api/chat/',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生产环境使用完整URL
|
||||||
|
return bytedeskConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取带用户信息的配置
|
||||||
|
* 用于已登录用户,自动传递用户信息到客服端
|
||||||
|
*
|
||||||
|
* @param {Object} user - 用户对象
|
||||||
|
* @param {string} user.id - 用户ID
|
||||||
|
* @param {string} user.name - 用户名
|
||||||
|
* @param {string} user.email - 用户邮箱
|
||||||
|
* @param {string} user.mobile - 用户手机号
|
||||||
|
* @returns {Object} 带用户信息的Bytedesk配置
|
||||||
|
*/
|
||||||
|
export const getBytedeskConfigWithUser = (user) => {
|
||||||
|
const config = getBytedeskConfig();
|
||||||
|
|
||||||
|
if (user && user.id) {
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
chatConfig: {
|
||||||
|
...config.chatConfig,
|
||||||
|
// 传递用户信息(可选)
|
||||||
|
customParams: {
|
||||||
|
userId: user.id,
|
||||||
|
userName: user.name || 'Guest',
|
||||||
|
userEmail: user.email || '',
|
||||||
|
userMobile: user.mobile || '',
|
||||||
|
source: 'web', // 来源标识
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据页面路径判断是否显示客服
|
||||||
|
*
|
||||||
|
* @param {string} pathname - 当前页面路径
|
||||||
|
* @returns {boolean} 是否显示客服
|
||||||
|
*/
|
||||||
|
export const shouldShowCustomerService = (pathname) => {
|
||||||
|
// 所有页面都显示Bytedesk客服
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
bytedeskConfig,
|
||||||
|
getBytedeskConfig,
|
||||||
|
getBytedeskConfigWithUser,
|
||||||
|
shouldShowCustomerService,
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// 集中管理应用的全局组件
|
// 集中管理应用的全局组件
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
import { useNotification } from '../contexts/NotificationContext';
|
import { useNotification } from '../contexts/NotificationContext';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
|
|
||||||
@@ -12,6 +13,10 @@ import NotificationTestTool from './NotificationTestTool';
|
|||||||
import ConnectionStatusBar from './ConnectionStatusBar';
|
import ConnectionStatusBar from './ConnectionStatusBar';
|
||||||
import ScrollToTop from './ScrollToTop';
|
import ScrollToTop from './ScrollToTop';
|
||||||
|
|
||||||
|
// Bytedesk客服组件
|
||||||
|
import BytedeskWidget from '../bytedesk-integration/components/BytedeskWidget';
|
||||||
|
import { getBytedeskConfig, shouldShowCustomerService } from '../bytedesk-integration/config/bytedesk.config';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ConnectionStatusBar 包装组件
|
* ConnectionStatusBar 包装组件
|
||||||
* 需要在 NotificationProvider 内部使用,所以在这里包装
|
* 需要在 NotificationProvider 内部使用,所以在这里包装
|
||||||
@@ -67,8 +72,12 @@ function ConnectionStatusBarWrapper() {
|
|||||||
* - AuthModalManager: 认证弹窗管理器
|
* - AuthModalManager: 认证弹窗管理器
|
||||||
* - NotificationContainer: 通知容器
|
* - NotificationContainer: 通知容器
|
||||||
* - NotificationTestTool: 通知测试工具 (仅开发环境)
|
* - NotificationTestTool: 通知测试工具 (仅开发环境)
|
||||||
|
* - BytedeskWidget: Bytedesk在线客服 (条件性显示,在/和/home页隐藏)
|
||||||
*/
|
*/
|
||||||
export function GlobalComponents() {
|
export function GlobalComponents() {
|
||||||
|
const location = useLocation();
|
||||||
|
const showBytedesk = shouldShowCustomerService(location.pathname);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Socket 连接状态条 */}
|
{/* Socket 连接状态条 */}
|
||||||
@@ -85,6 +94,14 @@ export function GlobalComponents() {
|
|||||||
|
|
||||||
{/* 通知测试工具 (仅开发环境) */}
|
{/* 通知测试工具 (仅开发环境) */}
|
||||||
<NotificationTestTool />
|
<NotificationTestTool />
|
||||||
|
|
||||||
|
{/* Bytedesk在线客服 - 根据路径条件性显示 */}
|
||||||
|
{showBytedesk && (
|
||||||
|
<BytedeskWidget
|
||||||
|
config={getBytedeskConfig()}
|
||||||
|
autoLoad={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import {
|
|||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { MdNotifications, MdClose, MdVolumeOff, MdVolumeUp, MdCampaign, MdTrendingUp, MdArticle, MdAssessment, MdWarning } from 'react-icons/md';
|
import { MdNotifications, MdClose, MdVolumeOff, MdVolumeUp, MdCampaign, MdTrendingUp, MdArticle, MdAssessment, MdWarning } from 'react-icons/md';
|
||||||
import { useNotification } from '../../contexts/NotificationContext';
|
import { useNotification } from '../../contexts/NotificationContext';
|
||||||
import { SOCKET_TYPE } from '../../services/socket';
|
|
||||||
import { NOTIFICATION_TYPES, PRIORITY_LEVELS } from '../../constants/notificationTypes';
|
import { NOTIFICATION_TYPES, PRIORITY_LEVELS } from '../../constants/notificationTypes';
|
||||||
|
|
||||||
const NotificationTestTool = () => {
|
const NotificationTestTool = () => {
|
||||||
@@ -295,7 +294,7 @@ const NotificationTestTool = () => {
|
|||||||
{isConnected ? 'Connected' : 'Disconnected'}
|
{isConnected ? 'Connected' : 'Disconnected'}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge colorScheme="purple">
|
<Badge colorScheme="purple">
|
||||||
{SOCKET_TYPE}
|
REAL
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge colorScheme={getPermissionColor()}>
|
<Badge colorScheme={getPermissionColor()}>
|
||||||
浏览器: {getPermissionLabel()}
|
浏览器: {getPermissionLabel()}
|
||||||
|
|||||||
@@ -58,7 +58,9 @@ export const AuthProvider = ({ children }) => {
|
|||||||
|
|
||||||
// 创建超时控制器
|
// 创建超时控制器
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时
|
const timeoutId = setTimeout(() => {
|
||||||
|
controller.abort(new Error('Session check timeout after 5 seconds'));
|
||||||
|
}, 5000); // 5秒超时
|
||||||
|
|
||||||
const response = await fetch(`/api/auth/session`, {
|
const response = await fetch(`/api/auth/session`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@@ -96,8 +98,18 @@ export const AuthProvider = ({ children }) => {
|
|||||||
setIsAuthenticated((prev) => prev === false ? prev : false);
|
setIsAuthenticated((prev) => prev === false ? prev : false);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('AuthContext', 'checkSession', error);
|
// ✅ 区分AbortError和真实错误
|
||||||
// 网络错误或超时,设置为未登录状态
|
if (error.name === 'AbortError') {
|
||||||
|
logger.debug('AuthContext', 'Session check aborted', {
|
||||||
|
reason: error.message || 'Request cancelled',
|
||||||
|
isTimeout: error.message?.includes('timeout')
|
||||||
|
});
|
||||||
|
// AbortError不改变登录状态(保持原状态)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只有真实错误才标记为未登录
|
||||||
|
logger.error('AuthContext', 'checkSession failed', error);
|
||||||
setUser((prev) => prev === null ? prev : null);
|
setUser((prev) => prev === null ? prev : null);
|
||||||
setIsAuthenticated((prev) => prev === false ? prev : false);
|
setIsAuthenticated((prev) => prev === false ? prev : false);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -108,7 +120,16 @@ export const AuthProvider = ({ children }) => {
|
|||||||
|
|
||||||
// ⚡ 初始化时检查Session - 并行执行,不阻塞页面渲染
|
// ⚡ 初始化时检查Session - 并行执行,不阻塞页面渲染
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
// 传递signal给checkSession(需要修改checkSession签名)
|
||||||
|
// 暂时使用原有方式,但添加cleanup防止组件卸载时的内存泄漏
|
||||||
checkSession(); // 直接调用,与页面渲染并行
|
checkSession(); // 直接调用,与页面渲染并行
|
||||||
|
|
||||||
|
// ✅ Cleanup: 组件卸载时abort可能正在进行的请求
|
||||||
|
return () => {
|
||||||
|
controller.abort(new Error('AuthProvider unmounted'));
|
||||||
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -2,20 +2,15 @@
|
|||||||
/**
|
/**
|
||||||
* 通知上下文 - 管理实时消息推送和通知显示
|
* 通知上下文 - 管理实时消息推送和通知显示
|
||||||
*
|
*
|
||||||
* 环境说明:
|
* 使用真实 Socket.IO 连接到后端服务器
|
||||||
* - SOCKET_TYPE === 'REAL': 使用真实 Socket.IO 连接(生产环境),连接到 wss://valuefrontier.cn
|
* 连接地址配置在环境变量中 (REACT_APP_API_URL)
|
||||||
* - SOCKET_TYPE === 'MOCK': 使用模拟 Socket 服务(开发环境),用于本地测试
|
|
||||||
*
|
|
||||||
* 环境切换:
|
|
||||||
* - 设置 REACT_APP_ENABLE_MOCK=true 或 REACT_APP_USE_MOCK_SOCKET=true 使用 MOCK 模式
|
|
||||||
* - 否则使用 REAL 模式连接生产环境
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react';
|
import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react';
|
||||||
import { useToast, Box, HStack, Text, Button, CloseButton, VStack, Icon } from '@chakra-ui/react';
|
import { useToast, Box, HStack, Text, Button, CloseButton, VStack, Icon } from '@chakra-ui/react';
|
||||||
import { BellIcon } from '@chakra-ui/icons';
|
import { BellIcon } from '@chakra-ui/icons';
|
||||||
import { logger } from '../utils/logger';
|
import { logger } from '../utils/logger';
|
||||||
import socket, { SOCKET_TYPE } from '../services/socket';
|
import socket from '../services/socket';
|
||||||
import notificationSound from '../assets/sounds/notification.wav';
|
import notificationSound from '../assets/sounds/notification.wav';
|
||||||
import { browserNotificationService } from '../services/browserNotificationService';
|
import { browserNotificationService } from '../services/browserNotificationService';
|
||||||
import { notificationMetricsService } from '../services/notificationMetricsService';
|
import { notificationMetricsService } from '../services/notificationMetricsService';
|
||||||
@@ -62,6 +57,7 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
const reconnectedTimerRef = useRef(null); // 用于自动消失 RECONNECTED 状态
|
const reconnectedTimerRef = useRef(null); // 用于自动消失 RECONNECTED 状态
|
||||||
const processedEventIds = useRef(new Set()); // 用于Socket层去重,记录已处理的事件ID
|
const processedEventIds = useRef(new Set()); // 用于Socket层去重,记录已处理的事件ID
|
||||||
const MAX_PROCESSED_IDS = 1000; // 最多保留1000个ID,避免内存泄漏
|
const MAX_PROCESSED_IDS = 1000; // 最多保留1000个ID,避免内存泄漏
|
||||||
|
const notificationTimers = useRef(new Map()); // 跟踪所有通知的自动关闭定时器
|
||||||
|
|
||||||
// ⚡ 使用权限引导管理 Hook
|
// ⚡ 使用权限引导管理 Hook
|
||||||
const { shouldShowGuide, markGuideAsShown } = usePermissionGuide();
|
const { shouldShowGuide, markGuideAsShown } = usePermissionGuide();
|
||||||
@@ -71,9 +67,20 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
try {
|
try {
|
||||||
audioRef.current = new Audio(notificationSound);
|
audioRef.current = new Audio(notificationSound);
|
||||||
audioRef.current.volume = 0.5;
|
audioRef.current.volume = 0.5;
|
||||||
|
logger.info('NotificationContext', 'Audio initialized');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('NotificationContext', 'Audio initialization failed', error);
|
logger.error('NotificationContext', 'Audio initialization failed', error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理函数:释放音频资源
|
||||||
|
return () => {
|
||||||
|
if (audioRef.current) {
|
||||||
|
audioRef.current.pause();
|
||||||
|
audioRef.current.src = '';
|
||||||
|
audioRef.current = null;
|
||||||
|
logger.info('NotificationContext', 'Audio resources cleaned up');
|
||||||
|
}
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,6 +111,13 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
const removeNotification = useCallback((id, wasClicked = false) => {
|
const removeNotification = useCallback((id, wasClicked = false) => {
|
||||||
logger.info('NotificationContext', 'Removing notification', { id, wasClicked });
|
logger.info('NotificationContext', 'Removing notification', { id, wasClicked });
|
||||||
|
|
||||||
|
// 清理对应的定时器
|
||||||
|
if (notificationTimers.current.has(id)) {
|
||||||
|
clearTimeout(notificationTimers.current.get(id));
|
||||||
|
notificationTimers.current.delete(id);
|
||||||
|
logger.info('NotificationContext', 'Cleared auto-close timer', { id });
|
||||||
|
}
|
||||||
|
|
||||||
// 监控埋点:追踪关闭(非点击的情况)
|
// 监控埋点:追踪关闭(非点击的情况)
|
||||||
setNotifications(prev => {
|
setNotifications(prev => {
|
||||||
const notification = prev.find(n => n.id === id);
|
const notification = prev.find(n => n.id === id);
|
||||||
@@ -119,6 +133,14 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
*/
|
*/
|
||||||
const clearAllNotifications = useCallback(() => {
|
const clearAllNotifications = useCallback(() => {
|
||||||
logger.info('NotificationContext', 'Clearing all notifications');
|
logger.info('NotificationContext', 'Clearing all notifications');
|
||||||
|
|
||||||
|
// 清理所有定时器
|
||||||
|
notificationTimers.current.forEach((timerId, id) => {
|
||||||
|
clearTimeout(timerId);
|
||||||
|
logger.info('NotificationContext', 'Cleared timer during clear all', { id });
|
||||||
|
});
|
||||||
|
notificationTimers.current.clear();
|
||||||
|
|
||||||
setNotifications([]);
|
setNotifications([]);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -446,9 +468,16 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
|
|
||||||
// 自动关闭
|
// 自动关闭
|
||||||
if (newNotification.autoClose && newNotification.autoClose > 0) {
|
if (newNotification.autoClose && newNotification.autoClose > 0) {
|
||||||
setTimeout(() => {
|
const timerId = setTimeout(() => {
|
||||||
removeNotification(newNotification.id);
|
removeNotification(newNotification.id);
|
||||||
}, newNotification.autoClose);
|
}, newNotification.autoClose);
|
||||||
|
|
||||||
|
// 将定时器ID保存到Map中
|
||||||
|
notificationTimers.current.set(newNotification.id, timerId);
|
||||||
|
logger.info('NotificationContext', 'Set auto-close timer', {
|
||||||
|
id: newNotification.id,
|
||||||
|
delay: newNotification.autoClose
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [playNotificationSound, removeNotification]);
|
}, [playNotificationSound, removeNotification]);
|
||||||
|
|
||||||
@@ -548,34 +577,11 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
|
|
||||||
const isPageHidden = document.hidden; // 页面是否在后台
|
const isPageHidden = document.hidden; // 页面是否在后台
|
||||||
|
|
||||||
// ========== 原分发策略(按优先级区分)- 已废弃 ==========
|
// ========== 通知分发策略(区分前后台) ==========
|
||||||
// 策略 1: 紧急通知 - 双重保障(浏览器 + 网页)
|
// 策略: 根据页面可见性智能分发通知
|
||||||
// if (priority === PRIORITY_LEVELS.URGENT) {
|
// - 页面在后台: 发送浏览器通知(系统级提醒)
|
||||||
// logger.info('NotificationContext', 'Urgent notification: sending browser + web');
|
// - 页面在前台: 发送网页通知(页面内 Toast)
|
||||||
// // 总是发送浏览器通知
|
// 注: 不再区分优先级,统一使用前后台策略
|
||||||
// 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);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// ========== 新分发策略(仅区分前后台) ==========
|
|
||||||
if (isPageHidden) {
|
if (isPageHidden) {
|
||||||
// 页面在后台:发送浏览器通知
|
// 页面在后台:发送浏览器通知
|
||||||
logger.info('NotificationContext', 'Page hidden: sending browser notification');
|
logger.info('NotificationContext', 'Page hidden: sending browser notification');
|
||||||
@@ -592,7 +598,7 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
// 连接到 Socket 服务
|
// 连接到 Socket 服务
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
logger.info('NotificationContext', 'Initializing socket connection...');
|
logger.info('NotificationContext', 'Initializing socket connection...');
|
||||||
console.log(`%c[NotificationContext] Initializing socket (type: ${SOCKET_TYPE})`, 'color: #673AB7; font-weight: bold;');
|
console.log('%c[NotificationContext] Initializing socket connection', 'color: #673AB7; font-weight: bold;');
|
||||||
|
|
||||||
// ✅ 第一步: 注册所有事件监听器
|
// ✅ 第一步: 注册所有事件监听器
|
||||||
console.log('%c[NotificationContext] Step 1: Registering event listeners...', 'color: #673AB7;');
|
console.log('%c[NotificationContext] Step 1: Registering event listeners...', 'color: #673AB7;');
|
||||||
@@ -624,30 +630,22 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
setConnectionStatus(CONNECTION_STATUS.CONNECTED);
|
setConnectionStatus(CONNECTION_STATUS.CONNECTED);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果使用 mock,可以启动定期推送
|
// 订阅事件推送
|
||||||
if (SOCKET_TYPE === 'MOCK') {
|
console.log('%c[NotificationContext] 🔔 订阅事件推送...', 'color: #FF9800; font-weight: bold;');
|
||||||
// 启动模拟推送:使用配置的间隔和数量
|
|
||||||
const { interval, maxBatch } = NOTIFICATION_CONFIG.mockPush;
|
|
||||||
socket.startMockPush(interval, maxBatch);
|
|
||||||
logger.info('NotificationContext', 'Mock push started', { interval, maxBatch });
|
|
||||||
} else {
|
|
||||||
// ✅ 真实模式下,订阅事件推送
|
|
||||||
console.log('%c[NotificationContext] 🔔 订阅事件推送...', 'color: #FF9800; font-weight: bold;');
|
|
||||||
|
|
||||||
if (socket.subscribeToEvents) {
|
if (socket.subscribeToEvents) {
|
||||||
socket.subscribeToEvents({
|
socket.subscribeToEvents({
|
||||||
eventType: 'all',
|
eventType: 'all',
|
||||||
importance: 'all',
|
importance: 'all',
|
||||||
onSubscribed: (data) => {
|
onSubscribed: (data) => {
|
||||||
console.log('%c[NotificationContext] ✅ 订阅成功!', 'color: #4CAF50; font-weight: bold;');
|
console.log('%c[NotificationContext] ✅ 订阅成功!', 'color: #4CAF50; font-weight: bold;');
|
||||||
console.log('[NotificationContext] 订阅确认:', data);
|
console.log('[NotificationContext] 订阅确认:', data);
|
||||||
logger.info('NotificationContext', 'Events subscribed', data);
|
logger.info('NotificationContext', 'Events subscribed', data);
|
||||||
},
|
},
|
||||||
// ⚠️ 不需要 onNewEvent 回调,因为 NotificationContext 已经通过 socket.on('new_event') 监听
|
// ⚠️ 不需要 onNewEvent 回调,因为 NotificationContext 已经通过 socket.on('new_event') 监听
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.warn('[NotificationContext] ⚠️ socket.subscribeToEvents 方法不可用');
|
console.error('[NotificationContext] ❌ socket.subscribeToEvents 方法不可用');
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -662,10 +660,10 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
logger.error('NotificationContext', 'Socket connect_error', error);
|
logger.error('NotificationContext', 'Socket connect_error', error);
|
||||||
setConnectionStatus(CONNECTION_STATUS.RECONNECTING);
|
setConnectionStatus(CONNECTION_STATUS.RECONNECTING);
|
||||||
|
|
||||||
// 获取重连次数(Real 和 Mock 都支持)
|
// 获取重连次数
|
||||||
const attempts = socket.getReconnectAttempts?.() || 0;
|
const attempts = socket.getReconnectAttempts?.() || 0;
|
||||||
setReconnectAttempt(attempts);
|
setReconnectAttempt(attempts);
|
||||||
logger.info('NotificationContext', 'Reconnection attempt', { attempts, socketType: SOCKET_TYPE });
|
logger.info('NotificationContext', 'Reconnection attempt', { attempts });
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听重连失败
|
// 监听重连失败
|
||||||
@@ -696,7 +694,18 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
logger.info('NotificationContext', 'Received new event', data);
|
logger.info('NotificationContext', 'Received new event', data);
|
||||||
|
|
||||||
// ========== Socket层去重检查 ==========
|
// ========== Socket层去重检查 ==========
|
||||||
const eventId = data.id || `${data.type}_${data.publishTime}`;
|
// 生成更健壮的事件ID
|
||||||
|
const eventId = data.id ||
|
||||||
|
`${data.type || 'unknown'}_${data.publishTime || Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
|
|
||||||
|
// 如果缺少原始ID,记录警告
|
||||||
|
if (!data.id) {
|
||||||
|
logger.warn('NotificationContext', 'Event missing ID, generated fallback', {
|
||||||
|
eventId,
|
||||||
|
eventType: data.type,
|
||||||
|
title: data.title
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (processedEventIds.current.has(eventId)) {
|
if (processedEventIds.current.has(eventId)) {
|
||||||
logger.debug('NotificationContext', 'Duplicate event ignored at socket level', { eventId });
|
logger.debug('NotificationContext', 'Duplicate event ignored at socket level', { eventId });
|
||||||
@@ -752,11 +761,19 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
return () => {
|
return () => {
|
||||||
logger.info('NotificationContext', 'Cleaning up socket connection');
|
logger.info('NotificationContext', 'Cleaning up socket connection');
|
||||||
|
|
||||||
// 如果是 mock service,停止推送
|
// 清理 reconnected 状态定时器
|
||||||
if (SOCKET_TYPE === 'MOCK') {
|
if (reconnectedTimerRef.current) {
|
||||||
socket.stopMockPush();
|
clearTimeout(reconnectedTimerRef.current);
|
||||||
|
reconnectedTimerRef.current = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 清理所有通知的自动关闭定时器
|
||||||
|
notificationTimers.current.forEach((timerId, id) => {
|
||||||
|
clearTimeout(timerId);
|
||||||
|
logger.info('NotificationContext', 'Cleared timer during cleanup', { id });
|
||||||
|
});
|
||||||
|
notificationTimers.current.clear();
|
||||||
|
|
||||||
socket.off('connect');
|
socket.off('connect');
|
||||||
socket.off('disconnect');
|
socket.off('disconnect');
|
||||||
socket.off('connect_error');
|
socket.off('connect_error');
|
||||||
@@ -776,11 +793,7 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
const handleVisibilityChange = () => {
|
const handleVisibilityChange = () => {
|
||||||
if (document.visibilityState === 'visible' && !isConnected && connectionStatus === CONNECTION_STATUS.FAILED) {
|
if (document.visibilityState === 'visible' && !isConnected && connectionStatus === CONNECTION_STATUS.FAILED) {
|
||||||
logger.info('NotificationContext', 'Tab refocused, attempting auto-reconnect');
|
logger.info('NotificationContext', 'Tab refocused, attempting auto-reconnect');
|
||||||
if (SOCKET_TYPE === 'REAL') {
|
socket.reconnect?.();
|
||||||
socket.reconnect?.();
|
|
||||||
} else {
|
|
||||||
socket.connect();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -806,11 +819,7 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
isClosable: true,
|
isClosable: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (SOCKET_TYPE === 'REAL') {
|
socket.reconnect?.();
|
||||||
socket.reconnect?.();
|
|
||||||
} else {
|
|
||||||
socket.connect();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -842,14 +851,51 @@ export const NotificationProvider = ({ children }) => {
|
|||||||
const retryConnection = useCallback(() => {
|
const retryConnection = useCallback(() => {
|
||||||
logger.info('NotificationContext', 'Manual reconnection triggered');
|
logger.info('NotificationContext', 'Manual reconnection triggered');
|
||||||
setConnectionStatus(CONNECTION_STATUS.RECONNECTING);
|
setConnectionStatus(CONNECTION_STATUS.RECONNECTING);
|
||||||
|
socket.reconnect?.();
|
||||||
if (SOCKET_TYPE === 'REAL') {
|
|
||||||
socket.reconnect?.();
|
|
||||||
} else {
|
|
||||||
socket.connect();
|
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步浏览器通知权限状态
|
||||||
|
* 场景:
|
||||||
|
* 1. 用户在其他标签页授权后返回
|
||||||
|
* 2. 用户在浏览器设置中修改权限
|
||||||
|
* 3. 页面长时间打开后权限状态变化
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
const checkPermission = () => {
|
||||||
|
const current = browserNotificationService.getPermissionStatus();
|
||||||
|
if (current !== browserPermission) {
|
||||||
|
logger.info('NotificationContext', 'Browser permission changed', {
|
||||||
|
old: browserPermission,
|
||||||
|
new: current
|
||||||
|
});
|
||||||
|
setBrowserPermission(current);
|
||||||
|
|
||||||
|
// 如果权限被授予,显示成功提示
|
||||||
|
if (current === 'granted' && browserPermission !== 'granted') {
|
||||||
|
toast({
|
||||||
|
title: '桌面通知已开启',
|
||||||
|
description: '您现在可以在后台接收重要通知',
|
||||||
|
status: 'success',
|
||||||
|
duration: 3000,
|
||||||
|
isClosable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 页面聚焦时检查
|
||||||
|
window.addEventListener('focus', checkPermission);
|
||||||
|
|
||||||
|
// 定期检查(可选,用于捕获浏览器设置中的变化)
|
||||||
|
const intervalId = setInterval(checkPermission, 30000); // 每30秒检查一次
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('focus', checkPermission);
|
||||||
|
clearInterval(intervalId);
|
||||||
|
};
|
||||||
|
}, [browserPermission, toast]);
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
notifications,
|
notifications,
|
||||||
isConnected,
|
isConnected,
|
||||||
|
|||||||
253
src/devtools/apiDebugger.js
Normal file
253
src/devtools/apiDebugger.js
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
// src/debug/apiDebugger.js
|
||||||
|
/**
|
||||||
|
* API 调试工具
|
||||||
|
* 生产环境临时调试使用,后期可整体删除 src/debug/ 目录
|
||||||
|
*/
|
||||||
|
|
||||||
|
import axios from 'axios';
|
||||||
|
import { getApiBase } from '@utils/apiConfig';
|
||||||
|
|
||||||
|
class ApiDebugger {
|
||||||
|
constructor() {
|
||||||
|
this.requestLog = [];
|
||||||
|
this.maxLogSize = 100;
|
||||||
|
this.isLogging = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化 Axios 拦截器
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
// 请求拦截器
|
||||||
|
axios.interceptors.request.use(
|
||||||
|
(config) => {
|
||||||
|
if (this.isLogging) {
|
||||||
|
const logEntry = {
|
||||||
|
type: 'request',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
method: config.method.toUpperCase(),
|
||||||
|
url: config.url,
|
||||||
|
baseURL: config.baseURL,
|
||||||
|
fullURL: this._getFullURL(config),
|
||||||
|
headers: config.headers,
|
||||||
|
data: config.data,
|
||||||
|
params: config.params,
|
||||||
|
};
|
||||||
|
|
||||||
|
this._addLog(logEntry);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`%c[API Request] ${logEntry.method} ${logEntry.fullURL}`,
|
||||||
|
'color: #2196F3; font-weight: bold;',
|
||||||
|
{
|
||||||
|
headers: config.headers,
|
||||||
|
data: config.data,
|
||||||
|
params: config.params,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
console.error('[API Request Error]', error);
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
axios.interceptors.response.use(
|
||||||
|
(response) => {
|
||||||
|
if (this.isLogging) {
|
||||||
|
const logEntry = {
|
||||||
|
type: 'response',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
method: response.config.method.toUpperCase(),
|
||||||
|
url: response.config.url,
|
||||||
|
fullURL: this._getFullURL(response.config),
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
headers: response.headers,
|
||||||
|
data: response.data,
|
||||||
|
};
|
||||||
|
|
||||||
|
this._addLog(logEntry);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`%c[API Response] ${logEntry.method} ${logEntry.fullURL} - ${logEntry.status}`,
|
||||||
|
'color: #4CAF50; font-weight: bold;',
|
||||||
|
{
|
||||||
|
status: response.status,
|
||||||
|
data: response.data,
|
||||||
|
headers: response.headers,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
if (this.isLogging) {
|
||||||
|
const logEntry = {
|
||||||
|
type: 'error',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
method: error.config?.method?.toUpperCase() || 'UNKNOWN',
|
||||||
|
url: error.config?.url || 'UNKNOWN',
|
||||||
|
fullURL: error.config ? this._getFullURL(error.config) : 'UNKNOWN',
|
||||||
|
status: error.response?.status,
|
||||||
|
statusText: error.response?.statusText,
|
||||||
|
message: error.message,
|
||||||
|
data: error.response?.data,
|
||||||
|
};
|
||||||
|
|
||||||
|
this._addLog(logEntry);
|
||||||
|
|
||||||
|
console.error(
|
||||||
|
`%c[API Error] ${logEntry.method} ${logEntry.fullURL}`,
|
||||||
|
'color: #F44336; font-weight: bold;',
|
||||||
|
{
|
||||||
|
status: error.response?.status,
|
||||||
|
message: error.message,
|
||||||
|
data: error.response?.data,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log('%c[API Debugger] Initialized', 'color: #FF9800; font-weight: bold;');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取完整 URL
|
||||||
|
*/
|
||||||
|
_getFullURL(config) {
|
||||||
|
const baseURL = config.baseURL || '';
|
||||||
|
const url = config.url || '';
|
||||||
|
const fullURL = baseURL + url;
|
||||||
|
|
||||||
|
// 添加查询参数
|
||||||
|
if (config.params) {
|
||||||
|
const params = new URLSearchParams(config.params).toString();
|
||||||
|
return params ? `${fullURL}?${params}` : fullURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加日志
|
||||||
|
*/
|
||||||
|
_addLog(entry) {
|
||||||
|
this.requestLog.unshift(entry);
|
||||||
|
if (this.requestLog.length > this.maxLogSize) {
|
||||||
|
this.requestLog = this.requestLog.slice(0, this.maxLogSize);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有日志
|
||||||
|
*/
|
||||||
|
getLogs(type = 'all') {
|
||||||
|
if (type === 'all') {
|
||||||
|
return this.requestLog;
|
||||||
|
}
|
||||||
|
return this.requestLog.filter((log) => log.type === type);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空日志
|
||||||
|
*/
|
||||||
|
clearLogs() {
|
||||||
|
this.requestLog = [];
|
||||||
|
console.log('[API Debugger] Logs cleared');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出日志为 JSON
|
||||||
|
*/
|
||||||
|
exportLogs() {
|
||||||
|
const blob = new Blob([JSON.stringify(this.requestLog, null, 2)], {
|
||||||
|
type: 'application/json',
|
||||||
|
});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `api-logs-${Date.now()}.json`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
console.log('[API Debugger] Logs exported');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印日志统计
|
||||||
|
*/
|
||||||
|
printStats() {
|
||||||
|
const stats = {
|
||||||
|
total: this.requestLog.length,
|
||||||
|
requests: this.requestLog.filter((log) => log.type === 'request').length,
|
||||||
|
responses: this.requestLog.filter((log) => log.type === 'response').length,
|
||||||
|
errors: this.requestLog.filter((log) => log.type === 'error').length,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.table(stats);
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动发送 API 请求(测试用)
|
||||||
|
*/
|
||||||
|
async testRequest(method, endpoint, data = null, config = {}) {
|
||||||
|
const apiBase = getApiBase();
|
||||||
|
const url = `${apiBase}${endpoint}`;
|
||||||
|
|
||||||
|
console.log(`[API Debugger] Testing ${method.toUpperCase()} ${url}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios({
|
||||||
|
method,
|
||||||
|
url,
|
||||||
|
data,
|
||||||
|
...config,
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[API Debugger] Test succeeded:', response.data);
|
||||||
|
return response.data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[API Debugger] Test failed:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开启/关闭日志记录
|
||||||
|
*/
|
||||||
|
toggleLogging(enabled) {
|
||||||
|
this.isLogging = enabled;
|
||||||
|
console.log(`[API Debugger] Logging ${enabled ? 'enabled' : 'disabled'}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最近的错误
|
||||||
|
*/
|
||||||
|
getRecentErrors(count = 10) {
|
||||||
|
return this.requestLog.filter((log) => log.type === 'error').slice(0, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按 URL 过滤日志
|
||||||
|
*/
|
||||||
|
getLogsByURL(urlPattern) {
|
||||||
|
return this.requestLog.filter((log) => log.url && log.url.includes(urlPattern));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按状态码过滤日志
|
||||||
|
*/
|
||||||
|
getLogsByStatus(status) {
|
||||||
|
return this.requestLog.filter((log) => log.status === status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出单例
|
||||||
|
export const apiDebugger = new ApiDebugger();
|
||||||
|
export default apiDebugger;
|
||||||
268
src/devtools/index.js
Normal file
268
src/devtools/index.js
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
// src/debug/index.js
|
||||||
|
/**
|
||||||
|
* 调试工具统一入口
|
||||||
|
*
|
||||||
|
* 使用方法:
|
||||||
|
* 1. 开启调试: 在 .env.production 中设置 REACT_APP_ENABLE_DEBUG=true
|
||||||
|
* 2. 使用控制台命令: window.__DEBUG__.api.getLogs()
|
||||||
|
* 3. 后期移除: 删除整个 src/debug/ 目录 + 从 src/index.js 移除导入
|
||||||
|
*
|
||||||
|
* 全局 API:
|
||||||
|
* - window.__DEBUG__ - 调试 API 主对象
|
||||||
|
* - window.__DEBUG__.api - API 调试工具
|
||||||
|
* - window.__DEBUG__.notification - 通知调试工具
|
||||||
|
* - window.__DEBUG__.socket - Socket 调试工具
|
||||||
|
* - window.__DEBUG__.help() - 显示帮助信息
|
||||||
|
* - window.__DEBUG__.exportAll() - 导出所有日志
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { apiDebugger } from './apiDebugger';
|
||||||
|
import { notificationDebugger } from './notificationDebugger';
|
||||||
|
import { socketDebugger } from './socketDebugger';
|
||||||
|
|
||||||
|
class DebugToolkit {
|
||||||
|
constructor() {
|
||||||
|
this.api = apiDebugger;
|
||||||
|
this.notification = notificationDebugger;
|
||||||
|
this.socket = socketDebugger;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化所有调试工具
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
console.log(
|
||||||
|
'%c╔════════════════════════════════════════════════════════════════╗',
|
||||||
|
'color: #FF9800; font-weight: bold;'
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
'%c║ 🔧 调试模式已启用 (Debug Mode Enabled) ║',
|
||||||
|
'color: #FF9800; font-weight: bold;'
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
'%c╚════════════════════════════════════════════════════════════════╝',
|
||||||
|
'color: #FF9800; font-weight: bold;'
|
||||||
|
);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// 初始化各个调试工具
|
||||||
|
this.api.init();
|
||||||
|
this.notification.init();
|
||||||
|
this.socket.init();
|
||||||
|
|
||||||
|
// 暴露到全局
|
||||||
|
window.__DEBUG__ = this;
|
||||||
|
|
||||||
|
// 打印帮助信息
|
||||||
|
this._printWelcome();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印欢迎信息
|
||||||
|
*/
|
||||||
|
_printWelcome() {
|
||||||
|
console.log('%c📚 调试工具使用指南:', 'color: #2196F3; font-weight: bold; font-size: 14px;');
|
||||||
|
console.log('');
|
||||||
|
console.log('%c1️⃣ API 调试:', 'color: #2196F3; font-weight: bold;');
|
||||||
|
console.log(' __DEBUG__.api.getLogs() - 获取所有 API 日志');
|
||||||
|
console.log(' __DEBUG__.api.getRecentErrors() - 获取最近的错误');
|
||||||
|
console.log(' __DEBUG__.api.exportLogs() - 导出 API 日志');
|
||||||
|
console.log(' __DEBUG__.api.testRequest(method, endpoint, data) - 测试 API 请求');
|
||||||
|
console.log('');
|
||||||
|
console.log('%c2️⃣ 通知调试:', 'color: #9C27B0; font-weight: bold;');
|
||||||
|
console.log(' __DEBUG__.notification.getLogs() - 获取所有通知日志');
|
||||||
|
console.log(' __DEBUG__.notification.forceNotification() - 发送测试通知');
|
||||||
|
console.log(' __DEBUG__.notification.checkPermission() - 检查通知权限');
|
||||||
|
console.log(' __DEBUG__.notification.exportLogs() - 导出通知日志');
|
||||||
|
console.log('');
|
||||||
|
console.log('%c3️⃣ Socket 调试:', 'color: #00BCD4; font-weight: bold;');
|
||||||
|
console.log(' __DEBUG__.socket.getLogs() - 获取所有 Socket 日志');
|
||||||
|
console.log(' __DEBUG__.socket.getStatus() - 获取连接状态');
|
||||||
|
console.log(' __DEBUG__.socket.reconnect() - 手动重连');
|
||||||
|
console.log(' __DEBUG__.socket.exportLogs() - 导出 Socket 日志');
|
||||||
|
console.log('');
|
||||||
|
console.log('%c4️⃣ 通用命令:', 'color: #4CAF50; font-weight: bold;');
|
||||||
|
console.log(' __DEBUG__.help() - 显示帮助信息');
|
||||||
|
console.log(' __DEBUG__.exportAll() - 导出所有日志');
|
||||||
|
console.log(' __DEBUG__.printStats() - 打印所有统计信息');
|
||||||
|
console.log(' __DEBUG__.clearAll() - 清空所有日志');
|
||||||
|
console.log('');
|
||||||
|
console.log(
|
||||||
|
'%c⚠️ 警告: 调试模式会记录所有 API 请求和响应,请勿在生产环境长期开启!',
|
||||||
|
'color: #F44336; font-weight: bold;'
|
||||||
|
);
|
||||||
|
console.log('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示帮助信息
|
||||||
|
*/
|
||||||
|
help() {
|
||||||
|
this._printWelcome();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出所有日志
|
||||||
|
*/
|
||||||
|
exportAll() {
|
||||||
|
console.log('[Debug Toolkit] Exporting all logs...');
|
||||||
|
|
||||||
|
const allLogs = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
api: this.api.getLogs(),
|
||||||
|
notification: this.notification.getLogs(),
|
||||||
|
socket: this.socket.getLogs(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const blob = new Blob([JSON.stringify(allLogs, null, 2)], {
|
||||||
|
type: 'application/json',
|
||||||
|
});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `debug-all-logs-${Date.now()}.json`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
console.log('[Debug Toolkit] ✅ All logs exported');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印所有统计信息
|
||||||
|
*/
|
||||||
|
printStats() {
|
||||||
|
console.log('\n%c=== 📊 调试统计信息 ===', 'color: #FF9800; font-weight: bold; font-size: 16px;');
|
||||||
|
console.log('\n%c[API 统计]', 'color: #2196F3; font-weight: bold;');
|
||||||
|
const apiStats = this.api.printStats();
|
||||||
|
|
||||||
|
console.log('\n%c[通知统计]', 'color: #9C27B0; font-weight: bold;');
|
||||||
|
const notificationStats = this.notification.printStats();
|
||||||
|
|
||||||
|
console.log('\n%c[Socket 统计]', 'color: #00BCD4; font-weight: bold;');
|
||||||
|
const socketStats = this.socket.printStats();
|
||||||
|
|
||||||
|
return {
|
||||||
|
api: apiStats,
|
||||||
|
notification: notificationStats,
|
||||||
|
socket: socketStats,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空所有日志
|
||||||
|
*/
|
||||||
|
clearAll() {
|
||||||
|
console.log('[Debug Toolkit] Clearing all logs...');
|
||||||
|
this.api.clearLogs();
|
||||||
|
this.notification.clearLogs();
|
||||||
|
this.socket.clearLogs();
|
||||||
|
console.log('[Debug Toolkit] ✅ All logs cleared');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速诊断(检查所有系统状态)
|
||||||
|
*/
|
||||||
|
diagnose() {
|
||||||
|
console.log('\n%c=== 🔍 系统诊断 ===', 'color: #FF9800; font-weight: bold; font-size: 16px;');
|
||||||
|
|
||||||
|
// 1. Socket 状态
|
||||||
|
console.log('\n%c[1/3] Socket 状态', 'color: #00BCD4; font-weight: bold;');
|
||||||
|
const socketStatus = this.socket.getStatus();
|
||||||
|
|
||||||
|
// 2. 通知权限
|
||||||
|
console.log('\n%c[2/3] 通知权限', 'color: #9C27B0; font-weight: bold;');
|
||||||
|
const notificationStatus = this.notification.checkPermission();
|
||||||
|
|
||||||
|
// 3. API 错误
|
||||||
|
console.log('\n%c[3/3] 最近的 API 错误', 'color: #F44336; font-weight: bold;');
|
||||||
|
const recentErrors = this.api.getRecentErrors(5);
|
||||||
|
if (recentErrors.length > 0) {
|
||||||
|
console.table(
|
||||||
|
recentErrors.map((err) => ({
|
||||||
|
时间: err.timestamp,
|
||||||
|
方法: err.method,
|
||||||
|
URL: err.url,
|
||||||
|
状态码: err.status,
|
||||||
|
错误信息: err.message,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log('✅ 没有 API 错误');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 汇总报告
|
||||||
|
const report = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
socket: socketStatus,
|
||||||
|
notification: notificationStatus,
|
||||||
|
apiErrors: recentErrors.length,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('\n%c=== 诊断报告 ===', 'color: #4CAF50; font-weight: bold;');
|
||||||
|
console.table(report);
|
||||||
|
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 性能监控
|
||||||
|
*/
|
||||||
|
performance() {
|
||||||
|
console.log('\n%c=== ⚡ 性能监控 ===', 'color: #FF9800; font-weight: bold; font-size: 16px;');
|
||||||
|
|
||||||
|
// 计算 API 平均响应时间
|
||||||
|
const apiLogs = this.api.getLogs();
|
||||||
|
const responseTimes = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < apiLogs.length - 1; i++) {
|
||||||
|
const log = apiLogs[i];
|
||||||
|
const prevLog = apiLogs[i + 1];
|
||||||
|
|
||||||
|
if (
|
||||||
|
log.type === 'response' &&
|
||||||
|
prevLog.type === 'request' &&
|
||||||
|
log.url === prevLog.url
|
||||||
|
) {
|
||||||
|
const responseTime =
|
||||||
|
new Date(log.timestamp).getTime() - new Date(prevLog.timestamp).getTime();
|
||||||
|
responseTimes.push({
|
||||||
|
url: log.url,
|
||||||
|
method: log.method,
|
||||||
|
time: responseTime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseTimes.length > 0) {
|
||||||
|
const avgTime =
|
||||||
|
responseTimes.reduce((sum, item) => sum + item.time, 0) / responseTimes.length;
|
||||||
|
const maxTime = Math.max(...responseTimes.map((item) => item.time));
|
||||||
|
const minTime = Math.min(...responseTimes.map((item) => item.time));
|
||||||
|
|
||||||
|
console.log('API 响应时间统计:');
|
||||||
|
console.table({
|
||||||
|
平均响应时间: `${avgTime.toFixed(2)}ms`,
|
||||||
|
最快响应: `${minTime}ms`,
|
||||||
|
最慢响应: `${maxTime}ms`,
|
||||||
|
请求总数: responseTimes.length,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 显示最慢的 5 个请求
|
||||||
|
console.log('\n最慢的 5 个请求:');
|
||||||
|
const slowest = responseTimes.sort((a, b) => b.time - a.time).slice(0, 5);
|
||||||
|
console.table(
|
||||||
|
slowest.map((item) => ({
|
||||||
|
方法: item.method,
|
||||||
|
URL: item.url,
|
||||||
|
响应时间: `${item.time}ms`,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log('暂无性能数据');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出单例
|
||||||
|
export const debugToolkit = new DebugToolkit();
|
||||||
|
export default debugToolkit;
|
||||||
166
src/devtools/notificationDebugger.js
Normal file
166
src/devtools/notificationDebugger.js
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
// src/debug/notificationDebugger.js
|
||||||
|
/**
|
||||||
|
* 通知系统调试工具
|
||||||
|
* 扩展现有的 window.__NOTIFY_DEBUG__,添加更多生产环境调试能力
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { browserNotificationService } from '@services/browserNotificationService';
|
||||||
|
|
||||||
|
class NotificationDebugger {
|
||||||
|
constructor() {
|
||||||
|
this.eventLog = [];
|
||||||
|
this.maxLogSize = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化调试工具
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
console.log('%c[Notification Debugger] Initialized', 'color: #FF9800; font-weight: bold;');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录通知事件
|
||||||
|
*/
|
||||||
|
logEvent(eventType, data) {
|
||||||
|
const logEntry = {
|
||||||
|
type: eventType,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.eventLog.unshift(logEntry);
|
||||||
|
if (this.eventLog.length > this.maxLogSize) {
|
||||||
|
this.eventLog = this.eventLog.slice(0, this.maxLogSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`%c[Notification Event] ${eventType}`,
|
||||||
|
'color: #9C27B0; font-weight: bold;',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有事件日志
|
||||||
|
*/
|
||||||
|
getLogs() {
|
||||||
|
return this.eventLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空日志
|
||||||
|
*/
|
||||||
|
clearLogs() {
|
||||||
|
this.eventLog = [];
|
||||||
|
console.log('[Notification Debugger] Logs cleared');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出日志
|
||||||
|
*/
|
||||||
|
exportLogs() {
|
||||||
|
const blob = new Blob([JSON.stringify(this.eventLog, null, 2)], {
|
||||||
|
type: 'application/json',
|
||||||
|
});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `notification-logs-${Date.now()}.json`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
console.log('[Notification Debugger] Logs exported');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制发送浏览器通知(测试用)
|
||||||
|
*/
|
||||||
|
forceNotification(options = {}) {
|
||||||
|
const defaultOptions = {
|
||||||
|
title: '🧪 测试通知',
|
||||||
|
body: `测试时间: ${new Date().toLocaleString()}`,
|
||||||
|
tag: `test_${Date.now()}`,
|
||||||
|
requireInteraction: false,
|
||||||
|
autoClose: 5000,
|
||||||
|
};
|
||||||
|
|
||||||
|
const finalOptions = { ...defaultOptions, ...options };
|
||||||
|
|
||||||
|
console.log('[Notification Debugger] Sending test notification:', finalOptions);
|
||||||
|
|
||||||
|
const notification = browserNotificationService.sendNotification(finalOptions);
|
||||||
|
|
||||||
|
if (notification) {
|
||||||
|
console.log('[Notification Debugger] ✅ Notification sent successfully');
|
||||||
|
} else {
|
||||||
|
console.error('[Notification Debugger] ❌ Failed to send notification');
|
||||||
|
}
|
||||||
|
|
||||||
|
return notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查通知权限状态
|
||||||
|
*/
|
||||||
|
checkPermission() {
|
||||||
|
const permission = browserNotificationService.getPermissionStatus();
|
||||||
|
const isSupported = browserNotificationService.isSupported();
|
||||||
|
|
||||||
|
const status = {
|
||||||
|
supported: isSupported,
|
||||||
|
permission,
|
||||||
|
canSend: isSupported && permission === 'granted',
|
||||||
|
};
|
||||||
|
|
||||||
|
console.table(status);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求通知权限
|
||||||
|
*/
|
||||||
|
async requestPermission() {
|
||||||
|
console.log('[Notification Debugger] Requesting notification permission...');
|
||||||
|
const result = await browserNotificationService.requestPermission();
|
||||||
|
console.log(`[Notification Debugger] Permission result: ${result}`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印事件统计
|
||||||
|
*/
|
||||||
|
printStats() {
|
||||||
|
const stats = {
|
||||||
|
total: this.eventLog.length,
|
||||||
|
byType: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.eventLog.forEach((log) => {
|
||||||
|
stats.byType[log.type] = (stats.byType[log.type] || 0) + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('=== Notification Stats ===');
|
||||||
|
console.table(stats.byType);
|
||||||
|
console.log(`Total events: ${stats.total}`);
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按类型过滤日志
|
||||||
|
*/
|
||||||
|
getLogsByType(eventType) {
|
||||||
|
return this.eventLog.filter((log) => log.type === eventType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最近的事件
|
||||||
|
*/
|
||||||
|
getRecentEvents(count = 10) {
|
||||||
|
return this.eventLog.slice(0, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出单例
|
||||||
|
export const notificationDebugger = new NotificationDebugger();
|
||||||
|
export default notificationDebugger;
|
||||||
194
src/devtools/socketDebugger.js
Normal file
194
src/devtools/socketDebugger.js
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
// src/debug/socketDebugger.js
|
||||||
|
/**
|
||||||
|
* Socket 调试工具
|
||||||
|
* 扩展现有的 window.__SOCKET_DEBUG__,添加更多生产环境调试能力
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { socket } from '@services/socket';
|
||||||
|
|
||||||
|
class SocketDebugger {
|
||||||
|
constructor() {
|
||||||
|
this.eventLog = [];
|
||||||
|
this.maxLogSize = 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化调试工具
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
// 监听所有 Socket 事件
|
||||||
|
this._attachEventListeners();
|
||||||
|
console.log('%c[Socket Debugger] Initialized', 'color: #FF9800; font-weight: bold;');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 附加事件监听器
|
||||||
|
*/
|
||||||
|
_attachEventListeners() {
|
||||||
|
const events = [
|
||||||
|
'connect',
|
||||||
|
'disconnect',
|
||||||
|
'connect_error',
|
||||||
|
'reconnect',
|
||||||
|
'reconnect_failed',
|
||||||
|
'new_event',
|
||||||
|
'system_notification',
|
||||||
|
];
|
||||||
|
|
||||||
|
events.forEach((event) => {
|
||||||
|
socket.on(event, (data) => {
|
||||||
|
this.logEvent(event, data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录 Socket 事件
|
||||||
|
*/
|
||||||
|
logEvent(eventType, data) {
|
||||||
|
const logEntry = {
|
||||||
|
type: eventType,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.eventLog.unshift(logEntry);
|
||||||
|
if (this.eventLog.length > this.maxLogSize) {
|
||||||
|
this.eventLog = this.eventLog.slice(0, this.maxLogSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`%c[Socket Event] ${eventType}`,
|
||||||
|
'color: #00BCD4; font-weight: bold;',
|
||||||
|
data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取所有事件日志
|
||||||
|
*/
|
||||||
|
getLogs() {
|
||||||
|
return this.eventLog;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空日志
|
||||||
|
*/
|
||||||
|
clearLogs() {
|
||||||
|
this.eventLog = [];
|
||||||
|
console.log('[Socket Debugger] Logs cleared');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出日志
|
||||||
|
*/
|
||||||
|
exportLogs() {
|
||||||
|
const blob = new Blob([JSON.stringify(this.eventLog, null, 2)], {
|
||||||
|
type: 'application/json',
|
||||||
|
});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `socket-logs-${Date.now()}.json`;
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
console.log('[Socket Debugger] Logs exported');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取连接状态
|
||||||
|
*/
|
||||||
|
getStatus() {
|
||||||
|
const status = {
|
||||||
|
connected: socket.connected || false,
|
||||||
|
type: window.SOCKET_TYPE || 'UNKNOWN',
|
||||||
|
reconnectAttempts: socket.getReconnectAttempts?.() || 0,
|
||||||
|
maxReconnectAttempts: socket.getMaxReconnectAttempts?.() || Infinity,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.table(status);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动触发连接
|
||||||
|
*/
|
||||||
|
connect() {
|
||||||
|
console.log('[Socket Debugger] Manually connecting...');
|
||||||
|
socket.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动断开连接
|
||||||
|
*/
|
||||||
|
disconnect() {
|
||||||
|
console.log('[Socket Debugger] Manually disconnecting...');
|
||||||
|
socket.disconnect();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动重连
|
||||||
|
*/
|
||||||
|
reconnect() {
|
||||||
|
console.log('[Socket Debugger] Manually reconnecting...');
|
||||||
|
socket.disconnect();
|
||||||
|
setTimeout(() => {
|
||||||
|
socket.connect();
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送测试事件
|
||||||
|
*/
|
||||||
|
emitTest(eventName, data = {}) {
|
||||||
|
console.log(`[Socket Debugger] Emitting test event: ${eventName}`, data);
|
||||||
|
socket.emit(eventName, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印事件统计
|
||||||
|
*/
|
||||||
|
printStats() {
|
||||||
|
const stats = {
|
||||||
|
total: this.eventLog.length,
|
||||||
|
byType: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.eventLog.forEach((log) => {
|
||||||
|
stats.byType[log.type] = (stats.byType[log.type] || 0) + 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('=== Socket Stats ===');
|
||||||
|
console.table(stats.byType);
|
||||||
|
console.log(`Total events: ${stats.total}`);
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按类型过滤日志
|
||||||
|
*/
|
||||||
|
getLogsByType(eventType) {
|
||||||
|
return this.eventLog.filter((log) => log.type === eventType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取最近的事件
|
||||||
|
*/
|
||||||
|
getRecentEvents(count = 10) {
|
||||||
|
return this.eventLog.slice(0, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取错误事件
|
||||||
|
*/
|
||||||
|
getErrors() {
|
||||||
|
return this.eventLog.filter(
|
||||||
|
(log) => log.type === 'connect_error' || log.type === 'reconnect_failed'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 导出单例
|
||||||
|
export const socketDebugger = new SocketDebugger();
|
||||||
|
export default socketDebugger;
|
||||||
13
src/index.js
13
src/index.js
@@ -13,6 +13,19 @@ import App from './App';
|
|||||||
import { browserNotificationService } from './services/browserNotificationService';
|
import { browserNotificationService } from './services/browserNotificationService';
|
||||||
window.browserNotificationService = browserNotificationService;
|
window.browserNotificationService = browserNotificationService;
|
||||||
|
|
||||||
|
// 🔧 条件导入调试工具(生产环境可选)
|
||||||
|
// 开启方式: 在 .env 文件中设置 REACT_APP_ENABLE_DEBUG=true
|
||||||
|
// 移除方式: 删除此段代码 + 删除 src/devtools/ 目录
|
||||||
|
if (process.env.REACT_APP_ENABLE_DEBUG === 'true') {
|
||||||
|
import('./devtools').then(({ debugToolkit }) => {
|
||||||
|
debugToolkit.init();
|
||||||
|
console.log(
|
||||||
|
'%c✅ 调试工具已加载!使用 window.__DEBUG__.help() 查看命令',
|
||||||
|
'color: #4CAF50; font-weight: bold; font-size: 14px;'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 注册 Service Worker(用于支持浏览器通知)
|
// 注册 Service Worker(用于支持浏览器通知)
|
||||||
function registerServiceWorker() {
|
function registerServiceWorker() {
|
||||||
// ⚠️ Mock 模式下跳过 Service Worker 注册(避免与 MSW 冲突)
|
// ⚠️ Mock 模式下跳过 Service Worker 注册(避免与 MSW 冲突)
|
||||||
|
|||||||
@@ -1,916 +0,0 @@
|
|||||||
// src/services/mockSocketService.js
|
|
||||||
/**
|
|
||||||
* Mock Socket 服务 - 用于开发环境模拟实时推送
|
|
||||||
* 模拟金融资讯、事件动向、分析报告等实时消息推送
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { logger } from '../utils/logger';
|
|
||||||
import { NOTIFICATION_TYPES, PRIORITY_LEVELS } from '../constants/notificationTypes';
|
|
||||||
|
|
||||||
// 模拟金融资讯数据
|
|
||||||
const mockFinancialNews = [
|
|
||||||
// ========== 公告通知 ==========
|
|
||||||
{
|
|
||||||
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: 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: 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: 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 {
|
|
||||||
constructor() {
|
|
||||||
this.connected = false;
|
|
||||||
this.connecting = false; // 新增:正在连接标志,防止重复连接
|
|
||||||
this.listeners = new Map();
|
|
||||||
this.intervals = [];
|
|
||||||
this.messageQueue = [];
|
|
||||||
this.reconnectAttempts = 0;
|
|
||||||
this.customReconnectTimer = null;
|
|
||||||
this.failConnection = false; // 是否模拟连接失败
|
|
||||||
this.pushPaused = false; // 新增:暂停推送标志(保持连接)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算指数退避延迟(Mock 模式使用更短的时间便于测试)
|
|
||||||
* 第1次: 10秒, 第2次: 20秒, 第3次: 40秒, 第4次及以后: 40秒
|
|
||||||
*/
|
|
||||||
getReconnectionDelay(attempt) {
|
|
||||||
const delays = [10000, 20000, 40000]; // 10s, 20s, 40s (缩短10倍便于测试)
|
|
||||||
const index = Math.min(attempt - 1, delays.length - 1);
|
|
||||||
return delays[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 连接到 mock socket
|
|
||||||
*/
|
|
||||||
connect() {
|
|
||||||
// ✅ 防止重复连接
|
|
||||||
if (this.connected) {
|
|
||||||
logger.warn('mockSocketService', 'Already connected');
|
|
||||||
console.log('%c[Mock Socket] Already connected, skipping', 'color: #FF9800; font-weight: bold;');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.connecting) {
|
|
||||||
logger.warn('mockSocketService', 'Connection in progress');
|
|
||||||
console.log('%c[Mock Socket] Connection already in progress, skipping', 'color: #FF9800; font-weight: bold;');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.connecting = true; // 标记为连接中
|
|
||||||
logger.info('mockSocketService', 'Connecting to mock socket service...');
|
|
||||||
console.log('%c[Mock Socket] 🔌 Connecting...', 'color: #2196F3; font-weight: bold;');
|
|
||||||
|
|
||||||
// 模拟连接延迟
|
|
||||||
setTimeout(() => {
|
|
||||||
// 检查是否应该模拟连接失败
|
|
||||||
if (this.failConnection) {
|
|
||||||
this.connecting = false; // 清除连接中标志
|
|
||||||
logger.warn('mockSocketService', 'Simulated connection failure');
|
|
||||||
console.log('%c[Mock Socket] ❌ Connection failed (simulated)', 'color: #F44336; font-weight: bold;');
|
|
||||||
|
|
||||||
// 触发连接错误事件
|
|
||||||
this.emit('connect_error', {
|
|
||||||
message: 'Mock connection error for testing',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// 安排下次重连(会继续失败,直到 failConnection 被清除)
|
|
||||||
this.scheduleReconnection();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 正常连接成功
|
|
||||||
this.connected = true;
|
|
||||||
this.connecting = false; // 清除连接中标志
|
|
||||||
this.reconnectAttempts = 0;
|
|
||||||
|
|
||||||
// 清除自定义重连定时器
|
|
||||||
if (this.customReconnectTimer) {
|
|
||||||
clearTimeout(this.customReconnectTimer);
|
|
||||||
this.customReconnectTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('mockSocketService', 'Mock socket connected successfully');
|
|
||||||
console.log('%c[Mock Socket] ✅ Connected successfully!', 'color: #4CAF50; font-weight: bold; font-size: 14px;');
|
|
||||||
console.log(`%c[Mock Socket] Status: connected=${this.connected}, connecting=${this.connecting}`, 'color: #4CAF50;');
|
|
||||||
|
|
||||||
// ✅ 使用 setTimeout(0) 确保监听器已注册后再触发事件
|
|
||||||
setTimeout(() => {
|
|
||||||
console.log('%c[Mock Socket] Emitting connect event...', 'color: #9C27B0;');
|
|
||||||
this.emit('connect', { timestamp: Date.now() });
|
|
||||||
console.log('%c[Mock Socket] Connect event emitted', 'color: #9C27B0;');
|
|
||||||
}, 0);
|
|
||||||
|
|
||||||
// 在连接后3秒发送欢迎消息
|
|
||||||
setTimeout(() => {
|
|
||||||
if (this.connected) {
|
|
||||||
this.emit('new_event', {
|
|
||||||
type: 'system_notification',
|
|
||||||
severity: 'info',
|
|
||||||
title: '连接成功',
|
|
||||||
message: '实时消息推送服务已启动 (Mock 模式)',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
autoClose: 5000,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, 3000);
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 断开连接
|
|
||||||
* @param {boolean} triggerReconnect - 是否触发自动重连(模拟意外断开)
|
|
||||||
*/
|
|
||||||
disconnect(triggerReconnect = false) {
|
|
||||||
if (!this.connected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('mockSocketService', 'Disconnecting from mock socket service...');
|
|
||||||
|
|
||||||
// 清除所有定时器
|
|
||||||
this.intervals.forEach(interval => clearInterval(interval));
|
|
||||||
this.intervals = [];
|
|
||||||
this.pushPaused = false; // 重置暂停状态
|
|
||||||
|
|
||||||
const wasConnected = this.connected;
|
|
||||||
this.connected = false;
|
|
||||||
this.emit('disconnect', {
|
|
||||||
timestamp: Date.now(),
|
|
||||||
reason: triggerReconnect ? 'transport close' : 'io client disconnect'
|
|
||||||
});
|
|
||||||
|
|
||||||
// 如果需要触发重连(模拟意外断开)
|
|
||||||
if (triggerReconnect && wasConnected) {
|
|
||||||
this.scheduleReconnection();
|
|
||||||
} else {
|
|
||||||
// 清除重连定时器
|
|
||||||
if (this.customReconnectTimer) {
|
|
||||||
clearTimeout(this.customReconnectTimer);
|
|
||||||
this.customReconnectTimer = null;
|
|
||||||
}
|
|
||||||
this.reconnectAttempts = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 使用指数退避策略安排重连
|
|
||||||
*/
|
|
||||||
scheduleReconnection() {
|
|
||||||
// 清除之前的定时器
|
|
||||||
if (this.customReconnectTimer) {
|
|
||||||
clearTimeout(this.customReconnectTimer);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.reconnectAttempts++;
|
|
||||||
const delay = this.getReconnectionDelay(this.reconnectAttempts);
|
|
||||||
logger.info('mockSocketService', `Scheduling reconnection in ${delay / 1000}s (attempt ${this.reconnectAttempts})`);
|
|
||||||
|
|
||||||
// 触发 connect_error 事件通知UI
|
|
||||||
this.emit('connect_error', {
|
|
||||||
message: 'Mock connection error for testing',
|
|
||||||
timestamp: Date.now(),
|
|
||||||
});
|
|
||||||
|
|
||||||
this.customReconnectTimer = setTimeout(() => {
|
|
||||||
if (!this.connected) {
|
|
||||||
logger.info('mockSocketService', 'Attempting reconnection...', {
|
|
||||||
attempt: this.reconnectAttempts,
|
|
||||||
});
|
|
||||||
this.connect();
|
|
||||||
}
|
|
||||||
}, delay);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 手动重连
|
|
||||||
* @returns {boolean} 是否触发重连
|
|
||||||
*/
|
|
||||||
reconnect() {
|
|
||||||
if (this.connected) {
|
|
||||||
logger.info('mockSocketService', 'Already connected, no need to reconnect');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('mockSocketService', 'Manually triggering reconnection...');
|
|
||||||
|
|
||||||
// 清除自动重连定时器
|
|
||||||
if (this.customReconnectTimer) {
|
|
||||||
clearTimeout(this.customReconnectTimer);
|
|
||||||
this.customReconnectTimer = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重置重连计数
|
|
||||||
this.reconnectAttempts = 0;
|
|
||||||
|
|
||||||
// 立即触发重连
|
|
||||||
this.connect();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 模拟意外断线(测试用)
|
|
||||||
* @param {number} duration - 断线持续时间(毫秒),0表示需要手动重连
|
|
||||||
*/
|
|
||||||
simulateDisconnection(duration = 0) {
|
|
||||||
logger.info('mockSocketService', `Simulating disconnection${duration > 0 ? ` for ${duration}ms` : ' (manual reconnect required)'}...`);
|
|
||||||
|
|
||||||
if (duration > 0) {
|
|
||||||
// 短暂断线,自动重连
|
|
||||||
this.disconnect(true);
|
|
||||||
} else {
|
|
||||||
// 需要手动重连
|
|
||||||
this.disconnect(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 模拟持续连接失败(测试用)
|
|
||||||
* 连接会一直失败,直到调用 allowReconnection()
|
|
||||||
*/
|
|
||||||
simulateConnectionFailure() {
|
|
||||||
logger.info('mockSocketService', '🚫 Simulating persistent connection failure...');
|
|
||||||
logger.info('mockSocketService', 'Connection will keep failing until allowReconnection() is called');
|
|
||||||
|
|
||||||
// 设置失败标志
|
|
||||||
this.failConnection = true;
|
|
||||||
|
|
||||||
// 如果当前已连接,先断开并触发重连(会失败)
|
|
||||||
if (this.connected) {
|
|
||||||
this.disconnect(true);
|
|
||||||
} else {
|
|
||||||
// 如果未连接,直接触发一次连接尝试(会失败)
|
|
||||||
this.connect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 允许重连成功(测试用)
|
|
||||||
* 清除连接失败标志,下次重连将会成功
|
|
||||||
*/
|
|
||||||
allowReconnection() {
|
|
||||||
logger.info('mockSocketService', '✅ Allowing reconnection to succeed...');
|
|
||||||
logger.info('mockSocketService', 'Next reconnection attempt will succeed');
|
|
||||||
|
|
||||||
// 清除失败标志
|
|
||||||
this.failConnection = false;
|
|
||||||
|
|
||||||
// 不立即重连,等待自动重连或手动重连
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 监听事件
|
|
||||||
* @param {string} event - 事件名称
|
|
||||||
* @param {Function} callback - 回调函数
|
|
||||||
*/
|
|
||||||
on(event, callback) {
|
|
||||||
if (!this.listeners.has(event)) {
|
|
||||||
this.listeners.set(event, []);
|
|
||||||
}
|
|
||||||
this.listeners.get(event).push(callback);
|
|
||||||
|
|
||||||
logger.info('mockSocketService', `Event listener added: ${event}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 移除事件监听
|
|
||||||
* @param {string} event - 事件名称
|
|
||||||
* @param {Function} callback - 回调函数
|
|
||||||
*/
|
|
||||||
off(event, callback) {
|
|
||||||
if (!this.listeners.has(event)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const callbacks = this.listeners.get(event);
|
|
||||||
const index = callbacks.indexOf(callback);
|
|
||||||
|
|
||||||
if (index !== -1) {
|
|
||||||
callbacks.splice(index, 1);
|
|
||||||
logger.info('mockSocketService', `Event listener removed: ${event}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有监听器了,删除该事件
|
|
||||||
if (callbacks.length === 0) {
|
|
||||||
this.listeners.delete(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 触发事件
|
|
||||||
* @param {string} event - 事件名称
|
|
||||||
* @param {*} data - 事件数据
|
|
||||||
*/
|
|
||||||
emit(event, data) {
|
|
||||||
if (!this.listeners.has(event)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const callbacks = this.listeners.get(event);
|
|
||||||
callbacks.forEach(callback => {
|
|
||||||
try {
|
|
||||||
callback(data);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error('mockSocketService', 'emit', error, { event, data });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 启动模拟消息推送
|
|
||||||
* @param {number} interval - 推送间隔(毫秒)
|
|
||||||
* @param {number} burstCount - 每次推送的消息数量(1-3条)
|
|
||||||
*/
|
|
||||||
startMockPush(interval = 15000, burstCount = 1) {
|
|
||||||
if (!this.connected) {
|
|
||||||
logger.warn('mockSocketService', 'Cannot start mock push: not connected');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.info('mockSocketService', `Starting mock push: interval=${interval}ms, burst=${burstCount}`);
|
|
||||||
|
|
||||||
const pushInterval = setInterval(() => {
|
|
||||||
// 检查是否暂停推送
|
|
||||||
if (this.pushPaused) {
|
|
||||||
logger.info('mockSocketService', '⏸️ Mock push is paused, skipping this cycle...');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 随机选择 1-burstCount 条消息
|
|
||||||
const count = Math.floor(Math.random() * burstCount) + 1;
|
|
||||||
|
|
||||||
for (let i = 0; i < count; i++) {
|
|
||||||
// 从模拟数据中随机选择一条
|
|
||||||
const randomIndex = Math.floor(Math.random() * mockFinancialNews.length);
|
|
||||||
const alert = {
|
|
||||||
...mockFinancialNews[randomIndex],
|
|
||||||
timestamp: Date.now(),
|
|
||||||
id: `mock_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
|
||||||
};
|
|
||||||
|
|
||||||
// 延迟发送(模拟层叠效果)
|
|
||||||
setTimeout(() => {
|
|
||||||
this.emit('new_event', alert);
|
|
||||||
logger.info('mockSocketService', 'Mock notification sent', alert);
|
|
||||||
}, i * 500); // 每条消息间隔500ms
|
|
||||||
}
|
|
||||||
}, interval);
|
|
||||||
|
|
||||||
this.intervals.push(pushInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 停止模拟推送
|
|
||||||
*/
|
|
||||||
stopMockPush() {
|
|
||||||
this.intervals.forEach(interval => clearInterval(interval));
|
|
||||||
this.intervals = [];
|
|
||||||
this.pushPaused = false; // 重置暂停状态
|
|
||||||
logger.info('mockSocketService', 'Mock push stopped');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 暂停自动推送(保持连接和定时器运行)
|
|
||||||
*/
|
|
||||||
pausePush() {
|
|
||||||
this.pushPaused = true;
|
|
||||||
logger.info('mockSocketService', '⏸️ Mock push paused (connection and intervals maintained)');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 恢复自动推送
|
|
||||||
*/
|
|
||||||
resumePush() {
|
|
||||||
this.pushPaused = false;
|
|
||||||
logger.info('mockSocketService', '▶️ Mock push resumed');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询推送暂停状态
|
|
||||||
* @returns {boolean} 是否已暂停
|
|
||||||
*/
|
|
||||||
isPushPaused() {
|
|
||||||
return this.pushPaused;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 手动触发一条测试消息
|
|
||||||
* @param {object} customData - 自定义消息数据(可选)
|
|
||||||
*/
|
|
||||||
sendTestNotification(customData = null) {
|
|
||||||
// 如果传入自定义数据,直接使用(向后兼容)
|
|
||||||
if (customData) {
|
|
||||||
this.emit('new_event', customData);
|
|
||||||
logger.info('mockSocketService', 'Custom test notification sent', customData);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 默认发送新格式的测试通知(符合当前通知系统规范)
|
|
||||||
const notification = {
|
|
||||||
type: 'announcement', // 公告通知类型
|
|
||||||
priority: 'important', // 重要优先级(30秒自动关闭)
|
|
||||||
title: '🧪 测试通知',
|
|
||||||
content: '这是一条手动触发的测试消息,用于验证通知系统是否正常工作',
|
|
||||||
publishTime: Date.now(),
|
|
||||||
pushTime: Date.now(),
|
|
||||||
id: `test_${Date.now()}`,
|
|
||||||
clickable: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.emit('new_event', notification);
|
|
||||||
logger.info('mockSocketService', 'Test notification sent', notification);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取连接状态
|
|
||||||
*/
|
|
||||||
isConnected() {
|
|
||||||
return this.connected;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取当前重连尝试次数
|
|
||||||
*/
|
|
||||||
getReconnectAttempts() {
|
|
||||||
return this.reconnectAttempts;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取最大重连次数(Mock 模式无限重试)
|
|
||||||
*/
|
|
||||||
getMaxReconnectAttempts() {
|
|
||||||
return Infinity;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 订阅事件推送(Mock 实现)
|
|
||||||
* @param {object} options - 订阅选项
|
|
||||||
* @param {string} options.eventType - 事件类型 ('all' | 'policy' | 'market' | 'tech' | ...)
|
|
||||||
* @param {string} options.importance - 重要性 ('all' | 'S' | 'A' | 'B' | 'C')
|
|
||||||
* @param {Function} options.onNewEvent - 收到新事件时的回调函数
|
|
||||||
* @param {Function} options.onSubscribed - 订阅成功的回调函数(可选)
|
|
||||||
*/
|
|
||||||
subscribeToEvents(options = {}) {
|
|
||||||
const {
|
|
||||||
eventType = 'all',
|
|
||||||
importance = 'all',
|
|
||||||
onNewEvent,
|
|
||||||
onSubscribed,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
logger.info('mockSocketService', 'Subscribing to events', { eventType, importance });
|
|
||||||
|
|
||||||
// Mock: 立即触发订阅成功回调
|
|
||||||
if (onSubscribed) {
|
|
||||||
setTimeout(() => {
|
|
||||||
onSubscribed({
|
|
||||||
success: true,
|
|
||||||
event_type: eventType,
|
|
||||||
importance: importance,
|
|
||||||
message: 'Mock subscription confirmed'
|
|
||||||
});
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mock: 如果提供了 onNewEvent 回调,监听 'new_event' 事件
|
|
||||||
if (onNewEvent) {
|
|
||||||
// 先移除之前的监听器(避免重复)
|
|
||||||
this.off('new_event', onNewEvent);
|
|
||||||
// 添加新的监听器
|
|
||||||
this.on('new_event', onNewEvent);
|
|
||||||
logger.info('mockSocketService', 'Event listener registered for new_event');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 取消订阅事件推送(Mock 实现)
|
|
||||||
* @param {object} options - 取消订阅选项
|
|
||||||
* @param {string} options.eventType - 事件类型
|
|
||||||
* @param {Function} options.onUnsubscribed - 取消订阅成功的回调函数(可选)
|
|
||||||
*/
|
|
||||||
unsubscribeFromEvents(options = {}) {
|
|
||||||
const {
|
|
||||||
eventType = 'all',
|
|
||||||
onUnsubscribed,
|
|
||||||
} = options;
|
|
||||||
|
|
||||||
logger.info('mockSocketService', 'Unsubscribing from events', { eventType });
|
|
||||||
|
|
||||||
// Mock: 移除 new_event 监听器
|
|
||||||
this.off('new_event');
|
|
||||||
|
|
||||||
// Mock: 立即触发取消订阅成功回调
|
|
||||||
if (onUnsubscribed) {
|
|
||||||
setTimeout(() => {
|
|
||||||
onUnsubscribed({
|
|
||||||
success: true,
|
|
||||||
event_type: eventType,
|
|
||||||
message: 'Mock unsubscription confirmed'
|
|
||||||
});
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 快捷方法:订阅所有类型的事件(Mock 实现)
|
|
||||||
* @param {Function} onNewEvent - 收到新事件时的回调函数
|
|
||||||
*/
|
|
||||||
subscribeToAllEvents(onNewEvent) {
|
|
||||||
this.subscribeToEvents({
|
|
||||||
eventType: 'all',
|
|
||||||
importance: 'all',
|
|
||||||
onNewEvent,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 快捷方法:订阅指定重要性的事件(Mock 实现)
|
|
||||||
* @param {string} importance - 重要性级别 ('S' | 'A' | 'B' | 'C')
|
|
||||||
* @param {Function} onNewEvent - 收到新事件时的回调函数
|
|
||||||
*/
|
|
||||||
subscribeToImportantEvents(importance, onNewEvent) {
|
|
||||||
this.subscribeToEvents({
|
|
||||||
eventType: 'all',
|
|
||||||
importance,
|
|
||||||
onNewEvent,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 快捷方法:订阅指定类型的事件(Mock 实现)
|
|
||||||
* @param {string} eventType - 事件类型
|
|
||||||
* @param {Function} onNewEvent - 收到新事件时的回调函数
|
|
||||||
*/
|
|
||||||
subscribeToEventType(eventType, onNewEvent) {
|
|
||||||
this.subscribeToEvents({
|
|
||||||
eventType,
|
|
||||||
importance: 'all',
|
|
||||||
onNewEvent,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 导出单例
|
|
||||||
export const mockSocketService = new MockSocketService();
|
|
||||||
|
|
||||||
// 开发模式下添加全局测试函数
|
|
||||||
if (process.env.NODE_ENV === 'development') {
|
|
||||||
window.__mockSocket = {
|
|
||||||
// 模拟意外断线(自动重连成功)
|
|
||||||
simulateDisconnection: () => {
|
|
||||||
logger.info('mockSocketService', '🔌 Simulating disconnection (will auto-reconnect)...');
|
|
||||||
mockSocketService.simulateDisconnection(1); // 触发自动重连
|
|
||||||
},
|
|
||||||
|
|
||||||
// 模拟持续连接失败
|
|
||||||
simulateConnectionFailure: () => {
|
|
||||||
logger.info('mockSocketService', '🚫 Simulating connection failure (will keep retrying)...');
|
|
||||||
mockSocketService.simulateConnectionFailure();
|
|
||||||
},
|
|
||||||
|
|
||||||
// 允许重连成功
|
|
||||||
allowReconnection: () => {
|
|
||||||
logger.info('mockSocketService', '✅ Allowing next reconnection to succeed...');
|
|
||||||
mockSocketService.allowReconnection();
|
|
||||||
},
|
|
||||||
|
|
||||||
// 获取连接状态
|
|
||||||
isConnected: () => {
|
|
||||||
const connected = mockSocketService.isConnected();
|
|
||||||
logger.info('mockSocketService', `Connection status: ${connected ? '✅ Connected' : '❌ Disconnected'}`);
|
|
||||||
return connected;
|
|
||||||
},
|
|
||||||
|
|
||||||
// 手动重连
|
|
||||||
reconnect: () => {
|
|
||||||
logger.info('mockSocketService', '🔄 Manually triggering reconnection...');
|
|
||||||
return mockSocketService.reconnect();
|
|
||||||
},
|
|
||||||
|
|
||||||
// 获取重连尝试次数
|
|
||||||
getAttempts: () => {
|
|
||||||
const attempts = mockSocketService.getReconnectAttempts();
|
|
||||||
logger.info('mockSocketService', `Current reconnection attempts: ${attempts}`);
|
|
||||||
return attempts;
|
|
||||||
},
|
|
||||||
|
|
||||||
// 暂停自动推送(保持连接)
|
|
||||||
pausePush: () => {
|
|
||||||
mockSocketService.pausePush();
|
|
||||||
logger.info('mockSocketService', '⏸️ Auto push paused');
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
// 恢复自动推送
|
|
||||||
resumePush: () => {
|
|
||||||
mockSocketService.resumePush();
|
|
||||||
logger.info('mockSocketService', '▶️ Auto push resumed');
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
// 查看推送暂停状态
|
|
||||||
isPushPaused: () => {
|
|
||||||
const paused = mockSocketService.isPushPaused();
|
|
||||||
logger.info('mockSocketService', `Push status: ${paused ? '⏸️ Paused' : '▶️ Active'}`);
|
|
||||||
return paused;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.info('mockSocketService', '💡 Mock Socket test functions available:');
|
|
||||||
logger.info('mockSocketService', ' __mockSocket.simulateDisconnection() - 模拟断线(自动重连成功)');
|
|
||||||
logger.info('mockSocketService', ' __mockSocket.simulateConnectionFailure() - 模拟连接失败(持续失败)');
|
|
||||||
logger.info('mockSocketService', ' __mockSocket.allowReconnection() - 允许重连成功');
|
|
||||||
logger.info('mockSocketService', ' __mockSocket.isConnected() - 查看连接状态');
|
|
||||||
logger.info('mockSocketService', ' __mockSocket.reconnect() - 手动重连');
|
|
||||||
logger.info('mockSocketService', ' __mockSocket.getAttempts() - 查看重连次数');
|
|
||||||
logger.info('mockSocketService', ' __mockSocket.pausePush() - ⏸️ 暂停自动推送(保持连接)');
|
|
||||||
logger.info('mockSocketService', ' __mockSocket.resumePush() - ▶️ 恢复自动推送');
|
|
||||||
logger.info('mockSocketService', ' __mockSocket.isPushPaused() - 查看推送状态');
|
|
||||||
}
|
|
||||||
|
|
||||||
export default mockSocketService;
|
|
||||||
@@ -1,364 +1,19 @@
|
|||||||
// src/services/socket/index.js
|
// src/services/socket/index.js
|
||||||
/**
|
/**
|
||||||
* Socket 服务统一导出
|
* Socket 服务统一导出
|
||||||
* 根据环境变量自动选择使用 Mock 或真实 Socket.IO 服务
|
* 使用真实 Socket.IO 服务连接后端
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { mockSocketService } from '../mockSocketService';
|
|
||||||
import { socketService } from '../socketService';
|
import { socketService } from '../socketService';
|
||||||
|
|
||||||
// 判断是否使用 Mock
|
// 导出 socket 服务
|
||||||
const useMock = process.env.REACT_APP_ENABLE_MOCK === 'true' || process.env.REACT_APP_USE_MOCK_SOCKET === 'true';
|
export const socket = socketService;
|
||||||
|
export { socketService };
|
||||||
// 根据环境选择服务
|
|
||||||
export const socket = useMock ? mockSocketService : socketService;
|
|
||||||
|
|
||||||
// 同时导出两个服务,方便测试和调试
|
|
||||||
export { mockSocketService, socketService };
|
|
||||||
|
|
||||||
// 导出服务类型标识
|
|
||||||
export const SOCKET_TYPE = useMock ? 'MOCK' : 'REAL';
|
|
||||||
|
|
||||||
// 打印当前使用的服务类型
|
// 打印当前使用的服务类型
|
||||||
console.log(
|
console.log(
|
||||||
`%c[Socket Service] Using ${SOCKET_TYPE} Socket Service`,
|
'%c[Socket Service] Using REAL Socket Service',
|
||||||
`color: ${useMock ? '#FF9800' : '#4CAF50'}; font-weight: bold; font-size: 12px;`
|
'color: #4CAF50; font-weight: bold; font-size: 12px;'
|
||||||
);
|
);
|
||||||
|
|
||||||
// ========== 暴露调试 API 到全局 ==========
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
// 暴露 Socket 类型到全局
|
|
||||||
window.SOCKET_TYPE = SOCKET_TYPE;
|
|
||||||
|
|
||||||
// 暴露调试 API
|
|
||||||
window.__SOCKET_DEBUG__ = {
|
|
||||||
// 获取当前连接状态
|
|
||||||
getStatus: () => {
|
|
||||||
const isConnected = socket.connected || false;
|
|
||||||
return {
|
|
||||||
type: SOCKET_TYPE,
|
|
||||||
connected: isConnected,
|
|
||||||
reconnectAttempts: socket.getReconnectAttempts?.() || 0,
|
|
||||||
maxReconnectAttempts: socket.getMaxReconnectAttempts?.() || Infinity,
|
|
||||||
service: useMock ? 'mockSocketService' : 'socketService',
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
// 手动重连
|
|
||||||
reconnect: () => {
|
|
||||||
console.log('[Socket Debug] Manual reconnect triggered');
|
|
||||||
if (socket.reconnect) {
|
|
||||||
socket.reconnect();
|
|
||||||
} else {
|
|
||||||
socket.disconnect();
|
|
||||||
socket.connect();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 断开连接
|
|
||||||
disconnect: () => {
|
|
||||||
console.log('[Socket Debug] Manual disconnect triggered');
|
|
||||||
socket.disconnect();
|
|
||||||
},
|
|
||||||
|
|
||||||
// 连接
|
|
||||||
connect: () => {
|
|
||||||
console.log('[Socket Debug] Manual connect triggered');
|
|
||||||
socket.connect();
|
|
||||||
},
|
|
||||||
|
|
||||||
// 获取服务实例 (仅用于调试)
|
|
||||||
getService: () => socket,
|
|
||||||
|
|
||||||
// 导出诊断信息
|
|
||||||
exportDiagnostics: () => {
|
|
||||||
const status = window.__SOCKET_DEBUG__.getStatus();
|
|
||||||
const diagnostics = {
|
|
||||||
...status,
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
userAgent: navigator.userAgent,
|
|
||||||
url: window.location.href,
|
|
||||||
env: {
|
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
|
||||||
REACT_APP_ENABLE_MOCK: process.env.REACT_APP_ENABLE_MOCK,
|
|
||||||
REACT_APP_USE_MOCK_SOCKET: process.env.REACT_APP_USE_MOCK_SOCKET,
|
|
||||||
REACT_APP_API_URL: process.env.REACT_APP_API_URL,
|
|
||||||
REACT_APP_ENV: process.env.REACT_APP_ENV,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
console.log('[Socket Diagnostics]', diagnostics);
|
|
||||||
return diagnostics;
|
|
||||||
},
|
|
||||||
|
|
||||||
// 手动订阅事件
|
|
||||||
subscribe: (options = {}) => {
|
|
||||||
const { eventType = 'all', importance = 'all' } = options;
|
|
||||||
console.log(`[Socket Debug] Subscribing to events: type=${eventType}, importance=${importance}`);
|
|
||||||
|
|
||||||
if (socket.subscribeToEvents) {
|
|
||||||
socket.subscribeToEvents({
|
|
||||||
eventType,
|
|
||||||
importance,
|
|
||||||
onNewEvent: (event) => {
|
|
||||||
console.log('[Socket Debug] ✅ New event received:', event);
|
|
||||||
},
|
|
||||||
onSubscribed: (data) => {
|
|
||||||
console.log('[Socket Debug] ✅ Subscription confirmed:', data);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.error('[Socket Debug] ❌ subscribeToEvents method not available');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 测试连接质量
|
|
||||||
testConnection: () => {
|
|
||||||
console.log('[Socket Debug] Testing connection...');
|
|
||||||
const start = Date.now();
|
|
||||||
|
|
||||||
if (socket.emit) {
|
|
||||||
socket.emit('ping', { timestamp: start }, (response) => {
|
|
||||||
const latency = Date.now() - start;
|
|
||||||
console.log(`[Socket Debug] ✅ Connection OK - Latency: ${latency}ms`, response);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
console.error('[Socket Debug] ❌ Cannot test connection - socket.emit not available');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// 检查配置是否正确
|
|
||||||
checkConfig: () => {
|
|
||||||
const config = {
|
|
||||||
socketType: SOCKET_TYPE,
|
|
||||||
useMock,
|
|
||||||
envVars: {
|
|
||||||
REACT_APP_ENABLE_MOCK: process.env.REACT_APP_ENABLE_MOCK,
|
|
||||||
REACT_APP_USE_MOCK_SOCKET: process.env.REACT_APP_USE_MOCK_SOCKET,
|
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
|
||||||
REACT_APP_API_URL: process.env.REACT_APP_API_URL,
|
|
||||||
},
|
|
||||||
socketMethods: {
|
|
||||||
connect: typeof socket.connect,
|
|
||||||
disconnect: typeof socket.disconnect,
|
|
||||||
on: typeof socket.on,
|
|
||||||
emit: typeof socket.emit,
|
|
||||||
subscribeToEvents: typeof socket.subscribeToEvents,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('[Socket Debug] Configuration Check:', config);
|
|
||||||
|
|
||||||
// 检查潜在问题
|
|
||||||
const issues = [];
|
|
||||||
if (SOCKET_TYPE === 'MOCK' && process.env.NODE_ENV === 'production') {
|
|
||||||
issues.push('⚠️ WARNING: Using MOCK socket in production!');
|
|
||||||
}
|
|
||||||
if (!socket.subscribeToEvents) {
|
|
||||||
issues.push('❌ ERROR: subscribeToEvents method missing');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (issues.length > 0) {
|
|
||||||
console.warn('[Socket Debug] Issues found:', issues);
|
|
||||||
} else {
|
|
||||||
console.log('[Socket Debug] ✅ No issues found');
|
|
||||||
}
|
|
||||||
|
|
||||||
return { config, issues };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
'%c[Socket Debug] Debug API available at window.__SOCKET_DEBUG__',
|
|
||||||
'color: #2196F3; font-weight: bold;'
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
'%cTry: window.__SOCKET_DEBUG__.getStatus()',
|
|
||||||
'color: #2196F3;'
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
'%c window.__SOCKET_DEBUG__.checkConfig() - 检查配置',
|
|
||||||
'color: #2196F3;'
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
'%c window.__SOCKET_DEBUG__.subscribe() - 手动订阅事件',
|
|
||||||
'color: #2196F3;'
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
'%c window.__SOCKET_DEBUG__.testConnection() - 测试连接',
|
|
||||||
'color: #2196F3;'
|
|
||||||
);
|
|
||||||
|
|
||||||
// ========== 通知系统专用调试 API ==========
|
|
||||||
window.__NOTIFY_DEBUG__ = {
|
|
||||||
// 完整检查(配置+连接+订阅状态)
|
|
||||||
checkAll: () => {
|
|
||||||
console.log('\n==========【通知系统诊断】==========');
|
|
||||||
|
|
||||||
// 1. 检查 Socket 配置
|
|
||||||
const socketCheck = window.__SOCKET_DEBUG__.checkConfig();
|
|
||||||
console.log('\n✓ Socket 配置检查完成');
|
|
||||||
|
|
||||||
// 2. 检查连接状态
|
|
||||||
const status = window.__SOCKET_DEBUG__.getStatus();
|
|
||||||
console.log('\n✓ 连接状态:', status.connected ? '✅ 已连接' : '❌ 未连接');
|
|
||||||
|
|
||||||
// 3. 检查环境变量
|
|
||||||
console.log('\n✓ API Base:', process.env.REACT_APP_API_URL || '(使用相对路径)');
|
|
||||||
|
|
||||||
// 4. 检查浏览器通知权限
|
|
||||||
const browserPermission = Notification?.permission || 'unsupported';
|
|
||||||
console.log('\n✓ 浏览器通知权限:', browserPermission);
|
|
||||||
|
|
||||||
// 5. 汇总报告
|
|
||||||
const report = {
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
socket: {
|
|
||||||
type: SOCKET_TYPE,
|
|
||||||
connected: status.connected,
|
|
||||||
reconnectAttempts: status.reconnectAttempts,
|
|
||||||
},
|
|
||||||
env: socketCheck.config.envVars,
|
|
||||||
browserNotification: browserPermission,
|
|
||||||
issues: socketCheck.issues,
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log('\n========== 诊断报告 ==========');
|
|
||||||
console.table(report);
|
|
||||||
|
|
||||||
if (report.issues.length > 0) {
|
|
||||||
console.warn('\n⚠️ 发现问题:', report.issues);
|
|
||||||
} else {
|
|
||||||
console.log('\n✅ 系统正常,未发现问题');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提供修复建议
|
|
||||||
if (!status.connected) {
|
|
||||||
console.log('\n💡 修复建议:');
|
|
||||||
console.log(' 1. 检查网络连接');
|
|
||||||
console.log(' 2. 尝试手动重连: __SOCKET_DEBUG__.reconnect()');
|
|
||||||
console.log(' 3. 检查后端服务是否运行');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (browserPermission === 'denied') {
|
|
||||||
console.log('\n💡 浏览器通知已被拒绝,请在浏览器设置中允许通知权限');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('\n====================================\n');
|
|
||||||
|
|
||||||
return report;
|
|
||||||
},
|
|
||||||
|
|
||||||
// 手动订阅事件(简化版)
|
|
||||||
subscribe: (eventType = 'all', importance = 'all') => {
|
|
||||||
console.log(`\n[通知调试] 手动订阅事件: type=${eventType}, importance=${importance}`);
|
|
||||||
window.__SOCKET_DEBUG__.subscribe({ eventType, importance });
|
|
||||||
},
|
|
||||||
|
|
||||||
// 模拟接收通知(用于测试UI)
|
|
||||||
testNotify: (type = 'announcement') => {
|
|
||||||
console.log('\n[通知调试] 模拟通知:', type);
|
|
||||||
|
|
||||||
const mockNotifications = {
|
|
||||||
announcement: {
|
|
||||||
id: `test_${Date.now()}`,
|
|
||||||
type: 'announcement',
|
|
||||||
priority: 'important',
|
|
||||||
title: '🧪 测试公告通知',
|
|
||||||
content: '这是一条测试消息,用于验证通知系统是否正常工作',
|
|
||||||
publishTime: Date.now(),
|
|
||||||
pushTime: Date.now(),
|
|
||||||
},
|
|
||||||
stock_alert: {
|
|
||||||
id: `test_${Date.now()}`,
|
|
||||||
type: 'stock_alert',
|
|
||||||
priority: 'urgent',
|
|
||||||
title: '🧪 测试股票预警',
|
|
||||||
content: '贵州茅台触发价格预警: 1850.00元 (+5.2%)',
|
|
||||||
publishTime: Date.now(),
|
|
||||||
pushTime: Date.now(),
|
|
||||||
},
|
|
||||||
event_alert: {
|
|
||||||
id: `test_${Date.now()}`,
|
|
||||||
type: 'event_alert',
|
|
||||||
priority: 'important',
|
|
||||||
title: '🧪 测试事件动向',
|
|
||||||
content: 'AI大模型新政策发布,影响科技板块',
|
|
||||||
publishTime: Date.now(),
|
|
||||||
pushTime: Date.now(),
|
|
||||||
},
|
|
||||||
analysis_report: {
|
|
||||||
id: `test_${Date.now()}`,
|
|
||||||
type: 'analysis_report',
|
|
||||||
priority: 'normal',
|
|
||||||
title: '🧪 测试分析报告',
|
|
||||||
content: '2024年Q1市场策略报告已发布',
|
|
||||||
publishTime: Date.now(),
|
|
||||||
pushTime: Date.now(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const notification = mockNotifications[type] || mockNotifications.announcement;
|
|
||||||
|
|
||||||
// 触发 new_event 事件
|
|
||||||
if (socket.emit) {
|
|
||||||
// 对于真实 Socket,模拟服务端推送(实际上客户端无法这样做,仅用于Mock模式)
|
|
||||||
console.warn('⚠️ 真实 Socket 无法模拟服务端推送,请使用 Mock 模式或等待真实推送');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 直接触发事件监听器(如果是 Mock 模式)
|
|
||||||
if (SOCKET_TYPE === 'MOCK' && socket.emit) {
|
|
||||||
socket.emit('new_event', notification);
|
|
||||||
console.log('✅ 已触发 Mock 通知事件');
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('通知数据:', notification);
|
|
||||||
return notification;
|
|
||||||
},
|
|
||||||
|
|
||||||
// 导出完整诊断报告
|
|
||||||
exportReport: () => {
|
|
||||||
const report = window.__NOTIFY_DEBUG__.checkAll();
|
|
||||||
|
|
||||||
// 生成可下载的 JSON
|
|
||||||
const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' });
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
a.download = `notification-debug-${Date.now()}.json`;
|
|
||||||
a.click();
|
|
||||||
URL.revokeObjectURL(url);
|
|
||||||
|
|
||||||
console.log('✅ 诊断报告已导出');
|
|
||||||
return report;
|
|
||||||
},
|
|
||||||
|
|
||||||
// 快捷帮助
|
|
||||||
help: () => {
|
|
||||||
console.log('\n========== 通知系统调试 API ==========');
|
|
||||||
console.log('window.__NOTIFY_DEBUG__.checkAll() - 完整诊断检查');
|
|
||||||
console.log('window.__NOTIFY_DEBUG__.subscribe() - 手动订阅事件');
|
|
||||||
console.log('window.__NOTIFY_DEBUG__.testNotify(type) - 模拟通知 (announcement/stock_alert/event_alert/analysis_report)');
|
|
||||||
console.log('window.__NOTIFY_DEBUG__.exportReport() - 导出诊断报告');
|
|
||||||
console.log('\n========== Socket 调试 API ==========');
|
|
||||||
console.log('window.__SOCKET_DEBUG__.getStatus() - 获取连接状态');
|
|
||||||
console.log('window.__SOCKET_DEBUG__.checkConfig() - 检查配置');
|
|
||||||
console.log('window.__SOCKET_DEBUG__.reconnect() - 手动重连');
|
|
||||||
console.log('====================================\n');
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
'%c[Notify Debug] Notification Debug API available at window.__NOTIFY_DEBUG__',
|
|
||||||
'color: #FF9800; font-weight: bold;'
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
'%cTry: window.__NOTIFY_DEBUG__.checkAll() - 完整诊断',
|
|
||||||
'color: #FF9800;'
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
'%c window.__NOTIFY_DEBUG__.help() - 查看所有命令',
|
|
||||||
'color: #FF9800;'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default socket;
|
export default socket;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class SocketService {
|
|||||||
this.reconnectAttempts = 0;
|
this.reconnectAttempts = 0;
|
||||||
this.maxReconnectAttempts = Infinity; // 无限重试
|
this.maxReconnectAttempts = Infinity; // 无限重试
|
||||||
this.customReconnectTimer = null; // 自定义重连定时器
|
this.customReconnectTimer = null; // 自定义重连定时器
|
||||||
|
this.pendingListeners = []; // 暂存等待注册的事件监听器
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,6 +51,15 @@ class SocketService {
|
|||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 注册所有暂存的事件监听器
|
||||||
|
if (this.pendingListeners.length > 0) {
|
||||||
|
console.log(`[socketService] 📦 注册 ${this.pendingListeners.length} 个暂存的事件监听器`);
|
||||||
|
this.pendingListeners.forEach(({ event, callback }) => {
|
||||||
|
this.on(event, callback);
|
||||||
|
});
|
||||||
|
this.pendingListeners = []; // 清空暂存队列
|
||||||
|
}
|
||||||
|
|
||||||
// 监听连接成功
|
// 监听连接成功
|
||||||
this.socket.on('connect', () => {
|
this.socket.on('connect', () => {
|
||||||
this.connected = true;
|
this.connected = true;
|
||||||
@@ -147,8 +157,10 @@ class SocketService {
|
|||||||
*/
|
*/
|
||||||
on(event, callback) {
|
on(event, callback) {
|
||||||
if (!this.socket) {
|
if (!this.socket) {
|
||||||
logger.warn('socketService', 'Cannot listen to event: socket not initialized', { event });
|
// Socket 未初始化,暂存监听器
|
||||||
console.warn(`[socketService] ❌ 无法监听事件 ${event}: Socket 未初始化`);
|
logger.info('socketService', 'Socket not ready, queuing listener', { event });
|
||||||
|
console.log(`[socketService] 📦 Socket 未初始化,暂存事件监听器: ${event}`);
|
||||||
|
this.pendingListeners.push({ event, callback });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user