Files
vf_react/docs/WEBSOCKET_INTEGRATION_GUIDE.md
zdl 09db05c448 docs: 将所有文档迁移到 docs/ 目录
- 移动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>
2025-10-30 14:51:22 +08:00

12 KiB
Raw Permalink Blame History

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 组件中使用:

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 中集成:

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 级)

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

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

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. 顶部通知栏

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. 角标提示(红点)

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. 浮动通知卡片

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

// 连接
socketService.connect(options)

// 断开
socketService.disconnect()

// 订阅所有事件
socketService.subscribeToAllEvents(callback)

// 订阅特定类型
socketService.subscribeToEventType('tech', callback)

// 订阅特定重要性
socketService.subscribeToImportantEvents('S', callback)

// 取消订阅
socketService.unsubscribeFromEvents({ eventType: 'all' })

// 检查连接状态
socketService.isConnected()

🔧 事件数据结构

收到的 event 对象包含:

{
  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. 条件订阅(用户设置)

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. 多个订阅(不同类型)

function MultiSubscriptionExample() {
  // 订阅科技类事件
  useEventNotifications({
    eventType: 'tech',
    onNewEvent: (event) => console.log('科技事件:', event)
  });

  // 订阅政策类事件
  useEventNotifications({
    eventType: 'policy',
    onNewEvent: (event) => console.log('政策事件:', event)
  });

  return <div>...</div>;
}

3. 防抖处理(避免通知过多)

import { debounce } from 'lodash';

const debouncedNotify = debounce((event) => {
  toast({
    title: '新事件',
    description: event.title,
  });
}, 1000);

useEventNotifications({
  eventType: 'all',
  onNewEvent: debouncedNotify
});

🧪 测试步骤

  1. 启动 Flask 服务

    python app.py
    
  2. 启动 React 应用

    npm start
    
  3. 创建测试事件

    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确保只在需要的地方订阅一次。


📚 更多资源


完成!🎉 现在你的前端可以实时接收事件推送了!