- 移动42个文档文件到 docs/ 目录 - 更新 .gitignore 允许 docs/ 下的 .md 文件 - 删除根目录下的重复文档文件 📁 文档分类: - StockDetailPanel 重构文档(3个) - PostHog 集成文档(6个) - 系统架构和API文档(33个) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
547 lines
12 KiB
Markdown
547 lines
12 KiB
Markdown
# WebSocket 事件实时推送 - 前端集成指南
|
||
|
||
## 📦 已创建的文件
|
||
|
||
1. **`src/services/socketService.js`** - WebSocket 服务(已扩展)
|
||
2. **`src/hooks/useEventNotifications.js`** - React Hook
|
||
3. **`test_websocket.html`** - 测试页面
|
||
4. **`test_create_event.py`** - 测试脚本
|
||
|
||
---
|
||
|
||
## 🚀 快速开始
|
||
|
||
### 方案 1:使用 React Hook(推荐)
|
||
|
||
在任何 React 组件中使用:
|
||
|
||
```jsx
|
||
import { useEventNotifications } from 'hooks/useEventNotifications';
|
||
import { useToast } from '@chakra-ui/react';
|
||
|
||
function EventsPage() {
|
||
const toast = useToast();
|
||
|
||
// 订阅事件推送
|
||
const { newEvent, isConnected } = useEventNotifications({
|
||
eventType: 'all', // 'all' | 'policy' | 'market' | 'tech' | ...
|
||
importance: 'all', // 'all' | 'S' | 'A' | 'B' | 'C'
|
||
enabled: true, // 是否启用订阅
|
||
onNewEvent: (event) => {
|
||
// 收到新事件时的处理
|
||
console.log('🔔 收到新事件:', event);
|
||
|
||
// 显示 Toast 通知
|
||
toast({
|
||
title: '新事件提醒',
|
||
description: event.title,
|
||
status: 'info',
|
||
duration: 5000,
|
||
isClosable: true,
|
||
position: 'top-right',
|
||
});
|
||
}
|
||
});
|
||
|
||
return (
|
||
<Box>
|
||
<Text>连接状态: {isConnected ? '已连接 ✅' : '未连接 ❌'}</Text>
|
||
{/* 你的事件列表 */}
|
||
</Box>
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 方案 2:在事件列表页面集成(完整示例)
|
||
|
||
**在 `src/views/Community/components/EventList.js` 中集成:**
|
||
|
||
```jsx
|
||
import React, { useState, useEffect } from 'react';
|
||
import { Box, Text, Badge, useToast } from '@chakra-ui/react';
|
||
import { useEventNotifications } from 'hooks/useEventNotifications';
|
||
|
||
function EventList() {
|
||
const [events, setEvents] = useState([]);
|
||
const [loading, setLoading] = useState(true);
|
||
const toast = useToast();
|
||
|
||
// 1️⃣ 初始加载事件列表(REST API)
|
||
useEffect(() => {
|
||
fetchEvents();
|
||
}, []);
|
||
|
||
const fetchEvents = async () => {
|
||
try {
|
||
const response = await fetch('/api/events?per_page=20');
|
||
const data = await response.json();
|
||
|
||
if (data.success) {
|
||
setEvents(data.data.events);
|
||
}
|
||
} catch (error) {
|
||
console.error('加载事件失败:', error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
// 2️⃣ 订阅 WebSocket 实时推送
|
||
const { newEvent, isConnected } = useEventNotifications({
|
||
eventType: 'all',
|
||
importance: 'all',
|
||
enabled: true, // 可以根据用户设置控制是否启用
|
||
onNewEvent: (event) => {
|
||
console.log('🔔 收到新事件:', event);
|
||
|
||
// 显示通知
|
||
toast({
|
||
title: '📰 新事件发布',
|
||
description: `${event.title}`,
|
||
status: 'info',
|
||
duration: 6000,
|
||
isClosable: true,
|
||
position: 'top-right',
|
||
});
|
||
|
||
// 将新事件添加到列表顶部
|
||
setEvents((prevEvents) => {
|
||
// 检查是否已存在(防止重复)
|
||
const exists = prevEvents.some(e => e.id === event.id);
|
||
if (exists) {
|
||
return prevEvents;
|
||
}
|
||
// 添加到顶部,最多保留 100 个
|
||
return [event, ...prevEvents].slice(0, 100);
|
||
});
|
||
}
|
||
});
|
||
|
||
return (
|
||
<Box>
|
||
{/* 连接状态指示器 */}
|
||
<Box mb={4} display="flex" alignItems="center" gap={2}>
|
||
<Badge colorScheme={isConnected ? 'green' : 'red'}>
|
||
{isConnected ? '实时推送已开启' : '实时推送未连接'}
|
||
</Badge>
|
||
</Box>
|
||
|
||
{/* 事件列表 */}
|
||
{loading ? (
|
||
<Text>加载中...</Text>
|
||
) : (
|
||
<Box>
|
||
{events.map((event) => (
|
||
<EventCard key={event.id} event={event} />
|
||
))}
|
||
</Box>
|
||
)}
|
||
</Box>
|
||
);
|
||
}
|
||
|
||
export default EventList;
|
||
```
|
||
|
||
---
|
||
|
||
### 方案 3:只订阅重要事件(S 和 A 级)
|
||
|
||
```jsx
|
||
import { useImportantEventNotifications } from 'hooks/useEventNotifications';
|
||
|
||
function Dashboard() {
|
||
const { importantEvents, isConnected } = useImportantEventNotifications((event) => {
|
||
// 只会收到 S 和 A 级别的重要事件
|
||
console.log('⚠️ 重要事件:', event);
|
||
|
||
// 播放提示音
|
||
new Audio('/notification.mp3').play();
|
||
});
|
||
|
||
return (
|
||
<Box>
|
||
<Heading>重要事件通知</Heading>
|
||
{importantEvents.map(event => (
|
||
<Alert key={event.id} status="warning">
|
||
<AlertIcon />
|
||
{event.title}
|
||
</Alert>
|
||
))}
|
||
</Box>
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 方案 4:直接使用 Service(不用 Hook)
|
||
|
||
```jsx
|
||
import { useEffect } from 'react';
|
||
import socketService from 'services/socketService';
|
||
|
||
function MyComponent() {
|
||
useEffect(() => {
|
||
// 连接
|
||
socketService.connect();
|
||
|
||
// 订阅
|
||
const unsubscribe = socketService.subscribeToAllEvents((event) => {
|
||
console.log('新事件:', event);
|
||
});
|
||
|
||
// 清理
|
||
return () => {
|
||
unsubscribe();
|
||
socketService.disconnect();
|
||
};
|
||
}, []);
|
||
|
||
return <div>...</div>;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 🎨 UI 集成示例
|
||
|
||
### 1. Toast 通知(Chakra UI)
|
||
|
||
```jsx
|
||
import { useToast } from '@chakra-ui/react';
|
||
|
||
const toast = useToast();
|
||
|
||
// 在 onNewEvent 回调中
|
||
onNewEvent: (event) => {
|
||
toast({
|
||
title: '新事件',
|
||
description: event.title,
|
||
status: 'info',
|
||
duration: 5000,
|
||
isClosable: true,
|
||
position: 'top-right',
|
||
});
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 2. 顶部通知栏
|
||
|
||
```jsx
|
||
import { Alert, AlertIcon, CloseButton } from '@chakra-ui/react';
|
||
|
||
function EventNotificationBanner() {
|
||
const [showNotification, setShowNotification] = useState(false);
|
||
const [latestEvent, setLatestEvent] = useState(null);
|
||
|
||
useEventNotifications({
|
||
eventType: 'all',
|
||
onNewEvent: (event) => {
|
||
setLatestEvent(event);
|
||
setShowNotification(true);
|
||
}
|
||
});
|
||
|
||
if (!showNotification || !latestEvent) return null;
|
||
|
||
return (
|
||
<Alert status="info" variant="solid">
|
||
<AlertIcon />
|
||
新事件:{latestEvent.title}
|
||
<CloseButton
|
||
position="absolute"
|
||
right="8px"
|
||
top="8px"
|
||
onClick={() => setShowNotification(false)}
|
||
/>
|
||
</Alert>
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3. 角标提示(红点)
|
||
|
||
```jsx
|
||
import { Badge } from '@chakra-ui/react';
|
||
|
||
function EventsMenuItem() {
|
||
const [unreadCount, setUnreadCount] = useState(0);
|
||
|
||
useEventNotifications({
|
||
eventType: 'all',
|
||
onNewEvent: () => {
|
||
setUnreadCount(prev => prev + 1);
|
||
}
|
||
});
|
||
|
||
return (
|
||
<MenuItem position="relative">
|
||
事件中心
|
||
{unreadCount > 0 && (
|
||
<Badge
|
||
colorScheme="red"
|
||
position="absolute"
|
||
top="-5px"
|
||
right="-5px"
|
||
borderRadius="full"
|
||
>
|
||
{unreadCount > 99 ? '99+' : unreadCount}
|
||
</Badge>
|
||
)}
|
||
</MenuItem>
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4. 浮动通知卡片
|
||
|
||
```jsx
|
||
import { Box, Slide, useDisclosure } from '@chakra-ui/react';
|
||
|
||
function FloatingEventNotification() {
|
||
const { isOpen, onClose, onOpen } = useDisclosure();
|
||
const [event, setEvent] = useState(null);
|
||
|
||
useEventNotifications({
|
||
eventType: 'all',
|
||
onNewEvent: (newEvent) => {
|
||
setEvent(newEvent);
|
||
onOpen();
|
||
|
||
// 5秒后自动关闭
|
||
setTimeout(onClose, 5000);
|
||
}
|
||
});
|
||
|
||
return (
|
||
<Slide direction="bottom" in={isOpen} style={{ zIndex: 10 }}>
|
||
<Box
|
||
p="40px"
|
||
color="white"
|
||
bg="blue.500"
|
||
rounded="md"
|
||
shadow="md"
|
||
m={4}
|
||
>
|
||
<Text fontWeight="bold">{event?.title}</Text>
|
||
<Text fontSize="sm">{event?.description}</Text>
|
||
<Button size="sm" mt={2} onClick={onClose}>
|
||
关闭
|
||
</Button>
|
||
</Box>
|
||
</Slide>
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📋 API 参考
|
||
|
||
### `useEventNotifications(options)`
|
||
|
||
**参数:**
|
||
| 参数 | 类型 | 默认值 | 说明 |
|
||
|------|------|--------|------|
|
||
| `eventType` | string | `'all'` | 事件类型:`'all'` / `'policy'` / `'market'` / `'tech'` 等 |
|
||
| `importance` | string | `'all'` | 重要性:`'all'` / `'S'` / `'A'` / `'B'` / `'C'` |
|
||
| `enabled` | boolean | `true` | 是否启用订阅 |
|
||
| `onNewEvent` | function | - | 收到新事件时的回调函数 |
|
||
|
||
**返回值:**
|
||
| 属性 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| `newEvent` | object | 最新收到的事件对象 |
|
||
| `isConnected` | boolean | WebSocket 连接状态 |
|
||
| `error` | object | 错误信息 |
|
||
| `clearNewEvent` | function | 清除新事件状态 |
|
||
|
||
---
|
||
|
||
### `socketService` API
|
||
|
||
```javascript
|
||
// 连接
|
||
socketService.connect(options)
|
||
|
||
// 断开
|
||
socketService.disconnect()
|
||
|
||
// 订阅所有事件
|
||
socketService.subscribeToAllEvents(callback)
|
||
|
||
// 订阅特定类型
|
||
socketService.subscribeToEventType('tech', callback)
|
||
|
||
// 订阅特定重要性
|
||
socketService.subscribeToImportantEvents('S', callback)
|
||
|
||
// 取消订阅
|
||
socketService.unsubscribeFromEvents({ eventType: 'all' })
|
||
|
||
// 检查连接状态
|
||
socketService.isConnected()
|
||
```
|
||
|
||
---
|
||
|
||
## 🔧 事件数据结构
|
||
|
||
收到的 `event` 对象包含:
|
||
|
||
```javascript
|
||
{
|
||
id: 123,
|
||
title: "事件标题",
|
||
description: "事件描述",
|
||
event_type: "tech", // 类型
|
||
importance: "S", // 重要性
|
||
status: "active",
|
||
created_at: "2025-01-21T14:30:00",
|
||
hot_score: 85.5,
|
||
view_count: 1234,
|
||
related_avg_chg: 5.2, // 平均涨幅
|
||
related_max_chg: 15.8, // 最大涨幅
|
||
keywords: ["AI", "芯片"], // 关键词
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## ⚙️ 高级配置
|
||
|
||
### 1. 条件订阅(用户设置)
|
||
|
||
```jsx
|
||
function EventsPage() {
|
||
const [enableNotifications, setEnableNotifications] = useState(
|
||
localStorage.getItem('enableEventNotifications') === 'true'
|
||
);
|
||
|
||
useEventNotifications({
|
||
eventType: 'all',
|
||
enabled: enableNotifications, // 根据用户设置控制
|
||
onNewEvent: handleNewEvent
|
||
});
|
||
|
||
return (
|
||
<Switch
|
||
isChecked={enableNotifications}
|
||
onChange={(e) => {
|
||
const enabled = e.target.checked;
|
||
setEnableNotifications(enabled);
|
||
localStorage.setItem('enableEventNotifications', enabled);
|
||
}}
|
||
>
|
||
启用事件实时通知
|
||
</Switch>
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 2. 多个订阅(不同类型)
|
||
|
||
```jsx
|
||
function MultiSubscriptionExample() {
|
||
// 订阅科技类事件
|
||
useEventNotifications({
|
||
eventType: 'tech',
|
||
onNewEvent: (event) => console.log('科技事件:', event)
|
||
});
|
||
|
||
// 订阅政策类事件
|
||
useEventNotifications({
|
||
eventType: 'policy',
|
||
onNewEvent: (event) => console.log('政策事件:', event)
|
||
});
|
||
|
||
return <div>...</div>;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3. 防抖处理(避免通知过多)
|
||
|
||
```jsx
|
||
import { debounce } from 'lodash';
|
||
|
||
const debouncedNotify = debounce((event) => {
|
||
toast({
|
||
title: '新事件',
|
||
description: event.title,
|
||
});
|
||
}, 1000);
|
||
|
||
useEventNotifications({
|
||
eventType: 'all',
|
||
onNewEvent: debouncedNotify
|
||
});
|
||
```
|
||
|
||
---
|
||
|
||
## 🧪 测试步骤
|
||
|
||
1. **启动 Flask 服务**
|
||
```bash
|
||
python app.py
|
||
```
|
||
|
||
2. **启动 React 应用**
|
||
```bash
|
||
npm start
|
||
```
|
||
|
||
3. **创建测试事件**
|
||
```bash
|
||
python test_create_event.py
|
||
```
|
||
|
||
4. **观察结果**
|
||
- 最多等待 30 秒
|
||
- 前端页面应该显示通知
|
||
- 控制台输出日志
|
||
|
||
---
|
||
|
||
## 🐛 常见问题
|
||
|
||
### Q: 没有收到推送?
|
||
**A:** 检查:
|
||
1. Flask 服务是否启动
|
||
2. 浏览器控制台是否有连接错误
|
||
3. 后端日志是否显示 `[轮询] 发现 X 个新事件`
|
||
|
||
### Q: 连接一直失败?
|
||
**A:** 检查:
|
||
1. API_BASE_URL 配置是否正确
|
||
2. CORS 配置是否包含前端域名
|
||
3. 防火墙/代理设置
|
||
|
||
### Q: 收到重复通知?
|
||
**A:** 检查是否多次调用了 Hook,确保只在需要的地方订阅一次。
|
||
|
||
---
|
||
|
||
## 📚 更多资源
|
||
|
||
- Socket.IO 文档: https://socket.io/docs/v4/
|
||
- Chakra UI Toast: https://chakra-ui.com/docs/components/toast
|
||
- React Hooks: https://react.dev/reference/react
|
||
|
||
---
|
||
|
||
**完成!🎉** 现在你的前端可以实时接收事件推送了!
|