diff --git a/app.py b/app.py
index 931591e6..556cc977 100755
--- a/app.py
+++ b/app.py
@@ -10754,6 +10754,240 @@ def get_calendar_combined_data():
}), 500
+@app.route('/api/v1/zt/theme-scatter', methods=['GET'])
+def get_zt_theme_scatter():
+ """获取题材流星图数据(散点图)
+
+ 基于词频关键词合并同义概念,统计最近5个交易日数据
+
+ 返回:
+ {
+ success: true,
+ data: {
+ latestDate: "2026-01-07",
+ themes: [
+ {
+ label: "光刻胶",
+ color: "#F59E0B",
+ x: 5, // 最高连板数(辨识度)
+ y: 12, // 涨停家数(热度)
+ countTrend: 3, // 较前日变化
+ boardTrend: 1, // 连板变化
+ status: "rising", // rising/declining/lurking/clustering
+ history: [...], // 5日历史数据
+ stocks: [...], // 今日涨停股票
+ matchedSectors: [] // 匹配的原始板块名
+ },
+ ...
+ ]
+ }
+ }
+ """
+ import os
+ import json
+ import re
+
+ # 核心主题词配置(基于词频,合并同义概念)
+ THEME_KEYWORDS = [
+ {'keyword': '光刻', 'aliases': ['光刻胶', '光刻机'], 'label': '光刻胶', 'color': '#F59E0B'},
+ {'keyword': '航天', 'aliases': ['商业航天', '卫星导航', '卫星互联', '低轨卫星'], 'label': '商业航天', 'color': '#FF6B6B'},
+ {'keyword': '脑机', 'aliases': ['脑机接口', '人脑工程', '脑科学'], 'label': '脑机接口', 'color': '#A78BFA'},
+ {'keyword': '算力', 'aliases': ['算力', 'AI算力', '智算中心', '东数西算'], 'label': 'AI算力', 'color': '#3B82F6'},
+ {'keyword': '机器人', 'aliases': ['机器人', '人形机器', '智能机器'], 'label': '机器人', 'color': '#4ECDC4'},
+ {'keyword': '液冷', 'aliases': ['液冷', '液冷服务器', '液冷技术'], 'label': 'AI液冷', 'color': '#06B6D4'},
+ {'keyword': '军工', 'aliases': ['军工', '军工信息', '国防军工'], 'label': '军工', 'color': '#EF4444'},
+ {'keyword': '核', 'aliases': ['核聚变', '可控核变', '核电', '核能'], 'label': '核聚变', 'color': '#EC4899'},
+ {'keyword': '人工智能', 'aliases': ['人工智能', 'AI应用', 'AIGC', 'AI+'], 'label': '人工智能', 'color': '#8B5CF6'},
+ {'keyword': '国产替代', 'aliases': ['国产替代', '国产软件', '信创'], 'label': '国产替代', 'color': '#10B981'},
+ {'keyword': '固态电池', 'aliases': ['固态电池', '锂电池', '钠电池'], 'label': '固态电池', 'color': '#22C55E'},
+ {'keyword': '数据中心', 'aliases': ['数据中心', 'IDC', '算力中心'], 'label': '数据中心', 'color': '#0EA5E9'},
+ ]
+
+ # 噪音板块黑名单
+ NOISE_SECTORS = ['公告', '业绩预告', '重大合同', '重组', '股权激励', '其他',
+ '增持', '回购', '解禁', '减持', '限售股', '转债']
+
+ try:
+ days = request.args.get('days', 5, type=int)
+ days = min(max(days, 1), 10) # 限制1-10天
+
+ # 查找数据目录
+ possible_paths = [
+ os.path.join(app.static_folder or 'static', 'data', 'zt'),
+ os.path.join('public', 'data', 'zt'),
+ os.path.join('data', 'zt'),
+ '/var/www/vf_react/data/zt',
+ ]
+
+ zt_dir = None
+ for path in possible_paths:
+ if os.path.isdir(path):
+ zt_dir = path
+ break
+
+ if not zt_dir:
+ return jsonify({'success': False, 'error': '找不到涨停数据目录'}), 404
+
+ # 读取日期列表
+ dates_file = os.path.join(zt_dir, 'dates.json')
+ if not os.path.exists(dates_file):
+ return jsonify({'success': False, 'error': '找不到日期数据'}), 404
+
+ with open(dates_file, 'r', encoding='utf-8') as f:
+ dates_data = json.load(f)
+
+ recent_dates = dates_data.get('dates', [])[:days]
+ if not recent_dates:
+ return jsonify({'success': False, 'error': '无可用日期数据'}), 404
+
+ # 读取每日数据
+ daily_data_list = []
+ daily_dir = os.path.join(zt_dir, 'daily')
+
+ for date_info in recent_dates:
+ date_str = date_info.get('date', '')
+ daily_file = os.path.join(daily_dir, f'{date_str}.json')
+
+ if os.path.exists(daily_file):
+ try:
+ with open(daily_file, 'r', encoding='utf-8') as f:
+ data = json.load(f)
+ daily_data_list.append({
+ 'date': date_str,
+ 'formatted_date': date_info.get('formatted_date', ''),
+ 'sector_data': data.get('sector_data', {}),
+ 'stocks': data.get('stocks', []),
+ 'word_freq': data.get('word_freq_data', []),
+ })
+ except Exception as e:
+ print(f"[theme-scatter] 读取 {daily_file} 失败: {e}")
+
+ if not daily_data_list:
+ return jsonify({'success': False, 'error': '无法读取每日数据'}), 404
+
+ def match_sectors_by_keyword(sector_data, theme_config):
+ """基于关键词匹配板块"""
+ matched = []
+ for sector_name, sector_info in sector_data.items():
+ # 过滤噪音板块
+ if any(noise in sector_name for noise in NOISE_SECTORS):
+ continue
+ # 检查是否匹配任何别名
+ is_match = any(
+ alias in sector_name or sector_name in alias
+ for alias in theme_config['aliases']
+ )
+ if is_match:
+ matched.append({
+ 'name': sector_name,
+ 'stock_codes': sector_info.get('stock_codes', []),
+ 'count': sector_info.get('count', 0),
+ })
+ return matched
+
+ def get_continuous_days(stock):
+ """提取连板天数"""
+ cd = stock.get('continuous_days', '1')
+ match = re.search(r'(\d+)', str(cd))
+ return int(match.group(1)) if match else 1
+
+ # 处理每个主题
+ themes = []
+ latest_date = daily_data_list[0]['formatted_date']
+
+ for theme_config in THEME_KEYWORDS:
+ # 收集每日数据
+ history = []
+
+ for day_data in daily_data_list:
+ matched_sectors = match_sectors_by_keyword(day_data['sector_data'], theme_config)
+
+ # 合并所有匹配板块的股票(去重)
+ all_stock_codes = set()
+ for sector in matched_sectors:
+ all_stock_codes.update(sector['stock_codes'])
+
+ # 计算最高连板数
+ max_board = 1
+ stock_details = []
+ for code in all_stock_codes:
+ stock_info = next((s for s in day_data['stocks'] if s.get('scode') == code), None)
+ if stock_info:
+ days_val = get_continuous_days(stock_info)
+ max_board = max(max_board, days_val)
+ stock_details.append({
+ 'scode': stock_info.get('scode', code),
+ 'sname': stock_info.get('sname', code),
+ 'continuous_days': days_val,
+ 'brief': stock_info.get('brief', '')[:100] if stock_info.get('brief') else '',
+ })
+ else:
+ stock_details.append({'scode': code, 'sname': code, 'continuous_days': 1, 'brief': ''})
+
+ # 按连板数排序
+ stock_details.sort(key=lambda x: x['continuous_days'], reverse=True)
+
+ history.append({
+ 'date': day_data['formatted_date'],
+ 'count': len(all_stock_codes),
+ 'maxBoard': max_board,
+ 'stocks': stock_details,
+ 'matchedSectors': [s['name'] for s in matched_sectors],
+ })
+
+ # 跳过无数据的主题
+ if not history or history[0]['count'] == 0:
+ continue
+
+ latest = history[0]
+ previous = history[1] if len(history) > 1 else {'count': 0, 'maxBoard': 1}
+
+ count_trend = latest['count'] - previous['count']
+ board_trend = latest['maxBoard'] - previous['maxBoard']
+
+ # 判断状态
+ if latest['maxBoard'] >= 3 and count_trend > 0:
+ status = 'rising'
+ elif latest['maxBoard'] >= 3 and count_trend < 0:
+ status = 'declining'
+ elif latest['maxBoard'] < 3 and count_trend > 0:
+ status = 'lurking'
+ else:
+ status = 'clustering'
+
+ themes.append({
+ 'label': theme_config['label'],
+ 'color': theme_config['color'],
+ 'x': latest['maxBoard'],
+ 'y': latest['count'],
+ 'countTrend': count_trend,
+ 'boardTrend': board_trend,
+ 'status': status,
+ 'history': history,
+ 'stocks': latest['stocks'],
+ 'matchedSectors': latest['matchedSectors'],
+ })
+
+ # 按热度排序
+ themes.sort(key=lambda x: x['y'], reverse=True)
+
+ return jsonify({
+ 'success': True,
+ 'data': {
+ 'latestDate': latest_date,
+ 'themes': themes,
+ }
+ })
+
+ except Exception as e:
+ import traceback
+ traceback.print_exc()
+ return jsonify({
+ 'success': False,
+ 'error': str(e)
+ }), 500
+
+
@app.route('/api/v1/calendar/events', methods=['GET'])
def get_calendar_events():
"""获取指定日期的事件列表"""
diff --git a/src/views/Community/components/ThemeCometChart.js b/src/views/Community/components/ThemeCometChart.js
index 4d48286f..e2583ff0 100644
--- a/src/views/Community/components/ThemeCometChart.js
+++ b/src/views/Community/components/ThemeCometChart.js
@@ -1,8 +1,8 @@
/**
* ThemeCometChart - 题材流星图(散点图版本)
- * X轴:辨识度(最高板高度,即板块内最大连板数)
+ * X轴:辨识度(最高板高度)
* Y轴:板块热度(涨停家数)
- * 散点大小/颜色:表示不同状态(主升、退潮、潜伏、抱团)
+ * 数据由后端 API /api/v1/zt/theme-scatter 提供
*/
import React, { useState, useEffect, useMemo, useCallback } from 'react';
import {
@@ -13,206 +13,88 @@ import {
Spinner,
Center,
useToast,
+ Modal,
+ ModalOverlay,
+ ModalContent,
+ ModalHeader,
+ ModalBody,
+ ModalCloseButton,
+ useDisclosure,
+ Table,
+ Thead,
+ Tbody,
+ Tr,
+ Th,
+ Td,
+ Badge,
} from '@chakra-ui/react';
import ReactECharts from 'echarts-for-react';
import { RocketOutlined } from '@ant-design/icons';
+import { getApiBase } from '@/utils/env';
-// 板块状态分类
-const SECTOR_STATUS = {
- RISING: { name: '主升', color: '#FF4D4F', icon: '🔥' }, // 高辨识度+高热度+上升趋势
- DECLINING: { name: '退潮', color: '#52C41A', icon: '❄️' }, // 高辨识度+热度下降
- LURKING: { name: '潜伏', color: '#1890FF', icon: '●' }, // 低辨识度+热度上升
- CLUSTERING: { name: '抱团', color: '#722ED1', icon: '●' }, // 中等辨识度+稳定热度
-};
-
-// 噪音板块黑名单(过滤无意义的板块)
-const NOISE_SECTORS = [
- '公告', '业绩预告', '重大合同签署', '重组', '股权激励',
- '增持', '回购', '解禁', '减持', '限售股解禁',
-];
-
-/**
- * 从 public/data/zt 读取最近两天数据用于比较趋势
- */
-const fetchSectorData = async () => {
- try {
- // 获取日期列表
- const datesRes = await fetch('/data/zt/dates.json');
- const datesData = await datesRes.json();
- const recentDates = datesData.dates.slice(0, 2); // 最近两天
-
- if (recentDates.length === 0) return { today: null, yesterday: null };
-
- // 获取今日数据
- const todayRes = await fetch(`/data/zt/daily/${recentDates[0].date}.json`);
- const todayData = await todayRes.json();
-
- // 获取昨日数据(用于计算趋势)
- let yesterdayData = null;
- if (recentDates.length > 1) {
- try {
- const yesterdayRes = await fetch(`/data/zt/daily/${recentDates[1].date}.json`);
- yesterdayData = await yesterdayRes.json();
- } catch {
- // 忽略
- }
- }
-
- return {
- today: {
- date: recentDates[0].formatted_date,
- sectorData: todayData.sector_data || {},
- stocks: todayData.stocks || [],
- },
- yesterday: yesterdayData ? {
- date: recentDates[1].formatted_date,
- sectorData: yesterdayData.sector_data || {},
- stocks: yesterdayData.stocks || [],
- } : null,
- };
- } catch (error) {
- console.error('获取板块数据失败:', error);
- return { today: null, yesterday: null };
- }
-};
-
-/**
- * 处理板块数据,计算散点图坐标
- */
-const processSectorData = (data) => {
- if (!data.today) return [];
-
- const { sectorData, stocks } = data.today;
- const yesterdaySectors = data.yesterday?.sectorData || {};
-
- // 构建股票代码到连板数的映射
- const stockContinuousDays = {};
- stocks.forEach((stock) => {
- // continuous_days 可能是 "1板" 或数字
- let days = 1;
- if (stock.continuous_days) {
- const match = String(stock.continuous_days).match(/(\d+)/);
- if (match) days = parseInt(match[1]);
- }
- stockContinuousDays[stock.scode] = days;
- });
-
- const sectors = [];
-
- Object.entries(sectorData).forEach(([sectorName, sectorInfo]) => {
- // 过滤噪音板块
- if (NOISE_SECTORS.some((noise) => sectorName.includes(noise))) return;
-
- const stockCodes = sectorInfo.stock_codes || [];
- const count = sectorInfo.count || stockCodes.length;
-
- // 计算该板块的最高连板数(辨识度)
- let maxContinuousDays = 1;
- stockCodes.forEach((code) => {
- const days = stockContinuousDays[code] || 1;
- if (days > maxContinuousDays) maxContinuousDays = days;
- });
-
- // 获取昨日数据计算趋势
- const yesterdayCount = yesterdaySectors[sectorName]?.count || 0;
- const trend = count - yesterdayCount;
-
- // 判断板块状态
- let status;
- if (maxContinuousDays >= 3 && trend > 0) {
- status = SECTOR_STATUS.RISING; // 主升:高辨识度+上升
- } else if (maxContinuousDays >= 3 && trend < 0) {
- status = SECTOR_STATUS.DECLINING; // 退潮:高辨识度+下降
- } else if (maxContinuousDays < 3 && trend > 0) {
- status = SECTOR_STATUS.LURKING; // 潜伏:低辨识度+上升
- } else {
- status = SECTOR_STATUS.CLUSTERING; // 抱团:其他
- }
-
- sectors.push({
- name: sectorName,
- x: maxContinuousDays, // X轴:最高板高度
- y: count, // Y轴:涨停家数
- trend,
- status,
- stockCodes,
- stocks: stockCodes.map((code) => {
- const stockInfo = stocks.find((s) => s.scode === code);
- return stockInfo || { scode: code, sname: code };
- }),
- });
- });
-
- // 按热度排序
- sectors.sort((a, b) => b.y - a.y);
-
- return sectors;
+// 板块状态配置
+const STATUS_CONFIG = {
+ rising: { name: '主升', color: '#FF4D4F' },
+ declining: { name: '退潮', color: '#52C41A' },
+ lurking: { name: '潜伏', color: '#1890FF' },
+ clustering: { name: '抱团', color: '#722ED1' },
};
/**
* 生成 ECharts 配置
*/
-const generateChartOption = (sectors) => {
- if (!sectors || sectors.length === 0) return {};
+const generateChartOption = (themes) => {
+ if (!themes || themes.length === 0) return {};
// 按状态分组
const groupedData = {
- [SECTOR_STATUS.RISING.name]: [],
- [SECTOR_STATUS.DECLINING.name]: [],
- [SECTOR_STATUS.LURKING.name]: [],
- [SECTOR_STATUS.CLUSTERING.name]: [],
+ 主升: [],
+ 退潮: [],
+ 潜伏: [],
+ 抱团: [],
};
- sectors.forEach((sector) => {
- groupedData[sector.status.name].push({
- name: sector.name,
- value: [sector.x, sector.y],
- trend: sector.trend,
- stockCount: sector.y,
- maxBoard: sector.x,
- stocks: sector.stocks,
+ themes.forEach((theme) => {
+ const statusName = STATUS_CONFIG[theme.status]?.name || '抱团';
+ groupedData[statusName].push({
+ name: theme.label,
+ value: [theme.x, theme.y],
+ countTrend: theme.countTrend,
+ boardTrend: theme.boardTrend,
+ themeColor: theme.color,
+ history: theme.history,
});
});
// 创建系列
- const series = Object.entries(SECTOR_STATUS).map(([key, status]) => ({
- name: status.name,
+ const series = Object.entries(STATUS_CONFIG).map(([key, config]) => ({
+ name: config.name,
type: 'scatter',
- data: groupedData[status.name],
- symbolSize: (data) => {
- // 根据热度调整大小
- const size = Math.max(20, Math.min(60, data[1] * 4));
- return size;
- },
+ data: groupedData[config.name] || [],
+ symbolSize: (data) => Math.max(25, Math.min(70, data[1] * 3.5)),
itemStyle: {
- color: status.color,
- shadowBlur: 10,
- shadowColor: status.color,
- opacity: 0.8,
+ color: (params) => params.data.themeColor || config.color,
+ shadowBlur: 12,
+ shadowColor: (params) => params.data.themeColor || config.color,
+ opacity: 0.85,
},
emphasis: {
- itemStyle: {
- opacity: 1,
- shadowBlur: 20,
- },
- label: {
- show: true,
- },
+ itemStyle: { opacity: 1, shadowBlur: 25 },
},
label: {
show: true,
formatter: (params) => params.data.name,
position: 'right',
color: '#fff',
- fontSize: 11,
+ fontSize: 12,
+ fontWeight: 'bold',
textShadowColor: 'rgba(0,0,0,0.8)',
textShadowBlur: 4,
},
}));
- // 计算坐标轴范围
- const maxX = Math.max(...sectors.map((s) => s.x), 5) + 1;
- const maxY = Math.max(...sectors.map((s) => s.y), 10) + 2;
+ const maxX = Math.max(...themes.map((t) => t.x), 5) + 1;
+ const maxY = Math.max(...themes.map((t) => t.y), 10) + 3;
return {
backgroundColor: 'transparent',
@@ -221,60 +103,58 @@ const generateChartOption = (sectors) => {
backgroundColor: 'rgba(15, 15, 30, 0.95)',
borderColor: 'rgba(255, 215, 0, 0.3)',
borderWidth: 1,
- textStyle: {
- color: '#fff',
- },
+ textStyle: { color: '#fff' },
formatter: (params) => {
- const { name, value, trend, stocks } = params.data;
- const trendText = trend > 0 ? `+${trend}` : trend;
- const trendIcon = trend > 0 ? '🔥' : trend < 0 ? '❄️' : '➡️';
+ const { name, value, countTrend, boardTrend, history } = params.data;
+ const countTrendText = countTrend > 0 ? `+${countTrend}` : countTrend;
+ const boardTrendText = boardTrend > 0 ? `+${boardTrend}` : boardTrend;
+ const countIcon = countTrend > 0 ? '🔥' : countTrend < 0 ? '❄️' : '➡️';
- // 显示前5只股票
- const topStocks = stocks.slice(0, 5);
- const stockList = topStocks
- .map((s) => `${s.sname || s.scode}`)
- .join('、');
+ const historyText = (history || [])
+ .slice(0, 5)
+ .map((h) => `${h.date?.slice(5) || ''}: ${h.count}家/${h.maxBoard}板`)
+ .join('
');
return `
- ${name} ${trendIcon}
+ ${name} ${countIcon}
涨停家数:
${value[1]}家
- (${trendText})
+ (${countTrendText})
最高连板:
${value[0]}板
+ (${boardTrendText})
-
- ${stockList}${stocks.length > 5 ? '...' : ''}
+
+
近5日趋势:
+
${historyText}
+
点击查看详情
`;
},
},
legend: {
show: true,
- top: 10,
+ top: 5,
right: 10,
orient: 'horizontal',
- textStyle: {
- color: 'rgba(255, 255, 255, 0.7)',
- fontSize: 11,
- },
- itemWidth: 14,
- itemHeight: 14,
- data: Object.values(SECTOR_STATUS).map((s) => ({
+ textStyle: { color: 'rgba(255, 255, 255, 0.7)', fontSize: 11 },
+ itemWidth: 12,
+ itemHeight: 12,
+ data: Object.values(STATUS_CONFIG).map((s) => ({
name: s.name,
icon: 'circle',
itemStyle: { color: s.color },
})),
},
grid: {
- left: '8%',
- right: '5%',
- top: '15%',
+ left: '10%',
+ right: '8%',
+ top: '12%',
bottom: '15%',
containLabel: true,
},
@@ -283,52 +163,25 @@ const generateChartOption = (sectors) => {
name: '辨识度(最高板)',
nameLocation: 'middle',
nameGap: 30,
- nameTextStyle: {
- color: 'rgba(255, 255, 255, 0.6)',
- fontSize: 12,
- },
+ nameTextStyle: { color: 'rgba(255, 255, 255, 0.6)', fontSize: 12 },
min: 0,
max: maxX,
interval: 1,
- axisLine: {
- lineStyle: {
- color: 'rgba(255, 255, 255, 0.2)',
- },
- },
- axisLabel: {
- color: 'rgba(255, 255, 255, 0.6)',
- fontSize: 11,
- formatter: '{value}板',
- },
- splitLine: {
- lineStyle: {
- color: 'rgba(255, 255, 255, 0.05)',
- },
- },
+ axisLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.2)' } },
+ axisLabel: { color: 'rgba(255, 255, 255, 0.6)', fontSize: 11, formatter: '{value}板' },
+ splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.05)' } },
},
yAxis: {
type: 'value',
name: '板块热度(家数)',
nameLocation: 'middle',
nameGap: 40,
- nameTextStyle: {
- color: 'rgba(255, 255, 255, 0.6)',
- fontSize: 12,
- },
+ nameTextStyle: { color: 'rgba(255, 255, 255, 0.6)', fontSize: 12 },
min: 0,
max: maxY,
- axisLine: {
- show: false,
- },
- axisLabel: {
- color: 'rgba(255, 255, 255, 0.6)',
- fontSize: 11,
- },
- splitLine: {
- lineStyle: {
- color: 'rgba(255, 255, 255, 0.05)',
- },
- },
+ axisLine: { show: false },
+ axisLabel: { color: 'rgba(255, 255, 255, 0.6)', fontSize: 11 },
+ splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.05)' } },
},
series,
};
@@ -339,28 +192,30 @@ const generateChartOption = (sectors) => {
*/
const ThemeCometChart = ({ onThemeSelect }) => {
const [loading, setLoading] = useState(true);
- const [sectors, setSectors] = useState([]);
- const [dateInfo, setDateInfo] = useState('');
+ const [data, setData] = useState({ themes: [], latestDate: '' });
+ const [selectedTheme, setSelectedTheme] = useState(null);
+ const { isOpen, onOpen, onClose } = useDisclosure();
const toast = useToast();
- // 加载数据
useEffect(() => {
const loadData = async () => {
setLoading(true);
try {
- const data = await fetchSectorData();
- const processed = processSectorData(data);
- setSectors(processed);
- if (data.today) {
- setDateInfo(data.today.date);
+ const apiBase = getApiBase();
+ const response = await fetch(`${apiBase}/api/v1/zt/theme-scatter?days=5`);
+ const result = await response.json();
+
+ if (result.success && result.data) {
+ setData({
+ themes: result.data.themes || [],
+ latestDate: result.data.latestDate || '',
+ });
+ } else {
+ throw new Error(result.error || '加载失败');
}
} catch (error) {
- console.error('加载板块数据失败:', error);
- toast({
- title: '加载数据失败',
- status: 'error',
- duration: 3000,
- });
+ console.error('加载题材数据失败:', error);
+ toast({ title: '加载数据失败', description: error.message, status: 'error', duration: 3000 });
} finally {
setLoading(false);
}
@@ -368,53 +223,38 @@ const ThemeCometChart = ({ onThemeSelect }) => {
loadData();
}, [toast]);
- // 图表配置
- const chartOption = useMemo(() => {
- return generateChartOption(sectors);
- }, [sectors]);
+ const chartOption = useMemo(() => generateChartOption(data.themes), [data.themes]);
- // 点击事件
const handleChartClick = useCallback(
(params) => {
- if (params.data && onThemeSelect) {
- const sector = sectors.find((s) => s.name === params.data.name);
- if (sector) {
- onThemeSelect({
- theme: { label: sector.name, color: sector.status.color },
- stocks: sector.stocks.map((s) => ({
- ...s,
- _continuousDays: 1,
- })),
- date: dateInfo,
- });
+ if (params.data) {
+ const theme = data.themes.find((t) => t.label === params.data.name);
+ if (theme) {
+ setSelectedTheme(theme);
+ onOpen();
}
}
},
- [sectors, dateInfo, onThemeSelect]
+ [data.themes, onOpen]
);
- const onChartEvents = useMemo(
- () => ({
- click: handleChartClick,
- }),
- [handleChartClick]
- );
+ const onChartEvents = useMemo(() => ({ click: handleChartClick }), [handleChartClick]);
if (loading) {
return (
- 加载板块数据...
+ 加载数据...
);
}
- if (!sectors || sectors.length === 0) {
+ if (!data.themes || data.themes.length === 0) {
return (
- 暂无板块数据
+ 暂无数据
);
}
@@ -430,12 +270,7 @@ const ThemeCometChart = ({ onThemeSelect }) => {
>
{/* 标题栏 */}
-
+
@@ -443,7 +278,7 @@ const ThemeCometChart = ({ onThemeSelect }) => {
AI 舆情 · 时空决策驾驶舱
- 题材流星图 · {dateInfo}
+ 题材流星图 · {data.latestDate}
@@ -457,6 +292,102 @@ const ThemeCometChart = ({ onThemeSelect }) => {
opts={{ renderer: 'canvas' }}
/>
+
+ {/* 详情弹窗 */}
+
+
+
+
+ {selectedTheme?.label} - 近5日趋势
+
+
+
+ {selectedTheme && (
+
+ {/* 趋势表格 */}
+
+ 历史数据
+
+
+
+ | 日期 |
+ 涨停家数 |
+ 最高连板 |
+ 变化 |
+
+
+
+ {selectedTheme.history?.map((h, idx) => {
+ const prev = selectedTheme.history[idx + 1];
+ const countChange = prev ? h.count - prev.count : 0;
+ return (
+
+ | {h.date} |
+ {h.count}家 |
+ {h.maxBoard}板 |
+
+ {countChange !== 0 && (
+ 0 ? 'green' : 'red'}>
+ {countChange > 0 ? '+' : ''}{countChange}
+
+ )}
+ |
+
+ );
+ })}
+
+
+
+
+ {/* 今日涨停股票 */}
+
+
+ 今日涨停股票({selectedTheme.stocks?.length || 0}只)
+
+
+
+
+
+ | 代码 |
+ 名称 |
+ 连板 |
+
+
+
+ {selectedTheme.stocks?.slice(0, 20).map((stock) => (
+
+ | {stock.scode} |
+ {stock.sname} |
+
+ = 3 ? 'red' : 'gray'}>
+ {stock.continuous_days}板
+
+ |
+
+ ))}
+
+
+
+
+
+ {/* 匹配的板块 */}
+ {selectedTheme.matchedSectors?.length > 0 && (
+
+ 匹配板块
+
+ {selectedTheme.matchedSectors.map((sector) => (
+
+ {sector}
+
+ ))}
+
+
+ )}
+
+ )}
+
+
+
);
};