Compare commits

...

12 Commits

Author SHA1 Message Date
zdl
433fc4a0f5 feat: API优化 2025-10-29 19:49:20 +08:00
zdl
5bac525147 feat: mock数据添加 2025-10-29 19:41:05 +08:00
zdl
a049d0365b feat: 修改内容:添加风险提示到K线图弹窗 2025-10-29 19:34:33 +08:00
zdl
fdbb6ceff5 feat:修复mock数据 2025-10-29 19:31:13 +08:00
zdl
35f8b5195a feat: 访问"概念中心"页面
2. 点击任意概念卡片进入概念详情
     3. 点击"历史时间轴"按钮(需要Max会员权限)
     4. 查看弹窗底部是否显示风险提示 & mock数据处理
2025-10-29 19:18:12 +08:00
zdl
77aafd5661 feat: 事件中心股票详情添加风险提示 2025-10-29 19:12:18 +08:00
zdl
ce1bf29270 feat: 涨停分析/股票详情弹窗 添加风险提示 2025-10-29 19:08:51 +08:00
zdl
ac7a6991bc feat: 添加mock数据 2025-10-29 18:43:57 +08:00
zdl
4435ef9392 feat: 事件中心 事件详情底部添加风险提示 2025-10-29 18:33:46 +08:00
zdl
224c6a12d4 feat: 添加mock数据 2025-10-29 18:02:58 +08:00
zdl
d0d8b1ebde feat: 核心页面添加风险提示 2025-10-29 17:49:05 +08:00
zdl
bf8aff9e7e feat: 创建风险提示通用组件 2025-10-29 17:42:24 +08:00
15 changed files with 901 additions and 14 deletions

View File

@@ -0,0 +1,60 @@
// src/components/RiskDisclaimer/RiskDisclaimer.js
import React from 'react';
import { Box, Text, HStack, Icon, useColorModeValue } from '@chakra-ui/react';
/**
* 风险提示组件
*
* @param {Object} props
* @param {string} props.text - 风险提示文本内容
* @param {string} props.variant - 文本变体类型 ('default', 'homepage', 'section')
* @param {Object} props.sx - 额外的样式对象
*/
const RiskDisclaimer = ({
text,
variant = 'default',
sx = {},
mt = 0,
mb = 0,
...rest
}) => {
// 极简风格 - 透明背景,固定灰色文字
const textColor = '#999999'; // 固定中性灰,不受主题影响
// 预定义的文本变体
const textVariants = {
homepage: '风险提示:解析内容由价值前沿人工采集整理自新闻、公告、研报等公开信息,团队辛苦编写,未经许可严禁转载。站内所有文章均不构成投资建议,请投资者注意风险,独立审慎决策。',
default: '风险提示:解析内容由价值前沿人工采集整理自新闻、公告、研报等公开信息,团队辛苦编写,未经许可严禁转载。本产品内容均不构成投资建议,请投资者注意风险,独立审慎决策。',
section: '风险提示:解析内容由价值前沿人工采集整理自新闻、公告、研报等公开信息,团队辛苦编写,未经许可严禁转载。本部分产品内容均不构成投资建议,请投资者注意风险,独立审慎决策。'
};
// 使用传入的text或预定义的variant
const displayText = text || textVariants[variant] || textVariants.default;
return (
<Box
bg="transparent"
p={0}
mt={mt}
mb={mb}
width="100%"
sx={sx}
{...rest}
>
<HStack spacing={0} align="flex-start">
<Text
fontSize="xs"
color={textColor}
lineHeight="1.6"
fontWeight="normal"
textAlign="center"
width="100%"
>
{displayText}
</Text>
</HStack>
</Box>
);
};
export default RiskDisclaimer;

View File

@@ -0,0 +1,2 @@
// src/components/RiskDisclaimer/index.js
export { default } from './RiskDisclaimer';

View File

@@ -7,6 +7,7 @@ import moment from 'moment';
import { stockService } from '../../services/eventService';
import CitedContent from '../Citation/CitedContent';
import { logger } from '../../utils/logger';
import RiskDisclaimer from '../RiskDisclaimer';
const { Text } = Typography;
@@ -563,19 +564,8 @@ const StockChartAntdModal = ({
</div>
) : null}
{/* 调试信息 */}
{process.env.NODE_ENV === 'development' && chartData && (
<div style={{ marginTop: 16, padding: 12, backgroundColor: '#f0f0f0', borderRadius: 6, fontSize: '12px' }}>
<Text strong style={{ display: 'block', marginBottom: 4 }}>调试信息:</Text>
<Text>数据条数: {chartData.data ? chartData.data.length : 0}</Text>
<br />
<Text>交易日期: {chartData.trade_date}</Text>
<br />
<Text>图表类型: {activeChartType}</Text>
<br />
<Text>原始事件时间: {eventTime}</Text>
</div>
)}
{/* 风险提示 */}
<RiskDisclaimer variant="default" />
</div>
</Modal>
);

View File

@@ -6,6 +6,7 @@ import * as echarts from 'echarts';
import moment from 'moment';
import { stockService } from '../../services/eventService';
import { logger } from '../../utils/logger';
import RiskDisclaimer from '../RiskDisclaimer';
const StockChartModal = ({
isOpen,
@@ -545,6 +546,11 @@ const StockChartModal = ({
</Box>
)}
{/* 风险提示 */}
<Box px={4} pb={4}>
<RiskDisclaimer variant="default" />
</Box>
{process.env.NODE_ENV === 'development' && chartData && (
<Box p={4} bg="gray.50" fontSize="xs" color="gray.600">
<Text fontWeight="bold">调试信息:</Text>

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { Box, Container, VStack, HStack, Text, Link, useColorModeValue } from '@chakra-ui/react';
import RiskDisclaimer from '../components/RiskDisclaimer';
/**
* 应用通用页脚组件
@@ -10,6 +11,7 @@ const AppFooter = () => {
<Box bg={useColorModeValue('gray.100', 'gray.800')} py={6} mt={8}>
<Container maxW="container.xl">
<VStack spacing={2}>
<RiskDisclaimer />
<Text color="gray.500" fontSize="sm">
© 2024 价值前沿. 保留所有权利.
</Text>

View File

@@ -155,5 +155,222 @@ export const conceptHandlers = [
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 }
);
}
}),
// 获取统计数据
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');
console.log('[Mock Concept] 获取统计数据:', { minStockCount, days });
return HttpResponse.json({
total_concepts: 150,
active_concepts: 120,
avg_stock_count: 25,
top_concepts: generatePopularConcepts(10),
min_stock_count: minStockCount,
days: days,
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
});
})
];

View File

@@ -923,4 +923,157 @@ export const eventHandlers = [
}
});
}),
// ==================== 历史事件对比相关 ====================
// 获取历史事件列表
http.get('/api/events/:eventId/historical', async ({ params }) => {
await delay(400);
const { eventId } = params;
console.log('[Mock] 获取历史事件列表, eventId:', eventId);
// 生成历史事件数据
const generateHistoricalEvents = (count = 5) => {
const events = [];
const eventTitles = [
'芯片产业链政策扶持升级',
'新能源汽车销量创历史新高',
'人工智能大模型技术突破',
'半导体设备国产化加速',
'数字经济政策利好发布',
'新能源产业链整合提速',
'医药创新药获批上市',
'5G应用场景扩展',
'智能驾驶技术迭代升级',
'储能行业景气度上行'
];
const importanceLevels = [1, 2, 3, 4, 5];
for (let i = 0; i < count; i++) {
const daysAgo = Math.floor(Math.random() * 180) + 30; // 30-210 天前
const date = new Date();
date.setDate(date.getDate() - daysAgo);
const importance = importanceLevels[Math.floor(Math.random() * importanceLevels.length)];
events.push({
id: `hist_event_${i + 1}`,
title: eventTitles[i % eventTitles.length],
description: `${eventTitles[i % eventTitles.length]}的详细描述。该事件对相关产业链产生重要影响,市场关注度高,相关概念股表现活跃。`,
date: date.toISOString().split('T')[0],
importance: importance,
similarity: parseFloat((Math.random() * 0.3 + 0.7).toFixed(2)), // 0.7-1.0
impact_sectors: [
['半导体', '芯片设计', 'EDA'],
['新能源汽车', '锂电池', '充电桩'],
['人工智能', '算力', '大模型'],
['半导体设备', '国产替代', '集成电路'],
['数字经济', '云计算', '大数据']
][i % 5],
affected_stocks_count: Math.floor(Math.random() * 30) + 10, // 10-40 只股票
avg_change_pct: parseFloat((Math.random() * 10 - 2).toFixed(2)), // -2% to +8%
created_at: date.toISOString()
});
}
// 按日期降序排序
return events.sort((a, b) => new Date(b.date) - new Date(a.date));
};
try {
const historicalEvents = generateHistoricalEvents(5);
return HttpResponse.json({
success: true,
data: historicalEvents,
total: historicalEvents.length,
message: '获取历史事件列表成功'
});
} catch (error) {
console.error('[Mock] 获取历史事件列表失败:', error);
return HttpResponse.json(
{
success: false,
error: '获取历史事件列表失败',
data: []
},
{ status: 500 }
);
}
}),
// 获取历史事件相关股票
http.get('/api/historical-events/:eventId/stocks', async ({ params }) => {
await delay(500);
const { eventId } = params;
console.log('[Mock] 获取历史事件相关股票, eventId:', eventId);
// 生成历史事件相关股票数据
const generateHistoricalEventStocks = (count = 10) => {
const stocks = [];
const sectors = ['半导体', '新能源', '医药', '消费电子', '人工智能', '5G通信'];
const stockNames = [
'中芯国际', '长江存储', '华为海思', '紫光国微', '兆易创新',
'宁德时代', '比亚迪', '隆基绿能', '阳光电源', '亿纬锂能',
'恒瑞医药', '迈瑞医疗', '药明康德', '泰格医药', '康龙化成',
'立讯精密', '歌尔声学', '京东方A', 'TCL科技', '海康威视',
'科大讯飞', '商汤科技', '寒武纪', '海光信息', '中兴通讯'
];
for (let i = 0; i < count; i++) {
const stockCode = `${Math.random() > 0.5 ? '6' : '0'}${String(Math.floor(Math.random() * 100000)).padStart(5, '0')}`;
const changePct = (Math.random() * 15 - 3).toFixed(2); // -3% ~ +12%
const correlation = (Math.random() * 0.4 + 0.6).toFixed(2); // 0.6 ~ 1.0
stocks.push({
id: `stock_${i}`,
stock_code: `${stockCode}.${Math.random() > 0.5 ? 'SH' : 'SZ'}`,
stock_name: stockNames[i % stockNames.length],
sector: sectors[Math.floor(Math.random() * sectors.length)],
correlation: parseFloat(correlation),
event_day_change_pct: parseFloat(changePct),
relation_desc: {
data: [
{
query_part: `该公司是${sectors[Math.floor(Math.random() * sectors.length)]}行业龙头,受事件影响显著,市场关注度高,订单量同比增长${Math.floor(Math.random() * 50 + 20)}%`,
sentences: `根据行业研究报告,该公司在${sectors[Math.floor(Math.random() * sectors.length)]}领域具有核心技术优势,产能利用率达到${Math.floor(Math.random() * 20 + 80)}%,随着事件的深入发展,公司业绩有望持续受益。机构预测未来三年复合增长率将达到${Math.floor(Math.random() * 30 + 15)}%以上`,
match_score: correlation > 0.8 ? '好' : (correlation > 0.6 ? '中' : '一般'),
author: ['中信证券', '国泰君安', '华泰证券', '招商证券'][Math.floor(Math.random() * 4)],
declare_date: new Date(Date.now() - Math.floor(Math.random() * 90) * 24 * 60 * 60 * 1000).toISOString(),
report_title: `${stockNames[i % stockNames.length]}深度研究报告`
}
]
}
});
}
// 按相关度降序排序
return stocks.sort((a, b) => b.correlation - a.correlation);
};
try {
const stocks = generateHistoricalEventStocks(15);
return HttpResponse.json({
success: true,
data: stocks,
message: '获取历史事件相关股票成功'
});
} catch (error) {
console.error('[Mock] 获取历史事件相关股票失败:', error);
return HttpResponse.json(
{
success: false,
error: '获取历史事件相关股票失败',
data: []
},
{ status: 500 }
);
}
}),
];

View File

@@ -12,6 +12,7 @@ import { stockHandlers } from './stock';
import { companyHandlers } from './company';
import { marketHandlers } from './market';
import { financialHandlers } from './financial';
import { limitAnalyseHandlers } from './limitAnalyse';
// 可以在这里添加更多的 handlers
// import { userHandlers } from './user';
@@ -28,5 +29,6 @@ export const handlers = [
...companyHandlers,
...marketHandlers,
...financialHandlers,
...limitAnalyseHandlers,
// ...userHandlers,
];

View File

@@ -0,0 +1,344 @@
// 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
export const limitAnalyseHandlers = [
// 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: '高位股数据获取成功',
});
}),
];

View File

@@ -17,6 +17,7 @@ import SubscriptionUpgradeModal from '../../../components/SubscriptionUpgradeMod
import moment from 'moment';
import { logger } from '../../../utils/logger';
import { getApiBase } from '../../../utils/apiConfig';
import RiskDisclaimer from '../../../components/RiskDisclaimer';
const { Title, Text } = Typography;
const { TabPane } = Tabs;
@@ -1037,6 +1038,11 @@ function StockDetailPanel({ visible, event, onClose }) {
className="stock-detail-panel"
>
<AntdTabs activeKey={activeTab} onChange={setActiveTab} items={tabItems} />
{/* 风险提示 */}
<div style={{ marginTop: '24px', paddingBottom: '20px' }}>
<RiskDisclaimer variant="default" />
</div>
</Drawer>
{/* 事件讨论模态框 */}

View File

@@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react';
import { logger } from '../../utils/logger';
import { useConceptTimelineEvents } from './hooks/useConceptTimelineEvents';
import RiskDisclaimer from '../../components/RiskDisclaimer';
import {
Modal,
ModalOverlay,
@@ -825,6 +826,11 @@ const ConceptTimelineModal = ({
</VStack>
</Center>
)}
{/* 风险提示 */}
<Box px={6}>
<RiskDisclaimer variant="default" />
</Box>
</ModalBody>
<ModalFooter borderTop="1px solid" borderColor="gray.200">

View File

@@ -274,7 +274,69 @@ export const useConceptEvents = ({ navigate } = {}) => {
});
}, [track]);
// ========== 别名函数 - 为保持向后兼容性 ==========
/**
* 追踪概念搜索(别名函数)
* @alias trackSearchQuerySubmitted
*/
const trackConceptSearched = useCallback((query, resultCount = 0) => {
return trackSearchQuerySubmitted(query, resultCount);
}, [trackSearchQuerySubmitted]);
/**
* 追踪筛选器应用(通用包装函数)
* @param {string} filterType - 筛选类型 (sort/date/view_mode)
* @param {any} filterValue - 筛选值
* @param {any} previousValue - 之前的值
*/
const trackFilterApplied = useCallback((filterType, filterValue, previousValue = null) => {
if (filterType === 'sort') {
return trackSortChanged(filterValue, previousValue);
} else if (filterType === 'date') {
return trackDateChanged(filterValue, previousValue);
} else if (filterType === 'view_mode') {
return trackViewModeChanged(filterValue, previousValue);
}
}, [trackSortChanged, trackDateChanged, trackViewModeChanged]);
/**
* 追踪概念股票列表查看
* @param {string} conceptName - 概念名称
* @param {number} stockCount - 股票数量
*/
const trackConceptStocksViewed = useCallback((conceptName, stockCount = 0) => {
track(RETENTION_EVENTS.CONCEPT_DETAIL_VIEWED, {
concept_name: conceptName,
stock_count: stockCount,
view_type: 'stocks_list',
source: 'concept_center',
});
logger.debug('useConceptEvents', '📈 Concept Stocks Viewed', {
conceptName,
stockCount,
});
}, [track]);
/**
* 追踪概念时间轴查看(别名函数)
* @alias trackConceptDetailViewed
*/
const trackConceptTimelineViewed = useCallback((conceptName, conceptId) => {
return trackConceptDetailViewed(conceptName, conceptId);
}, [trackConceptDetailViewed]);
/**
* 追踪分页变化(别名函数 - 不同时态)
* @alias trackPageChanged
*/
const trackPageChange = useCallback((page, filters = {}) => {
return trackPageChanged(page, filters);
}, [trackPageChanged]);
return {
// 原有函数
trackConceptListViewed,
trackSearchInitiated,
trackSearchQuerySubmitted,
@@ -288,5 +350,12 @@ export const useConceptEvents = ({ navigate } = {}) => {
trackStockDetailViewed,
trackPaywallShown,
trackUpgradeClicked,
// 别名函数 - 为保持向后兼容性
trackConceptSearched,
trackFilterApplied,
trackConceptStocksViewed,
trackConceptTimelineViewed,
trackPageChange,
};
};

View File

@@ -38,6 +38,7 @@ import {
} from '@chakra-ui/react';
import { getFormattedTextProps } from '../../../utils/textUtils';
import { ExternalLinkIcon } from '@chakra-ui/icons';
import RiskDisclaimer from '../../../components/RiskDisclaimer';
import './WordCloud.css';
import {
BarChart, Bar,
@@ -598,6 +599,9 @@ export const StockDetailModal = ({ isOpen, onClose, selectedStock }) => {
))}
</Wrap>
</Box>
{/* 风险提示 */}
<RiskDisclaimer variant="default" />
</VStack>
</ModalBody>
<ModalFooter>

View File

@@ -29,7 +29,7 @@ import {
import { StarIcon, ViewIcon, TimeIcon, ChevronDownIcon, ChevronUpIcon } from '@chakra-ui/icons';
import { getFormattedTextProps } from '../../../utils/textUtils';
const SectorDetails = ({ sortedSectors, totalStocks }) => {
const SectorDetails = ({ sortedSectors, totalStocks, onStockClick }) => {
// 使用 useRef 来维持展开状态,避免重新渲染时重置
const expandedSectorsRef = useRef([]);
const [expandedSectors, setExpandedSectors] = useState([]);
@@ -194,6 +194,8 @@ const SectorDetails = ({ sortedSectors, totalStocks }) => {
bg: 'gray.50'
}}
transition="all 0.2s"
cursor="pointer"
onClick={() => onStockClick && onStockClick(stock)}
>
<Flex justify="space-between" align="start">
<VStack align="start" spacing={2} flex={1}>

View File

@@ -60,6 +60,8 @@ export default function LimitAnalyse() {
const [wordCloudData, setWordCloudData] = useState([]);
const [searchResults, setSearchResults] = useState(null);
const [isSearchOpen, setIsSearchOpen] = useState(false);
const [selectedStock, setSelectedStock] = useState(null);
const [isStockDetailOpen, setIsStockDetailOpen] = useState(false);
const toast = useToast();
@@ -243,6 +245,20 @@ export default function LimitAnalyse() {
}
};
// 处理股票点击
const handleStockClick = (stock) => {
setSelectedStock(stock);
setIsStockDetailOpen(true);
// 🎯 追踪股票详情查看
trackStockDetailViewed(stock.scode, stock.sname, 'sector_details');
};
// 关闭股票详情弹窗
const handleCloseStockDetail = () => {
setIsStockDetailOpen(false);
setSelectedStock(null);
};
// 处理板块数据排序
const getSortedSectorData = () => {
if (!dailyData?.sector_data) return [];
@@ -470,6 +486,7 @@ export default function LimitAnalyse() {
<SectorDetails
sortedSectors={getSortedSectorData()}
totalStocks={dailyData?.total_stocks || 0}
onStockClick={handleStockClick}
/>
</Box>
)}
@@ -496,6 +513,13 @@ export default function LimitAnalyse() {
onStockClick={() => {}}
/>
{/* 股票详情弹窗 */}
<StockDetailModal
isOpen={isStockDetailOpen}
onClose={handleCloseStockDetail}
selectedStock={selectedStock}
/>
{/* 浮动按钮 */}
<Box position="fixed" bottom={8} right={8} zIndex={1000}>
<VStack spacing={3}>