diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index 790e08c6..1316e97e 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -1,7 +1,7 @@
{
"permissions": {
"allow": [
- "Bash(find src -name \"*.js\" -type f -exec grep -l \"process.env.REACT_APP_API_URL || [''''\"\"]http\" {})"
+ "Read(//Users/qiye/**)"
],
"deny": [],
"ask": []
diff --git a/NOTIFICATION_SYSTEM.md b/NOTIFICATION_SYSTEM.md
new file mode 100644
index 00000000..c1210635
--- /dev/null
+++ b/NOTIFICATION_SYSTEM.md
@@ -0,0 +1,455 @@
+# 实时消息推送系统使用指南
+
+## 🆕 最新更新 (v1.1.0)
+
+- ✅ **新消息置顶展示**:采用行业标准,新消息显示在最上方
+- ✅ **智能队列管理**:最多同时显示 5 条消息,超出自动移除最旧的
+- ✅ **视觉层次优化**:最新消息更强的阴影和边框高亮效果
+- ✅ **测试工具增强**:实时显示队列状态,新增"测试最大限制"功能
+
+## 📋 系统概述
+
+本系统实现了完整的实时消息推送功能,支持 Mock 模式(开发)和真实 Socket.IO 模式(生产)。消息以右下角层叠弹窗的形式显示,**新消息在最上方**,符合主流桌面应用的交互习惯。
+
+---
+
+## 🎯 功能特性
+
+### ✅ 已实现功能
+
+1. **实时推送**
+ - WebSocket (Socket.IO) 连接
+ - Mock 模式自动定期推送(开发环境)
+ - 支持多种消息类型(成功、错误、警告、信息)
+
+2. **UI 展示**
+ - 右下角固定定位
+ - **新消息在最上方**(符合行业标准)
+ - 层叠显示最多 5 条消息
+ - 智能队列管理:超出自动移除最旧消息
+ - 视觉层次:最新消息更突出(更强阴影、边框高亮)
+ - 自动关闭(可配置时长)
+ - 手动关闭按钮
+ - 流畅的进入/退出动画(从右侧滑入)
+
+3. **音效提示**
+ - 新消息音效播放
+ - 可开关音效
+
+4. **开发工具**
+ - 右上角测试工具面板(仅开发环境)
+ - 手动触发测试通知(4种类型)
+ - 层叠效果测试(4条消息)
+ - 最大限制测试(6条→5条)
+ - **实时队列状态显示**(当前消息数 / 5)
+ - 连接状态显示
+ - 音效开关
+ - 测试计数统计
+
+---
+
+## 📁 文件结构
+
+```
+src/
+├── services/
+│ ├── socket/
+│ │ └── index.js # Socket 服务统一导出
+│ ├── socketService.js # 真实 Socket.IO 服务
+│ └── mockSocketService.js # Mock Socket 服务
+├── contexts/
+│ └── NotificationContext.js # 通知上下文管理
+├── components/
+│ ├── NotificationContainer/
+│ │ └── index.js # 通知容器组件
+│ └── NotificationTestTool/
+│ └── index.js # 测试工具组件
+└── assets/
+ └── sounds/
+ └── notification.wav # 通知音效
+```
+
+---
+
+## 📐 展示逻辑说明
+
+### 消息排列方式
+
+本系统采用**新消息在最上方**的展示模式,这是桌面应用的行业标准(Windows、macOS、Slack、Discord等)。
+
+```
+用户视角(右下角):
+
+第1条消息到达:
+┌────────────────────┐
+│ 🔔 买入成功 🆕 │ ← 从右侧滑入
+└────────────────────┘
+
+第2条消息到达:
+┌────────────────────┐
+│ 🔔 价格预警 🆕 │ ← 新消息(最上方)
+├────────────────────┤
+│ 🔔 买入成功 │ ← 旧消息向下平移
+└────────────────────┘
+
+第6条消息到达(超过5条限制):
+┌────────────────────┐
+│ 🔔 第6条 🆕 │ ← 最新
+├────────────────────┤
+│ 🔔 第5条 │
+├────────────────────┤
+│ 🔔 第4条 │
+├────────────────────┤
+│ 🔔 第3条 │
+├────────────────────┤
+│ 🔔 第2条 │ ← 最旧(仍显示)
+└────────────────────┘
+ ↓ 自动移除
+│ 第1条(已移除) │
+```
+
+### 关键特性
+
+- **最新消息固定位置**:始终在右下角的顶部,便于快速注意
+- **自动队列管理**:最多保留 5 条,超出自动移除最旧的
+- **视觉区分**:最新消息有更强的阴影(2xl vs lg)和边框高亮
+- **z-index 层级**:最新消息层级最高(9999),依次递减
+- **间距优化**:消息之间 12px 间距(spacing={3})
+
+---
+
+## 🚀 快速开始
+
+### 1. 启用 Mock 模式
+
+在 `.env` 文件中添加:
+
+```bash
+REACT_APP_ENABLE_MOCK=true
+# 或
+REACT_APP_USE_MOCK_SOCKET=true
+```
+
+### 2. 启动项目
+
+```bash
+npm start
+```
+
+### 3. 测试通知
+
+打开浏览器,右上角会显示 **"通知测试工具"**,点击展开后可以:
+
+- **单条测试**:测试不同类型的通知(成功、错误、警告、信息)
+- **层叠测试**:一次发送 4 条消息,测试层叠效果
+- **最大限制测试**:发送 6 条消息,验证只保留最新 5 条
+- **队列状态**:实时显示当前队列中的消息数量(X / 5)
+- **音效控制**:切换音效开关
+- **清空功能**:一键清空所有通知
+- **连接状态**:查看 Socket 连接状态和服务类型(MOCK/REAL)
+
+### 4. 自动推送
+
+在 Mock 模式下,系统会自动每 20 秒推送 1-2 条随机消息,用于测试层叠效果。
+
+---
+
+## 💻 代码使用
+
+### 在组件中使用通知
+
+```javascript
+import { useNotification } from 'contexts/NotificationContext';
+
+function MyComponent() {
+ const { addNotification, isConnected } = useNotification();
+
+ const handleTradeSuccess = () => {
+ addNotification({
+ type: 'trade_alert',
+ severity: 'success',
+ title: '买入成功',
+ message: '您的订单已成功执行:买入 贵州茅台(600519) 100股',
+ autoClose: 8000, // 8秒后自动关闭
+ });
+ };
+
+ return (
+
+
连接状态: {isConnected ? '已连接' : '未连接'}
+
+
+ );
+}
+```
+
+### 消息格式
+
+```javascript
+{
+ type: 'trade_alert', // 消息类型
+ severity: 'info', // 'success' | 'error' | 'warning' | 'info'
+ title: '通知标题', // 主标题
+ message: '详细消息内容', // 详细内容
+ timestamp: Date.now(), // 时间戳(自动生成)
+ autoClose: 8000, // 自动关闭时长(毫秒),0 或 false 表示不自动关闭
+ id: 'unique_id' // 唯一ID(自动生成)
+}
+```
+
+---
+
+## 🔧 配置说明
+
+### Mock 服务配置
+
+在 `src/services/mockSocketService.js` 中可以配置:
+
+```javascript
+// 修改模拟数据
+const mockTradeAlerts = [
+ {
+ severity: 'success',
+ title: '自定义标题',
+ message: '自定义消息',
+ autoClose: 8000,
+ },
+ // 添加更多...
+];
+
+// 调整推送频率
+socket.startMockPush(20000, 2); // 每20秒推送1-2条
+```
+
+### NotificationContext 配置
+
+在 `src/contexts/NotificationContext.js` 中:
+
+```javascript
+// 修改最大消息数量(默认 5 条)
+const maxNotifications = 5; // 修改为其他数值
+
+// 修改默认音效状态
+const [soundEnabled, setSoundEnabled] = useState(true); // false 关闭音效
+
+// 修改音效音量
+audioRef.current.volume = 0.5; // 0.0 - 1.0
+```
+
+### NotificationContainer 配置
+
+在 `src/components/NotificationContainer/index.js` 中:
+
+```javascript
+// 调整消息间距
+ // 3 = 12px, 4 = 16px, 2 = 8px
+
+// 调整位置
+
+```
+
+---
+
+## 🌐 后端集成(生产环境)
+
+### 1. Flask 后端配置
+
+当 `REACT_APP_ENABLE_MOCK=false` 时,系统会连接真实的 Socket.IO 服务器。
+
+在 `app.py` 中初始化 Flask-SocketIO:
+
+```python
+from flask_socketio import SocketIO, emit
+
+# 初始化 SocketIO
+socketio = SocketIO(app, cors_allowed_origins=[
+ "http://localhost:3000",
+ "https://valuefrontier.cn"
+])
+
+# 连接事件
+@socketio.on('connect')
+def handle_connect():
+ print(f'Client connected: {request.sid}')
+
+# 推送交易通知
+def send_trade_notification(user_id, data):
+ """
+ 推送交易通知到指定用户
+ """
+ emit('trade_notification', data, room=user_id)
+
+# 启动服务器
+if __name__ == '__main__':
+ socketio.run(app, host='0.0.0.0', port=5001)
+```
+
+### 2. 后端推送示例
+
+```python
+# 交易成功后推送通知
+socketio.emit('trade_notification', {
+ 'type': 'trade_alert',
+ 'severity': 'success',
+ 'title': '买入成功',
+ 'message': f'买入 {stock_name}({stock_code}) {quantity}股',
+ 'timestamp': int(time.time() * 1000),
+ 'autoClose': 8000
+}, room=user_id)
+```
+
+---
+
+## 🎨 自定义样式
+
+### 修改通知位置
+
+在 `src/components/NotificationContainer/index.js`:
+
+```javascript
+
+```
+
+### 修改通知颜色
+
+在 `src/components/NotificationContainer/index.js` 中的 `NOTIFICATION_STYLES`:
+
+```javascript
+const NOTIFICATION_STYLES = {
+ success: {
+ icon: MdCheckCircle,
+ colorScheme: 'green', // 修改颜色主题
+ bg: 'green.50',
+ borderColor: 'green.400',
+ iconColor: 'green.500',
+ },
+ // ...
+};
+```
+
+---
+
+## 🐛 故障排除
+
+### 问题 1: 通知不显示
+
+**检查项:**
+1. 确认 `NotificationProvider` 已包裹应用
+2. 检查浏览器控制台是否有错误
+3. 确认 socket 连接状态(查看测试工具)
+
+### 问题 2: 音效不播放
+
+**解决方案:**
+1. 检查浏览器是否允许自动播放音频
+2. 确认音效开关已打开
+3. 检查音频文件路径是否正确
+
+### 问题 3: Mock 推送不工作
+
+**检查项:**
+1. 确认 `.env` 中设置了 `REACT_APP_ENABLE_MOCK=true`
+2. 查看控制台日志确认 Mock 服务已启动
+3. 检查 `startMockPush` 是否被调用
+
+### 问题 4: Socket 连接失败
+
+**解决方案:**
+1. 检查后端 Flask-SocketIO 是否正确运行
+2. 确认 CORS 配置正确
+3. 检查 `src/utils/apiConfig.js` 中的 API 地址
+
+---
+
+## 📊 性能优化建议
+
+1. **智能队列管理** ✅ 已实现
+ - 系统自动限制最多 5 条通知
+ - 超出自动移除最旧的,避免内存泄漏
+ - 建议根据实际需求调整 `maxNotifications` 值
+
+2. **合理设置自动关闭时长**
+ - 建议 5-10 秒(默认 8 秒)
+ - 重要消息可设置更长时间或 `autoClose: false`
+
+3. **避免频繁推送**
+ - 生产环境建议间隔至少 3 秒
+ - 避免短时间内大量推送造成用户困扰
+
+4. **视觉性能优化** ✅ 已实现
+ - 使用 Chakra UI 的优化动画(Slide、ScaleFade)
+ - z-index 合理分配,避免层叠问题
+ - 间距适中(12px),不会过于紧密
+
+---
+
+## 🔮 未来扩展
+
+可以考虑添加的功能:
+
+1. ✨ 通知历史记录
+2. ✨ 通知分类过滤
+3. ✨ 通知优先级
+4. ✨ 通知持久化(存储到 localStorage)
+5. ✨ 通知点击交互(跳转到相关页面)
+6. ✨ 用户偏好设置(通知类型开关)
+
+---
+
+## 📝 更新日志
+
+### v1.1.0 (2025-01-21) - 交互优化版
+
+- ✅ **新消息置顶展示**(行业标准,参考 Windows/macOS/Slack)
+- ✅ **智能队列管理**:最多保留 5 条,超出自动移除最旧的
+- ✅ **视觉层次优化**:
+ - 最新消息:boxShadow='2xl' + 边框高亮
+ - 其他消息:boxShadow='lg'
+ - 消息间距:12px(spacing={3})
+- ✅ **z-index 优化**:最新消息层级最高(9999),依次递减
+- ✅ **测试工具增强**:
+ - 新增"测试最大限制"按钮(6条→5条)
+ - 实时显示队列状态(X / 5)
+ - 队列满时红色提示
+- ✅ **文档完善**:添加展示逻辑说明、配置指南
+
+### v1.0.0 (2025-01-20) - 初始版本
+
+- ✅ 实现基础通知系统
+- ✅ 支持 Mock 和真实 Socket.IO 模式
+- ✅ 右下角层叠显示
+- ✅ 音效提示
+- ✅ 开发工具面板
+- ✅ 4 种消息类型(成功、错误、警告、信息)
+
+---
+
+## 💡 技术栈
+
+- **前端框架**: React 18.3.1
+- **UI 库**: Chakra UI 2.8.2
+- **实时通信**: Socket.IO Client 4.7.4
+- **后端框架**: Flask-SocketIO 5.3.6
+- **状态管理**: React Context API
+
+---
+
+## 📞 支持
+
+如有问题,请查看:
+- 项目文档: `CLAUDE.md`
+- 测试工具: 开发环境右上角
+- 控制台日志: 所有操作都有详细日志
+
+---
+
+**祝你使用愉快!** 🎉
diff --git a/src/App.js b/src/App.js
index ed8c6aac..8ff59802 100755
--- a/src/App.js
+++ b/src/App.js
@@ -43,11 +43,14 @@ const TradingSimulation = React.lazy(() => import("views/TradingSimulation"));
// Contexts
import { AuthProvider } from "contexts/AuthContext";
import { AuthModalProvider } from "contexts/AuthModalContext";
+import { NotificationProvider } from "contexts/NotificationContext";
// Components
import ProtectedRoute from "components/ProtectedRoute";
import ErrorBoundary from "components/ErrorBoundary";
import AuthModalManager from "components/Auth/AuthModalManager";
+import NotificationContainer from "components/NotificationContainer";
+import NotificationTestTool from "components/NotificationTestTool";
import ScrollToTop from "components/ScrollToTop";
import { logger } from "utils/logger";
@@ -190,8 +193,12 @@ export default function App() {
-
-
+
+
+
+
+
+
diff --git a/src/components/Citation/CitedContent.js b/src/components/Citation/CitedContent.js
index aa100671..a9726ab3 100644
--- a/src/components/Citation/CitedContent.js
+++ b/src/components/Citation/CitedContent.js
@@ -1,7 +1,7 @@
// src/components/Citation/CitedContent.js
import React from 'react';
-import { Typography, Space, Tag } from 'antd';
-import { RobotOutlined, FileSearchOutlined } from '@ant-design/icons';
+import { Typography, Tag } from 'antd';
+import { RobotOutlined } from '@ant-design/icons';
import CitationMark from './CitationMark';
import { processCitationData } from '../../utils/citationUtils';
import { logger } from '../../utils/logger';
@@ -9,20 +9,25 @@ import { logger } from '../../utils/logger';
const { Text } = Typography;
/**
- * 带引用标注的内容组件
+ * 带引用标注的内容组件(块级模式)
* 展示拼接的文本,每句话后显示上标引用【1】【2】【3】
* 支持鼠标悬浮和点击查看引用来源
+ * AI 标识统一显示在右上角,不占用布局高度
*
* @param {Object} props
* @param {Object} props.data - API 返回的原始数据 { data: [...] }
* @param {string} props.title - 标题文本,默认 "AI 分析结果"
- * @param {boolean} props.showAIBadge - 是否显示 AI 生成标识,默认 true
+ * @param {string} props.prefix - 内容前的前缀标签,如 "机制:"(可选)
+ * @param {Object} props.prefixStyle - 前缀标签的自定义样式(可选)
+ * @param {boolean} props.showAIBadge - 是否显示右上角 AI 标识,默认 true(可选)
* @param {Object} props.containerStyle - 容器额外样式(可选)
*
* @example
*
@@ -30,6 +35,8 @@ const { Text } = Typography;
const CitedContent = ({
data,
title = 'AI 分析结果',
+ prefix = '',
+ prefixStyle = {},
showAIBadge = true,
containerStyle = {}
}) => {
@@ -45,59 +52,65 @@ const CitedContent = ({
return null;
}
- // 判断是否显示标题栏(内联模式:title为空且不显示AI徽章)
- const showHeader = title || showAIBadge;
-
- // 根据是否显示标题栏决定容器样式
- const defaultContainerStyle = showHeader ? {
- backgroundColor: '#f5f5f5',
- borderRadius: 6,
- padding: 16
- } : {};
-
- // 检查是否为内联模式
- const isInlineMode = containerStyle?.display && containerStyle.display.includes('inline');
-
- // 根据内联模式选择容器元素类型
- const ContainerTag = isInlineMode ? 'span' : 'div';
- const ContentTag = isInlineMode ? 'span' : 'div';
-
return (
-
- {/* 标题栏 - 仅在需要时显示 */}
- {showHeader && (
- }
+ color="purple"
style={{
- width: '100%',
- justifyContent: 'space-between',
- marginBottom: 12
+ position: 'absolute',
+ top: 12,
+ right: 12,
+ margin: 0,
+ zIndex: 10,
+ fontSize: 12,
+ padding: '2px 8px'
}}
+ className="ai-badge-responsive"
>
-
-
-
- {title}
-
-
- {showAIBadge && (
- }
- color="purple"
- style={{ margin: 0 }}
- >
- AI 生成
-
- )}
-
+ AI合成
+
+ )}
+
+ {/* 标题栏 */}
+ {title && (
+
+
+ {title}
+
+
)}
{/* 带引用的文本内容 */}
-
+
+ {/* 前缀标签(如果有) */}
+ {prefix && (
+
+ {prefix}
+
+ )}
+
{processed.segments.map((segment, index) => (
{/* 文本片段 */}
@@ -117,8 +130,18 @@ const CitedContent = ({
)}
))}
-
-
+
+
+ {/* 响应式样式 */}
+
+
);
};
diff --git a/src/components/NotificationContainer/index.js b/src/components/NotificationContainer/index.js
new file mode 100644
index 00000000..3c2a962c
--- /dev/null
+++ b/src/components/NotificationContainer/index.js
@@ -0,0 +1,189 @@
+// src/components/NotificationContainer/index.js
+/**
+ * 通知容器组件 - 右下角层叠显示实时通知
+ */
+
+import React from 'react';
+import {
+ Box,
+ VStack,
+ HStack,
+ Text,
+ IconButton,
+ Icon,
+ useColorModeValue,
+ Slide,
+ ScaleFade,
+} from '@chakra-ui/react';
+import { MdClose, MdCheckCircle, MdError, MdWarning, MdInfo } from 'react-icons/md';
+import { useNotification } from '../../contexts/NotificationContext';
+
+// 通知类型对应的图标和颜色
+const NOTIFICATION_STYLES = {
+ success: {
+ icon: MdCheckCircle,
+ colorScheme: 'green',
+ bg: 'green.50',
+ borderColor: 'green.400',
+ iconColor: 'green.500',
+ },
+ error: {
+ icon: MdError,
+ colorScheme: 'red',
+ bg: 'red.50',
+ borderColor: 'red.400',
+ iconColor: 'red.500',
+ },
+ warning: {
+ icon: MdWarning,
+ colorScheme: 'orange',
+ bg: 'orange.50',
+ borderColor: 'orange.400',
+ iconColor: 'orange.500',
+ },
+ info: {
+ icon: MdInfo,
+ colorScheme: 'blue',
+ bg: 'blue.50',
+ borderColor: 'blue.400',
+ iconColor: 'blue.500',
+ },
+};
+
+/**
+ * 单个通知项组件
+ */
+const NotificationItem = ({ notification, onClose, isNewest = false }) => {
+ const { id, severity = 'info', title, message } = notification;
+ const style = NOTIFICATION_STYLES[severity] || NOTIFICATION_STYLES.info;
+
+ const bgColor = useColorModeValue(style.bg, `${style.colorScheme}.900`);
+ const borderColor = useColorModeValue(style.borderColor, `${style.colorScheme}.500`);
+ const textColor = useColorModeValue('gray.800', 'white');
+ const subTextColor = useColorModeValue('gray.600', 'gray.300');
+
+ return (
+
+
+
+ {/* 图标 */}
+
+
+ {/* 内容 */}
+
+
+ {title}
+
+ {message && (
+
+ {message}
+
+ )}
+
+
+ {/* 关闭按钮 */}
+ }
+ size="sm"
+ variant="ghost"
+ colorScheme={style.colorScheme}
+ aria-label="关闭通知"
+ onClick={() => onClose(id)}
+ position="absolute"
+ top={2}
+ right={2}
+ _hover={{
+ bg: useColorModeValue(`${style.colorScheme}.100`, `${style.colorScheme}.800`),
+ }}
+ />
+
+
+
+ );
+};
+
+/**
+ * 通知容器组件 - 主组件
+ */
+const NotificationContainer = () => {
+ const { notifications, removeNotification } = useNotification();
+
+ // 如果没有通知,不渲染
+ if (notifications.length === 0) {
+ return null;
+ }
+
+ return (
+
+
+ {notifications.map((notification, index) => (
+
+
+
+ ))}
+
+
+ );
+};
+
+export default NotificationContainer;
diff --git a/src/components/NotificationTestTool/index.js b/src/components/NotificationTestTool/index.js
new file mode 100644
index 00000000..3aa75741
--- /dev/null
+++ b/src/components/NotificationTestTool/index.js
@@ -0,0 +1,231 @@
+// src/components/NotificationTestTool/index.js
+/**
+ * 通知测试工具 - 仅在开发环境显示
+ * 用于手动测试通知功能
+ */
+
+import React, { useState } from 'react';
+import {
+ Box,
+ Button,
+ VStack,
+ HStack,
+ Text,
+ IconButton,
+ Collapse,
+ useDisclosure,
+ Badge,
+} from '@chakra-ui/react';
+import { MdNotifications, MdClose, MdVolumeOff, MdVolumeUp } from 'react-icons/md';
+import { useNotification } from '../../contexts/NotificationContext';
+import { SOCKET_TYPE } from '../../services/socket';
+
+const NotificationTestTool = () => {
+ const { isOpen, onToggle } = useDisclosure();
+ const { addNotification, soundEnabled, toggleSound, isConnected, clearAllNotifications, notifications } = useNotification();
+ const [testCount, setTestCount] = useState(0);
+
+ // 只在开发环境显示
+ if (process.env.NODE_ENV !== 'development') {
+ return null;
+ }
+
+ const testNotifications = [
+ {
+ severity: 'success',
+ title: '买入成功',
+ message: '您的订单已成功执行:买入 贵州茅台(600519) 100股',
+ },
+ {
+ severity: 'error',
+ title: '委托失败',
+ message: '卖出订单失败:资金不足',
+ },
+ {
+ severity: 'warning',
+ title: '价格预警',
+ message: '您关注的股票已触达预设价格',
+ },
+ {
+ severity: 'info',
+ title: '持仓提醒',
+ message: '您持有的股票今日涨幅达 5.2%',
+ },
+ ];
+
+ const handleTestNotification = (index) => {
+ const notif = testNotifications[index];
+ addNotification({
+ ...notif,
+ type: 'trade_alert',
+ autoClose: 8000,
+ });
+ setTestCount(prev => prev + 1);
+ };
+
+ const handleMultipleNotifications = () => {
+ testNotifications.forEach((notif, index) => {
+ setTimeout(() => {
+ addNotification({
+ ...notif,
+ type: 'trade_alert',
+ autoClose: 10000,
+ });
+ }, index * 600);
+ });
+ setTestCount(prev => prev + testNotifications.length);
+ };
+
+ const handleMaxLimitTest = () => {
+ // 测试最大限制:快速发送6条,验证只保留最新5条
+ for (let i = 1; i <= 6; i++) {
+ setTimeout(() => {
+ addNotification({
+ severity: i % 2 === 0 ? 'success' : 'info',
+ title: `测试消息 #${i}`,
+ message: `这是第 ${i} 条测试消息(共6条,应只保留最新5条)`,
+ type: 'trade_alert',
+ autoClose: 12000,
+ });
+ }, i * 400);
+ }
+ setTestCount(prev => prev + 6);
+ };
+
+ return (
+
+ {/* 折叠按钮 */}
+
+
+
+ 通知测试工具
+
+
+ {isConnected ? 'Connected' : 'Disconnected'}
+
+
+ {SOCKET_TYPE}
+
+ : }
+ size="xs"
+ variant="ghost"
+ colorScheme="whiteAlpha"
+ aria-label={isOpen ? '关闭' : '打开'}
+ />
+
+
+ {/* 工具面板 */}
+
+
+
+ 点击按钮测试不同类型的通知
+
+
+ {/* 测试按钮 */}
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* 功能按钮 */}
+
+
+
+ : }
+ colorScheme={soundEnabled ? 'blue' : 'gray'}
+ onClick={toggleSound}
+ aria-label="切换音效"
+ />
+
+
+ {/* 统计信息 */}
+
+
+
+ 当前队列:
+
+ = 5 ? 'red' : 'blue'}>
+ {notifications.length} / 5
+
+
+
+ 已测试: {testCount} 条通知
+
+
+
+
+
+ );
+};
+
+export default NotificationTestTool;
diff --git a/src/components/StockChart/StockChartAntdModal.js b/src/components/StockChart/StockChartAntdModal.js
index 70068505..2f84ef90 100644
--- a/src/components/StockChart/StockChartAntdModal.js
+++ b/src/components/StockChart/StockChartAntdModal.js
@@ -553,7 +553,6 @@ const StockChartAntdModal = ({
) : stock?.relation_desc ? (
diff --git a/src/contexts/NotificationContext.js b/src/contexts/NotificationContext.js
new file mode 100644
index 00000000..b91cf7e3
--- /dev/null
+++ b/src/contexts/NotificationContext.js
@@ -0,0 +1,207 @@
+// src/contexts/NotificationContext.js
+/**
+ * 通知上下文 - 管理实时消息推送和通知显示
+ */
+
+import React, { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react';
+import { logger } from '../utils/logger';
+import socket, { SOCKET_TYPE } from '../services/socket';
+import notificationSound from '../assets/sounds/notification.wav';
+
+// 创建通知上下文
+const NotificationContext = createContext();
+
+// 自定义Hook
+export const useNotification = () => {
+ const context = useContext(NotificationContext);
+ if (!context) {
+ throw new Error('useNotification must be used within a NotificationProvider');
+ }
+ return context;
+};
+
+// 通知提供者组件
+export const NotificationProvider = ({ children }) => {
+ const [notifications, setNotifications] = useState([]);
+ const [isConnected, setIsConnected] = useState(false);
+ const [soundEnabled, setSoundEnabled] = useState(true);
+ const audioRef = useRef(null);
+
+ // 初始化音频
+ useEffect(() => {
+ try {
+ audioRef.current = new Audio(notificationSound);
+ audioRef.current.volume = 0.5;
+ } catch (error) {
+ logger.error('NotificationContext', 'Audio initialization failed', error);
+ }
+ }, []);
+
+ /**
+ * 播放通知音效
+ */
+ const playNotificationSound = useCallback(() => {
+ if (!soundEnabled || !audioRef.current) {
+ return;
+ }
+
+ try {
+ // 重置音频到开始位置
+ audioRef.current.currentTime = 0;
+ // 播放音频
+ audioRef.current.play().catch(error => {
+ logger.warn('NotificationContext', 'Failed to play notification sound', error);
+ });
+ } catch (error) {
+ logger.error('NotificationContext', 'playNotificationSound', error);
+ }
+ }, [soundEnabled]);
+
+ /**
+ * 添加通知到队列
+ * @param {object} notification - 通知对象
+ */
+ const addNotification = useCallback((notification) => {
+ const newNotification = {
+ id: notification.id || `notif_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+ type: notification.type || 'info',
+ severity: notification.severity || 'info',
+ title: notification.title || '通知',
+ message: notification.message || '',
+ timestamp: notification.timestamp || Date.now(),
+ autoClose: notification.autoClose !== undefined ? notification.autoClose : 8000,
+ ...notification,
+ };
+
+ logger.info('NotificationContext', 'Adding notification', newNotification);
+
+ // 新消息插入到数组开头,最多保留5条
+ setNotifications(prev => {
+ const updated = [newNotification, ...prev];
+ const maxNotifications = 5;
+
+ // 如果超过最大数量,移除最旧的(数组末尾)
+ if (updated.length > maxNotifications) {
+ const removed = updated.slice(maxNotifications);
+ removed.forEach(old => {
+ logger.info('NotificationContext', 'Auto-removing old notification', { id: old.id });
+ });
+ return updated.slice(0, maxNotifications);
+ }
+
+ return updated;
+ });
+
+ // 播放音效
+ playNotificationSound();
+
+ // 自动关闭
+ if (newNotification.autoClose && newNotification.autoClose > 0) {
+ setTimeout(() => {
+ removeNotification(newNotification.id);
+ }, newNotification.autoClose);
+ }
+
+ return newNotification.id;
+ }, [playNotificationSound]);
+
+ /**
+ * 移除通知
+ * @param {string} id - 通知ID
+ */
+ const removeNotification = useCallback((id) => {
+ logger.info('NotificationContext', 'Removing notification', { id });
+ setNotifications(prev => prev.filter(notif => notif.id !== id));
+ }, []);
+
+ /**
+ * 清空所有通知
+ */
+ const clearAllNotifications = useCallback(() => {
+ logger.info('NotificationContext', 'Clearing all notifications');
+ setNotifications([]);
+ }, []);
+
+ /**
+ * 切换音效开关
+ */
+ const toggleSound = useCallback(() => {
+ setSoundEnabled(prev => {
+ const newValue = !prev;
+ logger.info('NotificationContext', 'Sound toggled', { enabled: newValue });
+ return newValue;
+ });
+ }, []);
+
+ // 连接到 Socket 服务
+ useEffect(() => {
+ logger.info('NotificationContext', 'Initializing socket connection...');
+
+ // 连接 socket
+ socket.connect();
+
+ // 监听连接状态
+ socket.on('connect', () => {
+ setIsConnected(true);
+ logger.info('NotificationContext', 'Socket connected');
+
+ // 如果使用 mock,可以启动定期推送
+ if (SOCKET_TYPE === 'MOCK') {
+ // 启动模拟推送:每20秒推送1-2条消息
+ socket.startMockPush(20000, 2);
+ logger.info('NotificationContext', 'Mock push started');
+ }
+ });
+
+ socket.on('disconnect', () => {
+ setIsConnected(false);
+ logger.warn('NotificationContext', 'Socket disconnected');
+ });
+
+ // 监听交易通知
+ socket.on('trade_notification', (data) => {
+ logger.info('NotificationContext', 'Received trade notification', data);
+ addNotification(data);
+ });
+
+ // 监听系统通知
+ socket.on('system_notification', (data) => {
+ logger.info('NotificationContext', 'Received system notification', data);
+ addNotification(data);
+ });
+
+ // 清理函数
+ return () => {
+ logger.info('NotificationContext', 'Cleaning up socket connection');
+
+ // 如果是 mock service,停止推送
+ if (SOCKET_TYPE === 'MOCK') {
+ socket.stopMockPush();
+ }
+
+ socket.off('connect');
+ socket.off('disconnect');
+ socket.off('trade_notification');
+ socket.off('system_notification');
+ socket.disconnect();
+ };
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+ const value = {
+ notifications,
+ isConnected,
+ soundEnabled,
+ addNotification,
+ removeNotification,
+ clearAllNotifications,
+ toggleSound,
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export default NotificationContext;
diff --git a/src/mocks/data/account.js b/src/mocks/data/account.js
index c4cab478..cd0fba60 100644
--- a/src/mocks/data/account.js
+++ b/src/mocks/data/account.js
@@ -372,6 +372,14 @@ export const mockFutureEvents = [
'招商银行',
{
data: [
+ {
+ author: '中信证券',
+ sentences: '作为国内领先的商业银行,招商银行对利率变化敏感度高,美联储货币政策调整将通过汇率、资本流动等渠道影响国内货币政策,进而影响银行净息差和资产质量',
+ query_part: '美联储政策通过汇率和资本流动影响国内银行业',
+ report_title: '美联储政策对中国银行业影响分析',
+ declare_date: '2025-10-18T00:00:00',
+ match_score: '好'
+ },
{
author: '中信证券',
sentences: '作为国内领先的商业银行,招商银行对利率变化敏感度高,美联储货币政策调整将通过汇率、资本流动等渠道影响国内货币政策,进而影响银行净息差和资产质量',
@@ -479,6 +487,22 @@ export const mockFutureEvents = [
'天齐锂业',
{
data: [
+ {
+ author: '天风证券',
+ sentences: '天齐锂业作为宁德时代的核心供应商,将直接受益于下游动力电池需求的增长,公司锂资源储量丰富,随着宁德时代产能扩张,锂盐需求持续旺盛,公司业绩增长确定性强',
+ query_part: '核心锂供应商直接受益于下游需求增长',
+ report_title: '天齐锂业:受益动力电池产业链景气',
+ declare_date: '2025-10-14T00:00:00',
+ match_score: '好'
+ },
+ {
+ author: '天风证券',
+ sentences: '天齐锂业作为宁德时代的核心供应商,将直接受益于下游动力电池需求的增长,公司锂资源储量丰富,随着宁德时代产能扩张,锂盐需求持续旺盛,公司业绩增长确定性强',
+ query_part: '核心锂供应商直接受益于下游需求增长',
+ report_title: '天齐锂业:受益动力电池产业链景气',
+ declare_date: '2025-10-14T00:00:00',
+ match_score: '好'
+ },
{
author: '天风证券',
sentences: '天齐锂业作为宁德时代的核心供应商,将直接受益于下游动力电池需求的增长,公司锂资源储量丰富,随着宁德时代产能扩张,锂盐需求持续旺盛,公司业绩增长确定性强',
diff --git a/src/mocks/handlers/event.js b/src/mocks/handlers/event.js
index a263a1ba..4c3f2a48 100644
--- a/src/mocks/handlers/event.js
+++ b/src/mocks/handlers/event.js
@@ -313,7 +313,7 @@ export const eventHandlers = [
children: [
{
id: '2',
- name: '半导体行业',
+ name: '半导体行业(正向影响)',
transmission_mechanism: {
data: [
{
@@ -323,20 +323,68 @@ export const eventHandlers = [
match_score: "好",
declare_date: "2024-07-10T00:00:00",
report_title: "2024年上半年中国半导体产业发展报告"
- },
- {
- author: "工信部电子信息司",
- sentences: "随着5G、人工智能、物联网等新一代信息技术的快速发展,半导体作为数字经济的基石,正迎来前所未有的发展机遇。预计未来三年,国内半导体市场年均增速将保持在25%以上",
- query_part: "新兴技术推动半导体产业高速增长",
- match_score: "好",
- declare_date: "2024-05-20T00:00:00",
- report_title: "新一代信息技术产业发展白皮书"
}
]
},
direction: 'positive',
strength: 80,
is_circular: false
+ },
+ {
+ id: '7',
+ name: '传统制造业(负向影响)',
+ transmission_mechanism: {
+ data: [
+ {
+ author: "经济观察报",
+ sentences: "随着半导体等高科技产业获得大量政策和资金支持,传统制造业面临融资难、用工成本上升等多重压力。部分劳动密集型企业利润率下降15%,行业整体投资意愿降低",
+ query_part: "资源向高科技倾斜导致传统制造业承压",
+ match_score: "好",
+ declare_date: "2024-06-15T00:00:00",
+ report_title: "传统制造业转型升级调研报告"
+ }
+ ]
+ },
+ direction: 'negative',
+ strength: 60,
+ is_circular: false
+ },
+ {
+ id: '8',
+ name: '能源行业(中性影响)',
+ transmission_mechanism: {
+ data: [
+ {
+ author: "能源研究所",
+ sentences: "半导体产业扩张带来电力需求增长约8%,但同时推动节能技术应用,整体能源消费结构趋于优化。新建芯片工厂虽增加用电负荷,但智能电网技术应用使能源利用效率提升12%",
+ query_part: "半导体产业对能源行业影响相对中性",
+ match_score: "中",
+ declare_date: "2024-07-01T00:00:00",
+ report_title: "高科技产业能源消费分析"
+ }
+ ]
+ },
+ direction: 'neutral',
+ strength: 40,
+ is_circular: false
+ },
+ {
+ id: '9',
+ name: '教育培训行业(未明确方向)',
+ transmission_mechanism: {
+ data: [
+ {
+ author: "教育部职业教育司",
+ sentences: "半导体产业快速发展催生大量专业人才需求,各类培训机构、职业院校纷纷开设相关课程。预计未来三年将新增半导体专业学员超过50万人,带动职业教育市场规模扩大",
+ query_part: "半导体产业推动职业教育发展",
+ match_score: "好",
+ declare_date: "2024-06-20T00:00:00",
+ report_title: "半导体人才培养白皮书"
+ }
+ ]
+ },
+ strength: 50,
+ is_circular: false
}
]
}
@@ -360,6 +408,14 @@ export const eventHandlers = [
name: '主要事件',
transmission_mechanism: {
data: [
+ {
+ author: "中国半导体行业协会",
+ sentences: "受益于新能源汽车、5G通信等新兴应用领域的爆发式增长,国内半导体市场需求持续旺盛,2024年Q1市场规模同比增长28%,创历史新高",
+ query_part: "新兴应用推动半导体需求增长28%",
+ match_score: "好",
+ declare_date: "2024-04-05T00:00:00",
+ report_title: "2024年Q1中国半导体行业景气度报告"
+ },
{
author: "中国半导体行业协会",
sentences: "受益于新能源汽车、5G通信等新兴应用领域的爆发式增长,国内半导体市场需求持续旺盛,2024年Q1市场规模同比增长28%,创历史新高",
diff --git a/src/services/mockSocketService.js b/src/services/mockSocketService.js
new file mode 100644
index 00000000..65ddef65
--- /dev/null
+++ b/src/services/mockSocketService.js
@@ -0,0 +1,254 @@
+// src/services/mockSocketService.js
+/**
+ * Mock Socket 服务 - 用于开发环境模拟实时推送
+ * 模拟交易提醒、系统通知等实时消息推送
+ */
+
+import { logger } from '../utils/logger';
+
+// 模拟交易提醒数据
+const mockTradeAlerts = [
+ {
+ type: 'trade_alert',
+ severity: 'success',
+ title: '买入成功',
+ message: '您的订单已成功执行:买入 贵州茅台(600519) 100股,成交价 ¥1,850.00',
+ timestamp: Date.now(),
+ autoClose: 8000,
+ },
+ {
+ type: 'trade_alert',
+ severity: 'warning',
+ title: '价格预警',
+ message: '您关注的股票 比亚迪(002594) 当前价格 ¥245.50,已触达预设价格',
+ timestamp: Date.now(),
+ autoClose: 10000,
+ },
+ {
+ type: 'trade_alert',
+ severity: 'info',
+ title: '持仓提醒',
+ message: '您持有的 宁德时代(300750) 今日涨幅达 5.2%,当前盈利 +¥12,350',
+ timestamp: Date.now(),
+ autoClose: 8000,
+ },
+ {
+ type: 'trade_alert',
+ severity: 'error',
+ title: '委托失败',
+ message: '卖出订单失败:五粮液(000858) 当前处于停牌状态,无法交易',
+ timestamp: Date.now(),
+ autoClose: 12000,
+ },
+ {
+ type: 'system_notification',
+ severity: 'info',
+ title: '系统公告',
+ message: '市场将于15:00收盘,请注意及时调整持仓',
+ timestamp: Date.now(),
+ autoClose: 10000,
+ },
+ {
+ type: 'trade_alert',
+ severity: 'success',
+ title: '分红到账',
+ message: '您持有的 中国平安(601318) 分红已到账,金额 ¥560.00',
+ timestamp: Date.now(),
+ autoClose: 8000,
+ },
+];
+
+class MockSocketService {
+ constructor() {
+ this.connected = false;
+ this.listeners = new Map();
+ this.intervals = [];
+ this.messageQueue = [];
+ }
+
+ /**
+ * 连接到 mock socket
+ */
+ connect() {
+ if (this.connected) {
+ logger.warn('mockSocketService', 'Already connected');
+ return;
+ }
+
+ logger.info('mockSocketService', 'Connecting to mock socket service...');
+
+ // 模拟连接延迟
+ setTimeout(() => {
+ this.connected = true;
+ logger.info('mockSocketService', 'Mock socket connected successfully');
+
+ // 触发连接成功事件
+ this.emit('connect', { timestamp: Date.now() });
+
+ // 在连接后3秒发送欢迎消息
+ setTimeout(() => {
+ this.emit('trade_notification', {
+ type: 'system_notification',
+ severity: 'info',
+ title: '连接成功',
+ message: '实时消息推送服务已启动 (Mock 模式)',
+ timestamp: Date.now(),
+ autoClose: 5000,
+ });
+ }, 3000);
+ }, 1000);
+ }
+
+ /**
+ * 断开连接
+ */
+ disconnect() {
+ if (!this.connected) {
+ return;
+ }
+
+ logger.info('mockSocketService', 'Disconnecting from mock socket service...');
+
+ // 清除所有定时器
+ this.intervals.forEach(interval => clearInterval(interval));
+ this.intervals = [];
+
+ this.connected = false;
+ this.emit('disconnect', { timestamp: Date.now() });
+ }
+
+ /**
+ * 监听事件
+ * @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(() => {
+ // 随机选择 1-burstCount 条消息
+ const count = Math.floor(Math.random() * burstCount) + 1;
+
+ for (let i = 0; i < count; i++) {
+ // 从模拟数据中随机选择一条
+ const randomIndex = Math.floor(Math.random() * mockTradeAlerts.length);
+ const alert = {
+ ...mockTradeAlerts[randomIndex],
+ timestamp: Date.now(),
+ id: `mock_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+ };
+
+ // 延迟发送(模拟层叠效果)
+ setTimeout(() => {
+ this.emit('trade_notification', 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 = [];
+ logger.info('mockSocketService', 'Mock push stopped');
+ }
+
+ /**
+ * 手动触发一条测试消息
+ * @param {object} customData - 自定义消息数据(可选)
+ */
+ sendTestNotification(customData = null) {
+ const notification = customData || {
+ type: 'trade_alert',
+ severity: 'info',
+ title: '测试消息',
+ message: '这是一条手动触发的测试消息',
+ timestamp: Date.now(),
+ autoClose: 5000,
+ id: `test_${Date.now()}`,
+ };
+
+ this.emit('trade_notification', notification);
+ logger.info('mockSocketService', 'Test notification sent', notification);
+ }
+
+ /**
+ * 获取连接状态
+ */
+ isConnected() {
+ return this.connected;
+ }
+}
+
+// 导出单例
+export const mockSocketService = new MockSocketService();
+
+export default mockSocketService;
diff --git a/src/services/socket/index.js b/src/services/socket/index.js
new file mode 100644
index 00000000..dfecdad3
--- /dev/null
+++ b/src/services/socket/index.js
@@ -0,0 +1,28 @@
+// src/services/socket/index.js
+/**
+ * Socket 服务统一导出
+ * 根据环境变量自动选择使用 Mock 或真实 Socket.IO 服务
+ */
+
+import { mockSocketService } from '../mockSocketService';
+import { socketService } from '../socketService';
+
+// 判断是否使用 Mock
+const useMock = process.env.REACT_APP_ENABLE_MOCK === 'true' || process.env.REACT_APP_USE_MOCK_SOCKET === 'true';
+
+// 根据环境选择服务
+export const socket = useMock ? mockSocketService : socketService;
+
+// 同时导出两个服务,方便测试和调试
+export { mockSocketService, socketService };
+
+// 导出服务类型标识
+export const SOCKET_TYPE = useMock ? 'MOCK' : 'REAL';
+
+// 打印当前使用的服务类型
+console.log(
+ `%c[Socket Service] Using ${SOCKET_TYPE} Socket Service`,
+ `color: ${useMock ? '#FF9800' : '#4CAF50'}; font-weight: bold; font-size: 12px;`
+);
+
+export default socket;
diff --git a/src/services/socketService.js b/src/services/socketService.js
new file mode 100644
index 00000000..32e129f0
--- /dev/null
+++ b/src/services/socketService.js
@@ -0,0 +1,194 @@
+// src/services/socketService.js
+/**
+ * 真实 Socket.IO 服务 - 用于生产环境连接真实后端
+ */
+
+import { io } from 'socket.io-client';
+import { logger } from '../utils/logger';
+import { getApiBase } from '../utils/apiConfig';
+
+const API_BASE_URL = getApiBase();
+
+class SocketService {
+ constructor() {
+ this.socket = null;
+ this.connected = false;
+ this.reconnectAttempts = 0;
+ this.maxReconnectAttempts = 5;
+ }
+
+ /**
+ * 连接到 Socket.IO 服务器
+ * @param {object} options - 连接选项
+ */
+ connect(options = {}) {
+ if (this.socket && this.connected) {
+ logger.warn('socketService', 'Already connected');
+ return;
+ }
+
+ logger.info('socketService', 'Connecting to Socket.IO server...', { url: API_BASE_URL });
+
+ // 创建 socket 连接
+ this.socket = io(API_BASE_URL, {
+ transports: ['websocket', 'polling'],
+ reconnection: true,
+ reconnectionDelay: 1000,
+ reconnectionDelayMax: 5000,
+ reconnectionAttempts: this.maxReconnectAttempts,
+ timeout: 20000,
+ autoConnect: true,
+ withCredentials: true, // 允许携带认证信息
+ ...options,
+ });
+
+ // 监听连接成功
+ this.socket.on('connect', () => {
+ this.connected = true;
+ this.reconnectAttempts = 0;
+ logger.info('socketService', 'Socket.IO connected successfully', {
+ socketId: this.socket.id,
+ });
+ });
+
+ // 监听断开连接
+ this.socket.on('disconnect', (reason) => {
+ this.connected = false;
+ logger.warn('socketService', 'Socket.IO disconnected', { reason });
+ });
+
+ // 监听连接错误
+ this.socket.on('connect_error', (error) => {
+ this.reconnectAttempts++;
+ logger.error('socketService', 'connect_error', error, {
+ attempts: this.reconnectAttempts,
+ maxAttempts: this.maxReconnectAttempts,
+ });
+
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
+ logger.error('socketService', 'Max reconnection attempts reached');
+ this.socket.close();
+ }
+ });
+
+ // 监听重连尝试
+ this.socket.io.on('reconnect_attempt', (attemptNumber) => {
+ logger.info('socketService', 'Reconnection attempt', { attemptNumber });
+ });
+
+ // 监听重连成功
+ this.socket.io.on('reconnect', (attemptNumber) => {
+ this.reconnectAttempts = 0;
+ logger.info('socketService', 'Reconnected successfully', { attemptNumber });
+ });
+
+ // 监听重连失败
+ this.socket.io.on('reconnect_failed', () => {
+ logger.error('socketService', 'Reconnection failed after max attempts');
+ });
+ }
+
+ /**
+ * 断开连接
+ */
+ disconnect() {
+ if (!this.socket) {
+ return;
+ }
+
+ logger.info('socketService', 'Disconnecting from Socket.IO server...');
+ this.socket.disconnect();
+ this.socket = null;
+ this.connected = false;
+ }
+
+ /**
+ * 监听事件
+ * @param {string} event - 事件名称
+ * @param {Function} callback - 回调函数
+ */
+ on(event, callback) {
+ if (!this.socket) {
+ logger.warn('socketService', 'Cannot listen to event: socket not initialized', { event });
+ return;
+ }
+
+ this.socket.on(event, callback);
+ logger.info('socketService', `Event listener added: ${event}`);
+ }
+
+ /**
+ * 移除事件监听
+ * @param {string} event - 事件名称
+ * @param {Function} callback - 回调函数(可选)
+ */
+ off(event, callback) {
+ if (!this.socket) {
+ return;
+ }
+
+ if (callback) {
+ this.socket.off(event, callback);
+ } else {
+ this.socket.off(event);
+ }
+
+ logger.info('socketService', `Event listener removed: ${event}`);
+ }
+
+ /**
+ * 发送消息到服务器
+ * @param {string} event - 事件名称
+ * @param {*} data - 发送的数据
+ * @param {Function} callback - 确认回调(可选)
+ */
+ emit(event, data, callback) {
+ if (!this.socket || !this.connected) {
+ logger.warn('socketService', 'Cannot emit: socket not connected', { event, data });
+ return;
+ }
+
+ if (callback) {
+ this.socket.emit(event, data, callback);
+ } else {
+ this.socket.emit(event, data);
+ }
+
+ logger.info('socketService', `Event emitted: ${event}`, data);
+ }
+
+ /**
+ * 加入房间
+ * @param {string} room - 房间名称
+ */
+ joinRoom(room) {
+ this.emit('join_room', { room });
+ }
+
+ /**
+ * 离开房间
+ * @param {string} room - 房间名称
+ */
+ leaveRoom(room) {
+ this.emit('leave_room', { room });
+ }
+
+ /**
+ * 获取连接状态
+ */
+ isConnected() {
+ return this.connected;
+ }
+
+ /**
+ * 获取 Socket ID
+ */
+ getSocketId() {
+ return this.socket?.id || null;
+ }
+}
+
+// 导出单例
+export const socketService = new SocketService();
+
+export default socketService;
diff --git a/src/views/Community/components/InvestmentCalendar.js b/src/views/Community/components/InvestmentCalendar.js
index 9c6966c3..cac3d1f9 100644
--- a/src/views/Community/components/InvestmentCalendar.js
+++ b/src/views/Community/components/InvestmentCalendar.js
@@ -739,7 +739,6 @@ const InvestmentCalendar = () => {
) : (
diff --git a/src/views/EventDetail/components/TransmissionChainAnalysis.js b/src/views/EventDetail/components/TransmissionChainAnalysis.js
index 62d412d5..92006cfb 100644
--- a/src/views/EventDetail/components/TransmissionChainAnalysis.js
+++ b/src/views/EventDetail/components/TransmissionChainAnalysis.js
@@ -866,15 +866,10 @@ const TransmissionChainAnalysis = ({ eventId }) => {
fontStyle="italic"
>
{selectedNode.extra.description?.data ? (
- <>
-
- (AI合成)
- >
+
) : (
`${selectedNode.extra.description}(AI合成)`
)}
@@ -916,36 +911,43 @@ const TransmissionChainAnalysis = ({ eventId }) => {
{nodeDetail.parents.map((parent, index) => (
-
-
-
- {parent.name}
- {parent.transmission_mechanism?.data ? (
-
- 机制:
-
- (AI合成)
-
- ) : parent.transmission_mechanism ? (
-
- 机制: {parent.transmission_mechanism}(AI合成)
-
- ) : null}
-
-
-
- {parent.direction}
+
+
+ {parent.direction && (
+
+ {parent.direction === 'positive' ? '正向影响' :
+ parent.direction === 'negative' ? '负向影响' :
+ parent.direction === 'neutral' ? '中性影响' : '未知'}
- {parent.is_circular && (
- 🔄 循环
- )}
-
+ )}
+ {parent.is_circular && (
+ 🔄 循环
+ )}
+
+ {parent.name}
+ {parent.transmission_mechanism?.data ? (
+
+ ) : parent.transmission_mechanism ? (
+
+ 机制: {parent.transmission_mechanism}(AI合成)
+
+ ) : null}
+
))}
@@ -967,33 +969,40 @@ const TransmissionChainAnalysis = ({ eventId }) => {
{nodeDetail.children.map((child, index) => (
-
-
-
- {child.name}
- {child.transmission_mechanism?.data ? (
-
- 机制:
-
- (AI合成)
-
- ) : child.transmission_mechanism ? (
-
- 机制: {child.transmission_mechanism}(AI合成)
-
- ) : null}
-
-
-
- {child.direction}
+
+ {child.direction && (
+
+
+ {child.direction === 'positive' ? '正向影响' :
+ child.direction === 'negative' ? '负向影响' :
+ child.direction === 'neutral' ? '中性影响' : '未知'}
-
-
+
+ )}
+
+ {child.name}
+ {child.transmission_mechanism?.data ? (
+
+ ) : child.transmission_mechanism ? (
+
+ 机制: {child.transmission_mechanism}(AI合成)
+
+ ) : null}
+
))}