Files
vf_react/src/mocks/handlers/limitAnalyse.js
zdl 573fa409e3 fix(Mock): 修复概念中心 mock 数据并扩充层级结构
修复:
- latest_date → latest_trade_date(与前端字段名一致)
- 日期格式使用 YYYY-MM-DD 确保 Date 解析正确

扩充:
- 新增 /concept-api/ 路径的 MSW handler(代理兼容)
- 层级结构数据从 8 个一级分类扩充到 15 个
- 添加更丰富的二级/三级概念数据
- 新增 limitAnalyse mock handler

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-16 13:14:16 +08:00

575 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// src/mocks/handlers/limitAnalyse.js
// 涨停分析相关的 Mock Handlers
import { http, HttpResponse } from 'msw';
// 模拟延迟
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
// 生成可用日期列表最近30个交易日
const generateAvailableDates = () => {
const dates = [];
const today = new Date();
let count = 0;
for (let i = 0; i < 60 && count < 30; i++) {
const date = new Date(today);
date.setDate(date.getDate() - i);
const dayOfWeek = date.getDay();
// 跳过周末
if (dayOfWeek !== 0 && dayOfWeek !== 6) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const dateStr = `${year}${month}${day}`;
// 返回包含 date 和 count 字段的对象
dates.push({
date: dateStr,
count: Math.floor(Math.random() * 80) + 30 // 30-110 只涨停股票
});
count++;
}
}
return dates;
};
// 生成板块数据
const generateSectors = (count = 8) => {
const sectorNames = [
'人工智能', 'ChatGPT', '数字经济',
'新能源汽车', '光伏', '锂电池',
'半导体', '芯片', '5G通信',
'医疗器械', '创新药', '中药',
'白酒', '食品饮料', '消费电子',
'军工', '航空航天', '新材料'
];
const sectors = [];
for (let i = 0; i < Math.min(count, sectorNames.length); i++) {
const stockCount = Math.floor(Math.random() * 15) + 5;
const stocks = [];
for (let j = 0; j < stockCount; j++) {
stocks.push({
code: `${Math.random() > 0.5 ? '6' : '0'}${String(Math.floor(Math.random() * 100000)).padStart(5, '0')}`,
name: `${sectorNames[i]}股票${j + 1}`,
latest_limit_time: `${Math.floor(Math.random() * 4) + 9}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')}`,
limit_up_count: Math.floor(Math.random() * 3) + 1,
price: (Math.random() * 100 + 10).toFixed(2),
change_pct: (Math.random() * 5 + 5).toFixed(2),
turnover_rate: (Math.random() * 30 + 5).toFixed(2),
volume: Math.floor(Math.random() * 100000000 + 10000000),
amount: (Math.random() * 1000000000 + 100000000).toFixed(2),
limit_type: Math.random() > 0.7 ? '一字板' : (Math.random() > 0.5 ? 'T字板' : '普通涨停'),
封单金额: (Math.random() * 500000000).toFixed(2),
});
}
sectors.push({
sector_name: sectorNames[i],
stock_count: stockCount,
avg_limit_time: `${Math.floor(Math.random() * 2) + 10}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')}`,
stocks: stocks,
});
}
return sectors;
};
// 生成高位股数据(用于 HighPositionStocks 组件)
const generateHighPositionStocks = () => {
const stocks = [];
const stockNames = [
'宁德时代', '比亚迪', '隆基绿能', '东方财富', '中际旭创',
'京东方A', '海康威视', '立讯精密', '三一重工', '恒瑞医药',
'三六零', '东方通信', '贵州茅台', '五粮液', '中国平安'
];
const industries = [
'锂电池', '新能源汽车', '光伏', '金融科技', '通信设备',
'显示器件', '安防设备', '电子元件', '工程机械', '医药制造',
'网络安全', '通信服务', '白酒', '食品饮料', '保险'
];
for (let i = 0; i < stockNames.length; i++) {
const code = `${Math.random() > 0.5 ? '6' : '0'}${String(Math.floor(Math.random() * 100000)).padStart(5, '0')}`;
const continuousDays = Math.floor(Math.random() * 8) + 2; // 2-9连板
const price = parseFloat((Math.random() * 100 + 20).toFixed(2));
const increaseRate = parseFloat((Math.random() * 3 + 8).toFixed(2)); // 8%-11%
const turnoverRate = parseFloat((Math.random() * 20 + 5).toFixed(2)); // 5%-25%
stocks.push({
stock_code: code,
stock_name: stockNames[i],
price: price,
increase_rate: increaseRate,
continuous_limit_up: continuousDays,
industry: industries[i],
turnover_rate: turnoverRate,
});
}
// 按连板天数降序排序
stocks.sort((a, b) => b.continuous_limit_up - a.continuous_limit_up);
return stocks;
};
// 生成高位股统计数据
const generateHighPositionStatistics = (stocks) => {
if (!stocks || stocks.length === 0) {
return {
total_count: 0,
avg_continuous_days: 0,
max_continuous_days: 0,
};
}
const totalCount = stocks.length;
const sumDays = stocks.reduce((sum, stock) => sum + stock.continuous_limit_up, 0);
const maxDays = Math.max(...stocks.map(s => s.continuous_limit_up));
return {
total_count: totalCount,
avg_continuous_days: parseFloat((sumDays / totalCount).toFixed(1)),
max_continuous_days: maxDays,
};
};
// 生成词云数据
const generateWordCloudData = () => {
const keywords = [
'人工智能', 'ChatGPT', 'AI芯片', '大模型', '算力',
'新能源', '光伏', '锂电池', '储能', '充电桩',
'半导体', '芯片', 'EDA', '国产替代', '集成电路',
'医疗', '创新药', 'CXO', '医疗器械', '生物医药',
'消费', '白酒', '食品', '零售', '餐饮',
'金融', '券商', '保险', '银行', '金融科技'
];
return keywords.map(keyword => ({
text: keyword,
value: Math.floor(Math.random() * 50) + 10,
category: ['科技', '新能源', '医疗', '消费', '金融'][Math.floor(Math.random() * 5)],
}));
};
// 生成每日分析数据
const generateDailyAnalysis = (date) => {
const sectorNames = [
'公告', '人工智能', 'ChatGPT', '数字经济',
'新能源汽车', '光伏', '锂电池',
'半导体', '芯片', '5G通信',
'医疗器械', '创新药', '其他'
];
const stockNameTemplates = [
'龙头', '科技', '新能源', '智能', '数字', '云计算', '创新',
'生物', '医疗', '通信', '电子', '材料', '能源', '互联'
];
// 生成 sector_dataSectorDetails 组件需要的格式)
const sectorData = {};
let totalStocks = 0;
sectorNames.forEach((sectorName, sectorIdx) => {
const stockCount = Math.floor(Math.random() * 12) + 3; // 每个板块 3-15 只股票
const stocks = [];
for (let i = 0; i < stockCount; i++) {
const code = `${Math.random() > 0.5 ? '6' : '0'}${String(Math.floor(Math.random() * 100000)).padStart(5, '0')}`;
const continuousDays = Math.floor(Math.random() * 6) + 1; // 1-6连板
const ztHour = Math.floor(Math.random() * 5) + 9; // 9-13点
const ztMinute = Math.floor(Math.random() * 60);
const ztSecond = Math.floor(Math.random() * 60);
const ztTime = `2024-10-28 ${String(ztHour).padStart(2, '0')}:${String(ztMinute).padStart(2, '0')}:${String(ztSecond).padStart(2, '0')}`;
const stockName = `${stockNameTemplates[i % stockNameTemplates.length]}${sectorName === '公告' ? '公告' : ''}股份${i + 1}`;
stocks.push({
scode: code,
sname: stockName,
zt_time: ztTime,
formatted_time: `${String(ztHour).padStart(2, '0')}:${String(ztMinute).padStart(2, '0')}`,
continuous_days: continuousDays === 1 ? '首板' : `${continuousDays}连板`,
brief: `${sectorName}板块异动,${stockName}${sectorName === '公告' ? '重大公告利好' : '板块热点'}涨停。公司是${sectorName}行业龙头企业之一。`,
summary: `${sectorName}概念持续活跃`,
first_time: `2024-10-${String(28 - (continuousDays - 1)).padStart(2, '0')}`,
change_pct: parseFloat((Math.random() * 2 + 9).toFixed(2)), // 9%-11%
core_sectors: [
sectorName,
sectorNames[Math.floor(Math.random() * sectorNames.length)],
sectorNames[Math.floor(Math.random() * sectorNames.length)]
].filter((v, i, a) => a.indexOf(v) === i) // 去重
});
}
sectorData[sectorName] = {
count: stockCount,
stocks: stocks.sort((a, b) => a.zt_time.localeCompare(b.zt_time)) // 按涨停时间排序
};
totalStocks += stockCount;
});
// 统计数据
const morningCount = Math.floor(totalStocks * 0.35); // 早盘涨停
const announcementCount = sectorData['公告']?.count || 0;
const topSector = sectorNames.filter(s => s !== '公告' && s !== '其他')
.reduce((max, name) =>
(sectorData[name]?.count || 0) > (sectorData[max]?.count || 0) ? name : max
, '人工智能');
return {
date: date,
total_stocks: totalStocks,
total_sectors: Object.keys(sectorData).length,
sector_data: sectorData, // 👈 SectorDetails 组件需要的数据
summary: {
top_sector: topSector,
top_sector_count: sectorData[topSector]?.count || 0,
announcement_stocks: announcementCount,
zt_time_distribution: {
morning: morningCount,
afternoon: totalStocks - morningCount,
}
}
};
};
// ==================== 静态文件 Mock Handlers ====================
// 这些 handlers 用于拦截 /data/zt/* 静态文件请求
// 生成 dates.json 数据
const generateDatesJson = () => {
const dates = [];
const today = new Date();
for (let i = 0; i < 60; i++) {
const date = new Date(today);
date.setDate(date.getDate() - i);
const dayOfWeek = date.getDay();
// 跳过周末
if (dayOfWeek !== 0 && dayOfWeek !== 6) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
dates.push({
date: `${year}${month}${day}`,
formatted_date: `${year}-${month}-${day}`,
count: Math.floor(Math.random() * 60) + 40 // 40-100 只涨停股票
});
if (dates.length >= 30) break;
}
}
return { dates };
};
// 生成每日分析 JSON 数据(用于 /data/zt/daily/${date}.json
const generateDailyJson = (date) => {
// 板块名称列表
const sectorNames = [
'公告', '人工智能', 'ChatGPT', '大模型', '算力',
'光伏', '新能源汽车', '锂电池', '储能', '充电桩',
'半导体', '芯片', '集成电路', '国产替代',
'医药', '创新药', 'CXO', '医疗器械',
'军工', '航空航天', '其他'
];
// 股票名称模板
const stockPrefixes = [
'龙头', '科技', '新能', '智能', '数字', '云计', '创新',
'生物', '医疗', '通信', '电子', '材料', '能源', '互联',
'天马', '华鑫', '中科', '东方', '西部', '南方', '北方',
'金龙', '银河', '星辰', '宏达', '盛世', '鹏程', '万里'
];
const stockSuffixes = [
'股份', '科技', '电子', '信息', '新材', '能源', '医药',
'通讯', '智造', '集团', '实业', '控股', '产业', '发展'
];
// 生成所有股票
const stocks = [];
const sectorData = {};
let stockIndex = 0;
sectorNames.forEach((sectorName, sectorIdx) => {
const stockCount = sectorName === '公告'
? Math.floor(Math.random() * 5) + 8 // 公告板块 8-12 只
: sectorName === '其他'
? Math.floor(Math.random() * 4) + 3 // 其他板块 3-6 只
: Math.floor(Math.random() * 8) + 3; // 普通板块 3-10 只
const sectorStockCodes = [];
for (let i = 0; i < stockCount; i++) {
const code = `${Math.random() > 0.6 ? '6' : Math.random() > 0.3 ? '0' : '3'}${String(Math.floor(Math.random() * 100000)).padStart(5, '0')}`;
const continuousDays = Math.floor(Math.random() * 6) + 1;
const ztHour = Math.floor(Math.random() * 4) + 9;
const ztMinute = Math.floor(Math.random() * 60);
const ztSecond = Math.floor(Math.random() * 60);
const prefix = stockPrefixes[Math.floor(Math.random() * stockPrefixes.length)];
const suffix = stockSuffixes[Math.floor(Math.random() * stockSuffixes.length)];
const stockName = `${prefix}${suffix}`;
// 生成关联板块
const coreSectors = [sectorName];
if (Math.random() > 0.5) {
const otherSector = sectorNames[Math.floor(Math.random() * (sectorNames.length - 1))];
if (otherSector !== sectorName && otherSector !== '其他' && otherSector !== '公告') {
coreSectors.push(otherSector);
}
}
stocks.push({
scode: code,
sname: stockName,
zt_time: `${date.slice(0,4)}-${date.slice(4,6)}-${date.slice(6,8)} ${String(ztHour).padStart(2,'0')}:${String(ztMinute).padStart(2,'0')}:${String(ztSecond).padStart(2,'0')}`,
formatted_time: `${String(ztHour).padStart(2,'0')}:${String(ztMinute).padStart(2,'0')}`,
continuous_days: continuousDays === 1 ? '首板' : `${continuousDays}连板`,
brief: sectorName === '公告'
? `${stockName}发布重大公告,公司拟收购资产/重组/增发等利好消息。`
: `${sectorName}板块异动,${stockName}因板块热点涨停。公司是${sectorName}行业核心标的。`,
summary: `${sectorName}概念活跃`,
first_time: `${date.slice(0,4)}-${date.slice(4,6)}-${String(parseInt(date.slice(6,8)) - (continuousDays - 1)).padStart(2,'0')}`,
change_pct: parseFloat((Math.random() * 1.5 + 9.5).toFixed(2)),
core_sectors: coreSectors
});
sectorStockCodes.push(code);
stockIndex++;
}
sectorData[sectorName] = {
count: stockCount,
stock_codes: sectorStockCodes
};
});
// 生成词频数据
const wordFreqData = [
{ name: '人工智能', value: Math.floor(Math.random() * 30) + 20 },
{ name: 'ChatGPT', value: Math.floor(Math.random() * 25) + 15 },
{ name: '大模型', value: Math.floor(Math.random() * 20) + 12 },
{ name: '算力', value: Math.floor(Math.random() * 18) + 10 },
{ name: '光伏', value: Math.floor(Math.random() * 15) + 10 },
{ name: '新能源', value: Math.floor(Math.random() * 15) + 8 },
{ name: '锂电池', value: Math.floor(Math.random() * 12) + 8 },
{ name: '储能', value: Math.floor(Math.random() * 12) + 6 },
{ name: '半导体', value: Math.floor(Math.random() * 15) + 10 },
{ name: '芯片', value: Math.floor(Math.random() * 15) + 8 },
{ name: '集成电路', value: Math.floor(Math.random() * 10) + 5 },
{ name: '国产替代', value: Math.floor(Math.random() * 10) + 5 },
{ name: '医药', value: Math.floor(Math.random() * 12) + 6 },
{ name: '创新药', value: Math.floor(Math.random() * 10) + 5 },
{ name: '医疗器械', value: Math.floor(Math.random() * 8) + 4 },
{ name: '军工', value: Math.floor(Math.random() * 10) + 5 },
{ name: '航空航天', value: Math.floor(Math.random() * 8) + 4 },
{ name: '数字经济', value: Math.floor(Math.random() * 12) + 6 },
{ name: '工业4.0', value: Math.floor(Math.random() * 8) + 4 },
{ name: '机器人', value: Math.floor(Math.random() * 10) + 5 },
{ name: '自动驾驶', value: Math.floor(Math.random() * 8) + 4 },
{ name: '元宇宙', value: Math.floor(Math.random() * 6) + 3 },
{ name: 'Web3.0', value: Math.floor(Math.random() * 5) + 2 },
{ name: '区块链', value: Math.floor(Math.random() * 5) + 2 },
];
return {
date: date,
total_stocks: stocks.length,
total_sectors: Object.keys(sectorData).length,
stocks: stocks,
sector_data: sectorData,
word_freq_data: wordFreqData,
summary: {
top_sector: '人工智能',
top_sector_count: sectorData['人工智能']?.count || 0,
announcement_stocks: sectorData['公告']?.count || 0,
zt_time_distribution: {
morning: Math.floor(stocks.length * 0.4),
afternoon: Math.floor(stocks.length * 0.6),
}
}
};
};
// 生成 stocks.jsonl 数据
const generateStocksJsonl = () => {
const stocks = [];
const today = new Date();
// 生成 200 只历史涨停股票记录
for (let i = 0; i < 200; i++) {
const daysAgo = Math.floor(Math.random() * 30);
const date = new Date(today);
date.setDate(date.getDate() - daysAgo);
// 跳过周末
while (date.getDay() === 0 || date.getDay() === 6) {
date.setDate(date.getDate() - 1);
}
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
stocks.push({
scode: `${Math.random() > 0.6 ? '6' : Math.random() > 0.3 ? '0' : '3'}${String(Math.floor(Math.random() * 100000)).padStart(5, '0')}`,
sname: ['龙头', '科技', '新能', '智能', '数字', '云计'][Math.floor(Math.random() * 6)] +
['股份', '科技', '电子', '信息', '新材'][Math.floor(Math.random() * 5)],
date: `${year}${month}${day}`,
formatted_date: `${year}-${month}-${day}`,
continuous_days: Math.floor(Math.random() * 5) + 1,
core_sectors: [['人工智能', 'ChatGPT', '光伏', '锂电池', '芯片'][Math.floor(Math.random() * 5)]]
});
}
return stocks;
};
// Mock Handlers
export const limitAnalyseHandlers = [
// ==================== 静态文件路径 Handlers ====================
// 1. /data/zt/dates.json - 可用日期列表
http.get('/data/zt/dates.json', async () => {
await delay(200);
console.log('[Mock LimitAnalyse] 获取 dates.json');
const data = generateDatesJson();
return HttpResponse.json(data);
}),
// 2. /data/zt/daily/:date.json - 每日分析数据
http.get('/data/zt/daily/:date', async ({ params }) => {
await delay(300);
// 移除 .json 后缀和查询参数
const dateParam = params.date.replace('.json', '').split('?')[0];
console.log('[Mock LimitAnalyse] 获取每日数据:', dateParam);
const data = generateDailyJson(dateParam);
return HttpResponse.json(data);
}),
// 3. /data/zt/stocks.jsonl - 股票列表(用于搜索)
http.get('/data/zt/stocks.jsonl', async () => {
await delay(200);
console.log('[Mock LimitAnalyse] 获取 stocks.jsonl');
const stocks = generateStocksJsonl();
// JSONL 格式:每行一个 JSON
const jsonl = stocks.map(s => JSON.stringify(s)).join('\n');
return new HttpResponse(jsonl, {
headers: { 'Content-Type': 'text/plain' }
});
}),
// ==================== API 路径 Handlers (兼容旧版本) ====================
// 1. 获取可用日期列表
http.get('http://111.198.58.126:5001/api/v1/dates/available', async () => {
await delay(300);
const availableDates = generateAvailableDates();
return HttpResponse.json({
success: true,
events: availableDates,
message: '可用日期列表获取成功',
});
}),
// 2. 获取每日分析数据
http.get('http://111.198.58.126:5001/api/v1/analysis/daily/:date', async ({ params }) => {
await delay(500);
const { date } = params;
const data = generateDailyAnalysis(date);
return HttpResponse.json({
success: true,
data: data,
message: `${date} 每日分析数据获取成功`,
});
}),
// 3. 获取词云数据
http.get('http://111.198.58.126:5001/api/v1/analysis/wordcloud/:date', async ({ params }) => {
await delay(300);
const { date } = params;
const wordCloudData = generateWordCloudData();
return HttpResponse.json({
success: true,
data: wordCloudData,
message: `${date} 词云数据获取成功`,
});
}),
// 4. 混合搜索POST
http.post('http://111.198.58.126:5001/api/v1/stocks/search/hybrid', async ({ request }) => {
await delay(400);
const body = await request.json();
const { query, type = 'all', mode = 'hybrid' } = body;
// 生成模拟搜索结果
const results = [];
const count = Math.floor(Math.random() * 10) + 5;
for (let i = 0; i < count; i++) {
results.push({
code: `${Math.random() > 0.5 ? '6' : '0'}${String(Math.floor(Math.random() * 100000)).padStart(5, '0')}`,
name: `${query || '搜索'}相关股票${i + 1}`,
sector: ['人工智能', 'ChatGPT', '新能源'][Math.floor(Math.random() * 3)],
limit_date: new Date().toISOString().split('T')[0].replace(/-/g, ''),
limit_time: `${Math.floor(Math.random() * 4) + 9}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')}`,
price: (Math.random() * 100 + 10).toFixed(2),
change_pct: (Math.random() * 10).toFixed(2),
match_score: (Math.random() * 0.5 + 0.5).toFixed(2),
});
}
return HttpResponse.json({
success: true,
data: {
query: query,
type: type,
mode: mode,
results: results,
total: results.length,
},
message: '搜索完成',
});
}),
// 5. 获取高位股列表(涨停股票列表)
http.get('http://111.198.58.126:5001/api/limit-analyse/high-position-stocks', async ({ request }) => {
await delay(400);
const url = new URL(request.url);
const date = url.searchParams.get('date');
console.log('[Mock LimitAnalyse] 获取高位股列表:', { date });
const stocks = generateHighPositionStocks();
const statistics = generateHighPositionStatistics(stocks);
return HttpResponse.json({
success: true,
data: {
stocks: stocks,
statistics: statistics,
date: date,
},
message: '高位股数据获取成功',
});
}),
];