feat(FlexScreen): Mock 模式下使用本地 Mock 数据替代 WebSocket
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -5,12 +5,24 @@
|
|||||||
import type { Exchange } from '../types';
|
import type { Exchange } from '../types';
|
||||||
import { getApiBase } from '@utils/apiConfig';
|
import { getApiBase } from '@utils/apiConfig';
|
||||||
|
|
||||||
|
/** 是否为 Mock 模式 */
|
||||||
|
export const IS_MOCK_MODE = process.env.REACT_APP_ENABLE_MOCK === 'true';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取 WebSocket 配置
|
* 获取 WebSocket 配置
|
||||||
|
* - Mock 模式: 返回空字符串,不连接 WebSocket
|
||||||
* - 生产环境 (HTTPS): 通过 API 服务器 Nginx 代理使用 wss://
|
* - 生产环境 (HTTPS): 通过 API 服务器 Nginx 代理使用 wss://
|
||||||
* - 开发环境 (HTTP): 直连 ws://
|
* - 开发环境 (HTTP): 直连 ws://
|
||||||
*/
|
*/
|
||||||
const getWsConfig = (): Record<Exchange, string> => {
|
const getWsConfig = (): Record<Exchange, string> => {
|
||||||
|
// Mock 模式:不连接 WebSocket
|
||||||
|
if (IS_MOCK_MODE) {
|
||||||
|
return {
|
||||||
|
SSE: '',
|
||||||
|
SZSE: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// 服务端渲染或测试环境使用默认配置
|
// 服务端渲染或测试环境使用默认配置
|
||||||
if (typeof window === 'undefined') {
|
if (typeof window === 'undefined') {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||||
import { logger } from '@utils/logger';
|
import { logger } from '@utils/logger';
|
||||||
import { getApiBase } from '@utils/apiConfig';
|
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 { getExchange, normalizeCode, calcChangePct } from './utils';
|
||||||
import type {
|
import type {
|
||||||
Exchange,
|
Exchange,
|
||||||
@@ -38,6 +38,43 @@ import type {
|
|||||||
/** 最大重连次数 */
|
/** 最大重连次数 */
|
||||||
const MAX_RECONNECT_ATTEMPTS = 5;
|
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 连接
|
* 创建 WebSocket 连接
|
||||||
*/
|
*/
|
||||||
const createConnection = useCallback((exchange: Exchange) => {
|
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 isHttps = typeof window !== 'undefined' && window.location.protocol === 'https:';
|
||||||
const wsUrl = WS_CONFIG[exchange];
|
const wsUrl = WS_CONFIG[exchange];
|
||||||
|
|
||||||
|
// WebSocket URL 为空时跳过
|
||||||
|
if (!wsUrl) {
|
||||||
|
logger.info('FlexScreen', `${exchange} WebSocket URL 为空,跳过连接`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const isInsecureWs = wsUrl.startsWith('ws://');
|
const isInsecureWs = wsUrl.startsWith('ws://');
|
||||||
|
|
||||||
if (isHttps && isInsecureWs) {
|
if (isHttps && isInsecureWs) {
|
||||||
@@ -736,6 +786,41 @@ export const useRealtimeQuote = (codes: string[] = []): UseRealtimeQuoteReturn =
|
|||||||
|
|
||||||
const allNewCodes = [...newSseCodes, ...newSzseCodes];
|
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));
|
const codesToLoad = allNewCodes.filter(c => !initialLoadedRef.current.has(c));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user