From e9c9f1ba7ca5e1ba09c8e6279788ec04a2bfa15b Mon Sep 17 00:00:00 2001 From: zdl <3489966805@qq.com> Date: Mon, 29 Dec 2025 17:31:47 +0800 Subject: [PATCH] =?UTF-8?q?feat(FlexScreen):=20Mock=20=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E4=B8=8B=E4=BD=BF=E7=94=A8=E6=9C=AC=E5=9C=B0=20Mock=20?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=9B=BF=E4=BB=A3=20WebSocket?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - constants.ts: 添加 IS_MOCK_MODE 常量,Mock 模式下跳过 WebSocket 连接 - useRealtimeQuote.ts: Mock 模式下生成本地行情数据并每 3 秒模拟更新 - 解决 Mock 模式下 WebSocket 连接失败的报错问题 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../components/FlexScreen/hooks/constants.ts | 12 +++ .../FlexScreen/hooks/useRealtimeQuote.ts | 87 ++++++++++++++++++- 2 files changed, 98 insertions(+), 1 deletion(-) diff --git a/src/views/StockOverview/components/FlexScreen/hooks/constants.ts b/src/views/StockOverview/components/FlexScreen/hooks/constants.ts index a0b1335d..1de70b9a 100644 --- a/src/views/StockOverview/components/FlexScreen/hooks/constants.ts +++ b/src/views/StockOverview/components/FlexScreen/hooks/constants.ts @@ -5,12 +5,24 @@ import type { Exchange } from '../types'; import { getApiBase } from '@utils/apiConfig'; +/** 是否为 Mock 模式 */ +export const IS_MOCK_MODE = process.env.REACT_APP_ENABLE_MOCK === 'true'; + /** * 获取 WebSocket 配置 + * - Mock 模式: 返回空字符串,不连接 WebSocket * - 生产环境 (HTTPS): 通过 API 服务器 Nginx 代理使用 wss:// * - 开发环境 (HTTP): 直连 ws:// */ const getWsConfig = (): Record => { + // Mock 模式:不连接 WebSocket + if (IS_MOCK_MODE) { + return { + SSE: '', + SZSE: '', + }; + } + // 服务端渲染或测试环境使用默认配置 if (typeof window === 'undefined') { return { diff --git a/src/views/StockOverview/components/FlexScreen/hooks/useRealtimeQuote.ts b/src/views/StockOverview/components/FlexScreen/hooks/useRealtimeQuote.ts index f4a6988f..ab46cd98 100644 --- a/src/views/StockOverview/components/FlexScreen/hooks/useRealtimeQuote.ts +++ b/src/views/StockOverview/components/FlexScreen/hooks/useRealtimeQuote.ts @@ -15,7 +15,7 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import { logger } from '@utils/logger'; import { getApiBase } from '@utils/apiConfig'; -import { WS_CONFIG, HEARTBEAT_INTERVAL, RECONNECT_INTERVAL } from './constants'; +import { WS_CONFIG, HEARTBEAT_INTERVAL, RECONNECT_INTERVAL, IS_MOCK_MODE } from './constants'; import { getExchange, normalizeCode, calcChangePct } from './utils'; import type { Exchange, @@ -38,6 +38,43 @@ import type { /** 最大重连次数 */ const MAX_RECONNECT_ATTEMPTS = 5; +/** + * 生成 Mock 行情数据 + */ +const generateMockQuote = (code: string): QuoteData => { + const exchange: Exchange = code.endsWith('.SH') ? 'SSE' : 'SZSE'; + const basePrice = 10 + Math.random() * 90; // 10-100 之间的随机价格 + const prevClose = basePrice * (0.95 + Math.random() * 0.1); // 前收盘价在基准价格附近 + const change = basePrice - prevClose; + const changePct = (change / prevClose) * 100; + + // 生成五档买卖盘 + const bidPrices = Array.from({ length: 5 }, (_, i) => +(basePrice - 0.01 * (i + 1)).toFixed(2)); + const askPrices = Array.from({ length: 5 }, (_, i) => +(basePrice + 0.01 * (i + 1)).toFixed(2)); + const bidVolumes = Array.from({ length: 5 }, () => Math.floor(Math.random() * 10000) * 100); + const askVolumes = Array.from({ length: 5 }, () => Math.floor(Math.random() * 10000) * 100); + + return { + code, + name: `Mock股票${code.slice(0, 4)}`, + price: +basePrice.toFixed(2), + prevClose: +prevClose.toFixed(2), + open: +(prevClose * (0.99 + Math.random() * 0.02)).toFixed(2), + high: +(basePrice * (1 + Math.random() * 0.05)).toFixed(2), + low: +(basePrice * (1 - Math.random() * 0.05)).toFixed(2), + volume: Math.floor(Math.random() * 100000000), + amount: Math.floor(Math.random() * 1000000000), + change: +change.toFixed(2), + changePct: +changePct.toFixed(2), + bidPrices, + bidVolumes, + askPrices, + askVolumes, + updateTime: new Date().toISOString(), + exchange, + } as QuoteData; +}; + /** * 处理上交所消息 */ @@ -564,8 +601,21 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn = * 创建 WebSocket 连接 */ const createConnection = useCallback((exchange: Exchange) => { + // Mock 模式:跳过 WebSocket 连接 + if (IS_MOCK_MODE) { + logger.info('FlexScreen', `${exchange} Mock 模式,跳过 WebSocket 连接`); + return; + } + const isHttps = typeof window !== 'undefined' && window.location.protocol === 'https:'; const wsUrl = WS_CONFIG[exchange]; + + // WebSocket URL 为空时跳过 + if (!wsUrl) { + logger.info('FlexScreen', `${exchange} WebSocket URL 为空,跳过连接`); + return; + } + const isInsecureWs = wsUrl.startsWith('ws://'); if (isHttps && isInsecureWs) { @@ -736,6 +786,41 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn = const allNewCodes = [...newSseCodes, ...newSzseCodes]; + // Mock 模式:生成 Mock 数据并定时更新 + if (IS_MOCK_MODE) { + logger.info('FlexScreen', 'Mock 模式,使用本地 Mock 数据'); + + // 生成初始 Mock 数据 + const mockQuotes: QuotesMap = {}; + allNewCodes.forEach(code => { + mockQuotes[code] = generateMockQuote(code); + }); + setQuotes(mockQuotes); + + // 定时更新 Mock 数据(模拟实时行情) + const mockInterval = setInterval(() => { + setQuotes(prev => { + const updated = { ...prev }; + Object.keys(updated).forEach(code => { + const quote = updated[code]; + // 小幅波动价格 + const priceChange = (Math.random() - 0.5) * 0.1; + const newPrice = +(quote.price + priceChange).toFixed(2); + updated[code] = { + ...quote, + price: newPrice, + change: +(newPrice - quote.prevClose).toFixed(2), + changePct: +((newPrice - quote.prevClose) / quote.prevClose * 100).toFixed(2), + updateTime: new Date().toISOString(), + }; + }); + return updated; + }); + }, 3000); // 每 3 秒更新一次 + + return () => clearInterval(mockInterval); + } + // 检查是否有新增的代码需要加载初始数据 const codesToLoad = allNewCodes.filter(c => !initialLoadedRef.current.has(c));