Files
vf_react/src/mocks/handlers/concept.js
2025-11-03 17:31:25 +08:00

526 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/concept.js
// 概念相关的 Mock Handlers
import { http, HttpResponse } from 'msw';
// 模拟延迟
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
// 生成历史触发时间3-5个历史日期
const generateHappenedTimes = (seed) => {
const times = [];
const count = 3 + (seed % 3); // 3-5个时间点
for (let i = 0; i < count; i++) {
const daysAgo = 30 + (seed * 7 + i * 11) % 330; // 30-360天前
const date = new Date();
date.setDate(date.getDate() - daysAgo);
times.push(date.toISOString().split('T')[0]);
}
return times.sort().reverse(); // 降序排列
};
// 生成核心相关股票
const generateStocksForConcept = (seed, count = 4) => {
const stockPool = [
{ name: '贵州茅台', code: '600519' },
{ name: '宁德时代', code: '300750' },
{ name: '中国平安', code: '601318' },
{ name: '比亚迪', code: '002594' },
{ name: '隆基绿能', code: '601012' },
{ name: '阳光电源', code: '300274' },
{ name: '三一重工', code: '600031' },
{ name: '中芯国际', code: '688981' },
{ name: '京东方A', code: '000725' },
{ name: '立讯精密', code: '002475' }
];
const stocks = [];
for (let i = 0; i < count; i++) {
const stockIndex = (seed + i * 7) % stockPool.length;
const stock = stockPool[stockIndex];
stocks.push({
stock_name: stock.name,
stock_code: stock.code,
reason: `作为行业龙头企业,${stock.name}在该领域具有核心竞争优势,市场份额领先,技术实力雄厚。`,
change_pct: parseFloat((Math.random() * 15 - 5).toFixed(2)) // -5% ~ +10%
});
}
return stocks;
};
// 生成热门概念数据
export const generatePopularConcepts = (size = 20) => {
const concepts = [
'人工智能', '新能源汽车', '半导体', '光伏', '锂电池',
'储能', '氢能源', '风电', '特高压', '工业母机',
'军工', '航空航天', '卫星导航', '量子科技', '数字货币',
'云计算', '大数据', '物联网', '5G', '6G',
'元宇宙', '虚拟现实', 'AIGC', 'ChatGPT', '算力',
'芯片设计', '芯片制造', '半导体设备', '半导体材料', 'EDA',
'新能源', '风光储', '充电桩', '智能电网', '特斯拉',
'比亚迪', '宁德时代', '华为', '苹果产业链', '鸿蒙',
'国产软件', '信创', '网络安全', '数据安全', '量子通信',
'医疗器械', '创新药', '医美', 'CXO', '生物医药',
'疫苗', '中药', '医疗信息化', '智慧医疗', '基因测序'
];
const conceptDescriptions = {
'人工智能': '人工智能是"技术突破+政策扶持"双轮驱动的硬科技主题。随着大模型技术的突破AI应用场景不断拓展预计将催化算力、数据、应用三大产业链。',
'新能源汽车': '新能源汽车行业景气度持续向好,渗透率不断提升。政策支持力度大,产业链上下游企业均受益明显。',
'半导体': '国产半导体替代加速,自主可控需求强烈。政策和资金支持力度大,行业迎来黄金发展期。',
'光伏': '光伏装机量快速增长,成本持续下降,行业景气度维持高位。双碳目标下,光伏行业前景广阔。',
'锂电池': '锂电池技术进步,成本优势扩大,下游应用领域持续扩张。新能源汽车和储能需求旺盛。',
'储能': '储能市场爆发式增长,政策支持力度大,应用场景不断拓展。未来市场空间巨大。',
'默认': '该概念市场关注度较高,具有一定的投资价值。相关企业技术实力雄厚,市场前景广阔。'
};
const matchTypes = ['hybrid_knn', 'keyword', 'semantic'];
const results = [];
for (let i = 0; i < Math.min(size, concepts.length); i++) {
const changePct = (Math.random() * 12 - 2).toFixed(2); // -2% 到 +10%
const stockCount = Math.floor(Math.random() * 50) + 10; // 10-60 只股票
const score = parseFloat((Math.random() * 5 + 3).toFixed(2)); // 3-8 分数范围
results.push({
concept: concepts[i],
concept_id: `CONCEPT_${1000 + i}`,
stock_count: stockCount,
score: score, // 相关度分数
match_type: matchTypes[i % 3], // 匹配类型
description: conceptDescriptions[concepts[i]] || conceptDescriptions['默认'],
price_info: {
avg_change_pct: parseFloat(changePct),
avg_price: parseFloat((Math.random() * 100 + 10).toFixed(2)),
total_market_cap: parseFloat((Math.random() * 1000 + 100).toFixed(2))
},
happened_times: generateHappenedTimes(i), // 历史触发时间
stocks: generateStocksForConcept(i, 4), // 核心相关股票
hot_score: Math.floor(Math.random() * 100)
});
}
// 按涨跌幅降序排序
results.sort((a, b) => b.price_info.avg_change_pct - a.price_info.avg_change_pct);
return results;
};
// 生成完整的概念统计数据(用于 ConceptStatsPanel
const generateConceptStats = () => {
// 热门概念涨幅最大的前5
const hot_concepts = [
{ name: '小米大模型', change_pct: 18.76, stock_count: 12, news_count: 35 },
{ name: '人工智能', change_pct: 15.67, stock_count: 45, news_count: 23 },
{ name: '新能源汽车', change_pct: 12.34, stock_count: 38, news_count: 18 },
{ name: '芯片概念', change_pct: 9.87, stock_count: 52, news_count: 31 },
{ name: '5G通信', change_pct: 8.45, stock_count: 29, news_count: 15 },
];
// 冷门概念跌幅最大的前5
const cold_concepts = [
{ name: '房地产', change_pct: -8.76, stock_count: 33, news_count: 12 },
{ name: '煤炭开采', change_pct: -6.54, stock_count: 25, news_count: 8 },
{ name: '传统零售', change_pct: -5.43, stock_count: 19, news_count: 6 },
{ name: '钢铁冶炼', change_pct: -4.21, stock_count: 28, news_count: 9 },
{ name: '纺织服装', change_pct: -3.98, stock_count: 15, news_count: 4 },
];
// 活跃概念(新闻+研报最多的前5
const active_concepts = [
{ name: '人工智能', news_count: 89, report_count: 15, total_mentions: 104 },
{ name: '芯片概念', news_count: 76, report_count: 12, total_mentions: 88 },
{ name: '新能源汽车', news_count: 65, report_count: 18, total_mentions: 83 },
{ name: '生物医药', news_count: 54, report_count: 9, total_mentions: 63 },
{ name: '量子科技', news_count: 41, report_count: 7, total_mentions: 48 },
];
// 波动最大的概念前5
const volatile_concepts = [
{ name: '区块链', volatility: 23.45, avg_change: 3.21, max_change: 12.34 },
{ name: '元宇宙', volatility: 21.87, avg_change: 2.98, max_change: 11.76 },
{ name: '虚拟现实', volatility: 19.65, avg_change: -1.23, max_change: 9.87 },
{ name: '游戏概念', volatility: 18.32, avg_change: 4.56, max_change: 10.45 },
{ name: '在线教育', volatility: 17.89, avg_change: -2.11, max_change: 8.76 },
];
// 动量概念连续上涨的前5
const momentum_concepts = [
{ name: '数字经济', consecutive_days: 5, total_change: 18.76, avg_daily: 3.75 },
{ name: '云计算', consecutive_days: 4, total_change: 14.32, avg_daily: 3.58 },
{ name: '物联网', consecutive_days: 4, total_change: 12.89, avg_daily: 3.22 },
{ name: '大数据', consecutive_days: 3, total_change: 11.45, avg_daily: 3.82 },
{ name: '工业互联网', consecutive_days: 3, total_change: 9.87, avg_daily: 3.29 },
];
return {
hot_concepts,
cold_concepts,
active_concepts,
volatile_concepts,
momentum_concepts
};
};
// 概念相关的 Handlers
export const conceptHandlers = [
// 搜索概念(热门概念)
http.post('/concept-api/search', async ({ request }) => {
await delay(300);
try {
const body = await request.json();
const { query = '', size = 20, page = 1, sort_by = 'change_pct' } = body;
console.log('[Mock Concept] 搜索概念:', { query, size, page, sort_by });
// 生成数据(不过滤,模拟真实 API 的语义搜索返回热门概念)
let results = generatePopularConcepts(size);
console.log('[Mock Concept] 生成概念数量:', results.length);
// Mock 环境下不做过滤,直接返回热门概念
// 真实环境会根据 query 进行语义搜索
// 根据排序字段排序
if (sort_by === 'change_pct') {
results.sort((a, b) => b.price_info.avg_change_pct - a.price_info.avg_change_pct);
} else if (sort_by === 'stock_count') {
results.sort((a, b) => b.stock_count - a.stock_count);
} else if (sort_by === 'hot_score') {
results.sort((a, b) => b.hot_score - a.hot_score);
}
return HttpResponse.json({
results,
total: results.length,
page,
size,
message: '搜索成功'
});
} catch (error) {
console.error('[Mock Concept] 搜索概念失败:', error);
return HttpResponse.json(
{
results: [],
total: 0,
error: '搜索失败'
},
{ status: 500 }
);
}
}),
// 获取单个概念详情
http.get('/concept-api/concepts/:conceptId', async ({ params }) => {
await delay(300);
const { conceptId } = params;
console.log('[Mock Concept] 获取概念详情:', conceptId);
const concepts = generatePopularConcepts(50);
const concept = concepts.find(c => c.concept_id === conceptId || c.concept === conceptId);
if (concept) {
return HttpResponse.json({
...concept,
related_stocks: [
{ stock_code: '600519', stock_name: '贵州茅台', change_pct: 2.34 },
{ stock_code: '000858', stock_name: '五粮液', change_pct: 1.89 },
{ stock_code: '000568', stock_name: '泸州老窖', change_pct: 3.12 }
],
news: [
{ title: `${concept.concept}板块异动`, date: '2024-10-24', source: '财经新闻' }
]
});
} else {
return HttpResponse.json(
{ error: '概念不存在' },
{ status: 404 }
);
}
}),
// 获取概念相关股票
http.get('/concept-api/concepts/:conceptId/stocks', async ({ params, request }) => {
await delay(300);
const { conceptId } = params;
const url = new URL(request.url);
const limit = parseInt(url.searchParams.get('limit') || '20');
console.log('[Mock Concept] 获取概念相关股票:', { conceptId, limit });
// 生成模拟股票数据
const stocks = [];
for (let i = 0; i < limit; i++) {
stocks.push({
stock_code: `${600000 + i}`,
stock_name: `股票${i + 1}`,
change_pct: (Math.random() * 10 - 2).toFixed(2),
price: (Math.random() * 100 + 10).toFixed(2),
market_cap: (Math.random() * 1000 + 100).toFixed(2)
});
}
return HttpResponse.json({
stocks,
total: stocks.length,
concept_id: conceptId
});
}),
// 获取最新交易日期
http.get('http://111.198.58.126:16801/price/latest', async () => {
await delay(200);
const today = new Date();
const dateStr = today.toISOString().split('T')[0].replace(/-/g, '');
console.log('[Mock Concept] 获取最新交易日期:', dateStr);
return HttpResponse.json({
latest_date: dateStr,
timestamp: today.toISOString()
});
}),
// 搜索概念(硬编码 URL
http.post('http://111.198.58.126:16801/search', async ({ request }) => {
await delay(300);
try {
const body = await request.json();
const { query = '', size = 20, page = 1, sort_by = 'change_pct' } = body;
console.log('[Mock Concept] 搜索概念 (硬编码URL):', { query, size, page, sort_by });
let results = generatePopularConcepts(size);
if (query) {
results = results.filter(item =>
item.concept.toLowerCase().includes(query.toLowerCase())
);
}
if (sort_by === 'change_pct') {
results.sort((a, b) => b.price_info.avg_change_pct - a.price_info.avg_change_pct);
} else if (sort_by === 'stock_count') {
results.sort((a, b) => b.stock_count - a.stock_count);
} else if (sort_by === 'hot_score') {
results.sort((a, b) => b.hot_score - a.hot_score);
}
return HttpResponse.json({
results,
total: results.length,
page,
size,
message: '搜索成功'
});
} catch (error) {
console.error('[Mock Concept] 搜索失败:', error);
return HttpResponse.json(
{ results: [], total: 0, error: '搜索失败' },
{ status: 500 }
);
}
}),
// 获取统计数据(直接访问外部 API
http.get('http://111.198.58.126:16801/statistics', async ({ request }) => {
await delay(300);
const url = new URL(request.url);
const minStockCount = parseInt(url.searchParams.get('min_stock_count') || '3');
const days = parseInt(url.searchParams.get('days') || '7');
const startDate = url.searchParams.get('start_date');
const endDate = url.searchParams.get('end_date');
console.log('[Mock Concept] 获取统计数据 (直接API):', { minStockCount, days, startDate, endDate });
// 生成完整的统计数据
const statsData = generateConceptStats();
return HttpResponse.json({
success: true,
data: statsData,
note: 'Mock 数据',
params: {
min_stock_count: minStockCount,
days: days,
start_date: startDate,
end_date: endDate
},
updated_at: new Date().toISOString()
});
}),
// 获取统计数据(通过 nginx 代理)
http.get('/concept-api/statistics', async ({ request }) => {
await delay(300);
const url = new URL(request.url);
const minStockCount = parseInt(url.searchParams.get('min_stock_count') || '3');
const days = parseInt(url.searchParams.get('days') || '7');
const startDate = url.searchParams.get('start_date');
const endDate = url.searchParams.get('end_date');
console.log('[Mock Concept] 获取统计数据 (nginx代理):', { minStockCount, days, startDate, endDate });
// 生成完整的统计数据
const statsData = generateConceptStats();
return HttpResponse.json({
success: true,
data: statsData,
note: 'Mock 数据(通过 nginx 代理)',
params: {
min_stock_count: minStockCount,
days: days,
start_date: startDate,
end_date: endDate
},
updated_at: new Date().toISOString()
});
}),
// 获取概念价格时间序列
http.get('http://111.198.58.126:16801/concept/:conceptId/price-timeseries', async ({ params, request }) => {
await delay(300);
const { conceptId } = params;
const url = new URL(request.url);
const startDate = url.searchParams.get('start_date');
const endDate = url.searchParams.get('end_date');
console.log('[Mock Concept] 获取价格时间序列:', { conceptId, startDate, endDate });
// 生成时间序列数据
const timeseries = [];
const start = new Date(startDate || '2024-01-01');
const end = new Date(endDate || new Date());
const daysDiff = Math.ceil((end - start) / (1000 * 60 * 60 * 24));
for (let i = 0; i <= daysDiff; i++) {
const date = new Date(start);
date.setDate(date.getDate() + i);
// 跳过周末
if (date.getDay() !== 0 && date.getDay() !== 6) {
timeseries.push({
trade_date: date.toISOString().split('T')[0], // 改为 trade_date
avg_change_pct: parseFloat((Math.random() * 8 - 2).toFixed(2)), // 转为数值
stock_count: Math.floor(Math.random() * 30) + 10,
volume: Math.floor(Math.random() * 1000000000)
});
}
}
return HttpResponse.json({
concept_id: conceptId,
timeseries: timeseries,
start_date: startDate,
end_date: endDate
});
}),
// 获取概念相关新闻 (search_china_news)
http.get('http://111.198.58.126:21891/search_china_news', async ({ request }) => {
await delay(300);
const url = new URL(request.url);
const query = url.searchParams.get('query');
const exactMatch = url.searchParams.get('exact_match');
const startDate = url.searchParams.get('start_date');
const endDate = url.searchParams.get('end_date');
const topK = parseInt(url.searchParams.get('top_k') || '100');
console.log('[Mock Concept] 搜索中国新闻:', { query, exactMatch, startDate, endDate, topK });
// 生成新闻数据
const news = [];
const newsCount = Math.min(topK, Math.floor(Math.random() * 15) + 5); // 5-20 条新闻
for (let i = 0; i < newsCount; i++) {
const daysAgo = Math.floor(Math.random() * 100); // 0-100 天前
const date = new Date();
date.setDate(date.getDate() - daysAgo);
const hour = Math.floor(Math.random() * 24);
const minute = Math.floor(Math.random() * 60);
const publishedTime = `${date.toISOString().split('T')[0]} ${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}:00`;
news.push({
id: `news_${i}`,
title: `${query || '概念'}板块动态:${['利好政策发布', '行业景气度提升', '龙头企业业绩超预期', '技术突破进展', '市场需求旺盛'][i % 5]}`,
detail: `${query || '概念'}相关新闻详细内容。近期${query || '概念'}板块表现活跃,市场关注度持续上升。多家券商研报指出,${query || '概念'}行业前景广阔,建议重点关注龙头企业投资机会。`,
description: `${query || '概念'}板块最新动态摘要...`,
source: ['新浪财经', '东方财富网', '财联社', '证券时报', '中国证券报', '上海证券报'][Math.floor(Math.random() * 6)],
published_time: publishedTime,
url: `https://finance.sina.com.cn/stock/news/${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, '0')}${String(date.getDate()).padStart(2, '0')}/news_${i}.html`
});
}
// 按时间降序排序
news.sort((a, b) => new Date(b.published_time) - new Date(a.published_time));
// 返回数组(不是对象)
return HttpResponse.json(news);
}),
// 获取概念相关研报 (search)
http.get('http://111.198.58.126:8811/search', async ({ request }) => {
await delay(300);
const url = new URL(request.url);
const query = url.searchParams.get('query');
const mode = url.searchParams.get('mode');
const exactMatch = url.searchParams.get('exact_match');
const size = parseInt(url.searchParams.get('size') || '30');
const startDate = url.searchParams.get('start_date');
console.log('[Mock Concept] 搜索研报:', { query, mode, exactMatch, size, startDate });
// 生成研报数据
const reports = [];
const reportCount = Math.min(size, Math.floor(Math.random() * 10) + 3); // 3-12 份研报
const publishers = ['中信证券', '国泰君安', '华泰证券', '招商证券', '海通证券', '广发证券', '申万宏源', '兴业证券'];
const authors = ['张明', '李华', '王强', '刘洋', '陈杰', '赵敏'];
const ratings = ['买入', '增持', '中性', '减持', '强烈推荐'];
const securityNames = ['行业研究', '公司研究', '策略研究', '宏观研究', '固收研究'];
for (let i = 0; i < reportCount; i++) {
const daysAgo = Math.floor(Math.random() * 100); // 0-100 天前
const date = new Date();
date.setDate(date.getDate() - daysAgo);
const declareDate = `${date.toISOString().split('T')[0]} ${String(Math.floor(Math.random() * 24)).padStart(2, '0')}:${String(Math.floor(Math.random() * 60)).padStart(2, '0')}:00`;
reports.push({
id: `report_${i}`,
report_title: `${query || '概念'}行业${['深度研究报告', '投资策略分析', '行业景气度跟踪', '估值分析报告', '竞争格局研究'][i % 5]}`,
content: `${query || '概念'}行业研究报告内容摘要。\n\n核心观点:\n1. ${query || '概念'}行业景气度持续向好,市场规模预计将保持高速增长。\n2. 龙头企业凭借技术优势和规模效应,市场份额有望进一步提升。\n3. 政策支持力度加大,为行业发展提供有力保障。\n\n投资建议:建议重点关注行业龙头企业,给予"${ratings[Math.floor(Math.random() * ratings.length)]}"评级。`,
abstract: `本报告深入分析了${query || '概念'}行业的发展趋势、竞争格局和投资机会,认为行业具备良好的成长性...`,
publisher: publishers[Math.floor(Math.random() * publishers.length)],
author: authors[Math.floor(Math.random() * authors.length)],
declare_date: declareDate,
rating: ratings[Math.floor(Math.random() * ratings.length)],
security_name: securityNames[Math.floor(Math.random() * securityNames.length)],
content_url: `https://pdf.dfcfw.com/pdf/H3_${1000000 + i}_1_${date.getFullYear()}${String(date.getMonth() + 1).padStart(2, '0')}${String(date.getDate()).padStart(2, '0')}.pdf`
});
}
// 按时间降序排序
reports.sort((a, b) => new Date(b.declare_date) - new Date(a.declare_date));
// 返回符合组件期望的格式
return HttpResponse.json({
results: reports,
total: reports.length,
query: query,
mode: mode
});
})
];