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:
zdl
2025-12-29 17:31:47 +08:00
parent e75d363ab1
commit e9c9f1ba7c
2 changed files with 98 additions and 1 deletions

View File

@@ -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 {

View File

@@ -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));