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 { 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<Exchange, string> => {
|
||||
// Mock 模式:不连接 WebSocket
|
||||
if (IS_MOCK_MODE) {
|
||||
return {
|
||||
SSE: '',
|
||||
SZSE: '',
|
||||
};
|
||||
}
|
||||
|
||||
// 服务端渲染或测试环境使用默认配置
|
||||
if (typeof window === 'undefined') {
|
||||
return {
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user